├── .github └── workflows │ ├── build_and_release.yml │ └── ci.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── Makefile ├── README.md ├── belt ├── Cargo.toml ├── README.md └── src │ ├── app.rs │ ├── config.rs │ ├── main.rs │ └── opts.rs └── dateparser ├── Cargo.toml ├── README.md ├── benches └── parse.rs ├── examples ├── convert_to_pacific.rs ├── parse.rs ├── parse_with.rs ├── parse_with_timezone.rs └── str_parse_method.rs └── src ├── datetime.rs ├── lib.rs └── timezone.rs /.github/workflows/build_and_release.yml: -------------------------------------------------------------------------------- 1 | name: build and release 2 | on: 3 | push: # run build and release only on new git tags 4 | tags: 5 | - "v*.*.*" # match v*.*.*, i.e. v0.1.5, v20.15.10 6 | 7 | jobs: 8 | build-linux: 9 | runs-on: ubuntu-20.04 10 | strategy: 11 | matrix: 12 | include: 13 | - { arch: "x86_64", libc: "musl" } 14 | - { arch: "i686", libc: "musl" } 15 | - { arch: "aarch64", libc: "musl" } 16 | - { arch: "armv7", libc: "musleabihf" } 17 | - { arch: "arm", libc: "musleabihf" } 18 | steps: 19 | - uses: actions/checkout@v3 20 | - name: Pull Docker image 21 | run: docker pull messense/rust-musl-cross:${{ matrix.arch }}-${{ matrix.libc }} 22 | - name: Build in Docker 23 | run: | 24 | docker run --rm -i \ 25 | -v "$(pwd)":/home/rust/src messense/rust-musl-cross:${{ matrix.arch }}-${{ matrix.libc }} \ 26 | cargo build --release 27 | - name: Strip binary 28 | run: | 29 | docker run --rm -i \ 30 | -v "$(pwd)":/home/rust/src messense/rust-musl-cross:${{ matrix.arch }}-${{ matrix.libc }} \ 31 | musl-strip -s /home/rust/src/target/${{ matrix.arch }}-unknown-linux-${{ matrix.libc }}/release/belt 32 | - name: Make package 33 | run: make package arch=${{ matrix.arch }} libc=${{ matrix.libc }} 34 | - uses: actions/upload-artifact@v3 35 | with: 36 | name: "linux-${{ matrix.arch }}-${{ matrix.libc }}" 37 | path: "target/package/*-*.*.*-${{ matrix.arch }}-unknown-linux-${{ matrix.libc }}.tar.gz" 38 | retention-days: 5 39 | 40 | build-macos: 41 | runs-on: macos-11 42 | steps: 43 | - uses: actions/checkout@v3 44 | - name: Install Rust 45 | uses: dtolnay/rust-toolchain@stable 46 | with: 47 | toolchain: stable 48 | - name: Build 49 | run: cargo build --release --target x86_64-apple-darwin 50 | - name: Make package 51 | run: make package 52 | - uses: actions/upload-artifact@v3 53 | with: 54 | name: macos-x86_64 55 | path: target/package/*-*.*.*-macos-x86_64.zip 56 | retention-days: 5 57 | 58 | build-windows: 59 | runs-on: windows-2022 60 | steps: 61 | - uses: actions/checkout@v3 62 | - name: Install Rust 63 | uses: dtolnay/rust-toolchain@stable 64 | with: 65 | toolchain: stable 66 | - name: Build 67 | run: cargo build --release --target x86_64-pc-windows-msvc 68 | - name: Make package 69 | run: make package 70 | - uses: actions/upload-artifact@v3 71 | with: 72 | name: windows-x86_64-msvc 73 | path: target/package/*-*-windows-x86_64-msvc.zip 74 | retention-days: 5 75 | 76 | release: 77 | needs: [ build-linux, build-macos, build-windows ] 78 | runs-on: ubuntu-latest 79 | steps: 80 | - uses: actions/checkout@v3 81 | - name: Download all artifacts 82 | uses: actions/download-artifact@v3 83 | - name: Move to files around 84 | run: | 85 | mkdir -p target/package 86 | [ -d macos-x86_64 ] && mv macos-x86_64/* target/package || true 87 | [ -d windows-x86_64-msvc ] && mv windows-x86_64-msvc/* target/package || true 88 | [ -d linux-x86_64-musl ] && mv linux-x86_64-musl/* target/package || true 89 | [ -d linux-i686-musl ] && mv linux-i686-musl/* target/package || true 90 | [ -d linux-aarch64-musl ] && mv linux-aarch64-musl/* target/package || true 91 | [ -d linux-armv7-musleabihf ] && mv linux-armv7-musleabihf/* target/package || true 92 | [ -d linux-arm-musleabihf ] && mv linux-arm-musleabihf/* target/package || true 93 | - name: Create checksum file 94 | run: shasum -a 256 target/package/*-*.*.*-*.{tar.gz,zip} > target/package/checksums.txt 95 | - name: List files 96 | run: ls -ahl target/package 97 | - name: Release 98 | uses: softprops/action-gh-release@v1 99 | if: startsWith(github.ref, 'refs/tags/') 100 | with: 101 | generate_release_notes: true 102 | fail_on_unmatched_files: true 103 | files: | 104 | target/package/*-*.*.*-aarch64-unknown-linux-musl.tar.gz 105 | target/package/*-*.*.*-arm-unknown-linux-musleabihf.tar.gz 106 | target/package/*-*.*.*-armv7-unknown-linux-musleabihf.tar.gz 107 | target/package/*-*.*.*-i686-unknown-linux-musl.tar.gz 108 | target/package/*-*.*.*-macos-x86_64.zip 109 | target/package/*-*.*.*-windows-x86_64-msvc.zip 110 | target/package/*-*.*.*-x86_64-unknown-linux-musl.tar.gz 111 | target/package/checksums.txt 112 | 113 | cargo-publish: 114 | needs: release 115 | runs-on: ubuntu-latest 116 | steps: 117 | - uses: actions/checkout@v3 118 | - name: Cargo publish 119 | if: startsWith(github.ref, 'refs/tags/') 120 | run: make publish token=${{ secrets.CARGO_REGISTRY_TOKEN }} 121 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: 3 | push: # run tests on every git push except pushing tags 4 | tags-ignore: 5 | - '**' 6 | pull_request: # run tests on every pull request 7 | 8 | jobs: 9 | check: 10 | name: check - ${{ matrix.platform.os_name }} with rust ${{ matrix.toolchain }} 11 | runs-on: ${{ matrix.platform.os }} 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | platform: 16 | - os_name: Linux 17 | os: ubuntu-latest 18 | - os_name: macOS 19 | os: macos-latest 20 | - os_name: Windows 21 | os: windows-latest 22 | toolchain: 23 | - stable 24 | - nightly 25 | steps: 26 | - uses: actions/checkout@v3 27 | - uses: dtolnay/rust-toolchain@stable 28 | with: 29 | toolchain: ${{ matrix.toolchain }} 30 | - run: cargo check --all --all-targets --all-features 31 | 32 | test: 33 | name: test - ${{ matrix.platform.os_name }} with rust ${{ matrix.toolchain }} 34 | runs-on: ${{ matrix.platform.os }} 35 | strategy: 36 | fail-fast: false 37 | matrix: 38 | platform: 39 | - os_name: Linux 40 | os: ubuntu-latest 41 | - os_name: macOS 42 | os: macos-latest 43 | - os_name: Windows 44 | os: windows-latest 45 | toolchain: 46 | - stable 47 | - nightly 48 | steps: 49 | - uses: actions/checkout@v3 50 | - uses: dtolnay/rust-toolchain@stable 51 | with: 52 | toolchain: ${{ matrix.toolchain }} 53 | - run: cargo test --workspace --all-features 54 | 55 | fmt: 56 | name: fmt 57 | runs-on: ubuntu-latest 58 | steps: 59 | - uses: actions/checkout@v3 60 | - uses: dtolnay/rust-toolchain@stable 61 | with: 62 | toolchain: stable 63 | - run: rustup component add rustfmt 64 | - run: cargo fmt --all -- --check 65 | 66 | clippy: 67 | name: clippy 68 | runs-on: ubuntu-latest 69 | steps: 70 | - uses: actions/checkout@v3 71 | - uses: dtolnay/rust-toolchain@stable 72 | with: 73 | toolchain: stable 74 | - run: rustup component add clippy 75 | - run: cargo clippy --workspace --tests --all-features -- -D warnings 76 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # These are backup files generated by rustfmt 6 | **/*.rs.bk 7 | 8 | 9 | # Added by cargo 10 | 11 | /target 12 | 13 | *.swp 14 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "1.1.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "android-tzdata" 16 | version = "0.1.1" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 19 | 20 | [[package]] 21 | name = "android_system_properties" 22 | version = "0.1.5" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 25 | dependencies = [ 26 | "libc", 27 | ] 28 | 29 | [[package]] 30 | name = "anes" 31 | version = "0.1.6" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" 34 | 35 | [[package]] 36 | name = "anstream" 37 | version = "0.6.4" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44" 40 | dependencies = [ 41 | "anstyle", 42 | "anstyle-parse", 43 | "anstyle-query", 44 | "anstyle-wincon", 45 | "colorchoice", 46 | "utf8parse", 47 | ] 48 | 49 | [[package]] 50 | name = "anstyle" 51 | version = "1.0.4" 52 | source = "registry+https://github.com/rust-lang/crates.io-index" 53 | checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" 54 | 55 | [[package]] 56 | name = "anstyle-parse" 57 | version = "0.2.2" 58 | source = "registry+https://github.com/rust-lang/crates.io-index" 59 | checksum = "317b9a89c1868f5ea6ff1d9539a69f45dffc21ce321ac1fd1160dfa48c8e2140" 60 | dependencies = [ 61 | "utf8parse", 62 | ] 63 | 64 | [[package]] 65 | name = "anstyle-query" 66 | version = "1.0.0" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" 69 | dependencies = [ 70 | "windows-sys", 71 | ] 72 | 73 | [[package]] 74 | name = "anstyle-wincon" 75 | version = "3.0.1" 76 | source = "registry+https://github.com/rust-lang/crates.io-index" 77 | checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" 78 | dependencies = [ 79 | "anstyle", 80 | "windows-sys", 81 | ] 82 | 83 | [[package]] 84 | name = "anyhow" 85 | version = "1.0.75" 86 | source = "registry+https://github.com/rust-lang/crates.io-index" 87 | checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" 88 | 89 | [[package]] 90 | name = "autocfg" 91 | version = "1.1.0" 92 | source = "registry+https://github.com/rust-lang/crates.io-index" 93 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 94 | 95 | [[package]] 96 | name = "belt" 97 | version = "0.2.1" 98 | dependencies = [ 99 | "anyhow", 100 | "chrono", 101 | "chrono-tz", 102 | "clap", 103 | "colored", 104 | "confy", 105 | "dateparser", 106 | "directories 5.0.1", 107 | "prettytable-rs", 108 | "rand", 109 | "regex", 110 | "serde", 111 | ] 112 | 113 | [[package]] 114 | name = "bitflags" 115 | version = "1.3.2" 116 | source = "registry+https://github.com/rust-lang/crates.io-index" 117 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 118 | 119 | [[package]] 120 | name = "bitflags" 121 | version = "2.4.1" 122 | source = "registry+https://github.com/rust-lang/crates.io-index" 123 | checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" 124 | 125 | [[package]] 126 | name = "bumpalo" 127 | version = "3.14.0" 128 | source = "registry+https://github.com/rust-lang/crates.io-index" 129 | checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" 130 | 131 | [[package]] 132 | name = "cast" 133 | version = "0.3.0" 134 | source = "registry+https://github.com/rust-lang/crates.io-index" 135 | checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" 136 | 137 | [[package]] 138 | name = "cc" 139 | version = "1.0.84" 140 | source = "registry+https://github.com/rust-lang/crates.io-index" 141 | checksum = "0f8e7c90afad890484a21653d08b6e209ae34770fb5ee298f9c699fcc1e5c856" 142 | dependencies = [ 143 | "libc", 144 | ] 145 | 146 | [[package]] 147 | name = "cfg-if" 148 | version = "1.0.0" 149 | source = "registry+https://github.com/rust-lang/crates.io-index" 150 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 151 | 152 | [[package]] 153 | name = "chrono" 154 | version = "0.4.31" 155 | source = "registry+https://github.com/rust-lang/crates.io-index" 156 | checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" 157 | dependencies = [ 158 | "android-tzdata", 159 | "iana-time-zone", 160 | "js-sys", 161 | "num-traits", 162 | "wasm-bindgen", 163 | "windows-targets", 164 | ] 165 | 166 | [[package]] 167 | name = "chrono-tz" 168 | version = "0.8.4" 169 | source = "registry+https://github.com/rust-lang/crates.io-index" 170 | checksum = "e23185c0e21df6ed832a12e2bda87c7d1def6842881fb634a8511ced741b0d76" 171 | dependencies = [ 172 | "chrono", 173 | "chrono-tz-build", 174 | "phf", 175 | ] 176 | 177 | [[package]] 178 | name = "chrono-tz-build" 179 | version = "0.2.1" 180 | source = "registry+https://github.com/rust-lang/crates.io-index" 181 | checksum = "433e39f13c9a060046954e0592a8d0a4bcb1040125cbf91cb8ee58964cfb350f" 182 | dependencies = [ 183 | "parse-zoneinfo", 184 | "phf", 185 | "phf_codegen", 186 | ] 187 | 188 | [[package]] 189 | name = "ciborium" 190 | version = "0.2.1" 191 | source = "registry+https://github.com/rust-lang/crates.io-index" 192 | checksum = "effd91f6c78e5a4ace8a5d3c0b6bfaec9e2baaef55f3efc00e45fb2e477ee926" 193 | dependencies = [ 194 | "ciborium-io", 195 | "ciborium-ll", 196 | "serde", 197 | ] 198 | 199 | [[package]] 200 | name = "ciborium-io" 201 | version = "0.2.1" 202 | source = "registry+https://github.com/rust-lang/crates.io-index" 203 | checksum = "cdf919175532b369853f5d5e20b26b43112613fd6fe7aee757e35f7a44642656" 204 | 205 | [[package]] 206 | name = "ciborium-ll" 207 | version = "0.2.1" 208 | source = "registry+https://github.com/rust-lang/crates.io-index" 209 | checksum = "defaa24ecc093c77630e6c15e17c51f5e187bf35ee514f4e2d67baaa96dae22b" 210 | dependencies = [ 211 | "ciborium-io", 212 | "half", 213 | ] 214 | 215 | [[package]] 216 | name = "clap" 217 | version = "4.4.8" 218 | source = "registry+https://github.com/rust-lang/crates.io-index" 219 | checksum = "2275f18819641850fa26c89acc84d465c1bf91ce57bc2748b28c420473352f64" 220 | dependencies = [ 221 | "clap_builder", 222 | "clap_derive", 223 | ] 224 | 225 | [[package]] 226 | name = "clap_builder" 227 | version = "4.4.8" 228 | source = "registry+https://github.com/rust-lang/crates.io-index" 229 | checksum = "07cdf1b148b25c1e1f7a42225e30a0d99a615cd4637eae7365548dd4529b95bc" 230 | dependencies = [ 231 | "anstream", 232 | "anstyle", 233 | "clap_lex", 234 | "strsim", 235 | ] 236 | 237 | [[package]] 238 | name = "clap_derive" 239 | version = "4.4.7" 240 | source = "registry+https://github.com/rust-lang/crates.io-index" 241 | checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" 242 | dependencies = [ 243 | "heck", 244 | "proc-macro2", 245 | "quote", 246 | "syn", 247 | ] 248 | 249 | [[package]] 250 | name = "clap_lex" 251 | version = "0.6.0" 252 | source = "registry+https://github.com/rust-lang/crates.io-index" 253 | checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" 254 | 255 | [[package]] 256 | name = "colorchoice" 257 | version = "1.0.0" 258 | source = "registry+https://github.com/rust-lang/crates.io-index" 259 | checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" 260 | 261 | [[package]] 262 | name = "colored" 263 | version = "2.0.4" 264 | source = "registry+https://github.com/rust-lang/crates.io-index" 265 | checksum = "2674ec482fbc38012cf31e6c42ba0177b431a0cb6f15fe40efa5aab1bda516f6" 266 | dependencies = [ 267 | "is-terminal", 268 | "lazy_static", 269 | "windows-sys", 270 | ] 271 | 272 | [[package]] 273 | name = "confy" 274 | version = "0.5.1" 275 | source = "registry+https://github.com/rust-lang/crates.io-index" 276 | checksum = "e37668cb35145dcfaa1931a5f37fde375eeae8068b4c0d2f289da28a270b2d2c" 277 | dependencies = [ 278 | "directories 4.0.1", 279 | "serde", 280 | "thiserror", 281 | "toml", 282 | ] 283 | 284 | [[package]] 285 | name = "core-foundation-sys" 286 | version = "0.8.4" 287 | source = "registry+https://github.com/rust-lang/crates.io-index" 288 | checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" 289 | 290 | [[package]] 291 | name = "criterion" 292 | version = "0.5.1" 293 | source = "registry+https://github.com/rust-lang/crates.io-index" 294 | checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" 295 | dependencies = [ 296 | "anes", 297 | "cast", 298 | "ciborium", 299 | "clap", 300 | "criterion-plot", 301 | "is-terminal", 302 | "itertools", 303 | "num-traits", 304 | "once_cell", 305 | "oorandom", 306 | "plotters", 307 | "rayon", 308 | "regex", 309 | "serde", 310 | "serde_derive", 311 | "serde_json", 312 | "tinytemplate", 313 | "walkdir", 314 | ] 315 | 316 | [[package]] 317 | name = "criterion-plot" 318 | version = "0.5.0" 319 | source = "registry+https://github.com/rust-lang/crates.io-index" 320 | checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" 321 | dependencies = [ 322 | "cast", 323 | "itertools", 324 | ] 325 | 326 | [[package]] 327 | name = "crossbeam-deque" 328 | version = "0.8.3" 329 | source = "registry+https://github.com/rust-lang/crates.io-index" 330 | checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" 331 | dependencies = [ 332 | "cfg-if", 333 | "crossbeam-epoch", 334 | "crossbeam-utils", 335 | ] 336 | 337 | [[package]] 338 | name = "crossbeam-epoch" 339 | version = "0.9.15" 340 | source = "registry+https://github.com/rust-lang/crates.io-index" 341 | checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" 342 | dependencies = [ 343 | "autocfg", 344 | "cfg-if", 345 | "crossbeam-utils", 346 | "memoffset", 347 | "scopeguard", 348 | ] 349 | 350 | [[package]] 351 | name = "crossbeam-utils" 352 | version = "0.8.16" 353 | source = "registry+https://github.com/rust-lang/crates.io-index" 354 | checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" 355 | dependencies = [ 356 | "cfg-if", 357 | ] 358 | 359 | [[package]] 360 | name = "csv" 361 | version = "1.3.0" 362 | source = "registry+https://github.com/rust-lang/crates.io-index" 363 | checksum = "ac574ff4d437a7b5ad237ef331c17ccca63c46479e5b5453eb8e10bb99a759fe" 364 | dependencies = [ 365 | "csv-core", 366 | "itoa", 367 | "ryu", 368 | "serde", 369 | ] 370 | 371 | [[package]] 372 | name = "csv-core" 373 | version = "0.1.11" 374 | source = "registry+https://github.com/rust-lang/crates.io-index" 375 | checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70" 376 | dependencies = [ 377 | "memchr", 378 | ] 379 | 380 | [[package]] 381 | name = "dateparser" 382 | version = "0.2.1" 383 | dependencies = [ 384 | "anyhow", 385 | "chrono", 386 | "chrono-tz", 387 | "criterion", 388 | "lazy_static", 389 | "regex", 390 | ] 391 | 392 | [[package]] 393 | name = "directories" 394 | version = "4.0.1" 395 | source = "registry+https://github.com/rust-lang/crates.io-index" 396 | checksum = "f51c5d4ddabd36886dd3e1438cb358cdcb0d7c499cb99cb4ac2e38e18b5cb210" 397 | dependencies = [ 398 | "dirs-sys 0.3.7", 399 | ] 400 | 401 | [[package]] 402 | name = "directories" 403 | version = "5.0.1" 404 | source = "registry+https://github.com/rust-lang/crates.io-index" 405 | checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35" 406 | dependencies = [ 407 | "dirs-sys 0.4.1", 408 | ] 409 | 410 | [[package]] 411 | name = "dirs-next" 412 | version = "2.0.0" 413 | source = "registry+https://github.com/rust-lang/crates.io-index" 414 | checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" 415 | dependencies = [ 416 | "cfg-if", 417 | "dirs-sys-next", 418 | ] 419 | 420 | [[package]] 421 | name = "dirs-sys" 422 | version = "0.3.7" 423 | source = "registry+https://github.com/rust-lang/crates.io-index" 424 | checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" 425 | dependencies = [ 426 | "libc", 427 | "redox_users", 428 | "winapi", 429 | ] 430 | 431 | [[package]] 432 | name = "dirs-sys" 433 | version = "0.4.1" 434 | source = "registry+https://github.com/rust-lang/crates.io-index" 435 | checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" 436 | dependencies = [ 437 | "libc", 438 | "option-ext", 439 | "redox_users", 440 | "windows-sys", 441 | ] 442 | 443 | [[package]] 444 | name = "dirs-sys-next" 445 | version = "0.1.2" 446 | source = "registry+https://github.com/rust-lang/crates.io-index" 447 | checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" 448 | dependencies = [ 449 | "libc", 450 | "redox_users", 451 | "winapi", 452 | ] 453 | 454 | [[package]] 455 | name = "either" 456 | version = "1.9.0" 457 | source = "registry+https://github.com/rust-lang/crates.io-index" 458 | checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" 459 | 460 | [[package]] 461 | name = "encode_unicode" 462 | version = "1.0.0" 463 | source = "registry+https://github.com/rust-lang/crates.io-index" 464 | checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" 465 | 466 | [[package]] 467 | name = "errno" 468 | version = "0.3.6" 469 | source = "registry+https://github.com/rust-lang/crates.io-index" 470 | checksum = "7c18ee0ed65a5f1f81cac6b1d213b69c35fa47d4252ad41f1486dbd8226fe36e" 471 | dependencies = [ 472 | "libc", 473 | "windows-sys", 474 | ] 475 | 476 | [[package]] 477 | name = "getrandom" 478 | version = "0.2.11" 479 | source = "registry+https://github.com/rust-lang/crates.io-index" 480 | checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" 481 | dependencies = [ 482 | "cfg-if", 483 | "libc", 484 | "wasi", 485 | ] 486 | 487 | [[package]] 488 | name = "half" 489 | version = "1.8.2" 490 | source = "registry+https://github.com/rust-lang/crates.io-index" 491 | checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" 492 | 493 | [[package]] 494 | name = "heck" 495 | version = "0.4.1" 496 | source = "registry+https://github.com/rust-lang/crates.io-index" 497 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 498 | 499 | [[package]] 500 | name = "hermit-abi" 501 | version = "0.3.3" 502 | source = "registry+https://github.com/rust-lang/crates.io-index" 503 | checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" 504 | 505 | [[package]] 506 | name = "iana-time-zone" 507 | version = "0.1.58" 508 | source = "registry+https://github.com/rust-lang/crates.io-index" 509 | checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" 510 | dependencies = [ 511 | "android_system_properties", 512 | "core-foundation-sys", 513 | "iana-time-zone-haiku", 514 | "js-sys", 515 | "wasm-bindgen", 516 | "windows-core", 517 | ] 518 | 519 | [[package]] 520 | name = "iana-time-zone-haiku" 521 | version = "0.1.2" 522 | source = "registry+https://github.com/rust-lang/crates.io-index" 523 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 524 | dependencies = [ 525 | "cc", 526 | ] 527 | 528 | [[package]] 529 | name = "is-terminal" 530 | version = "0.4.9" 531 | source = "registry+https://github.com/rust-lang/crates.io-index" 532 | checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" 533 | dependencies = [ 534 | "hermit-abi", 535 | "rustix", 536 | "windows-sys", 537 | ] 538 | 539 | [[package]] 540 | name = "itertools" 541 | version = "0.10.5" 542 | source = "registry+https://github.com/rust-lang/crates.io-index" 543 | checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" 544 | dependencies = [ 545 | "either", 546 | ] 547 | 548 | [[package]] 549 | name = "itoa" 550 | version = "1.0.9" 551 | source = "registry+https://github.com/rust-lang/crates.io-index" 552 | checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" 553 | 554 | [[package]] 555 | name = "js-sys" 556 | version = "0.3.65" 557 | source = "registry+https://github.com/rust-lang/crates.io-index" 558 | checksum = "54c0c35952f67de54bb584e9fd912b3023117cbafc0a77d8f3dee1fb5f572fe8" 559 | dependencies = [ 560 | "wasm-bindgen", 561 | ] 562 | 563 | [[package]] 564 | name = "lazy_static" 565 | version = "1.4.0" 566 | source = "registry+https://github.com/rust-lang/crates.io-index" 567 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 568 | 569 | [[package]] 570 | name = "libc" 571 | version = "0.2.150" 572 | source = "registry+https://github.com/rust-lang/crates.io-index" 573 | checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" 574 | 575 | [[package]] 576 | name = "libredox" 577 | version = "0.0.1" 578 | source = "registry+https://github.com/rust-lang/crates.io-index" 579 | checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" 580 | dependencies = [ 581 | "bitflags 2.4.1", 582 | "libc", 583 | "redox_syscall", 584 | ] 585 | 586 | [[package]] 587 | name = "linux-raw-sys" 588 | version = "0.4.11" 589 | source = "registry+https://github.com/rust-lang/crates.io-index" 590 | checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829" 591 | 592 | [[package]] 593 | name = "log" 594 | version = "0.4.20" 595 | source = "registry+https://github.com/rust-lang/crates.io-index" 596 | checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" 597 | 598 | [[package]] 599 | name = "memchr" 600 | version = "2.6.4" 601 | source = "registry+https://github.com/rust-lang/crates.io-index" 602 | checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" 603 | 604 | [[package]] 605 | name = "memoffset" 606 | version = "0.9.0" 607 | source = "registry+https://github.com/rust-lang/crates.io-index" 608 | checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" 609 | dependencies = [ 610 | "autocfg", 611 | ] 612 | 613 | [[package]] 614 | name = "num-traits" 615 | version = "0.2.17" 616 | source = "registry+https://github.com/rust-lang/crates.io-index" 617 | checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" 618 | dependencies = [ 619 | "autocfg", 620 | ] 621 | 622 | [[package]] 623 | name = "once_cell" 624 | version = "1.18.0" 625 | source = "registry+https://github.com/rust-lang/crates.io-index" 626 | checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" 627 | 628 | [[package]] 629 | name = "oorandom" 630 | version = "11.1.3" 631 | source = "registry+https://github.com/rust-lang/crates.io-index" 632 | checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" 633 | 634 | [[package]] 635 | name = "option-ext" 636 | version = "0.2.0" 637 | source = "registry+https://github.com/rust-lang/crates.io-index" 638 | checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" 639 | 640 | [[package]] 641 | name = "parse-zoneinfo" 642 | version = "0.3.0" 643 | source = "registry+https://github.com/rust-lang/crates.io-index" 644 | checksum = "c705f256449c60da65e11ff6626e0c16a0a0b96aaa348de61376b249bc340f41" 645 | dependencies = [ 646 | "regex", 647 | ] 648 | 649 | [[package]] 650 | name = "phf" 651 | version = "0.11.2" 652 | source = "registry+https://github.com/rust-lang/crates.io-index" 653 | checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" 654 | dependencies = [ 655 | "phf_shared", 656 | ] 657 | 658 | [[package]] 659 | name = "phf_codegen" 660 | version = "0.11.2" 661 | source = "registry+https://github.com/rust-lang/crates.io-index" 662 | checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a" 663 | dependencies = [ 664 | "phf_generator", 665 | "phf_shared", 666 | ] 667 | 668 | [[package]] 669 | name = "phf_generator" 670 | version = "0.11.2" 671 | source = "registry+https://github.com/rust-lang/crates.io-index" 672 | checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" 673 | dependencies = [ 674 | "phf_shared", 675 | "rand", 676 | ] 677 | 678 | [[package]] 679 | name = "phf_shared" 680 | version = "0.11.2" 681 | source = "registry+https://github.com/rust-lang/crates.io-index" 682 | checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" 683 | dependencies = [ 684 | "siphasher", 685 | ] 686 | 687 | [[package]] 688 | name = "plotters" 689 | version = "0.3.5" 690 | source = "registry+https://github.com/rust-lang/crates.io-index" 691 | checksum = "d2c224ba00d7cadd4d5c660deaf2098e5e80e07846537c51f9cfa4be50c1fd45" 692 | dependencies = [ 693 | "num-traits", 694 | "plotters-backend", 695 | "plotters-svg", 696 | "wasm-bindgen", 697 | "web-sys", 698 | ] 699 | 700 | [[package]] 701 | name = "plotters-backend" 702 | version = "0.3.5" 703 | source = "registry+https://github.com/rust-lang/crates.io-index" 704 | checksum = "9e76628b4d3a7581389a35d5b6e2139607ad7c75b17aed325f210aa91f4a9609" 705 | 706 | [[package]] 707 | name = "plotters-svg" 708 | version = "0.3.5" 709 | source = "registry+https://github.com/rust-lang/crates.io-index" 710 | checksum = "38f6d39893cca0701371e3c27294f09797214b86f1fb951b89ade8ec04e2abab" 711 | dependencies = [ 712 | "plotters-backend", 713 | ] 714 | 715 | [[package]] 716 | name = "ppv-lite86" 717 | version = "0.2.17" 718 | source = "registry+https://github.com/rust-lang/crates.io-index" 719 | checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" 720 | 721 | [[package]] 722 | name = "prettytable-rs" 723 | version = "0.10.0" 724 | source = "registry+https://github.com/rust-lang/crates.io-index" 725 | checksum = "eea25e07510aa6ab6547308ebe3c036016d162b8da920dbb079e3ba8acf3d95a" 726 | dependencies = [ 727 | "csv", 728 | "encode_unicode", 729 | "is-terminal", 730 | "lazy_static", 731 | "term", 732 | "unicode-width", 733 | ] 734 | 735 | [[package]] 736 | name = "proc-macro2" 737 | version = "1.0.69" 738 | source = "registry+https://github.com/rust-lang/crates.io-index" 739 | checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" 740 | dependencies = [ 741 | "unicode-ident", 742 | ] 743 | 744 | [[package]] 745 | name = "quote" 746 | version = "1.0.33" 747 | source = "registry+https://github.com/rust-lang/crates.io-index" 748 | checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" 749 | dependencies = [ 750 | "proc-macro2", 751 | ] 752 | 753 | [[package]] 754 | name = "rand" 755 | version = "0.8.5" 756 | source = "registry+https://github.com/rust-lang/crates.io-index" 757 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 758 | dependencies = [ 759 | "libc", 760 | "rand_chacha", 761 | "rand_core", 762 | ] 763 | 764 | [[package]] 765 | name = "rand_chacha" 766 | version = "0.3.1" 767 | source = "registry+https://github.com/rust-lang/crates.io-index" 768 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 769 | dependencies = [ 770 | "ppv-lite86", 771 | "rand_core", 772 | ] 773 | 774 | [[package]] 775 | name = "rand_core" 776 | version = "0.6.4" 777 | source = "registry+https://github.com/rust-lang/crates.io-index" 778 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 779 | dependencies = [ 780 | "getrandom", 781 | ] 782 | 783 | [[package]] 784 | name = "rayon" 785 | version = "1.8.0" 786 | source = "registry+https://github.com/rust-lang/crates.io-index" 787 | checksum = "9c27db03db7734835b3f53954b534c91069375ce6ccaa2e065441e07d9b6cdb1" 788 | dependencies = [ 789 | "either", 790 | "rayon-core", 791 | ] 792 | 793 | [[package]] 794 | name = "rayon-core" 795 | version = "1.12.0" 796 | source = "registry+https://github.com/rust-lang/crates.io-index" 797 | checksum = "5ce3fb6ad83f861aac485e76e1985cd109d9a3713802152be56c3b1f0e0658ed" 798 | dependencies = [ 799 | "crossbeam-deque", 800 | "crossbeam-utils", 801 | ] 802 | 803 | [[package]] 804 | name = "redox_syscall" 805 | version = "0.4.1" 806 | source = "registry+https://github.com/rust-lang/crates.io-index" 807 | checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" 808 | dependencies = [ 809 | "bitflags 1.3.2", 810 | ] 811 | 812 | [[package]] 813 | name = "redox_users" 814 | version = "0.4.4" 815 | source = "registry+https://github.com/rust-lang/crates.io-index" 816 | checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" 817 | dependencies = [ 818 | "getrandom", 819 | "libredox", 820 | "thiserror", 821 | ] 822 | 823 | [[package]] 824 | name = "regex" 825 | version = "1.10.2" 826 | source = "registry+https://github.com/rust-lang/crates.io-index" 827 | checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" 828 | dependencies = [ 829 | "aho-corasick", 830 | "memchr", 831 | "regex-automata", 832 | "regex-syntax", 833 | ] 834 | 835 | [[package]] 836 | name = "regex-automata" 837 | version = "0.4.3" 838 | source = "registry+https://github.com/rust-lang/crates.io-index" 839 | checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" 840 | dependencies = [ 841 | "aho-corasick", 842 | "memchr", 843 | "regex-syntax", 844 | ] 845 | 846 | [[package]] 847 | name = "regex-syntax" 848 | version = "0.8.2" 849 | source = "registry+https://github.com/rust-lang/crates.io-index" 850 | checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" 851 | 852 | [[package]] 853 | name = "rustix" 854 | version = "0.38.21" 855 | source = "registry+https://github.com/rust-lang/crates.io-index" 856 | checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3" 857 | dependencies = [ 858 | "bitflags 2.4.1", 859 | "errno", 860 | "libc", 861 | "linux-raw-sys", 862 | "windows-sys", 863 | ] 864 | 865 | [[package]] 866 | name = "rustversion" 867 | version = "1.0.14" 868 | source = "registry+https://github.com/rust-lang/crates.io-index" 869 | checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" 870 | 871 | [[package]] 872 | name = "ryu" 873 | version = "1.0.15" 874 | source = "registry+https://github.com/rust-lang/crates.io-index" 875 | checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" 876 | 877 | [[package]] 878 | name = "same-file" 879 | version = "1.0.6" 880 | source = "registry+https://github.com/rust-lang/crates.io-index" 881 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 882 | dependencies = [ 883 | "winapi-util", 884 | ] 885 | 886 | [[package]] 887 | name = "scopeguard" 888 | version = "1.2.0" 889 | source = "registry+https://github.com/rust-lang/crates.io-index" 890 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 891 | 892 | [[package]] 893 | name = "serde" 894 | version = "1.0.192" 895 | source = "registry+https://github.com/rust-lang/crates.io-index" 896 | checksum = "bca2a08484b285dcb282d0f67b26cadc0df8b19f8c12502c13d966bf9482f001" 897 | dependencies = [ 898 | "serde_derive", 899 | ] 900 | 901 | [[package]] 902 | name = "serde_derive" 903 | version = "1.0.192" 904 | source = "registry+https://github.com/rust-lang/crates.io-index" 905 | checksum = "d6c7207fbec9faa48073f3e3074cbe553af6ea512d7c21ba46e434e70ea9fbc1" 906 | dependencies = [ 907 | "proc-macro2", 908 | "quote", 909 | "syn", 910 | ] 911 | 912 | [[package]] 913 | name = "serde_json" 914 | version = "1.0.108" 915 | source = "registry+https://github.com/rust-lang/crates.io-index" 916 | checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" 917 | dependencies = [ 918 | "itoa", 919 | "ryu", 920 | "serde", 921 | ] 922 | 923 | [[package]] 924 | name = "siphasher" 925 | version = "0.3.11" 926 | source = "registry+https://github.com/rust-lang/crates.io-index" 927 | checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" 928 | 929 | [[package]] 930 | name = "strsim" 931 | version = "0.10.0" 932 | source = "registry+https://github.com/rust-lang/crates.io-index" 933 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 934 | 935 | [[package]] 936 | name = "syn" 937 | version = "2.0.39" 938 | source = "registry+https://github.com/rust-lang/crates.io-index" 939 | checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" 940 | dependencies = [ 941 | "proc-macro2", 942 | "quote", 943 | "unicode-ident", 944 | ] 945 | 946 | [[package]] 947 | name = "term" 948 | version = "0.7.0" 949 | source = "registry+https://github.com/rust-lang/crates.io-index" 950 | checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" 951 | dependencies = [ 952 | "dirs-next", 953 | "rustversion", 954 | "winapi", 955 | ] 956 | 957 | [[package]] 958 | name = "thiserror" 959 | version = "1.0.50" 960 | source = "registry+https://github.com/rust-lang/crates.io-index" 961 | checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" 962 | dependencies = [ 963 | "thiserror-impl", 964 | ] 965 | 966 | [[package]] 967 | name = "thiserror-impl" 968 | version = "1.0.50" 969 | source = "registry+https://github.com/rust-lang/crates.io-index" 970 | checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" 971 | dependencies = [ 972 | "proc-macro2", 973 | "quote", 974 | "syn", 975 | ] 976 | 977 | [[package]] 978 | name = "tinytemplate" 979 | version = "1.2.1" 980 | source = "registry+https://github.com/rust-lang/crates.io-index" 981 | checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" 982 | dependencies = [ 983 | "serde", 984 | "serde_json", 985 | ] 986 | 987 | [[package]] 988 | name = "toml" 989 | version = "0.5.11" 990 | source = "registry+https://github.com/rust-lang/crates.io-index" 991 | checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" 992 | dependencies = [ 993 | "serde", 994 | ] 995 | 996 | [[package]] 997 | name = "unicode-ident" 998 | version = "1.0.12" 999 | source = "registry+https://github.com/rust-lang/crates.io-index" 1000 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 1001 | 1002 | [[package]] 1003 | name = "unicode-width" 1004 | version = "0.1.11" 1005 | source = "registry+https://github.com/rust-lang/crates.io-index" 1006 | checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" 1007 | 1008 | [[package]] 1009 | name = "utf8parse" 1010 | version = "0.2.1" 1011 | source = "registry+https://github.com/rust-lang/crates.io-index" 1012 | checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" 1013 | 1014 | [[package]] 1015 | name = "walkdir" 1016 | version = "2.4.0" 1017 | source = "registry+https://github.com/rust-lang/crates.io-index" 1018 | checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" 1019 | dependencies = [ 1020 | "same-file", 1021 | "winapi-util", 1022 | ] 1023 | 1024 | [[package]] 1025 | name = "wasi" 1026 | version = "0.11.0+wasi-snapshot-preview1" 1027 | source = "registry+https://github.com/rust-lang/crates.io-index" 1028 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1029 | 1030 | [[package]] 1031 | name = "wasm-bindgen" 1032 | version = "0.2.88" 1033 | source = "registry+https://github.com/rust-lang/crates.io-index" 1034 | checksum = "7daec296f25a1bae309c0cd5c29c4b260e510e6d813c286b19eaadf409d40fce" 1035 | dependencies = [ 1036 | "cfg-if", 1037 | "wasm-bindgen-macro", 1038 | ] 1039 | 1040 | [[package]] 1041 | name = "wasm-bindgen-backend" 1042 | version = "0.2.88" 1043 | source = "registry+https://github.com/rust-lang/crates.io-index" 1044 | checksum = "e397f4664c0e4e428e8313a469aaa58310d302159845980fd23b0f22a847f217" 1045 | dependencies = [ 1046 | "bumpalo", 1047 | "log", 1048 | "once_cell", 1049 | "proc-macro2", 1050 | "quote", 1051 | "syn", 1052 | "wasm-bindgen-shared", 1053 | ] 1054 | 1055 | [[package]] 1056 | name = "wasm-bindgen-macro" 1057 | version = "0.2.88" 1058 | source = "registry+https://github.com/rust-lang/crates.io-index" 1059 | checksum = "5961017b3b08ad5f3fe39f1e79877f8ee7c23c5e5fd5eb80de95abc41f1f16b2" 1060 | dependencies = [ 1061 | "quote", 1062 | "wasm-bindgen-macro-support", 1063 | ] 1064 | 1065 | [[package]] 1066 | name = "wasm-bindgen-macro-support" 1067 | version = "0.2.88" 1068 | source = "registry+https://github.com/rust-lang/crates.io-index" 1069 | checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907" 1070 | dependencies = [ 1071 | "proc-macro2", 1072 | "quote", 1073 | "syn", 1074 | "wasm-bindgen-backend", 1075 | "wasm-bindgen-shared", 1076 | ] 1077 | 1078 | [[package]] 1079 | name = "wasm-bindgen-shared" 1080 | version = "0.2.88" 1081 | source = "registry+https://github.com/rust-lang/crates.io-index" 1082 | checksum = "0d046c5d029ba91a1ed14da14dca44b68bf2f124cfbaf741c54151fdb3e0750b" 1083 | 1084 | [[package]] 1085 | name = "web-sys" 1086 | version = "0.3.65" 1087 | source = "registry+https://github.com/rust-lang/crates.io-index" 1088 | checksum = "5db499c5f66323272151db0e666cd34f78617522fb0c1604d31a27c50c206a85" 1089 | dependencies = [ 1090 | "js-sys", 1091 | "wasm-bindgen", 1092 | ] 1093 | 1094 | [[package]] 1095 | name = "winapi" 1096 | version = "0.3.9" 1097 | source = "registry+https://github.com/rust-lang/crates.io-index" 1098 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1099 | dependencies = [ 1100 | "winapi-i686-pc-windows-gnu", 1101 | "winapi-x86_64-pc-windows-gnu", 1102 | ] 1103 | 1104 | [[package]] 1105 | name = "winapi-i686-pc-windows-gnu" 1106 | version = "0.4.0" 1107 | source = "registry+https://github.com/rust-lang/crates.io-index" 1108 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1109 | 1110 | [[package]] 1111 | name = "winapi-util" 1112 | version = "0.1.6" 1113 | source = "registry+https://github.com/rust-lang/crates.io-index" 1114 | checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" 1115 | dependencies = [ 1116 | "winapi", 1117 | ] 1118 | 1119 | [[package]] 1120 | name = "winapi-x86_64-pc-windows-gnu" 1121 | version = "0.4.0" 1122 | source = "registry+https://github.com/rust-lang/crates.io-index" 1123 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1124 | 1125 | [[package]] 1126 | name = "windows-core" 1127 | version = "0.51.1" 1128 | source = "registry+https://github.com/rust-lang/crates.io-index" 1129 | checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" 1130 | dependencies = [ 1131 | "windows-targets", 1132 | ] 1133 | 1134 | [[package]] 1135 | name = "windows-sys" 1136 | version = "0.48.0" 1137 | source = "registry+https://github.com/rust-lang/crates.io-index" 1138 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 1139 | dependencies = [ 1140 | "windows-targets", 1141 | ] 1142 | 1143 | [[package]] 1144 | name = "windows-targets" 1145 | version = "0.48.5" 1146 | source = "registry+https://github.com/rust-lang/crates.io-index" 1147 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 1148 | dependencies = [ 1149 | "windows_aarch64_gnullvm", 1150 | "windows_aarch64_msvc", 1151 | "windows_i686_gnu", 1152 | "windows_i686_msvc", 1153 | "windows_x86_64_gnu", 1154 | "windows_x86_64_gnullvm", 1155 | "windows_x86_64_msvc", 1156 | ] 1157 | 1158 | [[package]] 1159 | name = "windows_aarch64_gnullvm" 1160 | version = "0.48.5" 1161 | source = "registry+https://github.com/rust-lang/crates.io-index" 1162 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 1163 | 1164 | [[package]] 1165 | name = "windows_aarch64_msvc" 1166 | version = "0.48.5" 1167 | source = "registry+https://github.com/rust-lang/crates.io-index" 1168 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 1169 | 1170 | [[package]] 1171 | name = "windows_i686_gnu" 1172 | version = "0.48.5" 1173 | source = "registry+https://github.com/rust-lang/crates.io-index" 1174 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 1175 | 1176 | [[package]] 1177 | name = "windows_i686_msvc" 1178 | version = "0.48.5" 1179 | source = "registry+https://github.com/rust-lang/crates.io-index" 1180 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 1181 | 1182 | [[package]] 1183 | name = "windows_x86_64_gnu" 1184 | version = "0.48.5" 1185 | source = "registry+https://github.com/rust-lang/crates.io-index" 1186 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 1187 | 1188 | [[package]] 1189 | name = "windows_x86_64_gnullvm" 1190 | version = "0.48.5" 1191 | source = "registry+https://github.com/rust-lang/crates.io-index" 1192 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 1193 | 1194 | [[package]] 1195 | name = "windows_x86_64_msvc" 1196 | version = "0.48.5" 1197 | source = "registry+https://github.com/rust-lang/crates.io-index" 1198 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 1199 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = [ 4 | "belt", 5 | "dateparser", 6 | ] 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Rollie Ma 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: help 2 | help: 3 | @grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 4 | 5 | .PHONY: build 6 | build: ## Build release with cargo for the current OS 7 | cargo build --release 8 | 9 | .PHONY: lint 10 | lint: ## Run clippy linter 11 | cargo clippy --workspace --tests --all-features -- -D warnings 12 | 13 | .PHONY: test 14 | test: ## Run unit tests 15 | RUST_BACKTRACE=1 cargo test 16 | 17 | .PHONY: cover 18 | cover: ## Generate test coverage report 19 | docker run \ 20 | --security-opt seccomp=unconfined \ 21 | -v ${PWD}:/volume \ 22 | -e "RUST_BACKTRACE=1" \ 23 | xd009642/tarpaulin \ 24 | cargo tarpaulin --color auto --out Html --output-dir ./target 25 | open target/tarpaulin-report.html 26 | 27 | .PHONY: bench 28 | bench: ## Generate benchmark report 29 | cargo bench --bench parse -- --verbose 30 | open target/criterion/report/index.html 31 | 32 | APP = belt 33 | VERSION := $(shell cargo metadata -q | jq -r '.packages[] | select(.name == "$(APP)") | .version') 34 | UNAME_S := $(shell uname -s) 35 | NEXT_VERSION := $(shell echo "$(VERSION)" | awk -F. -v OFS=. '{$$NF += 1 ; print}') 36 | 37 | .PHONY: package 38 | package: ## Make release package based on the current OS 39 | ifdef OS # windows 40 | mkdir -p target/package 41 | tar -a -cvf target/package/$(APP)-$(VERSION)-windows-x86_64-msvc.zip \ 42 | -C $$PWD/target/x86_64-pc-windows-msvc/release $(APP).exe \ 43 | -C $$PWD LICENSE README.md 44 | else ifeq ($(UNAME_S),Darwin) # macOS 45 | mkdir -p target/package 46 | zip -j target/package/$(APP)-$(VERSION)-macos-x86_64.zip \ 47 | target/x86_64-apple-darwin/release/$(APP) LICENSE README.md 48 | else ifeq ($(UNAME_S),Linux) # linux 49 | sudo mkdir -p target/package 50 | sudo tar -z -cvf target/package/$(APP)-$(VERSION)-$(arch)-unknown-linux-$(libc).tar.gz \ 51 | -C $$PWD/target/$(arch)-unknown-linux-$(libc)/release $(APP) \ 52 | -C $$PWD LICENSE README.md 53 | endif 54 | 55 | .PHONY: show-version-files 56 | show-version-files: ## Find all files with the current version 57 | @grep -rn --color \ 58 | --exclude-dir={target,.git} \ 59 | --exclude Cargo.lock \ 60 | --fixed-strings '"$(VERSION)"' . 61 | 62 | .PHONY: bump-version 63 | bump-version: ## Bump version in files that contain the current version 64 | @echo "👉 Bumping version $(VERSION) -> $(NEXT_VERSION)..." 65 | @echo 66 | @echo "🚀 Create a git branch for the version bump:" 67 | git checkout -b bump-version-$(NEXT_VERSION) 68 | @echo 69 | @echo "🚀 Update version in files:" 70 | @for file in $(shell grep -rl --exclude-dir={target,.git} --exclude Cargo.lock --fixed-strings '"$(VERSION)"' .); do \ 71 | echo "✅ In file $$file"; \ 72 | sed -i '' -e 's/$(subst .,\.,$(VERSION))/$(NEXT_VERSION)/g' $$file; \ 73 | git add $$file; \ 74 | done 75 | @echo 76 | @echo "🚀 Update Cargo.lock:" 77 | cargo update 78 | git add Cargo.lock 79 | @echo 80 | @echo "🚀 Bumped version in the following files:" 81 | @make show-version-files 82 | @echo 83 | @echo "🚀 Commit the changes:" 84 | git commit -m "version: bump to $(NEXT_VERSION)" 85 | @echo "🚀 Push the branch to GitHub:" 86 | git push --set-upstream origin bump-version-$(NEXT_VERSION) 87 | @echo 88 | @echo "🎉 Next, create a pull request on GitHub and merge it." 89 | 90 | .PHONY: release 91 | release: ## Make a new tag based on the version from Cargo.toml and push to GitHub 92 | @if [[ "$(shell git tag -l)" == *"v$(VERSION)"* ]]; then \ 93 | echo "Tag v$(VERSION) already exists"; \ 94 | else \ 95 | echo "Tagging v$(VERSION) and pushing to GitHub..."; \ 96 | git tag -a v$(VERSION) -m "Release v$(VERSION)"; \ 97 | git push origin v$(VERSION); \ 98 | fi 99 | 100 | .PHONY: publish 101 | publish: ## Publish to crates.io 102 | cargo publish --manifest-path dateparser/Cargo.toml --token $(token) 103 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [dateparser](https://crates.io/crates/dateparser) 2 | 3 | [![Build Status][actions-badge]][actions-url] 4 | [![MIT licensed][mit-badge]][mit-url] 5 | [![Crates.io][cratesio-badge]][cratesio-url] 6 | [![Doc.rs][docrs-badge]][docrs-url] 7 | 8 | [actions-badge]: https://github.com/waltzofpearls/dateparser/workflows/ci/badge.svg 9 | [actions-url]: https://github.com/waltzofpearls/dateparser/actions?query=workflow%3Aci+branch%3Amain 10 | [mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg 11 | [mit-url]: https://github.com/waltzofpearls/dateparser/blob/main/LICENSE 12 | [cratesio-badge]: https://img.shields.io/crates/v/dateparser.svg 13 | [cratesio-url]: https://crates.io/crates/dateparser 14 | [docrs-badge]: https://docs.rs/dateparser/badge.svg 15 | [docrs-url]: https://docs.rs/crate/dateparser/ 16 | 17 | Parse dates in commonly used string formats with Rust. 18 | 19 | This repo contains 2 cargo workspaces: 20 | 21 | - [dateparser](./dateparser): Rust crate for parsing date strings in commonly used formats. 22 | - [belt](./belt): Command-line tool that can display a given time in a list of selected time zones. 23 | It also serves as an example showcasing how you could use dateparser in your project. 24 | 25 | ## [`dateparser`](./dateparser) crate 26 | 27 | ```rust 28 | use dateparser::parse; 29 | use std::error::Error; 30 | 31 | fn main() -> Result<(), Box> { 32 | let parsed = parse("6:15pm")?; 33 | println!("{:#?}", parsed); 34 | Ok(()) 35 | } 36 | ``` 37 | 38 | Will parse the input `6:15pm` and print parsed date and time in UTC time zone as `2023-03-26T01:15:00Z`. 39 | More about this crate on [Docs.rs][1] and in [examples][2] folder 40 | 41 | [1]: https://docs.rs/dateparser/latest/dateparser 42 | [2]: ./dateparser/examples 43 | 44 | #### Accepted date formats 45 | 46 | ```rust 47 | // unix timestamp 48 | "1511648546", 49 | "1620021848429", 50 | "1620024872717915000", 51 | // rfc3339 52 | "2021-05-01T01:17:02.604456Z", 53 | "2017-11-25T22:34:50Z", 54 | // rfc2822 55 | "Wed, 02 Jun 2021 06:31:39 GMT", 56 | // postgres timestamp yyyy-mm-dd hh:mm:ss z 57 | "2019-11-29 08:08-08", 58 | "2019-11-29 08:08:05-08", 59 | "2021-05-02 23:31:36.0741-07", 60 | "2021-05-02 23:31:39.12689-07", 61 | "2019-11-29 08:15:47.624504-08", 62 | "2017-07-19 03:21:51+00:00", 63 | // yyyy-mm-dd hh:mm:ss 64 | "2014-04-26 05:24:37 PM", 65 | "2021-04-30 21:14", 66 | "2021-04-30 21:14:10", 67 | "2021-04-30 21:14:10.052282", 68 | "2014-04-26 17:24:37.123", 69 | "2014-04-26 17:24:37.3186369", 70 | "2012-08-03 18:31:59.257000000", 71 | // yyyy-mm-dd hh:mm:ss z 72 | "2017-11-25 13:31:15 PST", 73 | "2017-11-25 13:31 PST", 74 | "2014-12-16 06:20:00 UTC", 75 | "2014-12-16 06:20:00 GMT", 76 | "2014-04-26 13:13:43 +0800", 77 | "2014-04-26 13:13:44 +09:00", 78 | "2012-08-03 18:31:59.257000000 +0000", 79 | "2015-09-30 18:48:56.35272715 UTC", 80 | // yyyy-mm-dd 81 | "2021-02-21", 82 | // yyyy-mm-dd z 83 | "2021-02-21 PST", 84 | "2021-02-21 UTC", 85 | "2020-07-20+08:00", 86 | // hh:mm:ss 87 | "01:06:06", 88 | "4:00pm", 89 | "6:00 AM", 90 | // hh:mm:ss z 91 | "01:06:06 PST", 92 | "4:00pm PST", 93 | "6:00 AM PST", 94 | "6:00pm UTC", 95 | // Mon dd hh:mm:ss 96 | "May 6 at 9:24 PM", 97 | "May 27 02:45:27", 98 | // Mon dd, yyyy, hh:mm:ss 99 | "May 8, 2009 5:57:51 PM", 100 | "September 17, 2012 10:09am", 101 | "September 17, 2012, 10:10:09", 102 | // Mon dd, yyyy hh:mm:ss z 103 | "May 02, 2021 15:51:31 UTC", 104 | "May 02, 2021 15:51 UTC", 105 | "May 26, 2021, 12:49 AM PDT", 106 | "September 17, 2012 at 10:09am PST", 107 | // yyyy-mon-dd 108 | "2021-Feb-21", 109 | // Mon dd, yyyy 110 | "May 25, 2021", 111 | "oct 7, 1970", 112 | "oct 7, 70", 113 | "oct. 7, 1970", 114 | "oct. 7, 70", 115 | "October 7, 1970", 116 | // dd Mon yyyy hh:mm:ss 117 | "12 Feb 2006, 19:17", 118 | "12 Feb 2006 19:17", 119 | "14 May 2019 19:11:40.164", 120 | // dd Mon yyyy 121 | "7 oct 70", 122 | "7 oct 1970", 123 | "03 February 2013", 124 | "1 July 2013", 125 | // mm/dd/yyyy hh:mm:ss 126 | "4/8/2014 22:05", 127 | "04/08/2014 22:05", 128 | "4/8/14 22:05", 129 | "04/2/2014 03:00:51", 130 | "8/8/1965 12:00:00 AM", 131 | "8/8/1965 01:00:01 PM", 132 | "8/8/1965 01:00 PM", 133 | "8/8/1965 1:00 PM", 134 | "8/8/1965 12:00 AM", 135 | "4/02/2014 03:00:51", 136 | "03/19/2012 10:11:59", 137 | "03/19/2012 10:11:59.3186369", 138 | // mm/dd/yyyy 139 | "3/31/2014", 140 | "03/31/2014", 141 | "08/21/71", 142 | "8/1/71", 143 | // yyyy/mm/dd hh:mm:ss 144 | "2014/4/8 22:05", 145 | "2014/04/08 22:05", 146 | "2014/04/2 03:00:51", 147 | "2014/4/02 03:00:51", 148 | "2012/03/19 10:11:59", 149 | "2012/03/19 10:11:59.3186369", 150 | // yyyy/mm/dd 151 | "2014/3/31", 152 | "2014/03/31", 153 | // mm.dd.yyyy 154 | "3.31.2014", 155 | "03.31.2014", 156 | "08.21.71", 157 | // yyyy.mm.dd 158 | "2014.03.30", 159 | "2014.03", 160 | // yymmdd hh:mm:ss mysql log 161 | "171113 14:14:20", 162 | // chinese yyyy mm dd hh mm ss 163 | "2014年04月08日11时25分18秒", 164 | // chinese yyyy mm dd 165 | "2014年04月08日", 166 | ``` 167 | 168 | ## [`belt`](./belt) CLI tool 169 | 170 | Run `belt` to parse a given date: 171 | 172 | ```shell 173 | $> belt 'MAY 12, 2021 16:44 UTC' 174 | +-------------------+---------------------------+ 175 | | Zone | Date & Time | 176 | +===================+===========================+ 177 | | Local | 2021-05-12 09:44:00 -0700 | 178 | | | 1620837840 | 179 | +-------------------+---------------------------+ 180 | | UTC | 2021-05-12 16:44:00 +0000 | 181 | | | 2021-05-12 16:44 UTC | 182 | +-------------------+---------------------------+ 183 | | America/Vancouver | 2021-05-12 09:44:00 -0700 | 184 | | | 2021-05-12 09:44 PDT | 185 | +-------------------+---------------------------+ 186 | | America/New_York | 2021-05-12 12:44:00 -0400 | 187 | | | 2021-05-12 12:44 EDT | 188 | +-------------------+---------------------------+ 189 | | Europe/London | 2021-05-12 17:44:00 +0100 | 190 | | | 2021-05-12 17:44 BST | 191 | +-------------------+---------------------------+ 192 | ``` 193 | 194 | #### Installation 195 | 196 | MacOS Homebrew or Linuxbrew: 197 | 198 | ```shell 199 | brew tap waltzofpearls/belt 200 | brew install belt 201 | ``` 202 | 203 | ## How to make a new release 204 | 205 | List files that need to be updated with new version number: 206 | 207 | ```shell 208 | make show-version-files 209 | ``` 210 | 211 | It will output something like this: 212 | 213 | ```shell 214 | ./dateparser/Cargo.toml:3:version = "0.1.5" 215 | ./dateparser/README.md:26:dateparser = "0.1.5" 216 | ./dateparser/README.md:60:dateparser = "0.1.5" 217 | ./belt/Cargo.toml:3:version = "0.1.5" 218 | ``` 219 | 220 | Next, automatically bump the version with `make bump-version` or manually update verion numbers in 221 | those listed files. When auto incrementing version with `make bump-version`, it will only bump the 222 | patch version, for example, 0.1.5 will become 0.1.6. Automatic version bump will create a git branch, 223 | commit and push the changes. You will need to create a pull request from GitHub to merge those changes 224 | from the git branch that's automatically created. 225 | 226 | **NOTE**: if those files with version numbers are manually edited, then you will need to run `cargo update` 227 | to update `dateparser` and `belt` versions in the `Cargo.lock` file, and then git commit and push those 228 | changes to a git branch, and create a pull request from that branch. 229 | 230 | Once the pull request is merged and those files are updated, run the following command to tag a new 231 | version with git and push the new tag to GitHub. This will trigger a build and release workflow run 232 | in GitHub Actions: 233 | 234 | ```shell 235 | make release 236 | ``` 237 | -------------------------------------------------------------------------------- /belt/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "belt" 3 | version = "0.2.1" 4 | authors = ["Rollie Ma "] 5 | edition = "2021" 6 | publish = false 7 | description = "Know your time from a list of selected time zones" 8 | 9 | [dependencies] 10 | anyhow = "1.0.75" 11 | chrono = "0.4.31" 12 | chrono-tz = "0.8.4" 13 | clap = { version = "4.4.8", features = ["derive"] } 14 | colored = "2.0.4" 15 | confy = "0.5.1" 16 | dateparser = { path = "../dateparser" } 17 | directories = "5.0.1" 18 | prettytable-rs = "0.10.0" 19 | serde = { version = "1.0.192", features = ["derive"] } 20 | 21 | [dev-dependencies] 22 | rand = "0.8.5" 23 | regex = "1.10.2" 24 | -------------------------------------------------------------------------------- /belt/README.md: -------------------------------------------------------------------------------- 1 | # `belt` CLI tool 2 | 3 | Command-line app that can show your time from a list of selected time zones. It uses `dateparser` 4 | rust crate to parse date strings in commonly used formats. 5 | 6 | ## Installation 7 | 8 | MacOS Homebrew or Linuxbrew: 9 | 10 | ```shell 11 | brew tap waltzofpearls/belt 12 | brew install belt 13 | ``` 14 | 15 | ## Run `belt` to parse a given date 16 | 17 | ```shell 18 | $ belt 'MAY 12, 2021 16:44 UTC' 19 | +-------------------+---------------------------+ 20 | | Zone | Date & Time | 21 | +===================+===========================+ 22 | | Local | 2021-05-12 09:44:00 -0700 | 23 | | | 1620837840 | 24 | +-------------------+---------------------------+ 25 | | UTC | 2021-05-12 16:44:00 +0000 | 26 | | | 2021-05-12 16:44 UTC | 27 | +-------------------+---------------------------+ 28 | | America/Vancouver | 2021-05-12 09:44:00 -0700 | 29 | | | 2021-05-12 09:44 PDT | 30 | +-------------------+---------------------------+ 31 | | America/New_York | 2021-05-12 12:44:00 -0400 | 32 | | | 2021-05-12 12:44 EDT | 33 | +-------------------+---------------------------+ 34 | | Europe/London | 2021-05-12 17:44:00 +0100 | 35 | | | 2021-05-12 17:44 BST | 36 | +-------------------+---------------------------+ 37 | ``` 38 | 39 | ## Display parsed date in the short form 40 | 41 | ```shell 42 | # parse a unix epoch timestamp 43 | $ belt 1511648546 --short 44 | 2017-11-25 14:22:26 -0800 45 | 46 | # or show the current local datetime 47 | $ belt --short 48 | 2021-05-15 22:54:34 -0700 49 | ``` 50 | 51 | ## Configure time zone 52 | 53 | ```shell 54 | $ belt config --help 55 | belt-config 56 | Configure time zones list 57 | 58 | USAGE: 59 | belt config [FLAGS] [OPTIONS] 60 | 61 | FLAGS: 62 | -h, --help Prints help information 63 | -l, --list List existing time zones 64 | -r, --reset Reset to default list of time zones 65 | -V, --version Prints version information 66 | 67 | OPTIONS: 68 | -a, --add Add a new time zone to the list 69 | -d, --delete Delete a time zone from the list 70 | ``` 71 | -------------------------------------------------------------------------------- /belt/src/app.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | config::Config, 3 | opts::{Opts, Subcommands}, 4 | }; 5 | use anyhow::{Error, Result}; 6 | use chrono::prelude::*; 7 | use chrono_tz::Tz; 8 | use colored::*; 9 | use dateparser::DateTimeUtc; 10 | use prettytable::{row, Table}; 11 | use std::io; 12 | 13 | pub struct App<'a, T> { 14 | pub opts: &'a Opts, 15 | pub config: &'a mut Config<'a, T>, 16 | } 17 | 18 | impl<'a, T> App<'a, T> 19 | where 20 | T: io::Write, 21 | { 22 | pub fn new(opts: &'a Opts, config: &'a mut Config<'a, T>) -> Self { 23 | Self { opts, config } 24 | } 25 | 26 | pub fn show_datetime(&mut self) -> Result<()> { 27 | if self.opts.subcommands.is_some() { 28 | // skip showing datetime when there is a subcommand 29 | return Ok(()); 30 | } 31 | 32 | let mut to_show = Utc::now(); 33 | if let Some(time) = &self.opts.time { 34 | to_show = time.parse::()?.0; 35 | } 36 | 37 | let local = to_show.with_timezone(&Local); 38 | let ymd_hms_z = "%Y-%m-%d %H:%M:%S %z"; 39 | let ymd_hm_z = "%Y-%m-%d %H:%M %Z"; 40 | 41 | if self.opts.short { 42 | writeln!(self.config.out, "{}", local.format(ymd_hms_z))?; 43 | } else { 44 | let mut table = Table::new(); 45 | table.set_titles(row!["Zone", "Date & Time"]); 46 | table.add_row(row![ 47 | "Local", 48 | format!("{}\n{}", local.format(ymd_hms_z), local.format("%s")) 49 | ]); 50 | for timezone in &self.config.store.timezones { 51 | let tz: Tz = timezone.parse().map_err(Error::msg)?; 52 | let dtz = to_show.with_timezone(&tz); 53 | table.add_row(row![ 54 | timezone, 55 | format!("{}\n{}", dtz.format(ymd_hms_z), dtz.format(ymd_hm_z)) 56 | ]); 57 | } 58 | table.print(&mut self.config.out)?; 59 | } 60 | 61 | Ok(()) 62 | } 63 | 64 | pub fn handle_subcommands(&mut self) -> Result<()> { 65 | if let Some(subcommands) = &self.opts.subcommands { 66 | match subcommands { 67 | Subcommands::Config(c) => { 68 | if c.list { 69 | let path = self.config.path(); 70 | writeln!(self.config.out, "{}", path.cyan().bold())?; 71 | self.config.list()?; 72 | } else if c.reset { 73 | self.config.reset()?; 74 | self.config.list()?; 75 | } else if let Some(add) = &c.add { 76 | self.config.add(add)?; 77 | self.config.list()?; 78 | } else if let Some(delete) = &c.delete { 79 | self.config.delete(delete)?; 80 | self.config.list()?; 81 | } 82 | } 83 | } 84 | } 85 | Ok(()) 86 | } 87 | } 88 | 89 | #[cfg(test)] 90 | mod tests { 91 | use super::*; 92 | use crate::opts::OptsConfig; 93 | use rand::{thread_rng, Rng}; 94 | use regex::Regex; 95 | use std::{thread::sleep, time::Duration}; 96 | 97 | #[test] 98 | fn test_app_show_datetime() { 99 | let mut opts = Opts::new(); 100 | opts.app = "unit-test".to_string(); 101 | let mut buf = vec![0u8]; 102 | let mut config = match Config::new(&opts.app, &mut buf) { 103 | Ok(config) => config, 104 | Err(_) => { 105 | sleep(Duration::from_millis(thread_rng().gen_range(100..500))); 106 | Config::new(&opts.app, &mut buf).expect("failed to create config") 107 | } 108 | }; 109 | let timezones = config.store.timezones.clone(); 110 | let num_timezones = timezones.len(); 111 | let mut app = App::new(&opts, &mut config); 112 | 113 | app.show_datetime().expect("failed showing time"); 114 | 115 | let printed = String::from_utf8_lossy(&buf); 116 | for tz in timezones { 117 | assert!(printed.contains(&tz)); 118 | } 119 | let re = Regex::new(r"[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2} [0-9-+]{5}") 120 | .expect("failed to parse regex"); 121 | assert_eq!(re.find_iter(&printed).count(), num_timezones + 1); // num_timezones + local 122 | } 123 | 124 | #[test] 125 | fn test_app_handle_subcommands() { 126 | let mut opts = Opts::new(); 127 | opts.app = "unit-test".to_string(); 128 | let mut buf = vec![0u8]; 129 | let mut config = match Config::new(&opts.app, &mut buf) { 130 | Ok(config) => config, 131 | Err(_) => { 132 | sleep(Duration::from_millis(thread_rng().gen_range(100..500))); 133 | Config::new(&opts.app, &mut buf).expect("failed to create config") 134 | } 135 | }; 136 | let timezones = config.store.timezones.clone(); 137 | let mut app = App::new(&opts, &mut config); 138 | 139 | let opts = Opts { 140 | subcommands: Some(Subcommands::Config(OptsConfig { 141 | list: true, 142 | reset: false, 143 | add: None, 144 | delete: None, 145 | })), 146 | time: None, 147 | short: false, 148 | app: opts.app.to_owned(), 149 | }; 150 | app.opts = &opts; 151 | app.handle_subcommands() 152 | .expect("failed handling subcommands"); 153 | 154 | let printed = String::from_utf8_lossy(&buf); 155 | for tz in timezones { 156 | assert!(printed.contains(&tz)); 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /belt/src/config.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Error, Result}; 2 | use chrono::prelude::*; 3 | use chrono_tz::{OffsetComponents, OffsetName, Tz}; 4 | use colored::*; 5 | use directories::ProjectDirs; 6 | use prettytable::{row, Table}; 7 | use serde::{Deserialize, Serialize}; 8 | use std::io; 9 | 10 | pub struct Config<'a, T> { 11 | pub store: Store, 12 | pub out: &'a mut T, 13 | pub app: String, 14 | } 15 | 16 | #[derive(Serialize, Deserialize)] 17 | pub struct Store { 18 | pub timezones: Vec, 19 | } 20 | 21 | impl ::std::default::Default for Store { 22 | fn default() -> Self { 23 | Self { 24 | timezones: vec![ 25 | "UTC".to_string(), 26 | "America/Vancouver".to_string(), 27 | "America/New_York".to_string(), 28 | "Europe/London".to_string(), 29 | ], 30 | } 31 | } 32 | } 33 | 34 | impl<'a, T> Config<'a, T> 35 | where 36 | T: io::Write, 37 | { 38 | pub fn new(app: &str, out: &'a mut T) -> Result { 39 | let store: Store = confy::load(app, None)?; 40 | Ok(Self { 41 | store, 42 | out, 43 | app: app.to_string(), 44 | }) 45 | } 46 | 47 | pub fn path(&self) -> String { 48 | ProjectDirs::from("rs", "", &self.app) 49 | .and_then(|project| project.config_dir().to_str().map(|s: &str| s.to_string())) 50 | .map(|s| format!("{}/{}.toml", s, self.app)) 51 | .unwrap_or_default() 52 | } 53 | 54 | pub fn list(&mut self) -> Result<()> { 55 | let now_utc = Local::now().naive_utc(); 56 | let mut table = Table::new(); 57 | table.set_titles(row![l -> "Zone", l -> "Abbr.", r -> "Offset"]); 58 | for timezone in &self.store.timezones { 59 | let tz: Tz = timezone.parse().map_err(Error::msg)?; 60 | let offset = tz.offset_from_utc_datetime(&now_utc); 61 | table.add_row(row![ 62 | l -> timezone, 63 | l -> offset.abbreviation(), 64 | r -> match offset.base_utc_offset().num_hours() { 65 | 0 => "0 hour ".to_string(), 66 | hours => format!("{} hours", hours), 67 | } 68 | ]); 69 | } 70 | table.print(self.out)?; 71 | Ok(()) 72 | } 73 | 74 | pub fn add(&mut self, to_add: &str) -> Result<()> { 75 | let result = to_add.parse::().and_then(|_| { 76 | self.store.timezones.push(to_add.to_string()); 77 | confy::store(&self.app, None, &self.store).map_err(|err| format!("{}", err)) 78 | }); 79 | 80 | match result { 81 | Ok(_) => writeln!( 82 | self.out, 83 | "{}", 84 | format!("Added '{}' to config.", to_add).green().bold() 85 | )?, 86 | Err(err) => writeln!( 87 | self.out, 88 | "{}", 89 | format!("Could not add time zone: {}.", err).red().bold() 90 | )?, 91 | }; 92 | Ok(()) 93 | } 94 | 95 | pub fn delete(&mut self, to_delete: &str) -> Result<()> { 96 | self.store.timezones.retain(|tz| tz != to_delete); 97 | match confy::store(&self.app, None, &self.store) { 98 | Ok(_) => writeln!( 99 | self.out, 100 | "{}", 101 | format!("Deleted '{}' from config.", to_delete) 102 | .green() 103 | .bold() 104 | )?, 105 | Err(err) => writeln!( 106 | self.out, 107 | "{}", 108 | format!("Could not delete time zone: {}.", err).red().bold() 109 | )?, 110 | }; 111 | Ok(()) 112 | } 113 | 114 | pub fn reset(&mut self) -> Result<()> { 115 | self.store.timezones = Store::default().timezones; 116 | match confy::store(&self.app, None, &self.store) { 117 | Ok(_) => writeln!( 118 | self.out, 119 | "{}", 120 | "Config has been reset to default.".green().bold() 121 | )?, 122 | Err(err) => writeln!( 123 | self.out, 124 | "{}", 125 | format!("Could not reset time zones: {}", err).red().bold() 126 | )?, 127 | }; 128 | Ok(()) 129 | } 130 | } 131 | 132 | #[cfg(test)] 133 | mod tests { 134 | use super::*; 135 | use rand::{thread_rng, Rng}; 136 | use std::{thread::sleep, time::Duration}; 137 | 138 | #[test] 139 | fn test_config_path() { 140 | let mut buf = vec![0u8]; 141 | let app = "unit-test"; 142 | let config = match Config::new(app, &mut buf) { 143 | Ok(config) => config, 144 | Err(_) => { 145 | sleep(Duration::from_millis(thread_rng().gen_range(100..500))); 146 | Config::new(app, &mut buf).expect("failed to create config") 147 | } 148 | }; 149 | let path = config.path(); 150 | if !path.contains(app) { 151 | panic!("path [{}] does not contain [unit-test]", path); 152 | } 153 | } 154 | 155 | #[test] 156 | fn test_config_list() { 157 | let mut buf = vec![0u8]; 158 | let app = "unit-test"; 159 | let mut config = match Config::new(app, &mut buf) { 160 | Ok(config) => config, 161 | Err(_) => { 162 | sleep(Duration::from_millis(thread_rng().gen_range(100..500))); 163 | Config::new(app, &mut buf).expect("failed to create config") 164 | } 165 | }; 166 | config.reset().expect("failed to reset config store"); 167 | config.out.clear(); 168 | 169 | config.list().expect("failed to list configured timezons"); 170 | let listed = String::from_utf8_lossy(&buf); 171 | for tz in Store::default().timezones { 172 | assert!(listed.contains(&tz)); 173 | } 174 | } 175 | 176 | #[test] 177 | fn test_config_add() { 178 | let mut buf = vec![0u8]; 179 | let app = "unit-test"; 180 | let mut config = match Config::new(app, &mut buf) { 181 | Ok(config) => config, 182 | Err(_) => { 183 | sleep(Duration::from_millis(thread_rng().gen_range(100..500))); 184 | Config::new(app, &mut buf).expect("failed to create config") 185 | } 186 | }; 187 | config.reset().expect("failed to reset config store"); 188 | config 189 | .add("Europe/Berlin") 190 | .expect("failed to add Europe/Berlin"); 191 | config.out.clear(); 192 | 193 | config.list().expect("failed to list configured timezons"); 194 | let listed = String::from_utf8_lossy(&buf); 195 | assert!(listed.contains("Europe/Berlin")); 196 | } 197 | 198 | #[test] 199 | fn test_config_delete() { 200 | let mut buf = vec![0u8]; 201 | let app = "unit-test"; 202 | let mut config = match Config::new(app, &mut buf) { 203 | Ok(config) => config, 204 | Err(_) => { 205 | sleep(Duration::from_millis(thread_rng().gen_range(100..500))); 206 | Config::new(app, &mut buf).expect("failed to create config") 207 | } 208 | }; 209 | config.reset().expect("failed to reset config store"); 210 | config.delete("UTC").expect("failed to delete UTC"); 211 | config.out.clear(); 212 | 213 | config.list().expect("failed to list configured timezons"); 214 | let listed = String::from_utf8_lossy(&buf); 215 | assert!(!listed.contains("UTC")); 216 | } 217 | 218 | #[test] 219 | fn test_config_reset() { 220 | let mut buf = vec![0u8]; 221 | let app = "unit-test"; 222 | let mut config = match Config::new(app, &mut buf) { 223 | Ok(config) => config, 224 | Err(_) => { 225 | sleep(Duration::from_millis(thread_rng().gen_range(100..500))); 226 | Config::new(app, &mut buf).expect("failed to create config") 227 | } 228 | }; 229 | config.reset().expect("failed to reset config store"); 230 | config 231 | .add("Europe/Berlin") 232 | .expect("failed to add Europe/Berlin"); 233 | config.delete("UTC").expect("failed to delete UTC"); 234 | config.reset().expect("failed to reset config store"); 235 | config.out.clear(); 236 | 237 | config.list().expect("failed to list configured timezons"); 238 | let listed = String::from_utf8_lossy(&buf); 239 | for tz in Store::default().timezones { 240 | assert!(listed.contains(&tz)); 241 | } 242 | assert!(!listed.contains("Europe/Berlin")); 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /belt/src/main.rs: -------------------------------------------------------------------------------- 1 | mod app; 2 | mod config; 3 | mod opts; 4 | 5 | use crate::{app::App, config::Config, opts::Opts}; 6 | use anyhow::Result; 7 | 8 | fn main() -> Result<()> { 9 | let opts = Opts::new(); 10 | let mut out = std::io::stdout(); 11 | let mut config = Config::new(&opts.app, &mut out)?; 12 | let mut app = App::new(&opts, &mut config); 13 | 14 | app.show_datetime()?; 15 | app.handle_subcommands()?; 16 | 17 | Ok(()) 18 | } 19 | -------------------------------------------------------------------------------- /belt/src/opts.rs: -------------------------------------------------------------------------------- 1 | use clap::{Parser, Subcommand}; 2 | 3 | #[derive(Parser, Debug)] 4 | #[command(author, about, version)] 5 | pub struct Opts { 6 | #[arg(name = "TIME")] 7 | pub time: Option, 8 | /// Provide a terse answer, and default to a verbose form 9 | #[arg(short, long)] 10 | pub short: bool, 11 | 12 | /// Name of the config 13 | #[arg(short, long, name = "NAME", default_value = "belt")] 14 | pub app: String, 15 | 16 | #[command(subcommand)] 17 | pub subcommands: Option, 18 | } 19 | 20 | #[derive(Subcommand, Debug)] 21 | pub enum Subcommands { 22 | /// Configure time zones list 23 | Config(OptsConfig), 24 | } 25 | 26 | #[derive(Parser, Debug)] 27 | pub struct OptsConfig { 28 | /// List existing time zones 29 | #[arg(short, long)] 30 | pub list: bool, 31 | /// Reset to default list of time zones 32 | #[arg(short, long)] 33 | pub reset: bool, 34 | /// Add a new time zone to the list 35 | #[arg(short, long, name = "timezone_to_add")] 36 | pub add: Option, 37 | /// Delete a time zone from the list 38 | #[arg(short, long, name = "timezone_to_delete")] 39 | pub delete: Option, 40 | } 41 | 42 | impl Opts { 43 | pub fn new() -> Self { 44 | Self::parse() 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /dateparser/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dateparser" 3 | version = "0.2.1" 4 | authors = ["Rollie Ma "] 5 | description = "Parse dates in string formats that are commonly used" 6 | readme = "README.md" 7 | homepage = "https://github.com/waltzofpearls/dateparser" 8 | repository = "https://github.com/waltzofpearls/dateparser" 9 | keywords = ["date", "time", "datetime", "parser", "parse"] 10 | license = "MIT" 11 | edition = "2021" 12 | 13 | [dependencies] 14 | anyhow = "1.0.75" 15 | chrono = "0.4.31" 16 | lazy_static = "1.4.0" 17 | regex = "1.10.2" 18 | 19 | [dev-dependencies] 20 | chrono-tz = "0.8.4" 21 | criterion = { version = "0.5.1", features = ["html_reports"] } 22 | 23 | [[bench]] 24 | name = "parse" 25 | harness = false 26 | -------------------------------------------------------------------------------- /dateparser/README.md: -------------------------------------------------------------------------------- 1 | # [dateparser](https://crates.io/crates/dateparser) 2 | 3 | [![Build Status][actions-badge]][actions-url] 4 | [![MIT licensed][mit-badge]][mit-url] 5 | [![Crates.io][cratesio-badge]][cratesio-url] 6 | [![Doc.rs][docrs-badge]][docrs-url] 7 | 8 | [actions-badge]: https://github.com/waltzofpearls/dateparser/workflows/ci/badge.svg 9 | [actions-url]: https://github.com/waltzofpearls/dateparser/actions?query=workflow%3Aci+branch%3Amain 10 | [mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg 11 | [mit-url]: https://github.com/waltzofpearls/dateparser/blob/main/LICENSE 12 | [cratesio-badge]: https://img.shields.io/crates/v/dateparser.svg 13 | [cratesio-url]: https://crates.io/crates/dateparser 14 | [docrs-badge]: https://docs.rs/dateparser/badge.svg 15 | [docrs-url]: https://docs.rs/crate/dateparser/ 16 | 17 | A rust library for parsing date strings in commonly used formats. Parsed date will be returned as `chrono`'s 18 | `DateTime`. 19 | 20 | ## Examples 21 | 22 | Add to your `Cargo.toml`: 23 | 24 | ```toml 25 | [dependencies] 26 | dateparser = "0.2.1" 27 | ``` 28 | 29 | And then use `dateparser` in your code: 30 | 31 | ```rust 32 | use dateparser::parse; 33 | use std::error::Error; 34 | 35 | fn main() -> Result<(), Box> { 36 | let parsed = parse("6:15pm")?; 37 | println!("{:#?}", parsed); 38 | Ok(()) 39 | } 40 | ``` 41 | 42 | Or use `str`'s `parse` method: 43 | 44 | ```rust 45 | use dateparser::DateTimeUtc; 46 | use std::error::Error; 47 | 48 | fn main() -> Result<(), Box> { 49 | let parsed = "2021-05-14 18:51 PDT".parse::()?.0; 50 | println!("{:#?}", parsed); 51 | Ok(()) 52 | } 53 | ``` 54 | 55 | Convert returned `DateTime` to pacific time zone datetime with `chrono-tz`: 56 | 57 | ```toml 58 | [dependencies] 59 | chrono-tz = "0.6.3" 60 | dateparser = "0.2.1" 61 | ``` 62 | 63 | ```rust 64 | use chrono_tz::US::Pacific; 65 | use dateparser::DateTimeUtc; 66 | use std::error::Error; 67 | 68 | fn main() -> Result<(), Box> { 69 | let parsed = "Wed, 02 Jun 2021 06:31:39 GMT".parse::()?.0; 70 | println!("{:#?}", parsed.with_timezone(&Pacific)); 71 | Ok(()) 72 | } 73 | ``` 74 | 75 | Parse using a custom timezone offset for a datetime string that doesn't come with a specific timezone: 76 | 77 | ```rust 78 | use dateparser::parse_with_timezone; 79 | use chrono::offset::{Local, Utc}; 80 | use chrono_tz::US::Pacific; 81 | use std::error::Error; 82 | 83 | fn main() -> Result<(), Box> { 84 | let parsed_in_local = parse_with_timezone("6:15pm", &Local)?; 85 | println!("{:#?}", parsed_in_local); 86 | 87 | let parsed_in_utc = parse_with_timezone("6:15pm", &Utc)?; 88 | println!("{:#?}", parsed_in_utc); 89 | 90 | let parsed_in_pacific = parse_with_timezone("6:15pm", &Pacific)?; 91 | println!("{:#?}", parsed_in_pacific); 92 | 93 | Ok(()) 94 | } 95 | ``` 96 | 97 | Parse with a custom timezone offset and default time when those are not given in datetime string. 98 | By default, `parse` and `parse_with_timezone` uses `Utc::now().time()` as `default_time`. 99 | 100 | ```rust 101 | use dateparser::parse_with; 102 | use chrono::{ 103 | offset::{Local, Utc}, 104 | naive::NaiveTime, 105 | }; 106 | use std::error::Error; 107 | 108 | fn main() -> Result<(), Box> { 109 | let parsed_in_local = parse_with("2021-10-09", &Local, NaiveTime::from_hms(0, 0, 0))?; 110 | println!("{:#?}", parsed_in_local); 111 | 112 | let parsed_in_utc = parse_with("2021-10-09", &Utc, NaiveTime::from_hms(0, 0, 0))?; 113 | println!("{:#?}", parsed_in_utc); 114 | 115 | Ok(()) 116 | } 117 | ``` 118 | 119 | ## Accepted date formats 120 | 121 | ```rust 122 | // unix timestamp 123 | "1511648546", 124 | "1620021848429", 125 | "1620024872717915000", 126 | // rfc3339 127 | "2021-05-01T01:17:02.604456Z", 128 | "2017-11-25T22:34:50Z", 129 | // rfc2822 130 | "Wed, 02 Jun 2021 06:31:39 GMT", 131 | // postgres timestamp yyyy-mm-dd hh:mm:ss z 132 | "2019-11-29 08:08-08", 133 | "2019-11-29 08:08:05-08", 134 | "2021-05-02 23:31:36.0741-07", 135 | "2021-05-02 23:31:39.12689-07", 136 | "2019-11-29 08:15:47.624504-08", 137 | "2017-07-19 03:21:51+00:00", 138 | // yyyy-mm-dd hh:mm:ss 139 | "2014-04-26 05:24:37 PM", 140 | "2021-04-30 21:14", 141 | "2021-04-30 21:14:10", 142 | "2021-04-30 21:14:10.052282", 143 | "2014-04-26 17:24:37.123", 144 | "2014-04-26 17:24:37.3186369", 145 | "2012-08-03 18:31:59.257000000", 146 | // yyyy-mm-dd hh:mm:ss z 147 | "2017-11-25 13:31:15 PST", 148 | "2017-11-25 13:31 PST", 149 | "2014-12-16 06:20:00 UTC", 150 | "2014-12-16 06:20:00 GMT", 151 | "2014-04-26 13:13:43 +0800", 152 | "2014-04-26 13:13:44 +09:00", 153 | "2012-08-03 18:31:59.257000000 +0000", 154 | "2015-09-30 18:48:56.35272715 UTC", 155 | // yyyy-mm-dd 156 | "2021-02-21", 157 | // yyyy-mm-dd z 158 | "2021-02-21 PST", 159 | "2021-02-21 UTC", 160 | "2020-07-20+08:00", 161 | // hh:mm:ss 162 | "01:06:06", 163 | "4:00pm", 164 | "6:00 AM", 165 | // hh:mm:ss z 166 | "01:06:06 PST", 167 | "4:00pm PST", 168 | "6:00 AM PST", 169 | "6:00pm UTC", 170 | // Mon dd hh:mm:ss 171 | "May 6 at 9:24 PM", 172 | "May 27 02:45:27", 173 | // Mon dd, yyyy, hh:mm:ss 174 | "May 8, 2009 5:57:51 PM", 175 | "September 17, 2012 10:09am", 176 | "September 17, 2012, 10:10:09", 177 | // Mon dd, yyyy hh:mm:ss z 178 | "May 02, 2021 15:51:31 UTC", 179 | "May 02, 2021 15:51 UTC", 180 | "May 26, 2021, 12:49 AM PDT", 181 | "September 17, 2012 at 10:09am PST", 182 | // yyyy-mon-dd 183 | "2021-Feb-21", 184 | // Mon dd, yyyy 185 | "May 25, 2021", 186 | "oct 7, 1970", 187 | "oct 7, 70", 188 | "oct. 7, 1970", 189 | "oct. 7, 70", 190 | "October 7, 1970", 191 | // dd Mon yyyy hh:mm:ss 192 | "12 Feb 2006, 19:17", 193 | "12 Feb 2006 19:17", 194 | "14 May 2019 19:11:40.164", 195 | // dd Mon yyyy 196 | "7 oct 70", 197 | "7 oct 1970", 198 | "03 February 2013", 199 | "1 July 2013", 200 | // mm/dd/yyyy hh:mm:ss 201 | "4/8/2014 22:05", 202 | "04/08/2014 22:05", 203 | "4/8/14 22:05", 204 | "04/2/2014 03:00:51", 205 | "8/8/1965 12:00:00 AM", 206 | "8/8/1965 01:00:01 PM", 207 | "8/8/1965 01:00 PM", 208 | "8/8/1965 1:00 PM", 209 | "8/8/1965 12:00 AM", 210 | "4/02/2014 03:00:51", 211 | "03/19/2012 10:11:59", 212 | "03/19/2012 10:11:59.3186369", 213 | // mm/dd/yyyy 214 | "3/31/2014", 215 | "03/31/2014", 216 | "08/21/71", 217 | "8/1/71", 218 | // yyyy/mm/dd hh:mm:ss 219 | "2014/4/8 22:05", 220 | "2014/04/08 22:05", 221 | "2014/04/2 03:00:51", 222 | "2014/4/02 03:00:51", 223 | "2012/03/19 10:11:59", 224 | "2012/03/19 10:11:59.3186369", 225 | // yyyy/mm/dd 226 | "2014/3/31", 227 | "2014/03/31", 228 | // mm.dd.yyyy 229 | "3.31.2014", 230 | "03.31.2014", 231 | "08.21.71", 232 | // yyyy.mm.dd 233 | "2014.03.30", 234 | "2014.03", 235 | // yymmdd hh:mm:ss mysql log 236 | "171113 14:14:20", 237 | // chinese yyyy mm dd hh mm ss 238 | "2014年04月08日11时25分18秒", 239 | // chinese yyyy mm dd 240 | "2014年04月08日", 241 | ``` 242 | -------------------------------------------------------------------------------- /dateparser/benches/parse.rs: -------------------------------------------------------------------------------- 1 | use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; 2 | use dateparser::parse; 3 | use lazy_static::lazy_static; 4 | 5 | lazy_static! { 6 | static ref SELECTED: Vec<&'static str> = vec![ 7 | "1511648546", // unix_timestamp 8 | "2017-11-25T22:34:50Z", // rfc3339 9 | "Wed, 02 Jun 2021 06:31:39 GMT", // rfc2822 10 | "2019-11-29 08:08:05-08", // postgres_timestamp 11 | "2021-04-30 21:14:10", // ymd_hms 12 | "2017-11-25 13:31:15 PST", // ymd_hms_z 13 | "2021-02-21", // ymd 14 | "2021-02-21 PST", // ymd_z 15 | "4:00pm", // hms 16 | "6:00 AM PST", // hms_z 17 | "May 27 02:45:27", // month_md_hms 18 | "May 8, 2009 5:57:51 PM", // month_mdy_hms 19 | "May 02, 2021 15:51 UTC", // month_mdy_hms_z 20 | "2021-Feb-21", // month_ymd 21 | "May 25, 2021", // month_mdy 22 | "14 May 2019 19:11:40.164", // month_dmy_hms 23 | "1 July 2013", // month_dmy 24 | "03/19/2012 10:11:59", // slash_mdy_hms 25 | "08/21/71", // slash_mdy 26 | "2012/03/19 10:11:59", // slash_ymd_hms 27 | "2014/3/31", // slash_ymd 28 | "2014.03.30", // dot_mdy_or_ymd 29 | "171113 14:14:20", // mysql_log_timestamp 30 | "2014年04月08日11时25分18秒", // chinese_ymd_hms 31 | "2014年04月08日", // chinese_ymd 32 | ]; 33 | } 34 | 35 | fn bench_parse_all(c: &mut Criterion) { 36 | c.bench_with_input( 37 | BenchmarkId::new("parse_all", "accepted_formats"), 38 | &SELECTED, 39 | |b, all| { 40 | b.iter(|| { 41 | for date_str in all.iter() { 42 | let _ = parse(*date_str); 43 | } 44 | }) 45 | }, 46 | ); 47 | } 48 | 49 | fn bench_parse_each(c: &mut Criterion) { 50 | let mut group = c.benchmark_group("parse_each"); 51 | for date_str in SELECTED.iter() { 52 | group.bench_with_input(*date_str, *date_str, |b, input| b.iter(|| parse(input))); 53 | } 54 | group.finish(); 55 | } 56 | 57 | criterion_group!(benches, bench_parse_all, bench_parse_each); 58 | criterion_main!(benches); 59 | -------------------------------------------------------------------------------- /dateparser/examples/convert_to_pacific.rs: -------------------------------------------------------------------------------- 1 | use chrono_tz::US::Pacific; 2 | use dateparser::DateTimeUtc; 3 | use std::error::Error; 4 | 5 | fn main() -> Result<(), Box> { 6 | let parsed = "Wed, 02 Jun 2021 06:31:39 GMT".parse::()?.0; 7 | println!("{:#?}", parsed.with_timezone(&Pacific)); 8 | Ok(()) 9 | } 10 | -------------------------------------------------------------------------------- /dateparser/examples/parse.rs: -------------------------------------------------------------------------------- 1 | use dateparser::parse; 2 | use std::error::Error; 3 | 4 | fn main() -> Result<(), Box> { 5 | let parsed = parse("6:15pm")?; 6 | println!("{:#?}", parsed); 7 | Ok(()) 8 | } 9 | -------------------------------------------------------------------------------- /dateparser/examples/parse_with.rs: -------------------------------------------------------------------------------- 1 | use chrono::{ 2 | naive::NaiveTime, 3 | offset::{Local, Utc}, 4 | }; 5 | use dateparser::parse_with; 6 | use std::error::Error; 7 | 8 | fn main() -> Result<(), Box> { 9 | let parsed_in_local = parse_with( 10 | "2021-10-09", 11 | &Local, 12 | NaiveTime::from_hms_opt(0, 0, 0).unwrap(), 13 | )?; 14 | println!("{:#?}", parsed_in_local); 15 | 16 | let parsed_in_utc = parse_with( 17 | "2021-10-09", 18 | &Utc, 19 | NaiveTime::from_hms_opt(0, 0, 0).unwrap(), 20 | )?; 21 | println!("{:#?}", parsed_in_utc); 22 | 23 | Ok(()) 24 | } 25 | -------------------------------------------------------------------------------- /dateparser/examples/parse_with_timezone.rs: -------------------------------------------------------------------------------- 1 | use chrono::offset::{Local, Utc}; 2 | use chrono_tz::US::Pacific; 3 | use dateparser::parse_with_timezone; 4 | use std::error::Error; 5 | 6 | fn main() -> Result<(), Box> { 7 | let parsed_in_local = parse_with_timezone("6:15pm", &Local)?; 8 | println!("{:#?}", parsed_in_local); 9 | 10 | let parsed_in_utc = parse_with_timezone("6:15pm", &Utc)?; 11 | println!("{:#?}", parsed_in_utc); 12 | 13 | let parsed_in_pacific = parse_with_timezone("6:15pm", &Pacific)?; 14 | println!("{:#?}", parsed_in_pacific); 15 | 16 | Ok(()) 17 | } 18 | -------------------------------------------------------------------------------- /dateparser/examples/str_parse_method.rs: -------------------------------------------------------------------------------- 1 | use dateparser::DateTimeUtc; 2 | use std::error::Error; 3 | 4 | fn main() -> Result<(), Box> { 5 | let parsed = "2021-05-14 18:51 PDT".parse::()?.0; 6 | println!("{:#?}", parsed); 7 | Ok(()) 8 | } 9 | -------------------------------------------------------------------------------- /dateparser/src/datetime.rs: -------------------------------------------------------------------------------- 1 | #![allow(deprecated)] 2 | use crate::timezone; 3 | use anyhow::{anyhow, Result}; 4 | use chrono::prelude::*; 5 | use lazy_static::lazy_static; 6 | use regex::Regex; 7 | 8 | /// Parse struct has methods implemented parsers for accepted formats. 9 | pub struct Parse<'z, Tz2> { 10 | tz: &'z Tz2, 11 | default_time: Option, 12 | } 13 | 14 | impl<'z, Tz2> Parse<'z, Tz2> 15 | where 16 | Tz2: TimeZone, 17 | { 18 | /// Create a new instrance of [`Parse`] with a custom parsing timezone that handles the 19 | /// datetime string without time offset. 20 | pub fn new(tz: &'z Tz2, default_time: Option) -> Self { 21 | Self { tz, default_time } 22 | } 23 | 24 | /// This method tries to parse the input datetime string with a list of accepted formats. See 25 | /// more exmaples from [`Parse`], [`crate::parse()`] and [`crate::parse_with_timezone()`]. 26 | pub fn parse(&self, input: &str) -> Result> { 27 | self.unix_timestamp(input) 28 | .or_else(|| self.rfc2822(input)) 29 | .or_else(|| self.ymd_family(input)) 30 | .or_else(|| self.hms_family(input)) 31 | .or_else(|| self.month_ymd(input)) 32 | .or_else(|| self.month_mdy_family(input)) 33 | .or_else(|| self.month_dmy_family(input)) 34 | .or_else(|| self.slash_mdy_family(input)) 35 | .or_else(|| self.hyphen_mdy_family(input)) 36 | .or_else(|| self.slash_ymd_family(input)) 37 | .or_else(|| self.dot_mdy_or_ymd(input)) 38 | .or_else(|| self.mysql_log_timestamp(input)) 39 | .or_else(|| self.chinese_ymd_family(input)) 40 | .unwrap_or_else(|| Err(anyhow!("{} did not match any formats.", input))) 41 | } 42 | 43 | fn ymd_family(&self, input: &str) -> Option>> { 44 | lazy_static! { 45 | static ref RE: Regex = Regex::new(r"^[0-9]{4}-[0-9]{2}").unwrap(); 46 | } 47 | if !RE.is_match(input) { 48 | return None; 49 | } 50 | self.rfc3339(input) 51 | .or_else(|| self.postgres_timestamp(input)) 52 | .or_else(|| self.ymd_hms(input)) 53 | .or_else(|| self.ymd_hms_z(input)) 54 | .or_else(|| self.ymd(input)) 55 | .or_else(|| self.ymd_z(input)) 56 | } 57 | 58 | fn hms_family(&self, input: &str) -> Option>> { 59 | lazy_static! { 60 | static ref RE: Regex = Regex::new(r"^[0-9]{1,2}:[0-9]{2}").unwrap(); 61 | } 62 | if !RE.is_match(input) { 63 | return None; 64 | } 65 | self.hms(input).or_else(|| self.hms_z(input)) 66 | } 67 | 68 | fn month_mdy_family(&self, input: &str) -> Option>> { 69 | lazy_static! { 70 | static ref RE: Regex = Regex::new(r"^[a-zA-Z]{3,9}\.?\s+[0-9]{1,2}").unwrap(); 71 | } 72 | if !RE.is_match(input) { 73 | return None; 74 | } 75 | self.month_md_hms(input) 76 | .or_else(|| self.month_mdy_hms(input)) 77 | .or_else(|| self.month_mdy_hms_z(input)) 78 | .or_else(|| self.month_mdy(input)) 79 | } 80 | 81 | fn month_dmy_family(&self, input: &str) -> Option>> { 82 | lazy_static! { 83 | static ref RE: Regex = Regex::new(r"^[0-9]{1,2}\s+[a-zA-Z]{3,9}").unwrap(); 84 | } 85 | if !RE.is_match(input) { 86 | return None; 87 | } 88 | self.month_dmy_hms(input).or_else(|| self.month_dmy(input)) 89 | } 90 | 91 | fn slash_mdy_family(&self, input: &str) -> Option>> { 92 | lazy_static! { 93 | static ref RE: Regex = Regex::new(r"^[0-9]{1,2}/[0-9]{1,2}").unwrap(); 94 | } 95 | if !RE.is_match(input) { 96 | return None; 97 | } 98 | self.slash_mdy_hms(input).or_else(|| self.slash_mdy(input)) 99 | } 100 | 101 | fn hyphen_mdy_family(&self, input: &str) -> Option>> { 102 | lazy_static! { 103 | static ref RE: Regex = Regex::new(r"^[0-9]{1,2}-[0-9]{1,2}").unwrap(); 104 | } 105 | if !RE.is_match(input) { 106 | return None; 107 | } 108 | self.hyphen_mdy_hms(input).or_else(|| self.hyphen_mdy(input)) 109 | } 110 | 111 | fn slash_ymd_family(&self, input: &str) -> Option>> { 112 | lazy_static! { 113 | static ref RE: Regex = Regex::new(r"^[0-9]{4}/[0-9]{1,2}").unwrap(); 114 | } 115 | if !RE.is_match(input) { 116 | return None; 117 | } 118 | self.slash_ymd_hms(input).or_else(|| self.slash_ymd(input)) 119 | } 120 | 121 | fn chinese_ymd_family(&self, input: &str) -> Option>> { 122 | lazy_static! { 123 | static ref RE: Regex = Regex::new(r"^[0-9]{4}年[0-9]{2}月").unwrap(); 124 | } 125 | if !RE.is_match(input) { 126 | return None; 127 | } 128 | self.chinese_ymd_hms(input) 129 | .or_else(|| self.chinese_ymd(input)) 130 | } 131 | 132 | // unix timestamp 133 | // - 1511648546 134 | // - 1620021848429 135 | // - 1620024872717915000 136 | fn unix_timestamp(&self, input: &str) -> Option>> { 137 | lazy_static! { 138 | static ref RE: Regex = Regex::new(r"^[0-9]{10,19}$").unwrap(); 139 | } 140 | if !RE.is_match(input) { 141 | return None; 142 | } 143 | 144 | input 145 | .parse::() 146 | .ok() 147 | .and_then(|timestamp| { 148 | match input.len() { 149 | 10 => Some(Utc.timestamp(timestamp, 0)), 150 | 13 => Some(Utc.timestamp_millis(timestamp)), 151 | 19 => Some(Utc.timestamp_nanos(timestamp)), 152 | _ => None, 153 | } 154 | .map(|datetime| datetime.with_timezone(&Utc)) 155 | }) 156 | .map(Ok) 157 | } 158 | 159 | // rfc3339 160 | // - 2021-05-01T01:17:02.604456Z 161 | // - 2017-11-25T22:34:50Z 162 | fn rfc3339(&self, input: &str) -> Option>> { 163 | DateTime::parse_from_rfc3339(input) 164 | .ok() 165 | .map(|parsed| parsed.with_timezone(&Utc)) 166 | .map(Ok) 167 | } 168 | 169 | // rfc2822 170 | // - Wed, 02 Jun 2021 06:31:39 GMT 171 | fn rfc2822(&self, input: &str) -> Option>> { 172 | DateTime::parse_from_rfc2822(input) 173 | .ok() 174 | .map(|parsed| parsed.with_timezone(&Utc)) 175 | .map(Ok) 176 | } 177 | 178 | // postgres timestamp yyyy-mm-dd hh:mm:ss z 179 | // - 2019-11-29 08:08-08 180 | // - 2019-11-29 08:08:05-08 181 | // - 2021-05-02 23:31:36.0741-07 182 | // - 2021-05-02 23:31:39.12689-07 183 | // - 2019-11-29 08:15:47.624504-08 184 | // - 2017-07-19 03:21:51+00:00 185 | fn postgres_timestamp(&self, input: &str) -> Option>> { 186 | lazy_static! { 187 | static ref RE: Regex = Regex::new( 188 | r"^[0-9]{4}-[0-9]{2}-[0-9]{2}\s+[0-9]{2}:[0-9]{2}(:[0-9]{2})?(\.[0-9]{1,9})?[+-:0-9]{3,6}$", 189 | ) 190 | .unwrap(); 191 | } 192 | if !RE.is_match(input) { 193 | return None; 194 | } 195 | 196 | DateTime::parse_from_str(input, "%Y-%m-%d %H:%M:%S%#z") 197 | .or_else(|_| DateTime::parse_from_str(input, "%Y-%m-%d %H:%M:%S%.f%#z")) 198 | .or_else(|_| DateTime::parse_from_str(input, "%Y-%m-%d %H:%M%#z")) 199 | .ok() 200 | .map(|parsed| parsed.with_timezone(&Utc)) 201 | .map(Ok) 202 | } 203 | 204 | // yyyy-mm-dd hh:mm:ss 205 | // - 2014-04-26 05:24:37 PM 206 | // - 2021-04-30 21:14 207 | // - 2021-04-30 21:14:10 208 | // - 2021-04-30 21:14:10.052282 209 | // - 2014-04-26 17:24:37.123 210 | // - 2014-04-26 17:24:37.3186369 211 | // - 2012-08-03 18:31:59.257000000 212 | fn ymd_hms(&self, input: &str) -> Option>> { 213 | lazy_static! { 214 | static ref RE: Regex = Regex::new( 215 | r"^[0-9]{4}-[0-9]{2}-[0-9]{2}\s+[0-9]{2}:[0-9]{2}(:[0-9]{2})?(\.[0-9]{1,9})?\s*(am|pm|AM|PM)?$", 216 | ) 217 | .unwrap(); 218 | } 219 | if !RE.is_match(input) { 220 | return None; 221 | } 222 | 223 | self.tz 224 | .datetime_from_str(input, "%Y-%m-%d %H:%M:%S") 225 | .or_else(|_| self.tz.datetime_from_str(input, "%Y-%m-%d %H:%M")) 226 | .or_else(|_| self.tz.datetime_from_str(input, "%Y-%m-%d %H:%M:%S%.f")) 227 | .or_else(|_| self.tz.datetime_from_str(input, "%Y-%m-%d %I:%M:%S %P")) 228 | .or_else(|_| self.tz.datetime_from_str(input, "%Y-%m-%d %I:%M %P")) 229 | .ok() 230 | .map(|parsed| parsed.with_timezone(&Utc)) 231 | .map(Ok) 232 | } 233 | 234 | // yyyy-mm-dd hh:mm:ss z 235 | // - 2017-11-25 13:31:15 PST 236 | // - 2017-11-25 13:31 PST 237 | // - 2014-12-16 06:20:00 UTC 238 | // - 2014-12-16 06:20:00 GMT 239 | // - 2014-04-26 13:13:43 +0800 240 | // - 2014-04-26 13:13:44 +09:00 241 | // - 2012-08-03 18:31:59.257000000 +0000 242 | // - 2015-09-30 18:48:56.35272715 UTC 243 | fn ymd_hms_z(&self, input: &str) -> Option>> { 244 | lazy_static! { 245 | static ref RE: Regex = Regex::new( 246 | r"^[0-9]{4}-[0-9]{2}-[0-9]{2}\s+[0-9]{2}:[0-9]{2}(:[0-9]{2})?(\.[0-9]{1,9})?(?P\s*[+-:a-zA-Z0-9]{3,6})$", 247 | ).unwrap(); 248 | } 249 | 250 | if !RE.is_match(input) { 251 | return None; 252 | } 253 | if let Some(caps) = RE.captures(input) { 254 | if let Some(matched_tz) = caps.name("tz") { 255 | let parse_from_str = NaiveDateTime::parse_from_str; 256 | return match timezone::parse(matched_tz.as_str().trim()) { 257 | Ok(offset) => parse_from_str(input, "%Y-%m-%d %H:%M:%S %Z") 258 | .or_else(|_| parse_from_str(input, "%Y-%m-%d %H:%M %Z")) 259 | .or_else(|_| parse_from_str(input, "%Y-%m-%d %H:%M:%S%.f %Z")) 260 | .ok() 261 | .and_then(|parsed| offset.from_local_datetime(&parsed).single()) 262 | .map(|datetime| datetime.with_timezone(&Utc)) 263 | .map(Ok), 264 | Err(err) => Some(Err(err)), 265 | }; 266 | } 267 | } 268 | None 269 | } 270 | 271 | // yyyy-mm-dd 272 | // - 2021-02-21 273 | fn ymd(&self, input: &str) -> Option>> { 274 | lazy_static! { 275 | static ref RE: Regex = Regex::new(r"^[0-9]{4}-[0-9]{2}-[0-9]{2}$").unwrap(); 276 | } 277 | 278 | if !RE.is_match(input) { 279 | return None; 280 | } 281 | 282 | // set time to use 283 | let time = match self.default_time { 284 | Some(v) => v, 285 | None => Utc::now().with_timezone(self.tz).time(), 286 | }; 287 | 288 | NaiveDate::parse_from_str(input, "%Y-%m-%d") 289 | .ok() 290 | .map(|parsed| parsed.and_time(time)) 291 | .and_then(|datetime| self.tz.from_local_datetime(&datetime).single()) 292 | .map(|at_tz| at_tz.with_timezone(&Utc)) 293 | .map(Ok) 294 | } 295 | 296 | // yyyy-mm-dd z 297 | // - 2021-02-21 PST 298 | // - 2021-02-21 UTC 299 | // - 2020-07-20+08:00 (yyyy-mm-dd-07:00) 300 | fn ymd_z(&self, input: &str) -> Option>> { 301 | lazy_static! { 302 | static ref RE: Regex = 303 | Regex::new(r"^[0-9]{4}-[0-9]{2}-[0-9]{2}(?P\s*[+-:a-zA-Z0-9]{3,6})$").unwrap(); 304 | } 305 | if !RE.is_match(input) { 306 | return None; 307 | } 308 | 309 | if let Some(caps) = RE.captures(input) { 310 | if let Some(matched_tz) = caps.name("tz") { 311 | return match timezone::parse(matched_tz.as_str().trim()) { 312 | Ok(offset) => { 313 | // set time to use 314 | let time = match self.default_time { 315 | Some(v) => v, 316 | None => Utc::now().with_timezone(&offset).time(), 317 | }; 318 | NaiveDate::parse_from_str(input, "%Y-%m-%d %Z") 319 | .ok() 320 | .map(|parsed| parsed.and_time(time)) 321 | .and_then(|datetime| offset.from_local_datetime(&datetime).single()) 322 | .map(|at_tz| at_tz.with_timezone(&Utc)) 323 | .map(Ok) 324 | } 325 | Err(err) => Some(Err(err)), 326 | }; 327 | } 328 | } 329 | None 330 | } 331 | 332 | // hh:mm:ss 333 | // - 01:06:06 334 | // - 4:00pm 335 | // - 6:00 AM 336 | fn hms(&self, input: &str) -> Option>> { 337 | lazy_static! { 338 | static ref RE: Regex = 339 | Regex::new(r"^[0-9]{1,2}:[0-9]{2}(:[0-9]{2})?\s*(am|pm|AM|PM)?$").unwrap(); 340 | } 341 | if !RE.is_match(input) { 342 | return None; 343 | } 344 | 345 | let now = Utc::now().with_timezone(self.tz); 346 | NaiveTime::parse_from_str(input, "%H:%M:%S") 347 | .or_else(|_| NaiveTime::parse_from_str(input, "%H:%M")) 348 | .or_else(|_| NaiveTime::parse_from_str(input, "%I:%M:%S %P")) 349 | .or_else(|_| NaiveTime::parse_from_str(input, "%I:%M %P")) 350 | .ok() 351 | .and_then(|parsed| now.date().and_time(parsed)) 352 | .map(|datetime| datetime.with_timezone(&Utc)) 353 | .map(Ok) 354 | } 355 | 356 | // hh:mm:ss z 357 | // - 01:06:06 PST 358 | // - 4:00pm PST 359 | // - 6:00 AM PST 360 | // - 6:00pm UTC 361 | fn hms_z(&self, input: &str) -> Option>> { 362 | lazy_static! { 363 | static ref RE: Regex = Regex::new( 364 | r"^[0-9]{1,2}:[0-9]{2}(:[0-9]{2})?\s*(am|pm|AM|PM)?(?P\s+[+-:a-zA-Z0-9]{3,6})$", 365 | ) 366 | .unwrap(); 367 | } 368 | if !RE.is_match(input) { 369 | return None; 370 | } 371 | 372 | if let Some(caps) = RE.captures(input) { 373 | if let Some(matched_tz) = caps.name("tz") { 374 | return match timezone::parse(matched_tz.as_str().trim()) { 375 | Ok(offset) => { 376 | let now = Utc::now().with_timezone(&offset); 377 | NaiveTime::parse_from_str(input, "%H:%M:%S %Z") 378 | .or_else(|_| NaiveTime::parse_from_str(input, "%H:%M %Z")) 379 | .or_else(|_| NaiveTime::parse_from_str(input, "%I:%M:%S %P %Z")) 380 | .or_else(|_| NaiveTime::parse_from_str(input, "%I:%M %P %Z")) 381 | .ok() 382 | .map(|parsed| now.date().naive_local().and_time(parsed)) 383 | .and_then(|datetime| offset.from_local_datetime(&datetime).single()) 384 | .map(|at_tz| at_tz.with_timezone(&Utc)) 385 | .map(Ok) 386 | } 387 | Err(err) => Some(Err(err)), 388 | }; 389 | } 390 | } 391 | None 392 | } 393 | 394 | // yyyy-mon-dd 395 | // - 2021-Feb-21 396 | fn month_ymd(&self, input: &str) -> Option>> { 397 | lazy_static! { 398 | static ref RE: Regex = Regex::new(r"^[0-9]{4}-[a-zA-Z]{3,9}-[0-9]{2}$").unwrap(); 399 | } 400 | if !RE.is_match(input) { 401 | return None; 402 | } 403 | 404 | // set time to use 405 | let time = match self.default_time { 406 | Some(v) => v, 407 | None => Utc::now().with_timezone(self.tz).time(), 408 | }; 409 | 410 | NaiveDate::parse_from_str(input, "%Y-%m-%d") 411 | .or_else(|_| NaiveDate::parse_from_str(input, "%Y-%b-%d")) 412 | .ok() 413 | .map(|parsed| parsed.and_time(time)) 414 | .and_then(|datetime| self.tz.from_local_datetime(&datetime).single()) 415 | .map(|at_tz| at_tz.with_timezone(&Utc)) 416 | .map(Ok) 417 | } 418 | 419 | // Mon dd hh:mm:ss 420 | // - May 6 at 9:24 PM 421 | // - May 27 02:45:27 422 | fn month_md_hms(&self, input: &str) -> Option>> { 423 | lazy_static! { 424 | static ref RE: Regex = Regex::new( 425 | r"^[a-zA-Z]{3}\s+[0-9]{1,2}\s*(at)?\s+[0-9]{1,2}:[0-9]{2}(:[0-9]{2})?\s*(am|pm|AM|PM)?$", 426 | ) 427 | .unwrap(); 428 | } 429 | if !RE.is_match(input) { 430 | return None; 431 | } 432 | 433 | let now = Utc::now().with_timezone(self.tz); 434 | let with_year = format!("{} {}", now.year(), input); 435 | self.tz 436 | .datetime_from_str(&with_year, "%Y %b %d at %I:%M %P") 437 | .or_else(|_| self.tz.datetime_from_str(&with_year, "%Y %b %d %H:%M:%S")) 438 | .ok() 439 | .map(|parsed| parsed.with_timezone(&Utc)) 440 | .map(Ok) 441 | } 442 | 443 | // Mon dd, yyyy, hh:mm:ss 444 | // - May 8, 2009 5:57:51 PM 445 | // - September 17, 2012 10:09am 446 | // - September 17, 2012, 10:10:09 447 | fn month_mdy_hms(&self, input: &str) -> Option>> { 448 | lazy_static! { 449 | static ref RE: Regex = Regex::new( 450 | r"^[a-zA-Z]{3,9}\.?\s+[0-9]{1,2},\s+[0-9]{2,4},?\s+[0-9]{1,2}:[0-9]{2}(:[0-9]{2})?\s*(am|pm|AM|PM)?$", 451 | ).unwrap(); 452 | } 453 | if !RE.is_match(input) { 454 | return None; 455 | } 456 | 457 | let dt = input.replace(", ", " ").replace(". ", " "); 458 | self.tz 459 | .datetime_from_str(&dt, "%B %d %Y %H:%M:%S") 460 | .or_else(|_| self.tz.datetime_from_str(&dt, "%B %d %Y %H:%M")) 461 | .or_else(|_| self.tz.datetime_from_str(&dt, "%B %d %Y %I:%M:%S %P")) 462 | .or_else(|_| self.tz.datetime_from_str(&dt, "%B %d %Y %I:%M %P")) 463 | .ok() 464 | .map(|at_tz| at_tz.with_timezone(&Utc)) 465 | .map(Ok) 466 | } 467 | 468 | // Mon dd, yyyy hh:mm:ss z 469 | // - May 02, 2021 15:51:31 UTC 470 | // - May 02, 2021 15:51 UTC 471 | // - May 26, 2021, 12:49 AM PDT 472 | // - September 17, 2012 at 10:09am PST 473 | fn month_mdy_hms_z(&self, input: &str) -> Option>> { 474 | lazy_static! { 475 | static ref RE: Regex = Regex::new( 476 | r"^[a-zA-Z]{3,9}\s+[0-9]{1,2},?\s+[0-9]{4}\s*,?(at)?\s+[0-9]{2}:[0-9]{2}(:[0-9]{2})?\s*(am|pm|AM|PM)?(?P\s+[+-:a-zA-Z0-9]{3,6})$", 477 | ).unwrap(); 478 | } 479 | if !RE.is_match(input) { 480 | return None; 481 | } 482 | 483 | if let Some(caps) = RE.captures(input) { 484 | if let Some(matched_tz) = caps.name("tz") { 485 | let parse_from_str = NaiveDateTime::parse_from_str; 486 | return match timezone::parse(matched_tz.as_str().trim()) { 487 | Ok(offset) => { 488 | let dt = input.replace(',', "").replace("at", ""); 489 | parse_from_str(&dt, "%B %d %Y %H:%M:%S %Z") 490 | .or_else(|_| parse_from_str(&dt, "%B %d %Y %H:%M %Z")) 491 | .or_else(|_| parse_from_str(&dt, "%B %d %Y %I:%M:%S %P %Z")) 492 | .or_else(|_| parse_from_str(&dt, "%B %d %Y %I:%M %P %Z")) 493 | .ok() 494 | .and_then(|parsed| offset.from_local_datetime(&parsed).single()) 495 | .map(|datetime| datetime.with_timezone(&Utc)) 496 | .map(Ok) 497 | } 498 | Err(err) => Some(Err(err)), 499 | }; 500 | } 501 | } 502 | None 503 | } 504 | 505 | // Mon dd, yyyy 506 | // - May 25, 2021 507 | // - oct 7, 1970 508 | // - oct 7, 70 509 | // - oct. 7, 1970 510 | // - oct. 7, 70 511 | // - October 7, 1970 512 | fn month_mdy(&self, input: &str) -> Option>> { 513 | lazy_static! { 514 | static ref RE: Regex = 515 | Regex::new(r"^[a-zA-Z]{3,9}\.?\s+[0-9]{1,2},\s+[0-9]{2,4}$").unwrap(); 516 | } 517 | if !RE.is_match(input) { 518 | return None; 519 | } 520 | 521 | // set time to use 522 | let time = match self.default_time { 523 | Some(v) => v, 524 | None => Utc::now().with_timezone(self.tz).time(), 525 | }; 526 | 527 | let dt = input.replace(", ", " ").replace(". ", " "); 528 | NaiveDate::parse_from_str(&dt, "%B %d %y") 529 | .or_else(|_| NaiveDate::parse_from_str(&dt, "%B %d %Y")) 530 | .ok() 531 | .map(|parsed| parsed.and_time(time)) 532 | .and_then(|datetime| self.tz.from_local_datetime(&datetime).single()) 533 | .map(|at_tz| at_tz.with_timezone(&Utc)) 534 | .map(Ok) 535 | } 536 | 537 | // dd Mon yyyy hh:mm:ss 538 | // - 12 Feb 2006, 19:17 539 | // - 12 Feb 2006 19:17 540 | // - 14 May 2019 19:11:40.164 541 | fn month_dmy_hms(&self, input: &str) -> Option>> { 542 | lazy_static! { 543 | static ref RE: Regex = Regex::new( 544 | r"^[0-9]{1,2}\s+[a-zA-Z]{3,9}\s+[0-9]{2,4},?\s+[0-9]{1,2}:[0-9]{2}(:[0-9]{2})?(\.[0-9]{1,9})?$", 545 | ).unwrap(); 546 | } 547 | if !RE.is_match(input) { 548 | return None; 549 | } 550 | 551 | let dt = input.replace(", ", " "); 552 | self.tz 553 | .datetime_from_str(&dt, "%d %B %Y %H:%M:%S") 554 | .or_else(|_| self.tz.datetime_from_str(&dt, "%d %B %Y %H:%M")) 555 | .or_else(|_| self.tz.datetime_from_str(&dt, "%d %B %Y %H:%M:%S%.f")) 556 | .or_else(|_| self.tz.datetime_from_str(&dt, "%d %B %Y %I:%M:%S %P")) 557 | .or_else(|_| self.tz.datetime_from_str(&dt, "%d %B %Y %I:%M %P")) 558 | .ok() 559 | .map(|at_tz| at_tz.with_timezone(&Utc)) 560 | .map(Ok) 561 | } 562 | 563 | // dd Mon yyyy 564 | // - 7 oct 70 565 | // - 7 oct 1970 566 | // - 03 February 2013 567 | // - 1 July 2013 568 | fn month_dmy(&self, input: &str) -> Option>> { 569 | lazy_static! { 570 | static ref RE: Regex = 571 | Regex::new(r"^[0-9]{1,2}\s+[a-zA-Z]{3,9}\s+[0-9]{2,4}$").unwrap(); 572 | } 573 | if !RE.is_match(input) { 574 | return None; 575 | } 576 | 577 | // set time to use 578 | let time = match self.default_time { 579 | Some(v) => v, 580 | None => Utc::now().with_timezone(self.tz).time(), 581 | }; 582 | 583 | NaiveDate::parse_from_str(input, "%d %B %y") 584 | .or_else(|_| NaiveDate::parse_from_str(input, "%d %B %Y")) 585 | .ok() 586 | .map(|parsed| parsed.and_time(time)) 587 | .and_then(|datetime| self.tz.from_local_datetime(&datetime).single()) 588 | .map(|at_tz| at_tz.with_timezone(&Utc)) 589 | .map(Ok) 590 | } 591 | 592 | // mm/dd/yyyy hh:mm:ss 593 | // - 4/8/2014 22:05 594 | // - 04/08/2014 22:05 595 | // - 4/8/14 22:05 596 | // - 04/2/2014 03:00:51 597 | // - 8/8/1965 12:00:00 AM 598 | // - 8/8/1965 01:00:01 PM 599 | // - 8/8/1965 01:00 PM 600 | // - 8/8/1965 1:00 PM 601 | // - 8/8/1965 12:00 AM 602 | // - 4/02/2014 03:00:51 603 | // - 03/19/2012 10:11:59 604 | // - 03/19/2012 10:11:59.3186369 605 | fn slash_mdy_hms(&self, input: &str) -> Option>> { 606 | lazy_static! { 607 | static ref RE: Regex = Regex::new( 608 | r"^[0-9]{1,2}/[0-9]{1,2}/[0-9]{2,4}\s+[0-9]{1,2}:[0-9]{2}(:[0-9]{2})?(\.[0-9]{1,9})?\s*(am|pm|AM|PM)?$" 609 | ) 610 | .unwrap(); 611 | } 612 | if !RE.is_match(input) { 613 | return None; 614 | } 615 | 616 | self.tz 617 | .datetime_from_str(input, "%m/%d/%y %H:%M:%S") 618 | .or_else(|_| self.tz.datetime_from_str(input, "%m/%d/%y %H:%M")) 619 | .or_else(|_| self.tz.datetime_from_str(input, "%m/%d/%y %H:%M:%S%.f")) 620 | .or_else(|_| self.tz.datetime_from_str(input, "%m/%d/%y %I:%M:%S %P")) 621 | .or_else(|_| self.tz.datetime_from_str(input, "%m/%d/%y %I:%M %P")) 622 | .or_else(|_| self.tz.datetime_from_str(input, "%m/%d/%Y %H:%M:%S")) 623 | .or_else(|_| self.tz.datetime_from_str(input, "%m/%d/%Y %H:%M")) 624 | .or_else(|_| self.tz.datetime_from_str(input, "%m/%d/%Y %H:%M:%S%.f")) 625 | .or_else(|_| self.tz.datetime_from_str(input, "%m/%d/%Y %I:%M:%S %P")) 626 | .or_else(|_| self.tz.datetime_from_str(input, "%m/%d/%Y %I:%M %P")) 627 | .ok() 628 | .map(|at_tz| at_tz.with_timezone(&Utc)) 629 | .map(Ok) 630 | } 631 | 632 | // mm/dd/yyyy 633 | // - 3/31/2014 634 | // - 03/31/2014 635 | // - 08/21/71 636 | // - 8/1/71 637 | fn slash_mdy(&self, input: &str) -> Option>> { 638 | lazy_static! { 639 | static ref RE: Regex = Regex::new(r"^[0-9]{1,2}/[0-9]{1,2}/[0-9]{2,4}$").unwrap(); 640 | } 641 | if !RE.is_match(input) { 642 | return None; 643 | } 644 | 645 | // set time to use 646 | let time = match self.default_time { 647 | Some(v) => v, 648 | None => Utc::now().with_timezone(self.tz).time(), 649 | }; 650 | 651 | NaiveDate::parse_from_str(input, "%m/%d/%y") 652 | .or_else(|_| NaiveDate::parse_from_str(input, "%m/%d/%Y")) 653 | .ok() 654 | .map(|parsed| parsed.and_time(time)) 655 | .and_then(|datetime| self.tz.from_local_datetime(&datetime).single()) 656 | .map(|at_tz| at_tz.with_timezone(&Utc)) 657 | .map(Ok) 658 | } 659 | 660 | // yyyy/mm/dd hh:mm:ss 661 | // - 2014/4/8 22:05 662 | // - 2014/04/08 22:05 663 | // - 2014/04/2 03:00:51 664 | // - 2014/4/02 03:00:51 665 | // - 2012/03/19 10:11:59 666 | // - 2012/03/19 10:11:59.3186369 667 | fn slash_ymd_hms(&self, input: &str) -> Option>> { 668 | lazy_static! { 669 | static ref RE: Regex = Regex::new( 670 | r"^[0-9]{4}/[0-9]{1,2}/[0-9]{1,2}\s+[0-9]{1,2}:[0-9]{2}(:[0-9]{2})?(\.[0-9]{1,9})?\s*(am|pm|AM|PM)?$" 671 | ) 672 | .unwrap(); 673 | } 674 | if !RE.is_match(input) { 675 | return None; 676 | } 677 | 678 | self.tz 679 | .datetime_from_str(input, "%Y/%m/%d %H:%M:%S") 680 | .or_else(|_| self.tz.datetime_from_str(input, "%Y/%m/%d %H:%M")) 681 | .or_else(|_| self.tz.datetime_from_str(input, "%Y/%m/%d %H:%M:%S%.f")) 682 | .or_else(|_| self.tz.datetime_from_str(input, "%Y/%m/%d %I:%M:%S %P")) 683 | .or_else(|_| self.tz.datetime_from_str(input, "%Y/%m/%d %I:%M %P")) 684 | .ok() 685 | .map(|at_tz| at_tz.with_timezone(&Utc)) 686 | .map(Ok) 687 | } 688 | 689 | // yyyy/mm/dd 690 | // - 2014/3/31 691 | // - 2014/03/31 692 | fn slash_ymd(&self, input: &str) -> Option>> { 693 | lazy_static! { 694 | static ref RE: Regex = Regex::new(r"^[0-9]{4}/[0-9]{1,2}/[0-9]{1,2}$").unwrap(); 695 | } 696 | if !RE.is_match(input) { 697 | return None; 698 | } 699 | 700 | // set time to use 701 | let time = match self.default_time { 702 | Some(v) => v, 703 | None => Utc::now().with_timezone(self.tz).time(), 704 | }; 705 | 706 | NaiveDate::parse_from_str(input, "%Y/%m/%d") 707 | .ok() 708 | .map(|parsed| parsed.and_time(time)) 709 | .and_then(|datetime| self.tz.from_local_datetime(&datetime).single()) 710 | .map(|at_tz| at_tz.with_timezone(&Utc)) 711 | .map(Ok) 712 | } 713 | 714 | // mm-dd-yyyy 715 | // - 3-31-2014 716 | // - 03-3-2014 717 | // - 08-21-71 718 | // - 8-1-71 719 | fn hyphen_mdy(&self, input: &str) -> Option>> { 720 | lazy_static! { 721 | static ref RE: Regex = Regex::new(r"^[0-9]{1,2}-[0-9]{1,2}-[0-9]{2,4}$").unwrap(); 722 | } 723 | if !RE.is_match(input) { 724 | return None; 725 | } 726 | 727 | // set time to use 728 | let time = match self.default_time { 729 | Some(v) => v, 730 | None => Utc::now().with_timezone(self.tz).time(), 731 | }; 732 | 733 | NaiveDate::parse_from_str(input, "%m-%d-%y") 734 | .or_else(|_| NaiveDate::parse_from_str(input, "%m-%d-%Y")) 735 | .ok() 736 | .map(|parsed| parsed.and_time(time)) 737 | .and_then(|datetime| self.tz.from_local_datetime(&datetime).single()) 738 | .map(|at_tz| at_tz.with_timezone(&Utc)) 739 | .map(Ok) 740 | } 741 | 742 | // mm-dd-yyyy hh:mm:ss 743 | // - 4-8-2014 22:05 744 | // - 04-08-2014 22:05 745 | // - 4-8-14 22:05 746 | // - 04-2-2014 03:00:51 747 | // - 8-8-1965 12:00:00 AM 748 | // - 8-8-1965 01:00:01 PM 749 | // - 8-8-1965 01:00 PM 750 | // - 8-8-1965 1:00 PM 751 | // - 8-8-1965 12:00 AM 752 | // - 4-02-2014 03:00:51 753 | // - 03-19-2012 10:11:59 754 | // - 03-19-2012 10:11:59.3186369 755 | fn hyphen_mdy_hms(&self, input: &str) -> Option>> { 756 | lazy_static! { 757 | static ref RE: Regex = Regex::new( 758 | r"^[0-9]{1,2}-[0-9]{1,2}-[0-9]{2,4}\s+[0-9]{1,2}:[0-9]{2}(:[0-9]{2})?(\.[0-9]{1,9})?\s*(am|pm|AM|PM)?$" 759 | ) 760 | .unwrap(); 761 | } 762 | if !RE.is_match(input) { 763 | return None; 764 | } 765 | 766 | self.tz 767 | .datetime_from_str(input, "%m-%d-%y %H:%M:%S") 768 | .or_else(|_| self.tz.datetime_from_str(input, "%m-%d-%y %H:%M")) 769 | .or_else(|_| self.tz.datetime_from_str(input, "%m-%d-%y %H:%M:%S%.f")) 770 | .or_else(|_| self.tz.datetime_from_str(input, "%m-%d-%y %I:%M:%S %P")) 771 | .or_else(|_| self.tz.datetime_from_str(input, "%m-%d-%y %I:%M %P")) 772 | .or_else(|_| self.tz.datetime_from_str(input, "%m-%d-%Y %H:%M:%S")) 773 | .or_else(|_| self.tz.datetime_from_str(input, "%m-%d-%Y %H:%M")) 774 | .or_else(|_| self.tz.datetime_from_str(input, "%m-%d-%Y %H:%M:%S%.f")) 775 | .or_else(|_| self.tz.datetime_from_str(input, "%m-%d-%Y %I:%M:%S %P")) 776 | .or_else(|_| self.tz.datetime_from_str(input, "%m-%d-%Y %I:%M %P")) 777 | .ok() 778 | .map(|at_tz| at_tz.with_timezone(&Utc)) 779 | .map(Ok) 780 | } 781 | 782 | // mm.dd.yyyy 783 | // - 3.31.2014 784 | // - 03.31.2014 785 | // - 08.21.71 786 | // yyyy.mm.dd 787 | // - 2014.03.30 788 | // - 2014.03 789 | fn dot_mdy_or_ymd(&self, input: &str) -> Option>> { 790 | lazy_static! { 791 | static ref RE: Regex = Regex::new(r"[0-9]{1,4}.[0-9]{1,4}[0-9]{1,4}").unwrap(); 792 | } 793 | if !RE.is_match(input) { 794 | return None; 795 | } 796 | 797 | // set time to use 798 | let time = match self.default_time { 799 | Some(v) => v, 800 | None => Utc::now().with_timezone(self.tz).time(), 801 | }; 802 | 803 | NaiveDate::parse_from_str(input, "%m.%d.%y") 804 | .or_else(|_| NaiveDate::parse_from_str(input, "%m.%d.%Y")) 805 | .or_else(|_| NaiveDate::parse_from_str(input, "%Y.%m.%d")) 806 | .or_else(|_| { 807 | NaiveDate::parse_from_str(&format!("{}.{}", input, Utc::now().day()), "%Y.%m.%d") 808 | }) 809 | .ok() 810 | .map(|parsed| parsed.and_time(time)) 811 | .and_then(|datetime| self.tz.from_local_datetime(&datetime).single()) 812 | .map(|at_tz| at_tz.with_timezone(&Utc)) 813 | .map(Ok) 814 | } 815 | 816 | // yymmdd hh:mm:ss mysql log 817 | // - 171113 14:14:20 818 | fn mysql_log_timestamp(&self, input: &str) -> Option>> { 819 | lazy_static! { 820 | static ref RE: Regex = Regex::new(r"[0-9]{6}\s+[0-9]{2}:[0-9]{2}:[0-9]{2}").unwrap(); 821 | } 822 | if !RE.is_match(input) { 823 | return None; 824 | } 825 | 826 | self.tz 827 | .datetime_from_str(input, "%y%m%d %H:%M:%S") 828 | .ok() 829 | .map(|at_tz| at_tz.with_timezone(&Utc)) 830 | .map(Ok) 831 | } 832 | 833 | // chinese yyyy mm dd hh mm ss 834 | // - 2014年04月08日11时25分18秒 835 | fn chinese_ymd_hms(&self, input: &str) -> Option>> { 836 | lazy_static! { 837 | static ref RE: Regex = 838 | Regex::new(r"^[0-9]{4}年[0-9]{2}月[0-9]{2}日[0-9]{2}时[0-9]{2}分[0-9]{2}秒$") 839 | .unwrap(); 840 | } 841 | if !RE.is_match(input) { 842 | return None; 843 | } 844 | 845 | self.tz 846 | .datetime_from_str(input, "%Y年%m月%d日%H时%M分%S秒") 847 | .ok() 848 | .map(|at_tz| at_tz.with_timezone(&Utc)) 849 | .map(Ok) 850 | } 851 | 852 | // chinese yyyy mm dd 853 | // - 2014年04月08日 854 | fn chinese_ymd(&self, input: &str) -> Option>> { 855 | lazy_static! { 856 | static ref RE: Regex = Regex::new(r"^[0-9]{4}年[0-9]{2}月[0-9]{2}日$").unwrap(); 857 | } 858 | if !RE.is_match(input) { 859 | return None; 860 | } 861 | 862 | // set time to use 863 | let time = match self.default_time { 864 | Some(v) => v, 865 | None => Utc::now().with_timezone(self.tz).time(), 866 | }; 867 | 868 | NaiveDate::parse_from_str(input, "%Y年%m月%d日") 869 | .ok() 870 | .map(|parsed| parsed.and_time(time)) 871 | .and_then(|datetime| self.tz.from_local_datetime(&datetime).single()) 872 | .map(|at_tz| at_tz.with_timezone(&Utc)) 873 | .map(Ok) 874 | } 875 | } 876 | 877 | #[cfg(test)] 878 | mod tests { 879 | use super::*; 880 | 881 | #[test] 882 | fn unix_timestamp() { 883 | let parse = Parse::new(&Utc, None); 884 | 885 | let test_cases = [ 886 | ("0000000000", Utc.ymd(1970, 1, 1).and_hms(0, 0, 0)), 887 | ("0000000000000", Utc.ymd(1970, 1, 1).and_hms(0, 0, 0)), 888 | ("0000000000000000000", Utc.ymd(1970, 1, 1).and_hms(0, 0, 0)), 889 | ("1511648546", Utc.ymd(2017, 11, 25).and_hms(22, 22, 26)), 890 | ( 891 | "1620021848429", 892 | Utc.ymd(2021, 5, 3).and_hms_milli(6, 4, 8, 429), 893 | ), 894 | ( 895 | "1620024872717915000", 896 | Utc.ymd(2021, 5, 3).and_hms_nano(6, 54, 32, 717915000), 897 | ), 898 | ]; 899 | 900 | for &(input, want) in test_cases.iter() { 901 | assert_eq!( 902 | parse.unix_timestamp(input).unwrap().unwrap(), 903 | want, 904 | "unix_timestamp/{}", 905 | input 906 | ) 907 | } 908 | assert!(parse.unix_timestamp("15116").is_none()); 909 | assert!(parse 910 | .unix_timestamp("16200248727179150001620024872717915000") 911 | .is_none()); 912 | assert!(parse.unix_timestamp("not-a-ts").is_none()); 913 | } 914 | 915 | #[test] 916 | fn rfc3339() { 917 | let parse = Parse::new(&Utc, None); 918 | 919 | let test_cases = [ 920 | ( 921 | "2021-05-01T01:17:02.604456Z", 922 | Utc.ymd(2021, 5, 1).and_hms_nano(1, 17, 2, 604456000), 923 | ), 924 | ( 925 | "2017-11-25T22:34:50Z", 926 | Utc.ymd(2017, 11, 25).and_hms(22, 34, 50), 927 | ), 928 | ]; 929 | 930 | for &(input, want) in test_cases.iter() { 931 | assert_eq!( 932 | parse.rfc3339(input).unwrap().unwrap(), 933 | want, 934 | "rfc3339/{}", 935 | input 936 | ) 937 | } 938 | assert!(parse.rfc3339("2017-11-25 22:34:50").is_none()); 939 | assert!(parse.rfc3339("not-date-time").is_none()); 940 | } 941 | 942 | #[test] 943 | fn rfc2822() { 944 | let parse = Parse::new(&Utc, None); 945 | 946 | let test_cases = [ 947 | ( 948 | "Wed, 02 Jun 2021 06:31:39 GMT", 949 | Utc.ymd(2021, 6, 2).and_hms(6, 31, 39), 950 | ), 951 | ( 952 | "Wed, 02 Jun 2021 06:31:39 PDT", 953 | Utc.ymd(2021, 6, 2).and_hms(13, 31, 39), 954 | ), 955 | ]; 956 | 957 | for &(input, want) in test_cases.iter() { 958 | assert_eq!( 959 | parse.rfc2822(input).unwrap().unwrap(), 960 | want, 961 | "rfc2822/{}", 962 | input 963 | ) 964 | } 965 | assert!(parse.rfc2822("02 Jun 2021 06:31:39").is_none()); 966 | assert!(parse.rfc2822("not-date-time").is_none()); 967 | } 968 | 969 | #[test] 970 | fn postgres_timestamp() { 971 | let parse = Parse::new(&Utc, None); 972 | 973 | let test_cases = [ 974 | ( 975 | "2019-11-29 08:08-08", 976 | Utc.ymd(2019, 11, 29).and_hms(16, 8, 0), 977 | ), 978 | ( 979 | "2019-11-29 08:08:05-08", 980 | Utc.ymd(2019, 11, 29).and_hms(16, 8, 5), 981 | ), 982 | ( 983 | "2021-05-02 23:31:36.0741-07", 984 | Utc.ymd(2021, 5, 3).and_hms_micro(6, 31, 36, 74100), 985 | ), 986 | ( 987 | "2021-05-02 23:31:39.12689-07", 988 | Utc.ymd(2021, 5, 3).and_hms_micro(6, 31, 39, 126890), 989 | ), 990 | ( 991 | "2019-11-29 08:15:47.624504-08", 992 | Utc.ymd(2019, 11, 29).and_hms_micro(16, 15, 47, 624504), 993 | ), 994 | ( 995 | "2017-07-19 03:21:51+00:00", 996 | Utc.ymd(2017, 7, 19).and_hms(3, 21, 51), 997 | ), 998 | ]; 999 | 1000 | for &(input, want) in test_cases.iter() { 1001 | assert_eq!( 1002 | parse.postgres_timestamp(input).unwrap().unwrap(), 1003 | want, 1004 | "postgres_timestamp/{}", 1005 | input 1006 | ) 1007 | } 1008 | assert!(parse.postgres_timestamp("not-date-time").is_none()); 1009 | } 1010 | 1011 | #[test] 1012 | fn ymd_hms() { 1013 | let parse = Parse::new(&Utc, None); 1014 | 1015 | let test_cases = vec![ 1016 | ("2021-04-30 21:14", Utc.ymd(2021, 4, 30).and_hms(21, 14, 0)), 1017 | ( 1018 | "2021-04-30 21:14:10", 1019 | Utc.ymd(2021, 4, 30).and_hms(21, 14, 10), 1020 | ), 1021 | ( 1022 | "2021-04-30 21:14:10.052282", 1023 | Utc.ymd(2021, 4, 30).and_hms_micro(21, 14, 10, 52282), 1024 | ), 1025 | ( 1026 | "2014-04-26 05:24:37 PM", 1027 | Utc.ymd(2014, 4, 26).and_hms(17, 24, 37), 1028 | ), 1029 | ( 1030 | "2014-04-26 17:24:37.123", 1031 | Utc.ymd(2014, 4, 26).and_hms_milli(17, 24, 37, 123), 1032 | ), 1033 | ( 1034 | "2014-04-26 17:24:37.3186369", 1035 | Utc.ymd(2014, 4, 26).and_hms_nano(17, 24, 37, 318636900), 1036 | ), 1037 | ( 1038 | "2012-08-03 18:31:59.257000000", 1039 | Utc.ymd(2012, 8, 3).and_hms_nano(18, 31, 59, 257000000), 1040 | ), 1041 | ]; 1042 | 1043 | for &(input, want) in test_cases.iter() { 1044 | assert_eq!( 1045 | parse.ymd_hms(input).unwrap().unwrap(), 1046 | want, 1047 | "ymd_hms/{}", 1048 | input 1049 | ) 1050 | } 1051 | assert!(parse.ymd_hms("not-date-time").is_none()); 1052 | } 1053 | 1054 | #[test] 1055 | fn ymd_hms_z() { 1056 | let parse = Parse::new(&Utc, None); 1057 | 1058 | let test_cases = vec![ 1059 | ( 1060 | "2017-11-25 13:31:15 PST", 1061 | Utc.ymd(2017, 11, 25).and_hms(21, 31, 15), 1062 | ), 1063 | ( 1064 | "2017-11-25 13:31 PST", 1065 | Utc.ymd(2017, 11, 25).and_hms(21, 31, 0), 1066 | ), 1067 | ( 1068 | "2014-12-16 06:20:00 UTC", 1069 | Utc.ymd(2014, 12, 16).and_hms(6, 20, 0), 1070 | ), 1071 | ( 1072 | "2014-12-16 06:20:00 GMT", 1073 | Utc.ymd(2014, 12, 16).and_hms(6, 20, 0), 1074 | ), 1075 | ( 1076 | "2014-04-26 13:13:43 +0800", 1077 | Utc.ymd(2014, 4, 26).and_hms(5, 13, 43), 1078 | ), 1079 | ( 1080 | "2014-04-26 13:13:44 +09:00", 1081 | Utc.ymd(2014, 4, 26).and_hms(4, 13, 44), 1082 | ), 1083 | ( 1084 | "2012-08-03 18:31:59.257000000 +0000", 1085 | Utc.ymd(2012, 8, 3).and_hms_nano(18, 31, 59, 257000000), 1086 | ), 1087 | ( 1088 | "2015-09-30 18:48:56.35272715 UTC", 1089 | Utc.ymd(2015, 9, 30).and_hms_nano(18, 48, 56, 352727150), 1090 | ), 1091 | ]; 1092 | 1093 | for &(input, want) in test_cases.iter() { 1094 | assert_eq!( 1095 | parse.ymd_hms_z(input).unwrap().unwrap(), 1096 | want, 1097 | "ymd_hms_z/{}", 1098 | input 1099 | ) 1100 | } 1101 | assert!(parse.ymd_hms_z("not-date-time").is_none()); 1102 | } 1103 | 1104 | #[test] 1105 | fn ymd() { 1106 | let parse = Parse::new(&Utc, Some(Utc::now().time())); 1107 | 1108 | let test_cases = [( 1109 | "2021-02-21", 1110 | Utc.ymd(2021, 2, 21).and_time(Utc::now().time()), 1111 | )]; 1112 | 1113 | for &(input, want) in test_cases.iter() { 1114 | assert_eq!( 1115 | parse 1116 | .ymd(input) 1117 | .unwrap() 1118 | .unwrap() 1119 | .trunc_subsecs(0) 1120 | .with_second(0) 1121 | .unwrap(), 1122 | want.unwrap().trunc_subsecs(0).with_second(0).unwrap(), 1123 | "ymd/{}", 1124 | input 1125 | ) 1126 | } 1127 | assert!(parse.ymd("not-date-time").is_none()); 1128 | } 1129 | 1130 | #[test] 1131 | fn ymd_z() { 1132 | let parse = Parse::new(&Utc, None); 1133 | let now_at_pst = Utc::now().with_timezone(&FixedOffset::west(8 * 3600)); 1134 | let now_at_cst = Utc::now().with_timezone(&FixedOffset::east(8 * 3600)); 1135 | 1136 | let test_cases = [ 1137 | ( 1138 | "2021-02-21 PST", 1139 | FixedOffset::west(8 * 3600) 1140 | .ymd(2021, 2, 21) 1141 | .and_time(now_at_pst.time()) 1142 | .map(|dt| dt.with_timezone(&Utc)), 1143 | ), 1144 | ( 1145 | "2021-02-21 UTC", 1146 | FixedOffset::west(0) 1147 | .ymd(2021, 2, 21) 1148 | .and_time(Utc::now().time()) 1149 | .map(|dt| dt.with_timezone(&Utc)), 1150 | ), 1151 | ( 1152 | "2020-07-20+08:00", 1153 | FixedOffset::east(8 * 3600) 1154 | .ymd(2020, 7, 20) 1155 | .and_time(now_at_cst.time()) 1156 | .map(|dt| dt.with_timezone(&Utc)), 1157 | ), 1158 | ]; 1159 | 1160 | for &(input, want) in test_cases.iter() { 1161 | assert_eq!( 1162 | parse 1163 | .ymd_z(input) 1164 | .unwrap() 1165 | .unwrap() 1166 | .trunc_subsecs(0) 1167 | .with_second(0) 1168 | .unwrap(), 1169 | want.unwrap().trunc_subsecs(0).with_second(0).unwrap(), 1170 | "ymd_z/{}", 1171 | input 1172 | ) 1173 | } 1174 | assert!(parse.ymd_z("not-date-time").is_none()); 1175 | } 1176 | 1177 | #[test] 1178 | fn hms() { 1179 | let parse = Parse::new(&Utc, None); 1180 | 1181 | let test_cases = [ 1182 | ( 1183 | "01:06:06", 1184 | Utc::now().date().and_time(NaiveTime::from_hms(1, 6, 6)), 1185 | ), 1186 | ( 1187 | "4:00pm", 1188 | Utc::now().date().and_time(NaiveTime::from_hms(16, 0, 0)), 1189 | ), 1190 | ( 1191 | "6:00 AM", 1192 | Utc::now().date().and_time(NaiveTime::from_hms(6, 0, 0)), 1193 | ), 1194 | ]; 1195 | 1196 | for &(input, want) in test_cases.iter() { 1197 | assert_eq!( 1198 | parse.hms(input).unwrap().unwrap(), 1199 | want.unwrap(), 1200 | "hms/{}", 1201 | input 1202 | ) 1203 | } 1204 | assert!(parse.hms("not-date-time").is_none()); 1205 | } 1206 | 1207 | #[test] 1208 | fn hms_z() { 1209 | let parse = Parse::new(&Utc, None); 1210 | let now_at_pst = Utc::now().with_timezone(&FixedOffset::west(8 * 3600)); 1211 | 1212 | let test_cases = [ 1213 | ( 1214 | "01:06:06 PST", 1215 | FixedOffset::west(8 * 3600) 1216 | .from_local_date(&now_at_pst.date().naive_local()) 1217 | .and_time(NaiveTime::from_hms(1, 6, 6)) 1218 | .map(|dt| dt.with_timezone(&Utc)), 1219 | ), 1220 | ( 1221 | "4:00pm PST", 1222 | FixedOffset::west(8 * 3600) 1223 | .from_local_date(&now_at_pst.date().naive_local()) 1224 | .and_time(NaiveTime::from_hms(16, 0, 0)) 1225 | .map(|dt| dt.with_timezone(&Utc)), 1226 | ), 1227 | ( 1228 | "6:00 AM PST", 1229 | FixedOffset::west(8 * 3600) 1230 | .from_local_date(&now_at_pst.date().naive_local()) 1231 | .and_time(NaiveTime::from_hms(6, 0, 0)) 1232 | .map(|dt| dt.with_timezone(&Utc)), 1233 | ), 1234 | ( 1235 | "6:00pm UTC", 1236 | FixedOffset::west(0) 1237 | .from_local_date(&Utc::now().date().naive_local()) 1238 | .and_time(NaiveTime::from_hms(18, 0, 0)) 1239 | .map(|dt| dt.with_timezone(&Utc)), 1240 | ), 1241 | ]; 1242 | 1243 | for &(input, want) in test_cases.iter() { 1244 | assert_eq!( 1245 | parse.hms_z(input).unwrap().unwrap(), 1246 | want.unwrap(), 1247 | "hms_z/{}", 1248 | input 1249 | ) 1250 | } 1251 | assert!(parse.hms_z("not-date-time").is_none()); 1252 | } 1253 | 1254 | #[test] 1255 | fn month_ymd() { 1256 | let parse = Parse::new(&Utc, None); 1257 | 1258 | let test_cases = [( 1259 | "2021-Feb-21", 1260 | Utc.ymd(2021, 2, 21).and_time(Utc::now().time()), 1261 | )]; 1262 | 1263 | for &(input, want) in test_cases.iter() { 1264 | assert_eq!( 1265 | parse 1266 | .month_ymd(input) 1267 | .unwrap() 1268 | .unwrap() 1269 | .trunc_subsecs(0) 1270 | .with_second(0) 1271 | .unwrap(), 1272 | want.unwrap().trunc_subsecs(0).with_second(0).unwrap(), 1273 | "month_ymd/{}", 1274 | input 1275 | ) 1276 | } 1277 | assert!(parse.month_ymd("not-date-time").is_none()); 1278 | } 1279 | 1280 | #[test] 1281 | fn month_md_hms() { 1282 | let parse = Parse::new(&Utc, None); 1283 | 1284 | let test_cases = [ 1285 | ( 1286 | "May 6 at 9:24 PM", 1287 | Utc.ymd(Utc::now().year(), 5, 6).and_hms(21, 24, 0), 1288 | ), 1289 | ( 1290 | "May 27 02:45:27", 1291 | Utc.ymd(Utc::now().year(), 5, 27).and_hms(2, 45, 27), 1292 | ), 1293 | ]; 1294 | 1295 | for &(input, want) in test_cases.iter() { 1296 | assert_eq!( 1297 | parse.month_md_hms(input).unwrap().unwrap(), 1298 | want, 1299 | "month_md_hms/{}", 1300 | input 1301 | ) 1302 | } 1303 | assert!(parse.month_md_hms("not-date-time").is_none()); 1304 | } 1305 | 1306 | #[test] 1307 | fn month_mdy_hms() { 1308 | let parse = Parse::new(&Utc, None); 1309 | 1310 | let test_cases = [ 1311 | ( 1312 | "May 8, 2009 5:57:51 PM", 1313 | Utc.ymd(2009, 5, 8).and_hms(17, 57, 51), 1314 | ), 1315 | ( 1316 | "September 17, 2012 10:09am", 1317 | Utc.ymd(2012, 9, 17).and_hms(10, 9, 0), 1318 | ), 1319 | ( 1320 | "September 17, 2012, 10:10:09", 1321 | Utc.ymd(2012, 9, 17).and_hms(10, 10, 9), 1322 | ), 1323 | ]; 1324 | 1325 | for &(input, want) in test_cases.iter() { 1326 | assert_eq!( 1327 | parse.month_mdy_hms(input).unwrap().unwrap(), 1328 | want, 1329 | "month_mdy_hms/{}", 1330 | input 1331 | ) 1332 | } 1333 | assert!(parse.month_mdy_hms("not-date-time").is_none()); 1334 | } 1335 | 1336 | #[test] 1337 | fn month_mdy_hms_z() { 1338 | let parse = Parse::new(&Utc, None); 1339 | 1340 | let test_cases = [ 1341 | ( 1342 | "May 02, 2021 15:51:31 UTC", 1343 | Utc.ymd(2021, 5, 2).and_hms(15, 51, 31), 1344 | ), 1345 | ( 1346 | "May 02, 2021 15:51 UTC", 1347 | Utc.ymd(2021, 5, 2).and_hms(15, 51, 0), 1348 | ), 1349 | ( 1350 | "May 26, 2021, 12:49 AM PDT", 1351 | Utc.ymd(2021, 5, 26).and_hms(7, 49, 0), 1352 | ), 1353 | ( 1354 | "September 17, 2012 at 10:09am PST", 1355 | Utc.ymd(2012, 9, 17).and_hms(18, 9, 0), 1356 | ), 1357 | ]; 1358 | 1359 | for &(input, want) in test_cases.iter() { 1360 | assert_eq!( 1361 | parse.month_mdy_hms_z(input).unwrap().unwrap(), 1362 | want, 1363 | "month_mdy_hms_z/{}", 1364 | input 1365 | ) 1366 | } 1367 | assert!(parse.month_mdy_hms_z("not-date-time").is_none()); 1368 | } 1369 | 1370 | #[test] 1371 | fn month_mdy() { 1372 | let parse = Parse::new(&Utc, None); 1373 | 1374 | let test_cases = [ 1375 | ( 1376 | "May 25, 2021", 1377 | Utc.ymd(2021, 5, 25).and_time(Utc::now().time()), 1378 | ), 1379 | ( 1380 | "oct 7, 1970", 1381 | Utc.ymd(1970, 10, 7).and_time(Utc::now().time()), 1382 | ), 1383 | ( 1384 | "oct 7, 70", 1385 | Utc.ymd(1970, 10, 7).and_time(Utc::now().time()), 1386 | ), 1387 | ( 1388 | "oct. 7, 1970", 1389 | Utc.ymd(1970, 10, 7).and_time(Utc::now().time()), 1390 | ), 1391 | ( 1392 | "oct. 7, 70", 1393 | Utc.ymd(1970, 10, 7).and_time(Utc::now().time()), 1394 | ), 1395 | ( 1396 | "October 7, 1970", 1397 | Utc.ymd(1970, 10, 7).and_time(Utc::now().time()), 1398 | ), 1399 | ]; 1400 | 1401 | for &(input, want) in test_cases.iter() { 1402 | assert_eq!( 1403 | parse 1404 | .month_mdy(input) 1405 | .unwrap() 1406 | .unwrap() 1407 | .trunc_subsecs(0) 1408 | .with_second(0) 1409 | .unwrap(), 1410 | want.unwrap().trunc_subsecs(0).with_second(0).unwrap(), 1411 | "month_mdy/{}", 1412 | input 1413 | ) 1414 | } 1415 | assert!(parse.month_mdy("not-date-time").is_none()); 1416 | } 1417 | 1418 | #[test] 1419 | fn month_dmy_hms() { 1420 | let parse = Parse::new(&Utc, None); 1421 | 1422 | let test_cases = [ 1423 | ( 1424 | "12 Feb 2006, 19:17", 1425 | Utc.ymd(2006, 2, 12).and_hms(19, 17, 0), 1426 | ), 1427 | ("12 Feb 2006 19:17", Utc.ymd(2006, 2, 12).and_hms(19, 17, 0)), 1428 | ( 1429 | "14 May 2019 19:11:40.164", 1430 | Utc.ymd(2019, 5, 14).and_hms_milli(19, 11, 40, 164), 1431 | ), 1432 | ]; 1433 | 1434 | for &(input, want) in test_cases.iter() { 1435 | assert_eq!( 1436 | parse.month_dmy_hms(input).unwrap().unwrap(), 1437 | want, 1438 | "month_dmy_hms/{}", 1439 | input 1440 | ) 1441 | } 1442 | assert!(parse.month_dmy_hms("not-date-time").is_none()); 1443 | } 1444 | 1445 | #[test] 1446 | fn month_dmy() { 1447 | let parse = Parse::new(&Utc, None); 1448 | 1449 | let test_cases = [ 1450 | ("7 oct 70", Utc.ymd(1970, 10, 7).and_time(Utc::now().time())), 1451 | ( 1452 | "7 oct 1970", 1453 | Utc.ymd(1970, 10, 7).and_time(Utc::now().time()), 1454 | ), 1455 | ( 1456 | "03 February 2013", 1457 | Utc.ymd(2013, 2, 3).and_time(Utc::now().time()), 1458 | ), 1459 | ( 1460 | "1 July 2013", 1461 | Utc.ymd(2013, 7, 1).and_time(Utc::now().time()), 1462 | ), 1463 | ]; 1464 | 1465 | for &(input, want) in test_cases.iter() { 1466 | assert_eq!( 1467 | parse 1468 | .month_dmy(input) 1469 | .unwrap() 1470 | .unwrap() 1471 | .trunc_subsecs(0) 1472 | .with_second(0) 1473 | .unwrap(), 1474 | want.unwrap().trunc_subsecs(0).with_second(0).unwrap(), 1475 | "month_dmy/{}", 1476 | input 1477 | ) 1478 | } 1479 | assert!(parse.month_dmy("not-date-time").is_none()); 1480 | } 1481 | 1482 | #[test] 1483 | fn slash_mdy_hms() { 1484 | let parse = Parse::new(&Utc, None); 1485 | 1486 | let test_cases = vec![ 1487 | ("4/8/2014 22:05", Utc.ymd(2014, 4, 8).and_hms(22, 5, 0)), 1488 | ("04/08/2014 22:05", Utc.ymd(2014, 4, 8).and_hms(22, 5, 0)), 1489 | ("4/8/14 22:05", Utc.ymd(2014, 4, 8).and_hms(22, 5, 0)), 1490 | ("04/2/2014 03:00:51", Utc.ymd(2014, 4, 2).and_hms(3, 0, 51)), 1491 | ("8/8/1965 12:00:00 AM", Utc.ymd(1965, 8, 8).and_hms(0, 0, 0)), 1492 | ( 1493 | "8/8/1965 01:00:01 PM", 1494 | Utc.ymd(1965, 8, 8).and_hms(13, 0, 1), 1495 | ), 1496 | ("8/8/1965 01:00 PM", Utc.ymd(1965, 8, 8).and_hms(13, 0, 0)), 1497 | ("8/8/1965 1:00 PM", Utc.ymd(1965, 8, 8).and_hms(13, 0, 0)), 1498 | ("8/8/1965 12:00 AM", Utc.ymd(1965, 8, 8).and_hms(0, 0, 0)), 1499 | ("4/02/2014 03:00:51", Utc.ymd(2014, 4, 2).and_hms(3, 0, 51)), 1500 | ( 1501 | "03/19/2012 10:11:59", 1502 | Utc.ymd(2012, 3, 19).and_hms(10, 11, 59), 1503 | ), 1504 | ( 1505 | "03/19/2012 10:11:59.3186369", 1506 | Utc.ymd(2012, 3, 19).and_hms_nano(10, 11, 59, 318636900), 1507 | ), 1508 | ]; 1509 | 1510 | for &(input, want) in test_cases.iter() { 1511 | assert_eq!( 1512 | parse.slash_mdy_hms(input).unwrap().unwrap(), 1513 | want, 1514 | "slash_mdy_hms/{}", 1515 | input 1516 | ) 1517 | } 1518 | assert!(parse.slash_mdy_hms("not-date-time").is_none()); 1519 | } 1520 | 1521 | #[test] 1522 | fn slash_mdy() { 1523 | let parse = Parse::new(&Utc, None); 1524 | 1525 | let test_cases = [ 1526 | ( 1527 | "3/31/2014", 1528 | Utc.ymd(2014, 3, 31).and_time(Utc::now().time()), 1529 | ), 1530 | ( 1531 | "03/31/2014", 1532 | Utc.ymd(2014, 3, 31).and_time(Utc::now().time()), 1533 | ), 1534 | ("08/21/71", Utc.ymd(1971, 8, 21).and_time(Utc::now().time())), 1535 | ("8/1/71", Utc.ymd(1971, 8, 1).and_time(Utc::now().time())), 1536 | ]; 1537 | 1538 | for &(input, want) in test_cases.iter() { 1539 | assert_eq!( 1540 | parse 1541 | .slash_mdy(input) 1542 | .unwrap() 1543 | .unwrap() 1544 | .trunc_subsecs(0) 1545 | .with_second(0) 1546 | .unwrap(), 1547 | want.unwrap().trunc_subsecs(0).with_second(0).unwrap(), 1548 | "slash_mdy/{}", 1549 | input 1550 | ) 1551 | } 1552 | assert!(parse.slash_mdy("not-date-time").is_none()); 1553 | } 1554 | 1555 | #[test] 1556 | fn hyphen_mdy() { 1557 | let parse = Parse::new(&Utc, None); 1558 | 1559 | let test_cases = [ 1560 | ( 1561 | "3-31-2014", 1562 | Utc.ymd(2014, 3, 31).and_time(Utc::now().time()), 1563 | ), 1564 | ( 1565 | "03-31-2014", 1566 | Utc.ymd(2014, 3, 31).and_time(Utc::now().time()), 1567 | ), 1568 | ("08-21-71", Utc.ymd(1971, 8, 21).and_time(Utc::now().time())), 1569 | ("8-1-71", Utc.ymd(1971, 8, 1).and_time(Utc::now().time())), 1570 | ]; 1571 | 1572 | for &(input, want) in test_cases.iter() { 1573 | assert_eq!( 1574 | parse 1575 | .hyphen_mdy(input) 1576 | .unwrap() 1577 | .unwrap() 1578 | .trunc_subsecs(0) 1579 | .with_second(0) 1580 | .unwrap(), 1581 | want.unwrap().trunc_subsecs(0).with_second(0).unwrap(), 1582 | "hyphen_mdy/{}", 1583 | input 1584 | ) 1585 | } 1586 | assert!(parse.hyphen_mdy("not-date-time").is_none()); 1587 | } 1588 | 1589 | #[test] 1590 | fn hyphen_mdy_hms() { 1591 | let parse = Parse::new(&Utc, None); 1592 | 1593 | let test_cases = vec![ 1594 | ("4-8-2014 22:05", Utc.ymd(2014, 4, 8).and_hms(22, 5, 0)), 1595 | ("04-08-2014 22:05", Utc.ymd(2014, 4, 8).and_hms(22, 5, 0)), 1596 | ("4-8-14 22:05", Utc.ymd(2014, 4, 8).and_hms(22, 5, 0)), 1597 | ("04-2-2014 03:00:51", Utc.ymd(2014, 4, 2).and_hms(3, 0, 51)), 1598 | ("8-8-1965 12:00:00 AM", Utc.ymd(1965, 8, 8).and_hms(0, 0, 0)), 1599 | ( 1600 | "8-8-1965 01:00:01 PM", 1601 | Utc.ymd(1965, 8, 8).and_hms(13, 0, 1), 1602 | ), 1603 | ("8-8-1965 01:00 PM", Utc.ymd(1965, 8, 8).and_hms(13, 0, 0)), 1604 | ("8-8-1965 1:00 PM", Utc.ymd(1965, 8, 8).and_hms(13, 0, 0)), 1605 | ("8-8-1965 12:00 AM", Utc.ymd(1965, 8, 8).and_hms(0, 0, 0)), 1606 | ("4-02-2014 03:00:51", Utc.ymd(2014, 4, 2).and_hms(3, 0, 51)), 1607 | ( 1608 | "03-19-2012 10:11:59", 1609 | Utc.ymd(2012, 3, 19).and_hms(10, 11, 59), 1610 | ), 1611 | ( 1612 | "03-19-2012 10:11:59.3186369", 1613 | Utc.ymd(2012, 3, 19).and_hms_nano(10, 11, 59, 318636900), 1614 | ), 1615 | ]; 1616 | 1617 | for &(input, want) in test_cases.iter() { 1618 | assert_eq!( 1619 | parse.hyphen_mdy_hms(input).unwrap().unwrap(), 1620 | want, 1621 | "hyphen_mdy_hms/{}", 1622 | input 1623 | ) 1624 | } 1625 | assert!(parse.hyphen_mdy_hms("not-date-time").is_none()); 1626 | } 1627 | 1628 | 1629 | #[test] 1630 | fn slash_ymd_hms() { 1631 | let parse = Parse::new(&Utc, None); 1632 | 1633 | let test_cases = [ 1634 | ("2014/4/8 22:05", Utc.ymd(2014, 4, 8).and_hms(22, 5, 0)), 1635 | ("2014/04/08 22:05", Utc.ymd(2014, 4, 8).and_hms(22, 5, 0)), 1636 | ("2014/04/2 03:00:51", Utc.ymd(2014, 4, 2).and_hms(3, 0, 51)), 1637 | ("2014/4/02 03:00:51", Utc.ymd(2014, 4, 2).and_hms(3, 0, 51)), 1638 | ( 1639 | "2012/03/19 10:11:59", 1640 | Utc.ymd(2012, 3, 19).and_hms(10, 11, 59), 1641 | ), 1642 | ( 1643 | "2012/03/19 10:11:59.3186369", 1644 | Utc.ymd(2012, 3, 19).and_hms_nano(10, 11, 59, 318636900), 1645 | ), 1646 | ]; 1647 | 1648 | for &(input, want) in test_cases.iter() { 1649 | assert_eq!( 1650 | parse.slash_ymd_hms(input).unwrap().unwrap(), 1651 | want, 1652 | "slash_ymd_hms/{}", 1653 | input 1654 | ) 1655 | } 1656 | assert!(parse.slash_ymd_hms("not-date-time").is_none()); 1657 | } 1658 | 1659 | #[test] 1660 | fn slash_ymd() { 1661 | let parse = Parse::new(&Utc, Some(Utc::now().time())); 1662 | 1663 | let test_cases = [ 1664 | ( 1665 | "2014/3/31", 1666 | Utc.ymd(2014, 3, 31).and_time(Utc::now().time()), 1667 | ), 1668 | ( 1669 | "2014/03/31", 1670 | Utc.ymd(2014, 3, 31).and_time(Utc::now().time()), 1671 | ), 1672 | ]; 1673 | 1674 | for &(input, want) in test_cases.iter() { 1675 | assert_eq!( 1676 | parse 1677 | .slash_ymd(input) 1678 | .unwrap() 1679 | .unwrap() 1680 | .trunc_subsecs(0) 1681 | .with_second(0) 1682 | .unwrap(), 1683 | want.unwrap().trunc_subsecs(0).with_second(0).unwrap(), 1684 | "slash_ymd/{}", 1685 | input 1686 | ) 1687 | } 1688 | assert!(parse.slash_ymd("not-date-time").is_none()); 1689 | } 1690 | 1691 | #[test] 1692 | fn dot_mdy_or_ymd() { 1693 | let parse = Parse::new(&Utc, Some(Utc::now().time())); 1694 | 1695 | let test_cases = [ 1696 | // mm.dd.yyyy 1697 | ( 1698 | "3.31.2014", 1699 | Utc.ymd(2014, 3, 31).and_time(Utc::now().time()), 1700 | ), 1701 | ( 1702 | "03.31.2014", 1703 | Utc.ymd(2014, 3, 31).and_time(Utc::now().time()), 1704 | ), 1705 | ("08.21.71", Utc.ymd(1971, 8, 21).and_time(Utc::now().time())), 1706 | // yyyy.mm.dd 1707 | ( 1708 | "2014.03.30", 1709 | Utc.ymd(2014, 3, 30).and_time(Utc::now().time()), 1710 | ), 1711 | ( 1712 | "2014.03", 1713 | Utc.ymd(2014, 3, Utc::now().day()) 1714 | .and_time(Utc::now().time()), 1715 | ), 1716 | ]; 1717 | 1718 | for &(input, want) in test_cases.iter() { 1719 | assert_eq!( 1720 | parse 1721 | .dot_mdy_or_ymd(input) 1722 | .unwrap() 1723 | .unwrap() 1724 | .trunc_subsecs(0) 1725 | .with_second(0) 1726 | .unwrap(), 1727 | want.unwrap().trunc_subsecs(0).with_second(0).unwrap(), 1728 | "dot_mdy_or_ymd/{}", 1729 | input 1730 | ) 1731 | } 1732 | assert!(parse.dot_mdy_or_ymd("not-date-time").is_none()); 1733 | } 1734 | 1735 | #[test] 1736 | fn mysql_log_timestamp() { 1737 | let parse = Parse::new(&Utc, None); 1738 | 1739 | let test_cases = [ 1740 | // yymmdd hh:mm:ss mysql log 1741 | ("171113 14:14:20", Utc.ymd(2017, 11, 13).and_hms(14, 14, 20)), 1742 | ]; 1743 | 1744 | for &(input, want) in test_cases.iter() { 1745 | assert_eq!( 1746 | parse.mysql_log_timestamp(input).unwrap().unwrap(), 1747 | want, 1748 | "mysql_log_timestamp/{}", 1749 | input 1750 | ) 1751 | } 1752 | assert!(parse.mysql_log_timestamp("not-date-time").is_none()); 1753 | } 1754 | 1755 | #[test] 1756 | fn chinese_ymd_hms() { 1757 | let parse = Parse::new(&Utc, None); 1758 | 1759 | let test_cases = [( 1760 | "2014年04月08日11时25分18秒", 1761 | Utc.ymd(2014, 4, 8).and_hms(11, 25, 18), 1762 | )]; 1763 | 1764 | for &(input, want) in test_cases.iter() { 1765 | assert_eq!( 1766 | parse.chinese_ymd_hms(input).unwrap().unwrap(), 1767 | want, 1768 | "chinese_ymd_hms/{}", 1769 | input 1770 | ) 1771 | } 1772 | assert!(parse.chinese_ymd_hms("not-date-time").is_none()); 1773 | } 1774 | 1775 | #[test] 1776 | fn chinese_ymd() { 1777 | let parse = Parse::new(&Utc, Some(Utc::now().time())); 1778 | 1779 | let test_cases = [( 1780 | "2014年04月08日", 1781 | Utc.ymd(2014, 4, 8).and_time(Utc::now().time()), 1782 | )]; 1783 | 1784 | for &(input, want) in test_cases.iter() { 1785 | assert_eq!( 1786 | parse 1787 | .chinese_ymd(input) 1788 | .unwrap() 1789 | .unwrap() 1790 | .trunc_subsecs(0) 1791 | .with_second(0) 1792 | .unwrap(), 1793 | want.unwrap().trunc_subsecs(0).with_second(0).unwrap(), 1794 | "chinese_ymd/{}", 1795 | input 1796 | ) 1797 | } 1798 | assert!(parse.chinese_ymd("not-date-time").is_none()); 1799 | } 1800 | } 1801 | -------------------------------------------------------------------------------- /dateparser/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(deprecated)] 2 | //! A rust library for parsing date strings in commonly used formats. Parsed date will be returned 3 | //! as `chrono`'s `DateTime`. 4 | //! 5 | //! # Quick Start 6 | //! 7 | //! ``` 8 | //! use chrono::prelude::*; 9 | //! use dateparser::parse; 10 | //! use std::error::Error; 11 | //! 12 | //! fn main() -> Result<(), Box> { 13 | //! assert_eq!( 14 | //! parse("6:15pm UTC")?, 15 | //! Utc::now().date().and_time( 16 | //! NaiveTime::from_hms(18, 15, 0), 17 | //! ).unwrap(), 18 | //! ); 19 | //! Ok(()) 20 | //! } 21 | //! ``` 22 | //! 23 | //! Use `str`'s `parse` method: 24 | //! 25 | //! ``` 26 | //! use chrono::prelude::*; 27 | //! use dateparser::DateTimeUtc; 28 | //! use std::error::Error; 29 | //! 30 | //! fn main() -> Result<(), Box> { 31 | //! assert_eq!( 32 | //! "2021-05-14 18:51 PDT".parse::()?.0, 33 | //! Utc.ymd(2021, 5, 15).and_hms(1, 51, 0), 34 | //! ); 35 | //! Ok(()) 36 | //! } 37 | //! ``` 38 | //! 39 | //! Parse using a custom timezone offset for a datetime string that doesn't come with a specific 40 | //! timezone: 41 | //! 42 | //! ``` 43 | //! use dateparser::parse_with_timezone; 44 | //! use chrono::offset::Utc; 45 | //! use std::error::Error; 46 | //! 47 | //! fn main() -> Result<(), Box> { 48 | //! let parsed_in_utc = parse_with_timezone("6:15pm", &Utc)?; 49 | //! assert_eq!( 50 | //! parsed_in_utc, 51 | //! Utc::now().date().and_hms(18, 15, 0), 52 | //! ); 53 | //! Ok(()) 54 | //! } 55 | //! ``` 56 | //! 57 | //! ## Accepted date formats 58 | //! 59 | //! ``` 60 | //! use dateparser::DateTimeUtc; 61 | //! 62 | //! let accepted = vec![ 63 | //! // unix timestamp 64 | //! "1511648546", 65 | //! "1620021848429", 66 | //! "1620024872717915000", 67 | //! // rfc3339 68 | //! "2021-05-01T01:17:02.604456Z", 69 | //! "2017-11-25T22:34:50Z", 70 | //! // rfc2822 71 | //! "Wed, 02 Jun 2021 06:31:39 GMT", 72 | //! // postgres timestamp yyyy-mm-dd hh:mm:ss z 73 | //! "2019-11-29 08:08-08", 74 | //! "2019-11-29 08:08:05-08", 75 | //! "2021-05-02 23:31:36.0741-07", 76 | //! "2021-05-02 23:31:39.12689-07", 77 | //! "2019-11-29 08:15:47.624504-08", 78 | //! "2017-07-19 03:21:51+00:00", 79 | //! // yyyy-mm-dd hh:mm:ss 80 | //! "2014-04-26 05:24:37 PM", 81 | //! "2021-04-30 21:14", 82 | //! "2021-04-30 21:14:10", 83 | //! "2021-04-30 21:14:10.052282", 84 | //! "2014-04-26 17:24:37.123", 85 | //! "2014-04-26 17:24:37.3186369", 86 | //! "2012-08-03 18:31:59.257000000", 87 | //! // yyyy-mm-dd hh:mm:ss z 88 | //! "2017-11-25 13:31:15 PST", 89 | //! "2017-11-25 13:31 PST", 90 | //! "2014-12-16 06:20:00 UTC", 91 | //! "2014-12-16 06:20:00 GMT", 92 | //! "2014-04-26 13:13:43 +0800", 93 | //! "2014-04-26 13:13:44 +09:00", 94 | //! "2012-08-03 18:31:59.257000000 +0000", 95 | //! "2015-09-30 18:48:56.35272715 UTC", 96 | //! // yyyy-mm-dd 97 | //! "2021-02-21", 98 | //! // yyyy-mm-dd z 99 | //! "2021-02-21 PST", 100 | //! "2021-02-21 UTC", 101 | //! "2020-07-20+08:00", 102 | //! // hh:mm:ss 103 | //! "01:06:06", 104 | //! "4:00pm", 105 | //! "6:00 AM", 106 | //! // hh:mm:ss z 107 | //! "01:06:06 PST", 108 | //! "4:00pm PST", 109 | //! "6:00 AM PST", 110 | //! "6:00pm UTC", 111 | //! // Mon dd hh:mm:ss 112 | //! "May 6 at 9:24 PM", 113 | //! "May 27 02:45:27", 114 | //! // Mon dd, yyyy, hh:mm:ss 115 | //! "May 8, 2009 5:57:51 PM", 116 | //! "September 17, 2012 10:09am", 117 | //! "September 17, 2012, 10:10:09", 118 | //! // Mon dd, yyyy hh:mm:ss z 119 | //! "May 02, 2021 15:51:31 UTC", 120 | //! "May 02, 2021 15:51 UTC", 121 | //! "May 26, 2021, 12:49 AM PDT", 122 | //! "September 17, 2012 at 10:09am PST", 123 | //! // yyyy-mon-dd 124 | //! "2021-Feb-21", 125 | //! // Mon dd, yyyy 126 | //! "May 25, 2021", 127 | //! "oct 7, 1970", 128 | //! "oct 7, 70", 129 | //! "oct. 7, 1970", 130 | //! "oct. 7, 70", 131 | //! "October 7, 1970", 132 | //! // dd Mon yyyy hh:mm:ss 133 | //! "12 Feb 2006, 19:17", 134 | //! "12 Feb 2006 19:17", 135 | //! "14 May 2019 19:11:40.164", 136 | //! // dd Mon yyyy 137 | //! "7 oct 70", 138 | //! "7 oct 1970", 139 | //! "03 February 2013", 140 | //! "1 July 2013", 141 | //! // mm/dd/yyyy hh:mm:ss 142 | //! "4/8/2014 22:05", 143 | //! "04/08/2014 22:05", 144 | //! "4/8/14 22:05", 145 | //! "04/2/2014 03:00:51", 146 | //! "8/8/1965 12:00:00 AM", 147 | //! "8/8/1965 01:00:01 PM", 148 | //! "8/8/1965 01:00 PM", 149 | //! "8/8/1965 1:00 PM", 150 | //! "8/8/1965 12:00 AM", 151 | //! "4/02/2014 03:00:51", 152 | //! "03/19/2012 10:11:59", 153 | //! "03/19/2012 10:11:59.3186369", 154 | //! // mm/dd/yyyy 155 | //! "3/31/2014", 156 | //! "03/31/2014", 157 | //! "08/21/71", 158 | //! "8/1/71", 159 | //! // yyyy/mm/dd hh:mm:ss 160 | //! "2014/4/8 22:05", 161 | //! "2014/04/08 22:05", 162 | //! "2014/04/2 03:00:51", 163 | //! "2014/4/02 03:00:51", 164 | //! "2012/03/19 10:11:59", 165 | //! "2012/03/19 10:11:59.3186369", 166 | //! // yyyy/mm/dd 167 | //! "2014/3/31", 168 | //! "2014/03/31", 169 | //! // mm.dd.yyyy 170 | //! "3.31.2014", 171 | //! "03.31.2014", 172 | //! "08.21.71", 173 | //! // yyyy.mm.dd 174 | //! "2014.03.29", 175 | //! "2014.03", 176 | //! // yymmdd hh:mm:ss mysql log 177 | //! "171113 14:14:20", 178 | //! // chinese yyyy mm dd hh mm ss 179 | //! "2014年04月08日11时25分18秒", 180 | //! // chinese yyyy mm dd 181 | //! "2014年04月08日", 182 | //! ]; 183 | //! 184 | //! for date_str in accepted { 185 | //! let result = date_str.parse::(); 186 | //! assert!(result.is_ok()) 187 | //! } 188 | //! ``` 189 | 190 | /// Datetime string parser 191 | /// 192 | /// ``` 193 | /// use chrono::prelude::*; 194 | /// use dateparser::datetime::Parse; 195 | /// use std::error::Error; 196 | /// 197 | /// fn main() -> Result<(), Box> { 198 | /// let parse_with_local = Parse::new(&Local, None); 199 | /// assert_eq!( 200 | /// parse_with_local.parse("2021-06-05 06:19 PM")?, 201 | /// Local.ymd(2021, 6, 5).and_hms(18, 19, 0).with_timezone(&Utc), 202 | /// ); 203 | /// 204 | /// let parse_with_utc = Parse::new(&Utc, None); 205 | /// assert_eq!( 206 | /// parse_with_utc.parse("2021-06-05 06:19 PM")?, 207 | /// Utc.ymd(2021, 6, 5).and_hms(18, 19, 0), 208 | /// ); 209 | /// 210 | /// Ok(()) 211 | /// } 212 | /// ``` 213 | pub mod datetime; 214 | 215 | /// Timezone offset string parser 216 | /// 217 | /// ``` 218 | /// use chrono::prelude::*; 219 | /// use dateparser::timezone::parse; 220 | /// use std::error::Error; 221 | /// 222 | /// fn main() -> Result<(), Box> { 223 | /// assert_eq!(parse("-0800")?, FixedOffset::west(8 * 3600)); 224 | /// assert_eq!(parse("+10:00")?, FixedOffset::east(10 * 3600)); 225 | /// assert_eq!(parse("PST")?, FixedOffset::west(8 * 3600)); 226 | /// assert_eq!(parse("PDT")?, FixedOffset::west(7 * 3600)); 227 | /// assert_eq!(parse("UTC")?, FixedOffset::west(0)); 228 | /// assert_eq!(parse("GMT")?, FixedOffset::west(0)); 229 | /// 230 | /// Ok(()) 231 | /// } 232 | /// ``` 233 | pub mod timezone; 234 | 235 | use crate::datetime::Parse; 236 | use anyhow::{Error, Result}; 237 | use chrono::prelude::*; 238 | 239 | /// DateTimeUtc is an alias for `chrono`'s `DateTime`. It implements `std::str::FromStr`'s 240 | /// `from_str` method, and it makes `str`'s `parse` method to understand the accepted date formats 241 | /// from this crate. 242 | /// 243 | /// ``` 244 | /// use dateparser::DateTimeUtc; 245 | /// 246 | /// // parsed is DateTimeUTC and parsed.0 is chrono's DateTime 247 | /// match "May 02, 2021 15:51:31 UTC".parse::() { 248 | /// Ok(parsed) => println!("PARSED into UTC datetime {:?}", parsed.0), 249 | /// Err(err) => println!("ERROR from parsing datetime string: {}", err) 250 | /// } 251 | /// ``` 252 | #[derive(Clone, Debug)] 253 | pub struct DateTimeUtc(pub DateTime); 254 | 255 | impl std::str::FromStr for DateTimeUtc { 256 | type Err = Error; 257 | 258 | fn from_str(s: &str) -> Result { 259 | parse(s).map(DateTimeUtc) 260 | } 261 | } 262 | 263 | /// This function tries to recognize the input datetime string with a list of accepted formats. 264 | /// When timezone is not provided, this function assumes it's a [`chrono::Local`] datetime. For 265 | /// custom timezone, use [`parse_with_timezone()`] instead.If all options are exhausted, 266 | /// [`parse()`] will return an error to let the caller know that no formats were matched. 267 | /// 268 | /// ``` 269 | /// use dateparser::parse; 270 | /// use chrono::offset::{Local, Utc}; 271 | /// use chrono_tz::US::Pacific; 272 | /// 273 | /// let parsed = parse("6:15pm").unwrap(); 274 | /// 275 | /// assert_eq!( 276 | /// parsed, 277 | /// Local::now().date().and_hms(18, 15, 0).with_timezone(&Utc), 278 | /// ); 279 | /// 280 | /// assert_eq!( 281 | /// parsed.with_timezone(&Pacific), 282 | /// Local::now().date().and_hms(18, 15, 0).with_timezone(&Utc).with_timezone(&Pacific), 283 | /// ); 284 | /// ``` 285 | pub fn parse(input: &str) -> Result> { 286 | Parse::new(&Local, None).parse(input) 287 | } 288 | 289 | /// Similar to [`parse()`], this function takes a datetime string and a custom [`chrono::TimeZone`], 290 | /// and tries to parse the datetime string. When timezone is not given in the string, this function 291 | /// will assume and parse the datetime by the custom timezone provided in this function's arguments. 292 | /// 293 | /// ``` 294 | /// use dateparser::parse_with_timezone; 295 | /// use chrono::offset::{Local, Utc}; 296 | /// use chrono_tz::US::Pacific; 297 | /// 298 | /// let parsed_in_local = parse_with_timezone("6:15pm", &Local).unwrap(); 299 | /// assert_eq!( 300 | /// parsed_in_local, 301 | /// Local::now().date().and_hms(18, 15, 0).with_timezone(&Utc), 302 | /// ); 303 | /// 304 | /// let parsed_in_utc = parse_with_timezone("6:15pm", &Utc).unwrap(); 305 | /// assert_eq!( 306 | /// parsed_in_utc, 307 | /// Utc::now().date().and_hms(18, 15, 0), 308 | /// ); 309 | /// 310 | /// let parsed_in_pacific = parse_with_timezone("6:15pm", &Pacific).unwrap(); 311 | /// assert_eq!( 312 | /// parsed_in_pacific, 313 | /// Utc::now().with_timezone(&Pacific).date().and_hms(18, 15, 0).with_timezone(&Utc), 314 | /// ); 315 | /// ``` 316 | pub fn parse_with_timezone(input: &str, tz: &Tz2) -> Result> { 317 | Parse::new(tz, None).parse(input) 318 | } 319 | 320 | /// Similar to [`parse()`] and [`parse_with_timezone()`], this function takes a datetime string, a 321 | /// custom [`chrono::TimeZone`] and a default naive time. In addition to assuming timezone when 322 | /// it's not given in datetime string, this function also use provided default naive time in parsed 323 | /// [`chrono::DateTime`]. 324 | /// 325 | /// ``` 326 | /// use dateparser::parse_with; 327 | /// use chrono::prelude::*; 328 | /// 329 | /// let utc_now = Utc::now().time().trunc_subsecs(0); 330 | /// let local_now = Local::now().time().trunc_subsecs(0); 331 | /// let midnight_naive = NaiveTime::from_hms_opt(0, 0, 0).unwrap(); 332 | /// let before_midnight_naive = NaiveTime::from_hms_opt(23, 59, 59).unwrap(); 333 | /// 334 | /// let parsed_with_local_now = parse_with("2021-10-09", &Local, local_now); 335 | /// let parsed_with_local_midnight = parse_with("2021-10-09", &Local, midnight_naive); 336 | /// let parsed_with_local_before_midnight = parse_with("2021-10-09", &Local, before_midnight_naive); 337 | /// let parsed_with_utc_now = parse_with("2021-10-09", &Utc, utc_now); 338 | /// let parsed_with_utc_midnight = parse_with("2021-10-09", &Utc, midnight_naive); 339 | /// 340 | /// assert_eq!( 341 | /// parsed_with_local_now.unwrap(), 342 | /// Local.ymd(2021, 10, 9).and_time(local_now).unwrap().with_timezone(&Utc), 343 | /// "parsed_with_local_now" 344 | /// ); 345 | /// assert_eq!( 346 | /// parsed_with_local_midnight.unwrap(), 347 | /// Local.ymd(2021, 10, 9).and_time(midnight_naive).unwrap().with_timezone(&Utc), 348 | /// "parsed_with_local_midnight" 349 | /// ); 350 | /// assert_eq!( 351 | /// parsed_with_local_before_midnight.unwrap(), 352 | /// Local.ymd(2021, 10, 9).and_time(before_midnight_naive).unwrap().with_timezone(&Utc), 353 | /// "parsed_with_local_before_midnight" 354 | /// ); 355 | /// assert_eq!( 356 | /// parsed_with_utc_now.unwrap(), 357 | /// Utc.ymd(2021, 10, 9).and_time(utc_now).unwrap(), 358 | /// "parsed_with_utc_now" 359 | /// ); 360 | /// assert_eq!( 361 | /// parsed_with_utc_midnight.unwrap(), 362 | /// Utc.ymd(2021, 10, 9).and_hms(0, 0, 0), 363 | /// "parsed_with_utc_midnight" 364 | /// ); 365 | /// ``` 366 | pub fn parse_with( 367 | input: &str, 368 | tz: &Tz2, 369 | default_time: NaiveTime, 370 | ) -> Result> { 371 | Parse::new(tz, Some(default_time)).parse(input) 372 | } 373 | 374 | #[cfg(test)] 375 | mod tests { 376 | use super::*; 377 | 378 | #[derive(Clone, Copy)] 379 | enum Trunc { 380 | Seconds, 381 | None, 382 | } 383 | 384 | #[test] 385 | fn parse_in_local() { 386 | let test_cases = vec![ 387 | ( 388 | "unix_timestamp", 389 | "1511648546", 390 | Utc.ymd(2017, 11, 25).and_hms(22, 22, 26), 391 | Trunc::None, 392 | ), 393 | ( 394 | "rfc3339", 395 | "2017-11-25T22:34:50Z", 396 | Utc.ymd(2017, 11, 25).and_hms(22, 34, 50), 397 | Trunc::None, 398 | ), 399 | ( 400 | "rfc2822", 401 | "Wed, 02 Jun 2021 06:31:39 GMT", 402 | Utc.ymd(2021, 6, 2).and_hms(6, 31, 39), 403 | Trunc::None, 404 | ), 405 | ( 406 | "postgres_timestamp", 407 | "2019-11-29 08:08:05-08", 408 | Utc.ymd(2019, 11, 29).and_hms(16, 8, 5), 409 | Trunc::None, 410 | ), 411 | ( 412 | "ymd_hms", 413 | "2021-04-30 21:14:10", 414 | Local 415 | .ymd(2021, 4, 30) 416 | .and_hms(21, 14, 10) 417 | .with_timezone(&Utc), 418 | Trunc::None, 419 | ), 420 | ( 421 | "ymd_hms_z", 422 | "2017-11-25 13:31:15 PST", 423 | Utc.ymd(2017, 11, 25).and_hms(21, 31, 15), 424 | Trunc::None, 425 | ), 426 | ( 427 | "ymd", 428 | "2021-02-21", 429 | Local 430 | .ymd(2021, 2, 21) 431 | .and_time(Local::now().time()) 432 | .unwrap() 433 | .with_timezone(&Utc), 434 | Trunc::Seconds, 435 | ), 436 | ( 437 | "ymd_z", 438 | "2021-02-21 PST", 439 | FixedOffset::west(8 * 3600) 440 | .ymd(2021, 2, 21) 441 | .and_time( 442 | Utc::now() 443 | .with_timezone(&FixedOffset::west(8 * 3600)) 444 | .time(), 445 | ) 446 | .unwrap() 447 | .with_timezone(&Utc), 448 | Trunc::Seconds, 449 | ), 450 | ( 451 | "hms", 452 | "4:00pm", 453 | Local::now() 454 | .date() 455 | .and_time(NaiveTime::from_hms(16, 0, 0)) 456 | .unwrap() 457 | .with_timezone(&Utc), 458 | Trunc::None, 459 | ), 460 | ( 461 | "hms_z", 462 | "6:00 AM PST", 463 | Utc::now() 464 | .with_timezone(&FixedOffset::west(8 * 3600)) 465 | .date() 466 | .and_time(NaiveTime::from_hms(6, 0, 0)) 467 | .unwrap() 468 | .with_timezone(&Utc), 469 | Trunc::None, 470 | ), 471 | ( 472 | "month_ymd", 473 | "2021-Feb-21", 474 | Local 475 | .ymd(2021, 2, 21) 476 | .and_time(Local::now().time()) 477 | .unwrap() 478 | .with_timezone(&Utc), 479 | Trunc::Seconds, 480 | ), 481 | ( 482 | "month_md_hms", 483 | "May 27 02:45:27", 484 | Local 485 | .ymd(Local::now().year(), 5, 27) 486 | .and_hms(2, 45, 27) 487 | .with_timezone(&Utc), 488 | Trunc::None, 489 | ), 490 | ( 491 | "month_mdy_hms", 492 | "May 8, 2009 5:57:51 PM", 493 | Local 494 | .ymd(2009, 5, 8) 495 | .and_hms(17, 57, 51) 496 | .with_timezone(&Utc), 497 | Trunc::None, 498 | ), 499 | ( 500 | "month_mdy_hms_z", 501 | "May 02, 2021 15:51 UTC", 502 | Utc.ymd(2021, 5, 2).and_hms(15, 51, 0), 503 | Trunc::None, 504 | ), 505 | ( 506 | "month_mdy", 507 | "May 25, 2021", 508 | Local 509 | .ymd(2021, 5, 25) 510 | .and_time(Local::now().time()) 511 | .unwrap() 512 | .with_timezone(&Utc), 513 | Trunc::Seconds, 514 | ), 515 | ( 516 | "month_dmy_hms", 517 | "14 May 2019 19:11:40.164", 518 | Local 519 | .ymd(2019, 5, 14) 520 | .and_hms_milli(19, 11, 40, 164) 521 | .with_timezone(&Utc), 522 | Trunc::None, 523 | ), 524 | ( 525 | "month_dmy", 526 | "1 July 2013", 527 | Local 528 | .ymd(2013, 7, 1) 529 | .and_time(Local::now().time()) 530 | .unwrap() 531 | .with_timezone(&Utc), 532 | Trunc::Seconds, 533 | ), 534 | ( 535 | "slash_mdy_hms", 536 | "03/19/2012 10:11:59", 537 | Local 538 | .ymd(2012, 3, 19) 539 | .and_hms(10, 11, 59) 540 | .with_timezone(&Utc), 541 | Trunc::None, 542 | ), 543 | ( 544 | "slash_mdy", 545 | "08/21/71", 546 | Local 547 | .ymd(1971, 8, 21) 548 | .and_time(Local::now().time()) 549 | .unwrap() 550 | .with_timezone(&Utc), 551 | Trunc::Seconds, 552 | ), 553 | ( 554 | "slash_ymd_hms", 555 | "2012/03/19 10:11:59", 556 | Local 557 | .ymd(2012, 3, 19) 558 | .and_hms(10, 11, 59) 559 | .with_timezone(&Utc), 560 | Trunc::None, 561 | ), 562 | ( 563 | "slash_ymd", 564 | "2014/3/31", 565 | Local 566 | .ymd(2014, 3, 31) 567 | .and_time(Local::now().time()) 568 | .unwrap() 569 | .with_timezone(&Utc), 570 | Trunc::Seconds, 571 | ), 572 | ( 573 | "dot_mdy_or_ymd", 574 | "2014.03.29", 575 | Local 576 | .ymd(2014, 3, 29) 577 | .and_time(Local::now().time()) 578 | .unwrap() 579 | .with_timezone(&Utc), 580 | Trunc::Seconds, 581 | ), 582 | ( 583 | "mysql_log_timestamp", 584 | "171113 14:14:20", 585 | Local 586 | .ymd(2017, 11, 13) 587 | .and_hms(14, 14, 20) 588 | .with_timezone(&Utc), 589 | Trunc::None, 590 | ), 591 | ( 592 | "chinese_ymd_hms", 593 | "2014年04月08日11时25分18秒", 594 | Local 595 | .ymd(2014, 4, 8) 596 | .and_hms(11, 25, 18) 597 | .with_timezone(&Utc), 598 | Trunc::None, 599 | ), 600 | ( 601 | "chinese_ymd", 602 | "2014年04月08日", 603 | Local 604 | .ymd(2014, 4, 8) 605 | .and_time(Local::now().time()) 606 | .unwrap() 607 | .with_timezone(&Utc), 608 | Trunc::Seconds, 609 | ), 610 | ]; 611 | 612 | for &(test, input, want, trunc) in test_cases.iter() { 613 | match trunc { 614 | Trunc::None => { 615 | assert_eq!( 616 | super::parse(input).unwrap(), 617 | want, 618 | "parse_in_local/{}/{}", 619 | test, 620 | input 621 | ) 622 | } 623 | Trunc::Seconds => assert_eq!( 624 | super::parse(input) 625 | .unwrap() 626 | .trunc_subsecs(0) 627 | .with_second(0) 628 | .unwrap(), 629 | want.trunc_subsecs(0).with_second(0).unwrap(), 630 | "parse_in_local/{}/{}", 631 | test, 632 | input 633 | ), 634 | }; 635 | } 636 | } 637 | 638 | #[test] 639 | fn parse_with_timezone_in_utc() { 640 | let test_cases = vec![ 641 | ( 642 | "unix_timestamp", 643 | "1511648546", 644 | Utc.ymd(2017, 11, 25).and_hms(22, 22, 26), 645 | Trunc::None, 646 | ), 647 | ( 648 | "rfc3339", 649 | "2017-11-25T22:34:50Z", 650 | Utc.ymd(2017, 11, 25).and_hms(22, 34, 50), 651 | Trunc::None, 652 | ), 653 | ( 654 | "rfc2822", 655 | "Wed, 02 Jun 2021 06:31:39 GMT", 656 | Utc.ymd(2021, 6, 2).and_hms(6, 31, 39), 657 | Trunc::None, 658 | ), 659 | ( 660 | "postgres_timestamp", 661 | "2019-11-29 08:08:05-08", 662 | Utc.ymd(2019, 11, 29).and_hms(16, 8, 5), 663 | Trunc::None, 664 | ), 665 | ( 666 | "ymd_hms", 667 | "2021-04-30 21:14:10", 668 | Utc.ymd(2021, 4, 30).and_hms(21, 14, 10), 669 | Trunc::None, 670 | ), 671 | ( 672 | "ymd_hms_z", 673 | "2017-11-25 13:31:15 PST", 674 | Utc.ymd(2017, 11, 25).and_hms(21, 31, 15), 675 | Trunc::None, 676 | ), 677 | ( 678 | "ymd", 679 | "2021-02-21", 680 | Utc.ymd(2021, 2, 21).and_time(Utc::now().time()).unwrap(), 681 | Trunc::Seconds, 682 | ), 683 | ( 684 | "ymd_z", 685 | "2021-02-21 PST", 686 | FixedOffset::west(8 * 3600) 687 | .ymd(2021, 2, 21) 688 | .and_time( 689 | Utc::now() 690 | .with_timezone(&FixedOffset::west(8 * 3600)) 691 | .time(), 692 | ) 693 | .unwrap() 694 | .with_timezone(&Utc), 695 | Trunc::Seconds, 696 | ), 697 | ( 698 | "hms", 699 | "4:00pm", 700 | Utc::now() 701 | .date() 702 | .and_time(NaiveTime::from_hms(16, 0, 0)) 703 | .unwrap(), 704 | Trunc::None, 705 | ), 706 | ( 707 | "hms_z", 708 | "6:00 AM PST", 709 | FixedOffset::west(8 * 3600) 710 | .from_local_date( 711 | &Utc::now() 712 | .with_timezone(&FixedOffset::west(8 * 3600)) 713 | .date() 714 | .naive_local(), 715 | ) 716 | .and_time(NaiveTime::from_hms(6, 0, 0)) 717 | .unwrap() 718 | .with_timezone(&Utc), 719 | Trunc::None, 720 | ), 721 | ( 722 | "month_ymd", 723 | "2021-Feb-21", 724 | Utc.ymd(2021, 2, 21).and_time(Utc::now().time()).unwrap(), 725 | Trunc::Seconds, 726 | ), 727 | ( 728 | "month_md_hms", 729 | "May 27 02:45:27", 730 | Utc.ymd(Utc::now().year(), 5, 27).and_hms(2, 45, 27), 731 | Trunc::None, 732 | ), 733 | ( 734 | "month_mdy_hms", 735 | "May 8, 2009 5:57:51 PM", 736 | Utc.ymd(2009, 5, 8).and_hms(17, 57, 51), 737 | Trunc::None, 738 | ), 739 | ( 740 | "month_mdy_hms_z", 741 | "May 02, 2021 15:51 UTC", 742 | Utc.ymd(2021, 5, 2).and_hms(15, 51, 0), 743 | Trunc::None, 744 | ), 745 | ( 746 | "month_mdy", 747 | "May 25, 2021", 748 | Utc.ymd(2021, 5, 25).and_time(Utc::now().time()).unwrap(), 749 | Trunc::Seconds, 750 | ), 751 | ( 752 | "month_dmy_hms", 753 | "14 May 2019 19:11:40.164", 754 | Utc.ymd(2019, 5, 14).and_hms_milli(19, 11, 40, 164), 755 | Trunc::None, 756 | ), 757 | ( 758 | "month_dmy", 759 | "1 July 2013", 760 | Utc.ymd(2013, 7, 1).and_time(Utc::now().time()).unwrap(), 761 | Trunc::Seconds, 762 | ), 763 | ( 764 | "slash_mdy_hms", 765 | "03/19/2012 10:11:59", 766 | Utc.ymd(2012, 3, 19).and_hms(10, 11, 59), 767 | Trunc::None, 768 | ), 769 | ( 770 | "slash_mdy", 771 | "08/21/71", 772 | Utc.ymd(1971, 8, 21).and_time(Utc::now().time()).unwrap(), 773 | Trunc::Seconds, 774 | ), 775 | ( 776 | "slash_ymd_hms", 777 | "2012/03/19 10:11:59", 778 | Utc.ymd(2012, 3, 19).and_hms(10, 11, 59), 779 | Trunc::None, 780 | ), 781 | ( 782 | "slash_ymd", 783 | "2014/3/31", 784 | Utc.ymd(2014, 3, 31).and_time(Utc::now().time()).unwrap(), 785 | Trunc::Seconds, 786 | ), 787 | ( 788 | "dot_mdy_or_ymd", 789 | "2014.03.29", 790 | Utc.ymd(2014, 3, 29).and_time(Utc::now().time()).unwrap(), 791 | Trunc::Seconds, 792 | ), 793 | ( 794 | "mysql_log_timestamp", 795 | "171113 14:14:20", 796 | Utc.ymd(2017, 11, 13).and_hms(14, 14, 20), 797 | Trunc::None, 798 | ), 799 | ( 800 | "chinese_ymd_hms", 801 | "2014年04月08日11时25分18秒", 802 | Utc.ymd(2014, 4, 8).and_hms(11, 25, 18), 803 | Trunc::None, 804 | ), 805 | ( 806 | "chinese_ymd", 807 | "2014年04月08日", 808 | Utc.ymd(2014, 4, 8).and_time(Utc::now().time()).unwrap(), 809 | Trunc::Seconds, 810 | ), 811 | ]; 812 | 813 | for &(test, input, want, trunc) in test_cases.iter() { 814 | match trunc { 815 | Trunc::None => { 816 | assert_eq!( 817 | super::parse_with_timezone(input, &Utc).unwrap(), 818 | want, 819 | "parse_with_timezone_in_utc/{}/{}", 820 | test, 821 | input 822 | ) 823 | } 824 | Trunc::Seconds => assert_eq!( 825 | super::parse_with_timezone(input, &Utc) 826 | .unwrap() 827 | .trunc_subsecs(0) 828 | .with_second(0) 829 | .unwrap(), 830 | want.trunc_subsecs(0).with_second(0).unwrap(), 831 | "parse_with_timezone_in_utc/{}/{}", 832 | test, 833 | input 834 | ), 835 | }; 836 | } 837 | } 838 | 839 | // test parse_with() with various timezones and times 840 | 841 | #[test] 842 | fn parse_with_edt() { 843 | // Eastern Daylight Time (EDT) is from (as of 2023) 2nd Sun in Mar to 1st Sun in Nov 844 | // It is UTC -4 845 | 846 | let midnight_naive = NaiveTime::from_hms_opt(0, 0, 0).unwrap(); 847 | let before_midnight_naive = NaiveTime::from_hms_opt(23, 59, 59).unwrap(); 848 | let us_edt = &FixedOffset::west_opt(4 * 3600).unwrap(); 849 | 850 | let edt_test_cases = vec![ 851 | ("ymd", "2023-04-21"), 852 | ("ymd_z", "2023-04-21 EDT"), 853 | ("month_ymd", "2023-Apr-21"), 854 | ("month_mdy", "April 21, 2023"), 855 | ("month_dmy", "21 April 2023"), 856 | ("slash_mdy", "04/21/23"), 857 | ("slash_ymd", "2023/4/21"), 858 | ("dot_mdy_or_ymd", "2023.04.21"), 859 | ("chinese_ymd", "2023年04月21日"), 860 | ]; 861 | 862 | // test us_edt at midnight 863 | let us_edt_midnight_as_utc = Utc.ymd(2023, 4, 21).and_hms(4, 0, 0); 864 | 865 | for &(test, input) in edt_test_cases.iter() { 866 | assert_eq!( 867 | super::parse_with(input, us_edt, midnight_naive).unwrap(), 868 | us_edt_midnight_as_utc, 869 | "parse_with/{test}/{input}", 870 | ) 871 | } 872 | 873 | // test us_edt at 23:59:59 - UTC will be one day ahead 874 | let us_edt_before_midnight_as_utc = Utc.ymd(2023, 4, 22).and_hms(3, 59, 59); 875 | for &(test, input) in edt_test_cases.iter() { 876 | assert_eq!( 877 | super::parse_with(input, us_edt, before_midnight_naive).unwrap(), 878 | us_edt_before_midnight_as_utc, 879 | "parse_with/{test}/{input}", 880 | ) 881 | } 882 | } 883 | 884 | #[test] 885 | fn parse_with_est() { 886 | // Eastern Standard Time (EST) is from (as of 2023) 1st Sun in Nov to 2nd Sun in Mar 887 | // It is UTC -5 888 | 889 | let midnight_naive = NaiveTime::from_hms_opt(0, 0, 0).unwrap(); 890 | let before_midnight_naive = NaiveTime::from_hms_opt(23, 59, 59).unwrap(); 891 | let us_est = &FixedOffset::west(5 * 3600); 892 | 893 | let est_test_cases = vec![ 894 | ("ymd", "2023-12-21"), 895 | ("ymd_z", "2023-12-21 EST"), 896 | ("month_ymd", "2023-Dec-21"), 897 | ("month_mdy", "December 21, 2023"), 898 | ("month_dmy", "21 December 2023"), 899 | ("slash_mdy", "12/21/23"), 900 | ("slash_ymd", "2023/12/21"), 901 | ("dot_mdy_or_ymd", "2023.12.21"), 902 | ("chinese_ymd", "2023年12月21日"), 903 | ]; 904 | 905 | // test us_est at midnight 906 | let us_est_midnight_as_utc = Utc.ymd(2023, 12, 21).and_hms(5, 0, 0); 907 | 908 | for &(test, input) in est_test_cases.iter() { 909 | assert_eq!( 910 | super::parse_with(input, us_est, midnight_naive).unwrap(), 911 | us_est_midnight_as_utc, 912 | "parse_with/{test}/{input}", 913 | ) 914 | } 915 | 916 | // test us_est at 23:59:59 - UTC will be one day ahead 917 | let us_est_before_midnight_as_utc = Utc.ymd(2023, 12, 22).and_hms(4, 59, 59); 918 | for &(test, input) in est_test_cases.iter() { 919 | assert_eq!( 920 | super::parse_with(input, us_est, before_midnight_naive).unwrap(), 921 | us_est_before_midnight_as_utc, 922 | "parse_with/{test}/{input}", 923 | ) 924 | } 925 | } 926 | 927 | #[test] 928 | fn parse_with_utc() { 929 | let midnight_naive = NaiveTime::from_hms_opt(0, 0, 0).unwrap(); 930 | let before_midnight_naive = NaiveTime::from_hms_opt(23, 59, 59).unwrap(); 931 | let utc_test_cases = vec![ 932 | ("ymd", "2023-12-21"), 933 | ("ymd_z", "2023-12-21 UTC"), 934 | ("month_ymd", "2023-Dec-21"), 935 | ("month_mdy", "December 21, 2023"), 936 | ("month_dmy", "21 December 2023"), 937 | ("slash_mdy", "12/21/23"), 938 | ("slash_ymd", "2023/12/21"), 939 | ("dot_mdy_or_ymd", "2023.12.21"), 940 | ("chinese_ymd", "2023年12月21日"), 941 | ]; 942 | // test utc at midnight 943 | let utc_midnight = Utc.ymd(2023, 12, 21).and_hms(0, 0, 0); 944 | 945 | for &(test, input) in utc_test_cases.iter() { 946 | assert_eq!( 947 | super::parse_with(input, &Utc, midnight_naive).unwrap(), 948 | utc_midnight, 949 | "parse_with/{test}/{input}", 950 | ) 951 | } 952 | 953 | // test utc at 23:59:59 954 | let utc_before_midnight = Utc.ymd(2023, 12, 21).and_hms(23, 59, 59); 955 | for &(test, input) in utc_test_cases.iter() { 956 | assert_eq!( 957 | super::parse_with(input, &Utc, before_midnight_naive).unwrap(), 958 | utc_before_midnight, 959 | "parse_with/{test}/{input}", 960 | ) 961 | } 962 | } 963 | 964 | #[test] 965 | fn parse_with_local() { 966 | let midnight_naive = NaiveTime::from_hms_opt(0, 0, 0).unwrap(); 967 | let before_midnight_naive = NaiveTime::from_hms_opt(23, 59, 59).unwrap(); 968 | let local_test_cases = vec![ 969 | ("ymd", "2023-12-21"), 970 | ("month_ymd", "2023-Dec-21"), 971 | ("month_mdy", "December 21, 2023"), 972 | ("month_dmy", "21 December 2023"), 973 | ("slash_mdy", "12/21/23"), 974 | ("slash_ymd", "2023/12/21"), 975 | ("dot_mdy_or_ymd", "2023.12.21"), 976 | ("chinese_ymd", "2023年12月21日"), 977 | ]; 978 | 979 | // test local at midnight 980 | let local_midnight_as_utc = Local.ymd(2023, 12, 21).and_hms(0, 0, 0).with_timezone(&Utc); 981 | 982 | for &(test, input) in local_test_cases.iter() { 983 | assert_eq!( 984 | super::parse_with(input, &Local, midnight_naive).unwrap(), 985 | local_midnight_as_utc, 986 | "parse_with/{test}/{input}", 987 | ) 988 | } 989 | 990 | // test local at 23:59:59 991 | let local_before_midnight_as_utc = Local 992 | .ymd(2023, 12, 21) 993 | .and_hms(23, 59, 59) 994 | .with_timezone(&Utc); 995 | 996 | for &(test, input) in local_test_cases.iter() { 997 | assert_eq!( 998 | super::parse_with(input, &Local, before_midnight_naive).unwrap(), 999 | local_before_midnight_as_utc, 1000 | "parse_with/{test}/{input}", 1001 | ) 1002 | } 1003 | } 1004 | } 1005 | -------------------------------------------------------------------------------- /dateparser/src/timezone.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use chrono::offset::FixedOffset; 3 | 4 | /// Tries to parse `[-+]\d\d` continued by `\d\d`. Return FixedOffset if possible. 5 | /// It can parse RFC 2822 legacy timezones. If offset cannot be determined, -0000 will be returned. 6 | /// 7 | /// The additional `colon` may be used to parse a mandatory or optional `:` between hours and minutes, 8 | /// and should return a valid FixedOffset or `Err` when parsing fails. 9 | pub fn parse(s: &str) -> Result { 10 | let offset = if s.contains(':') { 11 | parse_offset_internal(s, colon_or_space, false)? 12 | } else { 13 | parse_offset_2822(s)? 14 | }; 15 | Ok(FixedOffset::east(offset)) 16 | } 17 | 18 | fn parse_offset_2822(s: &str) -> Result { 19 | // tries to parse legacy time zone names 20 | let upto = s 21 | .as_bytes() 22 | .iter() 23 | .position(|&c| !c.is_ascii_alphabetic()) 24 | .unwrap_or(s.len()); 25 | if upto > 0 { 26 | let name = &s[..upto]; 27 | let offset_hours = |o| Ok(o * 3600); 28 | if equals(name, "gmt") || equals(name, "ut") || equals(name, "utc") { 29 | offset_hours(0) 30 | } else if equals(name, "edt") { 31 | offset_hours(-4) 32 | } else if equals(name, "est") || equals(name, "cdt") { 33 | offset_hours(-5) 34 | } else if equals(name, "cst") || equals(name, "mdt") { 35 | offset_hours(-6) 36 | } else if equals(name, "mst") || equals(name, "pdt") { 37 | offset_hours(-7) 38 | } else if equals(name, "pst") { 39 | offset_hours(-8) 40 | } else { 41 | Ok(0) // recommended by RFC 2822: consume but treat it as -0000 42 | } 43 | } else { 44 | let offset = parse_offset_internal(s, |s| Ok(s), false)?; 45 | Ok(offset) 46 | } 47 | } 48 | 49 | fn parse_offset_internal( 50 | mut s: &str, 51 | mut consume_colon: F, 52 | allow_missing_minutes: bool, 53 | ) -> Result 54 | where 55 | F: FnMut(&str) -> Result<&str>, 56 | { 57 | let err_out_of_range = "input is out of range"; 58 | let err_invalid = "input contains invalid characters"; 59 | let err_too_short = "premature end of input"; 60 | 61 | let digits = |s: &str| -> Result<(u8, u8)> { 62 | let b = s.as_bytes(); 63 | if b.len() < 2 { 64 | Err(anyhow!(err_too_short)) 65 | } else { 66 | Ok((b[0], b[1])) 67 | } 68 | }; 69 | let negative = match s.as_bytes().first() { 70 | Some(&b'+') => false, 71 | Some(&b'-') => true, 72 | Some(_) => return Err(anyhow!(err_invalid)), 73 | None => return Err(anyhow!(err_too_short)), 74 | }; 75 | s = &s[1..]; 76 | 77 | // hours (00--99) 78 | let hours = match digits(s)? { 79 | (h1 @ b'0'..=b'9', h2 @ b'0'..=b'9') => i32::from((h1 - b'0') * 10 + (h2 - b'0')), 80 | _ => return Err(anyhow!(err_invalid)), 81 | }; 82 | s = &s[2..]; 83 | 84 | // colons (and possibly other separators) 85 | s = consume_colon(s)?; 86 | 87 | // minutes (00--59) 88 | // if the next two items are digits then we have to add minutes 89 | let minutes = if let Ok(ds) = digits(s) { 90 | match ds { 91 | (m1 @ b'0'..=b'5', m2 @ b'0'..=b'9') => i32::from((m1 - b'0') * 10 + (m2 - b'0')), 92 | (b'6'..=b'9', b'0'..=b'9') => return Err(anyhow!(err_out_of_range)), 93 | _ => return Err(anyhow!(err_invalid)), 94 | } 95 | } else if allow_missing_minutes { 96 | 0 97 | } else { 98 | return Err(anyhow!(err_too_short)); 99 | }; 100 | 101 | let seconds = hours * 3600 + minutes * 60; 102 | Ok(if negative { -seconds } else { seconds }) 103 | } 104 | 105 | /// Returns true when two slices are equal case-insensitively (in ASCII). 106 | /// Assumes that the `pattern` is already converted to lower case. 107 | fn equals(s: &str, pattern: &str) -> bool { 108 | let mut xs = s.as_bytes().iter().map(|&c| match c { 109 | b'A'..=b'Z' => c + 32, 110 | _ => c, 111 | }); 112 | let mut ys = pattern.as_bytes().iter().cloned(); 113 | loop { 114 | match (xs.next(), ys.next()) { 115 | (None, None) => return true, 116 | (None, _) | (_, None) => return false, 117 | (Some(x), Some(y)) if x != y => return false, 118 | _ => (), 119 | } 120 | } 121 | } 122 | 123 | /// Consumes any number (including zero) of colon or spaces. 124 | fn colon_or_space(s: &str) -> Result<&str> { 125 | Ok(s.trim_start_matches(|c: char| c == ':' || c.is_whitespace())) 126 | } 127 | 128 | #[cfg(test)] 129 | mod tests { 130 | use super::*; 131 | 132 | #[test] 133 | fn parse() { 134 | let test_cases = [ 135 | ("-0800", FixedOffset::west(8 * 3600)), 136 | ("+10:00", FixedOffset::east(10 * 3600)), 137 | ("PST", FixedOffset::west(8 * 3600)), 138 | ("PDT", FixedOffset::west(7 * 3600)), 139 | ("UTC", FixedOffset::west(0)), 140 | ("GMT", FixedOffset::west(0)), 141 | ]; 142 | 143 | for &(input, want) in test_cases.iter() { 144 | assert_eq!(super::parse(input).unwrap(), want, "parse/{}", input) 145 | } 146 | } 147 | } 148 | --------------------------------------------------------------------------------