interim-0.2.1/.cargo_vcs_info.json0000644000000001360000000000100125030ustar { "git": { "sha1": "63d88696f8a3a1c8ca653860e688f1a8eb71114b" }, "path_in_vcs": "" }interim-0.2.1/.github/workflows/lint.yml000064400000000000000000000015151046102023000163630ustar 00000000000000name: Code Style on: push: branches: [master] paths: - ".github/workflows/lint.yml" - "**.rs" - "Cargo.toml" - "Cargo.lock" pull_request: branches: [master] paths: - ".github/workflows/lint.yml" - "**.rs" - "Cargo.toml" - "Cargo.lock" jobs: test: name: Lints on ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest] steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup Rust toolchain uses: dtolnay/rust-toolchain@stable with: components: rustfmt, clippy - name: cargo fmt run: cargo fmt --check - name: cargo clippy run: cargo clippy --tests --workspace -- -D warnings interim-0.2.1/.github/workflows/test.yml000064400000000000000000000040621046102023000163740ustar 00000000000000name: Tests on: push: branches: [master] paths: - ".github/workflows/test.yml" - "**.rs" - "Cargo.toml" - "Cargo.lock" pull_request: branches: [master] paths: - ".github/workflows/test.yml" - "**.rs" - "Cargo.toml" - "Cargo.lock" jobs: build: name: Test target ${{ matrix.target }} on ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: target: - x86_64-unknown-linux-gnu - x86_64-pc-windows-msvc - x86_64-apple-darwin - wasm32-wasip1 include: - target: x86_64-unknown-linux-gnu os: ubuntu-latest - target: wasm32-wasip1 os: ubuntu-latest - target: x86_64-pc-windows-msvc os: windows-latest - target: x86_64-apple-darwin os: macos-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup Rust toolchain uses: dtolnay/rust-toolchain@master with: targets: ${{ matrix.target }} toolchain: stable - name: cargo build run: cargo build --target=${{ matrix.target }} docs: name: Doc Tests runs-on: ubuntu-latest strategy: fail-fast: false steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup Rust toolchain uses: dtolnay/rust-toolchain@master with: toolchain: stable - name: Test run: cargo test --features=chrono_0_4 --doc test: name: Test feature ${{ matrix.feature }} runs-on: ubuntu-latest strategy: fail-fast: false matrix: feature: - chrono_0_4 - time_0_3 - jiff_0_1 - jiff_0_2 steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup Rust toolchain uses: dtolnay/rust-toolchain@master with: toolchain: stable - name: Test run: cargo test --features=${{ matrix.feature }} --tests interim-0.2.1/.gitignore000064400000000000000000000000501046102023000132560ustar 00000000000000scratch/ /target/ **/*.rs.bk Cargo.lock interim-0.2.1/Cargo.lock0000644000000415010000000000100104570ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "aho-corasick" version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] [[package]] name = "android-tzdata" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" [[package]] name = "android_system_properties" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" dependencies = [ "libc", ] [[package]] name = "autocfg" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "beef" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a8241f3ebb85c056b509d4327ad0358fbbba6ffb340bf388f26350aeda225b1" [[package]] name = "bumpalo" version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "cc" version = "1.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a012a0df96dd6d06ba9a1b29d6402d1a5d77c6befd2566afdc26e10603dc93d7" dependencies = [ "shlex", ] [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" version = "0.4.39" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" dependencies = [ "android-tzdata", "iana-time-zone", "num-traits", "windows-targets", ] [[package]] name = "chrono-tz" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd6dd8046d00723a59a2f8c5f295c515b9bb9a331ee4f8f3d4dd49e428acd3b6" dependencies = [ "chrono", "chrono-tz-build", "phf", ] [[package]] name = "chrono-tz-build" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e94fea34d77a245229e7746bd2beb786cd2a896f306ff491fb8cecb3074b10a7" dependencies = [ "parse-zoneinfo", "phf_codegen", ] [[package]] name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "deranged" version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" dependencies = [ "powerfmt", ] [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "iana-time-zone" version = "0.1.61" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "wasm-bindgen", "windows-core", ] [[package]] name = "iana-time-zone-haiku" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" dependencies = [ "cc", ] [[package]] name = "interim" version = "0.2.1" dependencies = [ "chrono", "chrono-tz", "jiff 0.1.21", "jiff 0.2.1", "logos", "time", ] [[package]] name = "itoa" version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "jiff" version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed0ce60560149333a8e41ca7dc78799c47c5fd435e2bc18faf6a054382eec037" dependencies = [ "jiff-tzdb-platform", "log", "portable-atomic", "portable-atomic-util", "serde", "windows-sys", ] [[package]] name = "jiff" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3590fea8e9e22d449600c9bbd481a8163bef223e4ff938e5f55899f8cf1adb93" dependencies = [ "jiff-tzdb-platform", "log", "portable-atomic", "portable-atomic-util", "serde", "windows-sys", ] [[package]] name = "jiff-tzdb" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf2cec2f5d266af45a071ece48b1fb89f3b00b2421ac3a5fe10285a6caaa60d3" [[package]] name = "jiff-tzdb-platform" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a63c62e404e7b92979d2792352d885a7f8f83fd1d0d31eea582d77b2ceca697e" dependencies = [ "jiff-tzdb", ] [[package]] name = "js-sys" version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" dependencies = [ "once_cell", "wasm-bindgen", ] [[package]] name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" [[package]] name = "log" version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "logos" version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab6f536c1af4c7cc81edf73da1f8029896e7e1e16a219ef09b184e76a296f3db" dependencies = [ "logos-derive", ] [[package]] name = "logos-codegen" version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "189bbfd0b61330abea797e5e9276408f2edbe4f822d7ad08685d67419aafb34e" dependencies = [ "beef", "fnv", "lazy_static", "proc-macro2", "quote", "regex-syntax", "rustc_version", "syn", ] [[package]] name = "logos-derive" version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebfe8e1a19049ddbfccbd14ac834b215e11b85b90bab0c2dba7c7b92fb5d5cba" dependencies = [ "logos-codegen", ] [[package]] name = "memchr" version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "num-conv" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" [[package]] name = "num-traits" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] [[package]] name = "once_cell" version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "parse-zoneinfo" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24" dependencies = [ "regex", ] [[package]] name = "phf" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" dependencies = [ "phf_shared", ] [[package]] name = "phf_codegen" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a" dependencies = [ "phf_generator", "phf_shared", ] [[package]] name = "phf_generator" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" dependencies = [ "phf_shared", "rand", ] [[package]] name = "phf_shared" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" dependencies = [ "siphasher", ] [[package]] name = "portable-atomic" version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "280dc24453071f1b63954171985a0b0d30058d287960968b9b2aca264c8d4ee6" [[package]] name = "portable-atomic-util" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" dependencies = [ "portable-atomic", ] [[package]] name = "powerfmt" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "proc-macro2" version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" dependencies = [ "proc-macro2", ] [[package]] name = "rand" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "rand_core", ] [[package]] name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" [[package]] name = "regex" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", "regex-automata", "regex-syntax", ] [[package]] name = "regex-automata" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-syntax" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "rustc_version" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ "semver", ] [[package]] name = "semver" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba" [[package]] name = "serde" version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "siphasher" version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" [[package]] name = "syn" version = "2.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46f71c0377baf4ef1cc3e3402ded576dccc315800fbc62dfc7fe04b009773b4a" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "time" version = "0.3.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" dependencies = [ "deranged", "itoa", "num-conv", "powerfmt", "serde", "time-core", "time-macros", ] [[package]] name = "time-core" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" dependencies = [ "num-conv", "time-core", ] [[package]] name = "unicode-ident" version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" [[package]] name = "wasm-bindgen" version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" dependencies = [ "cfg-if", "once_cell", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" dependencies = [ "bumpalo", "log", "proc-macro2", "quote", "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" dependencies = [ "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" dependencies = [ "proc-macro2", "quote", "syn", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" [[package]] name = "windows-core" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ "windows-targets", ] [[package]] name = "windows-sys" version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ "windows-targets", ] [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc", "windows_i686_gnu", "windows_i686_gnullvm", "windows_i686_msvc", "windows_x86_64_gnu", "windows_x86_64_gnullvm", "windows_x86_64_msvc", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" interim-0.2.1/Cargo.toml0000644000000040010000000000100104740ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" rust-version = "1.74.0" name = "interim" version = "0.2.1" authors = ["Conrad Ludgate >().join(" "); #[cfg(feature = "time_0_3")] { use interim::{parse_date_string, Dialect}; use time::OffsetDateTime; println!( "{}", parse_date_string(arg.as_str(), OffsetDateTime::now_utc(), Dialect::Us).unwrap() ); } #[cfg(feature = "chrono_0_4")] { use chrono::Local; use interim::{parse_date_string, Dialect}; println!( "{}", parse_date_string(arg.as_str(), Local::now(), Dialect::Us).unwrap() ); } #[cfg(not(any(feature = "time_0_3", feature = "chrono_0_4")))] { eprintln!("Please enable either time or chrono feature") } } interim-0.2.1/src/datetime.rs000064400000000000000000000266141046102023000142350ustar 00000000000000mod sealed { pub trait Sealed {} } pub trait Date: Clone + PartialOrd + sealed::Sealed { #[doc(hidden)] fn from_ymd(year: i32, month: u8, day: u8) -> Option; #[doc(hidden)] fn offset_months(self, months: i32) -> Option; #[doc(hidden)] fn offset_days(self, days: i64) -> Option; #[doc(hidden)] fn year(&self) -> i32; #[doc(hidden)] fn weekday(&self) -> u8; } pub trait Time: Clone + PartialOrd + sealed::Sealed { #[doc(hidden)] fn from_hms(h: u32, m: u32, s: u32) -> Option; #[doc(hidden)] fn with_micros(self, ms: u32) -> Option; } pub trait DateTime: Sized + sealed::Sealed { type TimeZone; type Date: Date; type Time: Time; #[doc(hidden)] fn new(tz: Self::TimeZone, date: Self::Date, time: Self::Time) -> Self; #[doc(hidden)] fn split(self) -> (Self::TimeZone, Self::Date, Self::Time); #[doc(hidden)] fn with_offset(self, secs: i64) -> Option; #[doc(hidden)] fn offset_seconds(self, secs: i64) -> Option; } #[cfg(feature = "chrono_0_4")] mod chrono { use chrono::{Duration, NaiveDate, NaiveTime, Offset, TimeZone, Timelike}; impl super::sealed::Sealed for NaiveDate {} impl super::sealed::Sealed for NaiveTime {} impl super::sealed::Sealed for chrono::DateTime {} use super::{Date, DateTime, Time}; #[cfg_attr(docsrs, doc(cfg(feature = "chrono_0_4")))] impl Date for NaiveDate { fn from_ymd(year: i32, month: u8, day: u8) -> Option { NaiveDate::from_ymd_opt(year, month as u32, day as u32) } fn offset_months(self, months: i32) -> Option { if months >= 0 { self.checked_add_months(chrono::Months::new(months as u32)) } else { self.checked_sub_months(chrono::Months::new(-months as u32)) } } fn offset_days(self, days: i64) -> Option { self.checked_add_signed(Duration::days(days)) } fn year(&self) -> i32 { chrono::Datelike::year(self) } fn weekday(&self) -> u8 { chrono::Datelike::weekday(self).num_days_from_monday() as u8 } } #[cfg_attr(docsrs, doc(cfg(feature = "chrono_0_4")))] impl Time for NaiveTime { fn from_hms(h: u32, m: u32, s: u32) -> Option { NaiveTime::from_hms_opt(h, m, s) } fn with_micros(self, ms: u32) -> Option { self.with_nanosecond(ms.checked_mul(1_000)?) } } #[cfg_attr(docsrs, doc(cfg(feature = "chrono_0_4")))] impl DateTime for chrono::DateTime { type TimeZone = Tz; type Date = NaiveDate; type Time = NaiveTime; fn new(tz: Self::TimeZone, date: Self::Date, time: Self::Time) -> Self { let datetime = date.and_time(time); let offset = tz.offset_from_utc_datetime(&datetime); Self::from_naive_utc_and_offset(datetime - offset.fix(), offset) } fn split(self) -> (Self::TimeZone, Self::Date, Self::Time) { (self.timezone(), self.date_naive(), self.time()) } fn with_offset(self, secs: i64) -> Option { let offset = self .timezone() .offset_from_utc_date(&self.date_naive()) .fix() .local_minus_utc() as i64; self.offset_seconds(offset - secs) } fn offset_seconds(self, secs: i64) -> Option { self.checked_add_signed(Duration::seconds(secs)) } } } #[cfg(feature = "time_0_3")] mod time { use super::{Date, DateTime, Time}; impl super::sealed::Sealed for time::Date {} impl super::sealed::Sealed for time::Time {} impl super::sealed::Sealed for time::OffsetDateTime {} #[cfg_attr(docsrs, doc(cfg(feature = "time_0_3")))] impl Date for time::Date { fn from_ymd(year: i32, month: u8, day: u8) -> Option { time::Date::from_calendar_date(year, time::Month::try_from(month).ok()?, day).ok() } fn offset_months(self, months: i32) -> Option { // need to calculate this manually :( let (mut y, mut m, d) = self.to_calendar_date(); y += months / 12; let mut months = months % 12; if months < 0 { months += 12; y -= 1; } // months will be between 0..12 let mut m1 = m as u8 + months as u8; if m1 > 12 { m1 -= 12; y += 1; } m = time::Month::try_from(m1).ok()?; let max_day = m.length(y); let d = d.min(max_day); time::Date::from_calendar_date(y, m, d).ok() } fn offset_days(self, days: i64) -> Option { self.checked_add(time::Duration::days(days)) } fn year(&self) -> i32 { time::Date::year(*self) } fn weekday(&self) -> u8 { time::Date::weekday(*self).number_days_from_monday() } } #[cfg_attr(docsrs, doc(cfg(feature = "time_0_3")))] impl Time for time::Time { fn from_hms(h: u32, m: u32, s: u32) -> Option { time::Time::from_hms( u8::try_from(h).ok()?, u8::try_from(m).ok()?, u8::try_from(s).ok()?, ) .ok() } fn with_micros(self, ms: u32) -> Option { self.replace_microsecond(ms).ok() } } #[cfg_attr(docsrs, doc(cfg(feature = "time_0_3")))] impl DateTime for time::OffsetDateTime { type TimeZone = time::UtcOffset; type Date = time::Date; type Time = time::Time; fn new(tz: Self::TimeZone, date: Self::Date, time: Self::Time) -> Self { time::PrimitiveDateTime::new(date, time).assume_offset(tz) } fn split(self) -> (Self::TimeZone, Self::Date, Self::Time) { (self.offset(), self.date(), self.time()) } fn with_offset(self, secs: i64) -> Option { let offset = self.offset().whole_seconds() as i64; self.offset_seconds(offset - secs) } fn offset_seconds(self, secs: i64) -> Option { self.checked_add(time::Duration::seconds(secs)) } } } #[cfg(feature = "jiff_0_1")] mod jiff_0_1 { use jiff::Span; use jiff_0_1 as jiff; use super::{Date, DateTime, Time}; impl super::sealed::Sealed for jiff::civil::Date {} impl super::sealed::Sealed for jiff::civil::Time {} impl super::sealed::Sealed for jiff::Zoned {} #[cfg_attr(docsrs, doc(cfg(feature = "jiff_0_1")))] impl Date for jiff::civil::Date { fn from_ymd(year: i32, month: u8, day: u8) -> Option { jiff::civil::Date::new(year as i16, month as i8, day as i8).ok() } fn offset_months(self, months: i32) -> Option { self.checked_add(Span::new().months(months)).ok() } fn offset_days(self, days: i64) -> Option { self.checked_add(Span::new().days(days)).ok() } fn year(&self) -> i32 { jiff::civil::Date::year(*self) as i32 } fn weekday(&self) -> u8 { jiff::civil::Date::weekday(*self).to_monday_zero_offset() as u8 } } #[cfg_attr(docsrs, doc(cfg(feature = "jiff_0_1")))] impl Time for jiff::civil::Time { fn from_hms(h: u32, m: u32, s: u32) -> Option { jiff::civil::Time::new( i8::try_from(h).ok()?, i8::try_from(m).ok()?, i8::try_from(s).ok()?, 0, ) .ok() } fn with_micros(self, ms: u32) -> Option { jiff::civil::Time::new( self.hour(), self.minute(), self.second(), i32::try_from(ms).ok()?, ) .ok() } } #[cfg_attr(docsrs, doc(cfg(feature = "jiff_0_1")))] impl DateTime for jiff::Zoned { type TimeZone = jiff::tz::TimeZone; type Date = jiff::civil::Date; type Time = jiff::civil::Time; fn new(tz: Self::TimeZone, date: Self::Date, time: Self::Time) -> Self { tz.to_ambiguous_zoned(date.to_datetime(time)) .compatible() .unwrap() } fn split(self) -> (Self::TimeZone, Self::Date, Self::Time) { (self.time_zone().clone(), self.date(), self.time()) } fn with_offset(self, secs: i64) -> Option { let offset = self.time_zone().to_offset(self.timestamp()).0.seconds() as i64; self.offset_seconds(offset - secs) } fn offset_seconds(self, secs: i64) -> Option { self.checked_add(jiff::Span::new().seconds(secs)).ok() } } } #[cfg(feature = "jiff_0_2")] mod jiff_0_2 { use jiff::Span; use jiff_0_2 as jiff; use super::{Date, DateTime, Time}; impl super::sealed::Sealed for jiff::civil::Date {} impl super::sealed::Sealed for jiff::civil::Time {} impl super::sealed::Sealed for jiff::Zoned {} #[cfg_attr(docsrs, doc(cfg(feature = "jiff_0_1")))] impl Date for jiff::civil::Date { fn from_ymd(year: i32, month: u8, day: u8) -> Option { jiff::civil::Date::new(year as i16, month as i8, day as i8).ok() } fn offset_months(self, months: i32) -> Option { self.checked_add(Span::new().months(months)).ok() } fn offset_days(self, days: i64) -> Option { self.checked_add(Span::new().days(days)).ok() } fn year(&self) -> i32 { jiff::civil::Date::year(*self) as i32 } fn weekday(&self) -> u8 { jiff::civil::Date::weekday(*self).to_monday_zero_offset() as u8 } } #[cfg_attr(docsrs, doc(cfg(feature = "jiff_0_1")))] impl Time for jiff::civil::Time { fn from_hms(h: u32, m: u32, s: u32) -> Option { jiff::civil::Time::new( i8::try_from(h).ok()?, i8::try_from(m).ok()?, i8::try_from(s).ok()?, 0, ) .ok() } fn with_micros(self, ms: u32) -> Option { jiff::civil::Time::new( self.hour(), self.minute(), self.second(), i32::try_from(ms).ok()?, ) .ok() } } #[cfg_attr(docsrs, doc(cfg(feature = "jiff_0_1")))] impl DateTime for jiff::Zoned { type TimeZone = jiff::tz::TimeZone; type Date = jiff::civil::Date; type Time = jiff::civil::Time; fn new(tz: Self::TimeZone, date: Self::Date, time: Self::Time) -> Self { tz.to_ambiguous_zoned(date.to_datetime(time)) .compatible() .unwrap() } fn split(self) -> (Self::TimeZone, Self::Date, Self::Time) { (self.time_zone().clone(), self.date(), self.time()) } fn with_offset(self, secs: i64) -> Option { let offset = self.time_zone().to_offset(self.timestamp()).seconds() as i64; self.offset_seconds(offset - secs) } fn offset_seconds(self, secs: i64) -> Option { self.checked_add(jiff::Span::new().seconds(secs)).ok() } } } interim-0.2.1/src/errors.rs000064400000000000000000000025441046102023000137510ustar 00000000000000// use core::error::Error; use core::fmt; use logos::Span; #[derive(Debug, PartialEq, Eq, Clone)] /// Error types for parsing and processing date/time inputs pub enum DateError { ExpectedToken(&'static str, Span), EndOfText(&'static str), MissingDate, MissingTime, UnexpectedDate, UnexpectedAbsoluteDate, UnexpectedTime, } impl fmt::Display for DateError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { DateError::ExpectedToken(message, span) => { write!(f, "expected {message} as position {span:?}") } DateError::EndOfText(message) => { write!(f, "expected {message} at the end of the input") } DateError::MissingDate => f.write_str("date could not be parsed from input"), DateError::MissingTime => f.write_str("time could not be parsed from input"), DateError::UnexpectedDate => f.write_str("expected relative date, found a named date"), DateError::UnexpectedAbsoluteDate => { f.write_str("expected relative date, found an exact date") } DateError::UnexpectedTime => f.write_str("expected duration, found time"), } } } #[cfg(feature = "std")] impl std::error::Error for DateError {} pub type DateResult = Result; interim-0.2.1/src/lib.rs000064400000000000000000000502141046102023000132000ustar 00000000000000//! # interim //! //! interim started as a fork, but ended up being a complete over-haul of [chrono-english](https://github.com/stevedonovan/chrono-english). //! //! The API surface is the same, and all the original tests from chrono-english still pass, although there's some key differences //! //! ## Improvements //! //! Why use interim over chrono-english? //! //! 1. chrono-english is not actively maintained: //! 2. interim simplifies a lot of the code, removing a lot of potential panics and adds some optimisations. //! 3. supports `no_std`, as well as the `time` and `jiff` crates //! //! ## Features //! //! * `std`: This crate is `no_std` compatible. Disable the default-features to disable the std-lib features (just error reporting) //! * `time_0_3`: This crate is compatible with the [time crate](https://github.com/time-rs/time). //! * `chrono_0_4`: This crate is compatible with the [chrono crate](https://github.com/chronotope/chrono). //! * `jiff_0_1`: This crate is compatible with the [jiff crate](https://github.com/BurntSushi/jiff). //! //! ## Supported Formats //! //! `chrono-english` does _absolute_ dates: ISO-like dates "2018-04-01" and the month name forms //! "1 April 2018" and "April 1, 2018". (There's no ambiguity so both of these forms are fine) //! //! The informal "01/04/18" or American form "04/01/18" is supported. //! There is a `Dialect` enum to specify what kind of date English you would like to speak. //! Both short and long years are accepted in this form; short dates pivot between 1940 and 2040. //! //! Then there are are _relative_ dates like 'April 1' and '9/11' (this //! if using `Dialect::Us`). The current year is assumed, but this can be modified by 'next' //! and 'last'. For instance, it is now the 13th of March, 2018: 'April 1' and 'next April 1' //! are in 2018; 'last April 1' is in 2017. //! //! Another relative form is simply a month name //! like 'apr' or 'April' (case-insensitive, only first three letters significant) where the //! day is assumed to be the 1st. //! //! A week-day works in the same way: 'friday' means this //! coming Friday, relative to today. 'last Friday' is unambiguous, //! but 'next Friday' has different meanings; in the US it means the same as 'Friday' //! but otherwise it means the Friday of next week (plus 7 days) //! //! Date and time can be specified also by a number of time units. So "2 days", "3 hours". //! Again, first three letters, but 'd','m' and 'y' are understood (so "3h"). We make //! a distinction between _second_ intervals (seconds,minutes,hours,days,weeks) and _month_ //! intervals (months,years). Month intervals always give us the same date, if possible //! But adding a month to "30 Jan" will give "28 Feb" or "29 Feb" depending if a leap year. //! //! Finally, dates may be followed by time. Either 'formal' like 18:03, with optional //! second (like 18:03:40) or 'informal' like 6.03pm. So one gets "next friday 8pm' and so //! forth. //! //! ## API //! //! There are two entry points: `parse_date_string` and `parse_duration`. The //! first is given the date string, a `DateTime` from which relative dates and //! times operate, and a dialect (either `Dialect::Uk` or `Dialect::Us` //! currently.) The base time also specifies the desired timezone. //! //! ```ignore //! use interim::{parse_date_string, Dialect}; //! use chrono::Local; //! //! let date_time = parse_date_string("next friday 8pm", Local::now(), Dialect::Uk)?; //! println!("{}", date_time.format("%c")); //! ``` //! //! There is a little command-line program `parse-date` in the `examples` folder which can be used to play //! with these expressions. //! //! The other function, `parse_duration`, lets you access just the relative part //! of a string like 'two days ago' or '12 hours'. If successful, returns an //! `Interval`, which is a number of seconds, days, or months. //! //! ``` //! use interim::{parse_duration, Interval}; //! //! assert_eq!(parse_duration("15m ago").unwrap(), Interval::Seconds(-15 * 60)); //! ``` #![cfg_attr(docsrs, feature(doc_cfg))] #![no_std] #![warn(clippy::pedantic)] #![allow( clippy::if_not_else, clippy::missing_errors_doc, clippy::module_name_repetitions, clippy::too_many_lines, clippy::cast_lossless, clippy::cast_possible_truncation, clippy::cast_possible_wrap, clippy::cast_sign_loss )] #[cfg(test)] extern crate alloc; #[cfg(feature = "std")] extern crate std; /// A collection of traits to abstract over date-time implementations pub mod datetime; mod errors; mod parser; mod types; use datetime::DateTime; pub use errors::{DateError, DateResult}; pub use types::Interval; use types::{DateSpec, DateTimeSpec}; #[derive(Clone, Copy, Debug, PartialEq, Eq)] /// Form of english dates to parse pub enum Dialect { Uk, Us, } /// Parse a date-time from the text, potentially relative to `now`. Accepts /// a [`Dialect`] to support some slightly different text parsing behaviour. /// /// ``` /// use interim::{parse_date_string, Dialect}; /// use chrono::{Utc, TimeZone}; /// /// let now = Utc.with_ymd_and_hms(2022, 9, 17, 13, 27, 0).unwrap(); /// let this_friday = parse_date_string("friday 8pm", now, Dialect::Uk).unwrap(); /// /// assert_eq!(this_friday, Utc.with_ymd_and_hms(2022, 9, 23, 20, 0, 0).unwrap()); /// ``` pub fn parse_date_string(s: &str, now: Dt, dialect: Dialect) -> DateResult
{ into_date_string(parser::DateParser::new(s).parse(dialect)?, now, dialect) } fn into_date_string(d: DateTimeSpec, now: Dt, dialect: Dialect) -> DateResult
{ // we may have explicit hour:minute:sec if let Some(dspec) = d.date { dspec .into_date_time(now, d.time, dialect) .ok_or(DateError::MissingDate) } else if let Some(tspec) = d.time { let (tz, date, _) = now.split(); // no date, use todays date tspec.into_date_time(tz, date).ok_or(DateError::MissingTime) } else { Err(DateError::MissingTime) } } /// Parse an [`Interval`] from the text /// /// ``` /// use interim::{parse_duration, Interval}; /// use chrono::{Utc, TimeZone}; /// /// let now = Utc.with_ymd_and_hms(2022, 9, 17, 13, 27, 0).unwrap(); /// let week_ago = parse_duration("1 week ago").unwrap(); /// let minutes = parse_duration("10m").unwrap(); /// /// assert_eq!(week_ago, Interval::Days(-7)); /// assert_eq!(minutes, Interval::Seconds(10*60)); /// ``` pub fn parse_duration(s: &str) -> DateResult { let d = parser::DateParser::new(s).parse(Dialect::Uk)?; if d.time.is_some() { return Err(DateError::UnexpectedTime); } match d.date { Some(DateSpec::Relative(skip)) => Ok(skip), Some(DateSpec::Absolute(_)) => Err(DateError::UnexpectedAbsoluteDate), Some(DateSpec::FromName(..)) => Err(DateError::UnexpectedDate), None => Err(DateError::MissingDate), } } #[cfg(test)] mod tests { #![allow(unused_imports)] use crate::{parse_duration, DateError, Dialect, Interval}; use alloc::string::String; use alloc::string::ToString; #[cfg(feature = "chrono_0_4")] #[track_caller] fn format_chrono(d: &crate::types::DateTimeSpec, dialect: Dialect) -> String { use chrono::{FixedOffset, TimeZone}; let base = FixedOffset::east_opt(7200) .unwrap() .with_ymd_and_hms(2018, 3, 21, 11, 00, 00) .unwrap(); match crate::into_date_string(d.clone(), base, dialect) { Err(e) => { panic!("unexpected error attempting to format [chrono] {d:?}\n\t{e:?}") } Ok(date) => date.format("%+").to_string(), } } #[cfg(feature = "time_0_3")] #[track_caller] fn format_time(d: &crate::types::DateTimeSpec, dialect: Dialect) -> String { use time::{Date, Month, PrimitiveDateTime, Time, UtcOffset}; let base = PrimitiveDateTime::new( Date::from_calendar_date(2018, Month::March, 21).unwrap(), Time::from_hms(11, 00, 00).unwrap(), ) .assume_offset(UtcOffset::from_whole_seconds(7200).unwrap()); match crate::into_date_string(d.clone(), base, dialect) { Err(e) => { panic!("unexpected error attempting to format [time] {d:?}\n\t{e:?}") } Ok(date) => { let format = time::format_description::parse( "[year]-[month]-[day]T[hour]:[minute]:[second][offset_hour sign:mandatory]:[offset_minute]", ).unwrap(); date.format(&format).unwrap() } } } #[cfg(feature = "jiff_0_1")] #[track_caller] fn format_jiff_0_1(d: &crate::types::DateTimeSpec, dialect: Dialect) -> String { use jiff_0_1::{ civil::Date, civil::DateTime, civil::Time, tz::Offset, tz::TimeZone, Zoned, }; let tz = TimeZone::fixed(Offset::from_seconds(7200).unwrap()); let base = DateTime::from_parts(Date::constant(2018, 3, 21), Time::constant(11, 00, 00, 0)); let base = tz.to_zoned(base).unwrap(); match crate::into_date_string(d.clone(), base, dialect) { Err(e) => { panic!("unexpected error attempting to format [time] {d:?}\n\t{e:?}") } Ok(date) => { // let format = time::format_description::parse( // "[year]-[month]-[day]T[hour]:[minute]:[second][offset_hour sign:mandatory]:[offset_minute]", // ).unwrap(); date.strftime("%FT%T%:z").to_string() } } } #[cfg(feature = "jiff_0_2")] #[track_caller] fn format_jiff_0_2(d: &crate::types::DateTimeSpec, dialect: Dialect) -> String { use jiff_0_2::{ civil::Date, civil::DateTime, civil::Time, tz::Offset, tz::TimeZone, Zoned, }; let tz = TimeZone::fixed(Offset::from_seconds(7200).unwrap()); let base = DateTime::from_parts(Date::constant(2018, 3, 21), Time::constant(11, 00, 00, 0)); let base = tz.to_zoned(base).unwrap(); match crate::into_date_string(d.clone(), base, dialect) { Err(e) => { panic!("unexpected error attempting to format [time] {d:?}\n\t{e:?}") } Ok(date) => { // let format = time::format_description::parse( // "[year]-[month]-[day]T[hour]:[minute]:[second][offset_hour sign:mandatory]:[offset_minute]", // ).unwrap(); date.strftime("%FT%T%:z").to_string() } } } macro_rules! assert_date_string { ($s:literal, $dialect:ident, $expect:literal) => { let dialect = Dialect::$dialect; let input = $s; let _date = match crate::parser::DateParser::new(input).parse(dialect) { Err(e) => { panic!("unexpected error attempting to parse [chrono] {input:?}\n\t{e:?}") } Ok(date) => date, }; #[cfg(feature = "chrono_0_4")] { let output = format_chrono(&_date, dialect); let expected: &str = $expect; if output != expected { panic!("unexpected output attempting to format [chrono] {input:?}.\nexpected: {expected:?}\n parsed: {_date:?} [{output:?}]"); } } #[cfg(feature = "time_0_3")] { let output = format_time(&_date, dialect); let expected: &str = $expect; if output != expected { panic!("unexpected output attempting to format [time] {input:?}.\nexpected: {expected:?}\n parsed: {_date:?} [{output:?}]"); } } #[cfg(feature = "jiff_0_1")] { let output = format_jiff_0_1(&_date, dialect); let expected: &str = $expect; if output != expected { panic!("unexpected output attempting to format [jiff] {input:?}.\nexpected: {expected:?}\n parsed: {_date:?} [{output:?}]"); } } #[cfg(feature = "jiff_0_2")] { let output = format_jiff_0_2(&_date, dialect); let expected: &str = $expect; if output != expected { panic!("unexpected output attempting to format [jiff] {input:?}.\nexpected: {expected:?}\n parsed: {_date:?} [{output:?}]"); } } }; } #[test] fn basics() { // Day of week - relative to today. May have a time part assert_date_string!("friday", Uk, "2018-03-23T00:00:00+02:00"); assert_date_string!("friday 10:30", Uk, "2018-03-23T10:30:00+02:00"); assert_date_string!("friday 8pm", Uk, "2018-03-23T20:00:00+02:00"); assert_date_string!("12am", Uk, "2018-03-21T00:00:00+02:00"); assert_date_string!("12pm", Uk, "2018-03-21T12:00:00+02:00"); assert_date_string!("7:26 AM", Uk, "2018-03-21T07:26:00+02:00"); assert_date_string!("7:26 PM", Uk, "2018-03-21T19:26:00+02:00"); // The day of week is the _next_ day after today, so "Tuesday" is the next Tuesday after Wednesday assert_date_string!("tues", Uk, "2018-03-27T00:00:00+02:00"); // The expression 'next Monday' is ambiguous; in the US it means the day following (same as 'Monday') // (This is how the `date` command interprets it) assert_date_string!("next mon", Us, "2018-03-26T00:00:00+02:00"); // but otherwise it means the day in the next week.. assert_date_string!("next mon", Uk, "2018-04-02T00:00:00+02:00"); assert_date_string!("last year", Uk, "2017-03-21T00:00:00+02:00"); assert_date_string!("this year", Uk, "2018-03-21T00:00:00+02:00"); assert_date_string!("next year", Uk, "2019-03-21T00:00:00+02:00"); assert_date_string!("last fri 9.30", Uk, "2018-03-16T09:30:00+02:00"); // date expressed as month, day - relative to today. May have a time part assert_date_string!("8/11", Us, "2018-08-11T00:00:00+02:00"); assert_date_string!("last 8/11", Us, "2017-08-11T00:00:00+02:00"); assert_date_string!("last 8/11 9am", Us, "2017-08-11T09:00:00+02:00"); assert_date_string!("8/11", Uk, "2018-11-08T00:00:00+02:00"); assert_date_string!("last 8/11", Uk, "2017-11-08T00:00:00+02:00"); assert_date_string!("last 8/11 9am", Uk, "2017-11-08T09:00:00+02:00"); assert_date_string!("April 1 8.30pm", Uk, "2018-04-01T20:30:00+02:00"); // advance by time unit from today // without explicit time, use base time - otherwise override assert_date_string!("2d", Uk, "2018-03-23T11:00:00+02:00"); assert_date_string!("2d 03:00", Uk, "2018-03-23T03:00:00+02:00"); assert_date_string!("3 weeks", Uk, "2018-04-11T11:00:00+02:00"); assert_date_string!("3h", Uk, "2018-03-21T14:00:00+02:00"); assert_date_string!("6 months", Uk, "2018-09-21T00:00:00+02:00"); assert_date_string!("6 months ago", Uk, "2017-09-21T00:00:00+02:00"); assert_date_string!("3 hours ago", Uk, "2018-03-21T08:00:00+02:00"); assert_date_string!(" -3h", Uk, "2018-03-21T08:00:00+02:00"); assert_date_string!(" -3 month", Uk, "2017-12-21T00:00:00+02:00"); // absolute date with year, month, day - formal ISO and informal UK or US assert_date_string!("2017-06-30", Uk, "2017-06-30T00:00:00+02:00"); assert_date_string!("30/06/17", Uk, "2017-06-30T00:00:00+02:00"); assert_date_string!("06/30/17", Us, "2017-06-30T00:00:00+02:00"); // may be followed by time part, formal and informal assert_date_string!("2017-06-30 08:20:30", Uk, "2017-06-30T08:20:30+02:00"); assert_date_string!( "2017-06-30 08:20:30 +04:00", Uk, "2017-06-30T06:20:30+02:00" ); assert_date_string!("2017-06-30 08:20:30 +0400", Uk, "2017-06-30T06:20:30+02:00"); assert_date_string!("2017-06-30T08:20:30Z", Uk, "2017-06-30T10:20:30+02:00"); assert_date_string!("2017-06-30T08:20:30", Uk, "2017-06-30T08:20:30+02:00"); assert_date_string!("2017-06-30 12.20", Uk, "2017-06-30T12:20:00+02:00"); assert_date_string!("2017-06-30 8.20", Uk, "2017-06-30T08:20:00+02:00"); assert_date_string!("2017-06-30 12.15am", Uk, "2017-06-30T00:15:00+02:00"); assert_date_string!("2017-06-30 12.25pm", Uk, "2017-06-30T12:25:00+02:00"); assert_date_string!("2017-06-30 12:30pm", Uk, "2017-06-30T12:30:00+02:00"); assert_date_string!("2017-06-30 8.30pm", Uk, "2017-06-30T20:30:00+02:00"); assert_date_string!("2017-06-30 8:30pm", Uk, "2017-06-30T20:30:00+02:00"); assert_date_string!("2017-06-30 2am", Uk, "2017-06-30T02:00:00+02:00"); assert_date_string!("30 June 2018", Uk, "2018-06-30T00:00:00+02:00"); assert_date_string!("June 30, 2018", Uk, "2018-06-30T00:00:00+02:00"); assert_date_string!("June 30, 2018", Uk, "2018-06-30T00:00:00+02:00"); } #[test] fn durations() { macro_rules! assert_duration { ($s:literal, $expect:expr) => { let dur = parse_duration($s).unwrap(); assert_eq!(dur, $expect); }; } macro_rules! assert_duration_err { ($s:literal, $expect:expr) => { let err = parse_duration($s).unwrap_err(); assert_eq!(err, $expect); }; } assert_duration!("1 seconds", Interval::Seconds(1)); assert_duration!("24 seconds", Interval::Seconds(24)); assert_duration!("34 s", Interval::Seconds(34)); assert_duration!("34 sec", Interval::Seconds(34)); assert_duration!("6h", Interval::Seconds(6 * 3600)); assert_duration!("4 hours ago", Interval::Seconds(-4 * 3600)); assert_duration!("5 min", Interval::Seconds(5 * 60)); assert_duration!("10m", Interval::Seconds(10 * 60)); assert_duration!("15m ago", Interval::Seconds(-15 * 60)); assert_duration!("1 day", Interval::Days(1)); assert_duration!("2 days ago", Interval::Days(-2)); assert_duration!("3 weeks", Interval::Days(21)); assert_duration!("2 weeks ago", Interval::Days(-14)); assert_duration!("1 month", Interval::Months(1)); assert_duration!("6 months", Interval::Months(6)); assert_duration!("8 years", Interval::Months(12 * 8)); // errors assert_duration_err!("2020-01-01", DateError::UnexpectedAbsoluteDate); assert_duration_err!("2 days 15:00", DateError::UnexpectedTime); assert_duration_err!("tuesday", DateError::UnexpectedDate); assert_duration_err!( "bananas", DateError::ExpectedToken("unsupported identifier", 0..7) ); } #[cfg(feature = "chrono_0_4")] #[test] /// fn regression_12_chrono() { use chrono::TimeZone; let now: chrono::DateTime<_> = chrono_tz::America::Los_Angeles .with_ymd_and_hms(2024, 1, 1, 12, 00, 00) .unwrap(); let without_timezone = crate::parse_date_string("2024-06-01 12:00:00", now, Dialect::Us).unwrap(); let with_timezone = crate::parse_date_string("2024-06-01 12:00:00 -07:00", now, Dialect::Us).unwrap(); assert_eq!(without_timezone, with_timezone); assert_eq!(with_timezone.to_string(), "2024-06-01 12:00:00 PDT"); } #[cfg(feature = "jiff_0_1")] #[test] fn regression_12_jiff() { use jiff_0_1::{ civil::Date, civil::DateTime, civil::Time, tz::Offset, tz::TimeZone, Zoned, }; let tz = TimeZone::get("America/Los_Angeles").unwrap(); let base = DateTime::from_parts(Date::constant(2024, 1, 1), Time::constant(12, 00, 00, 0)); let now = tz.to_zoned(base).unwrap(); let without_timezone = crate::parse_date_string("2024-06-01 12:00:00", now.clone(), Dialect::Us).unwrap(); let with_timezone = crate::parse_date_string("2024-06-01 12:00:00 -07:00", now, Dialect::Us).unwrap(); assert_eq!(without_timezone, with_timezone); assert_eq!( with_timezone.to_string(), "2024-06-01T12:00:00-07:00[America/Los_Angeles]" ); } } interim-0.2.1/src/parser.rs000064400000000000000000000455671046102023000137450ustar 00000000000000use logos::{Lexer, Logos}; use crate::{ types::{ month_name, time_unit, week_day, AbsDate, ByName, DateSpec, DateTimeSpec, Direction, Lowercase, TimeSpec, }, DateError, DateResult, Dialect, Interval, }; // when we parse dates, there's often a bit of time parsed.. #[derive(Clone, Copy, Debug)] enum TimeKind { Formal, Informal, Am, Pm, Unknown, } pub struct DateParser<'a> { s: Lexer<'a, Tokens>, maybe_time: Option<(u32, TimeKind)>, } #[derive(logos::Logos, Debug, PartialEq, Eq, Clone, Copy)] #[logos(skip r"[ \t\n\f]+")] enum Tokens { #[regex("[0-9]{1,4}", |lex| lex.slice().parse().map_err(|_| ()))] Number(u32), #[regex("[a-zA-Z]+")] Ident, // punctuation #[token("-")] Dash, #[token("/")] Slash, #[token(":")] Colon, #[token(".")] Dot, #[token(",")] Comma, #[token("+")] Plus, } const NOW: Lowercase = Lowercase::literal("now"); const TODAY: Lowercase = Lowercase::literal("today"); const YESTERDAY: Lowercase = Lowercase::literal("yesterday"); const TOMORROW: Lowercase = Lowercase::literal("tomorrow"); const NEXT: Lowercase = Lowercase::literal("next"); const LAST: Lowercase = Lowercase::literal("last"); const THIS: Lowercase = Lowercase::literal("this"); const AM: Lowercase = Lowercase::literal("am"); const PM: Lowercase = Lowercase::literal("pm"); const Z: Lowercase = Lowercase::literal("z"); impl<'a> DateParser<'a> { pub fn new(text: &'a str) -> DateParser<'a> { DateParser { s: Tokens::lexer(text), maybe_time: None, } } fn next_num(&mut self) -> DateResult { match self.s.next() { Some(Ok(Tokens::Number(n))) => Ok(n), Some(_) => Err(DateError::ExpectedToken("number", self.s.span())), None => Err(DateError::EndOfText("number")), } } fn iso_date(&mut self, year: i32) -> DateResult { let month = self.next_num()?; match self.s.next() { Some(Ok(Tokens::Dash)) => {} Some(_) => return Err(DateError::ExpectedToken("'-'", self.s.span())), None => return Err(DateError::EndOfText("'-'")), } let day = self.next_num()?; Ok(DateSpec::Absolute(AbsDate { year, month, day })) } // We have already parsed maybe the next/last/... // and the first set of numbers followed by the slash // // US: // mm/dd/yy // mm/dd/yyyy // next mm/dd // // UK: // dd/mm/yy // dd/mm/yyyy // next dd/mm fn informal_date( &mut self, day_or_month: u32, dialect: Dialect, direct: Direction, ) -> DateResult { let month_or_day = self.next_num()?; let (day, month) = if dialect == Dialect::Us { (month_or_day, day_or_month) } else { (day_or_month, month_or_day) }; let s = self.s.clone(); if self.s.next() != Some(Ok(Tokens::Slash)) { // backtrack self.s = s; Ok(DateSpec::FromName(ByName::DayMonth { day, month }, direct)) } else { // pivot (1940, 2040) let year = match self.next_num()? as i32 { y @ 0..=40 => 2000 + y, y @ 41..=99 => 1900 + y, y => y, }; Ok(DateSpec::Absolute(AbsDate { year, month, day })) } } fn parse_date(&mut self, dialect: Dialect) -> DateResult> { let (sign, direct); let token = match self.s.next() { Some(Ok(Tokens::Dash)) => { sign = true; direct = None; self.s.next() } Some(Ok(Tokens::Ident)) => { sign = false; direct = match Lowercase::from(self.s.slice()) { NOW | TODAY => return Ok(Some(DateSpec::Relative(Interval::Days(0)))), YESTERDAY => return Ok(Some(DateSpec::Relative(Interval::Days(-1)))), TOMORROW => return Ok(Some(DateSpec::Relative(Interval::Days(1)))), NEXT => Some(Direction::Next), LAST => Some(Direction::Last), THIS => Some(Direction::Here), _ => None, }; if direct.is_some() { // consume self.s.next() } else { Some(Ok(Tokens::Ident)) } } t => { sign = false; direct = None; t } }; match token { // date needs some token None => Err(DateError::EndOfText("empty date string")), // none of these characters begin a date or duration Some( Ok( Tokens::Colon | Tokens::Comma | Tokens::Dash | Tokens::Dot | Tokens::Slash | Tokens::Plus, ) | Err(()), ) => Err(DateError::MissingDate), // '-June' doesn't make sense Some(Ok(Tokens::Ident)) if sign => { Err(DateError::ExpectedToken("number", self.s.span())) } // {weekday} [{time}] // {month} [{day}, {year}] [{time}] // {month} [{day}] [{time}] Some(Ok(Tokens::Ident)) => { let direct = direct.unwrap_or(Direction::Here); if let Some(month) = month_name(Lowercase::from(self.s.slice())) { // {month} [{day}, {year}] // {month} [{day}] [{time}] if let Some(Ok(Tokens::Number(day))) = self.s.next() { let s = self.s.clone(); if self.s.next() == Some(Ok(Tokens::Comma)) { // comma found, expect year let year = self.next_num()? as i32; Ok(Some(DateSpec::Absolute(AbsDate { year, month, day }))) } else { // no comma found, we might expect a time component (if any) // backtrack, we'll try parse the time component later self.s = s; Ok(Some(DateSpec::FromName( ByName::DayMonth { day, month }, direct, ))) } } else { // We only have a month name to work with Ok(Some(DateSpec::FromName(ByName::MonthName(month), direct))) } } else if let Some(weekday) = week_day(Lowercase::from(self.s.slice())) { // {weekday} [{time}] // we'll try parse the time component later Ok(Some(DateSpec::FromName(ByName::WeekDay(weekday), direct))) } else if let Some(interval) = time_unit(Lowercase::from(self.s.slice())) { let interval = match direct { Direction::Last => interval * -1, #[allow(clippy::erasing_op)] Direction::Here => interval * 0, Direction::Next => interval, }; Ok(Some(DateSpec::Relative(interval))) } else { Err(DateError::ExpectedToken( "unsupported identifier", self.s.span(), )) } } // {day}/{month} // {month}/{day} // {day} {month} // {n} {interval} // {year}-{month}-{day} Some(Ok(Tokens::Number(n))) => { match self.s.next() { // if sign is set, we should expect something like '- 5 minutes' None if sign => Err(DateError::EndOfText("duration")), // we want a full date Some(Ok(Tokens::Comma | Tokens::Plus | Tokens::Number(_)) | Err(())) => { Err(DateError::ExpectedToken("date", self.s.span())) } // if direct is set, we should expect a day or month to direct against None | Some(Ok(Tokens::Colon | Tokens::Dot | Tokens::Dash)) if direct.is_some() => { Err(DateError::EndOfText("day or month name")) } // if no extra tokens, this is probably just a year None => Ok(Some(DateSpec::Absolute(AbsDate { year: n as i32, month: 1, day: 1, }))), Some(Ok(Tokens::Ident)) => { let direct = direct.unwrap_or(Direction::Here); let name = Lowercase::from(self.s.slice()); if let Some(month) = month_name(name) { let day = n; if let Some(Ok(Tokens::Number(year))) = self.s.next() { // 4 July 2017 let year = year as i32; Ok(Some(DateSpec::Absolute(AbsDate { year, month, day }))) } else { // 4 July Ok(Some(DateSpec::FromName( ByName::DayMonth { day, month }, direct, ))) } } else if let Some(u) = time_unit(name) { let n = n as i32; // '2 days' if sign { Ok(Some(DateSpec::Relative(u * -n))) } else { match self.s.next() { Some(Ok(Tokens::Ident)) if Lowercase::from(self.s.slice()) == Lowercase::literal("ago") => { Ok(Some(DateSpec::Relative(u * -n))) } Some(Ok(Tokens::Ident)) => { Err(DateError::ExpectedToken("'ago'", self.s.span())) } Some(Ok(Tokens::Number(h))) => { self.maybe_time = Some((h, TimeKind::Unknown)); Ok(Some(DateSpec::Relative(u * n))) } _ => Ok(Some(DateSpec::Relative(u * n))), } } } else if name == AM { self.maybe_time = Some((n, TimeKind::Am)); Ok(None) } else if name == PM { self.maybe_time = Some((n, TimeKind::Pm)); Ok(None) } else { Err(DateError::ExpectedToken( "month or time unit", self.s.span(), )) } } Some(Ok(Tokens::Colon)) => { self.maybe_time = Some((n, TimeKind::Formal)); Ok(None) } Some(Ok(Tokens::Dot)) => { self.maybe_time = Some((n, TimeKind::Informal)); Ok(None) } Some(Ok(Tokens::Dash)) => Ok(Some(self.iso_date(n as i32)?)), Some(Ok(Tokens::Slash)) => Ok(Some(self.informal_date( n, dialect, direct.unwrap_or(Direction::Here), )?)), } } } } fn formal_time(&mut self, hour: u32) -> DateResult { let min = self.next_num()?; let mut sec = 0; let mut micros = 0; // minute may be followed by [:secs][am|pm] let tnext = match self.s.next() { Some(Ok(Tokens::Colon)) => { sec = self.next_num()?; match self.s.next() { Some(Ok(Tokens::Dot)) => { // after a `.` implies these are subseconds. // We only care for microsecond precision, so let's // get only the 6 most significant digits micros = self.next_num()?; while micros > 1_000_000 { micros /= 10; } self.s.next() } t => t, } } // we don't expect any of these after parsing minutes Some( Ok(Tokens::Dash | Tokens::Slash | Tokens::Dot | Tokens::Comma | Tokens::Plus) | Err(()), ) => { return Err(DateError::ExpectedToken("':'", self.s.span())); } t => t, }; match tnext { // we need no timezone or hour offset. All good :) None => Ok(TimeSpec::new(hour, min, sec, micros)), // +/- timezone offset Some(Ok(tok @ (Tokens::Plus | Tokens::Dash))) => { let sign = if tok == Tokens::Dash { -1 } else { 1 }; // after a +/-, we expect a numerical offset. // either HH:MM or HHMM let mut hours = self.next_num()?; let s = self.s.clone(); let minutes = if self.s.next() != Some(Ok(Tokens::Colon)) { // backtrack, we should have the hours and minutes in the single number self.s = s; // 0030 // ^^ let minutes = hours % 100; hours /= 100; minutes } else { // 02:00 // ^^ self.next_num()? }; // hours and minutes offset in seconds let res = 60 * (minutes + 60 * hours); let offset = i64::from(res) * sign; Ok(TimeSpec::new(hour, min, sec, micros).with_offset(offset)) } Some(Ok(Tokens::Ident)) => match Lowercase::from(self.s.slice()) { // 0-offset timezone Z => Ok(TimeSpec::new(hour, min, sec, micros).with_offset(0)), // morning AM if hour == 12 => Ok(TimeSpec::new(0, min, sec, micros)), AM => Ok(TimeSpec::new(hour, min, sec, micros)), // afternoon PM if hour == 12 => Ok(TimeSpec::new(12, min, sec, micros)), PM => Ok(TimeSpec::new(hour + 12, min, sec, micros)), _ => Err(DateError::ExpectedToken("expected Z/am/pm", self.s.span())), }, Some( Ok(Tokens::Slash | Tokens::Colon | Tokens::Dot | Tokens::Comma | Tokens::Number(_)) | Err(()), ) => Err(DateError::ExpectedToken("expected timezone", self.s.span())), } } fn informal_time(&mut self, hour: u32) -> DateResult { let min = self.next_num()?; let hour = match self.s.next() { None => hour, Some(Ok(Tokens::Ident)) if Lowercase::from(self.s.slice()) == AM && hour == 12 => 0, Some(Ok(Tokens::Ident)) if Lowercase::from(self.s.slice()) == AM => hour, Some(Ok(Tokens::Ident)) if Lowercase::from(self.s.slice()) == PM && hour == 12 => 12, Some(Ok(Tokens::Ident)) if Lowercase::from(self.s.slice()) == PM => hour + 12, Some(_) => return Err(DateError::ExpectedToken("expected am/pm", self.s.span())), }; Ok(TimeSpec::new(hour, min, 0, 0)) } pub fn parse_time(&mut self) -> DateResult> { // here the date parser looked ahead and saw an hour followed by some separator if let Some((h, kind)) = self.maybe_time { Ok(Some(match kind { TimeKind::Formal => self.formal_time(h)?, TimeKind::Informal => self.informal_time(h)?, TimeKind::Am if h == 12 => TimeSpec::new(0, 0, 0, 0), TimeKind::Am => TimeSpec::new(h, 0, 0, 0), TimeKind::Pm if h == 12 => TimeSpec::new(12, 0, 0, 0), TimeKind::Pm => TimeSpec::new(h + 12, 0, 0, 0), TimeKind::Unknown => match self.s.next() { Some(Ok(Tokens::Colon)) => self.formal_time(h)?, Some(Ok(Tokens::Dot)) => self.informal_time(h)?, Some(_) => return Err(DateError::ExpectedToken(": or .", self.s.span())), None => return Err(DateError::EndOfText(": or .")), }, })) } else { let s = self.s.clone(); if self.s.next() != Some(Ok(Tokens::Ident)) || self.s.slice() != "T" { // backtrack if we weren't able to consume a 'T' time separator self.s = s; } // we're parsing times so we should expect an hour number. // if we don't find one, then there's no time here let hour = match self.s.next() { None => return Ok(None), Some(Ok(Tokens::Number(n))) => n, Some(_) => return Err(DateError::ExpectedToken("number", self.s.span())), }; match self.s.next() { // hh:mm Some(Ok(Tokens::Colon)) => self.formal_time(hour).map(Some), // hh.mm Some(Ok(Tokens::Dot)) => self.informal_time(hour).map(Some), // 9am Some(Ok(Tokens::Ident)) => match Lowercase::literal(self.s.slice()) { AM => Ok(Some(TimeSpec::new(hour, 0, 0, 0))), PM => Ok(Some(TimeSpec::new(hour + 12, 0, 0, 0))), _ => Err(DateError::ExpectedToken("am/pm", self.s.span())), }, Some(_) => Err(DateError::ExpectedToken("am/pm, ':' or '.'", self.s.span())), None => Err(DateError::EndOfText("am/pm, ':' or '.'")), } } } pub fn parse(&mut self, dialect: Dialect) -> DateResult { let date = self.parse_date(dialect)?; let time = self.parse_time()?; Ok(DateTimeSpec { date, time }) } } interim-0.2.1/src/types.rs000064400000000000000000000267411046102023000136060ustar 00000000000000use core::ops::Mul; use crate::datetime::{Date, DateTime, Time}; use crate::Dialect; // implements next/last direction in expressions like 'next friday' and 'last 4 july' #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Direction { Next, Last, Here, } // all expressions modifiable with next/last; 'fri', 'jul', '5 may'. #[derive(Debug, Clone)] pub enum ByName { WeekDay(u8), MonthName(u32), DayMonth { day: u32, month: u32 }, } // fn add_days(base: DateTime, days: i64) -> Option> { // base.checked_add_signed(Duration::days(days)) // } fn next_last_direction(date: &T, base: &T, direct: Direction) -> Option { match (date.partial_cmp(base), direct) { (Some(core::cmp::Ordering::Greater), Direction::Last) => Some(-1), (Some(core::cmp::Ordering::Less), Direction::Next) => Some(1), _ => None, } } impl ByName { pub fn into_date_time( self, base: Dt, ts: Option, dialect: Dialect, mut direct: Direction, ) -> Option
{ let (tz, base_date, base_time) = base.split(); let ts = ts.unwrap_or(TimeSpec::new(0, 0, 0, 0)); let this_year = base_date.year(); let date = match self { ByName::WeekDay(nd) => { // a plain 'Friday' means the same as 'next Friday'. // an _explicit_ 'next Friday' has dialect-dependent meaning! // In UK English, it means 'Friday of next week', // but in US English, just the next Friday let mut extra_week = false; match direct { Direction::Here => direct = Direction::Next, Direction::Next if dialect == Dialect::Uk => { extra_week = true; } _ => (), }; let this_day = base_date.weekday() as i64; let that_day = nd as i64; let diff_days = that_day - this_day; let mut date = base_date.clone().offset_days(diff_days)?; if let Some(correct) = next_last_direction(&date, &base_date, direct) { date = date.offset_days(7 * correct as i64)?; } if extra_week { date = date.offset_days(7)?; } if diff_days == 0 { // same day - comparing times will determine which way we swing... let this_time = ::from_hms(ts.hour, ts.min, ts.sec)?; if let Some(correct) = next_last_direction(&this_time, &base_time, direct) { date = date.offset_days(7 * correct as i64)?; } } date } ByName::MonthName(month) => { let mut date = ::from_ymd(this_year, month as u8, 1)?; if let Some(correct) = next_last_direction(&date, &base_date, direct) { date = ::from_ymd(this_year + correct, month as u8, 1)?; } date } ByName::DayMonth { day, month } => { let mut date = ::from_ymd(this_year, month as u8, day as u8)?; if let Some(correct) = next_last_direction(&date, &base_date, direct) { date = ::from_ymd(this_year + correct, month as u8, day as u8)?; } date } }; ts.into_date_time(tz, date) } } #[derive(Debug, Clone)] pub struct AbsDate { pub year: i32, pub month: u32, pub day: u32, } impl AbsDate { pub fn into_date(self) -> Option { D::from_ymd(self.year, self.month as u8, self.day as u8) } } /// A generic amount of time, in either seconds, days, or months. /// /// This way, a user can decide how they want to treat days (which do /// not always have the same number of seconds) or months (which do not always /// have the same number of days). // // Skipping a given number of time units. // The subtlety is that we treat duration as seconds until we get // to months, where we want to preserve dates. So adding a month to // '5 May' gives '5 June'. Adding a month to '30 Jan' gives 'Feb 28' or 'Feb 29' // depending on whether this is a leap year. #[derive(Debug, PartialEq, Eq, Clone)] pub enum Interval { Seconds(i32), Days(i32), Months(i32), } impl Mul for Interval { type Output = Interval; fn mul(self, rhs: i32) -> Self::Output { match self { Interval::Seconds(x) => Interval::Seconds(x * rhs), Interval::Days(x) => Interval::Days(x * rhs), Interval::Months(x) => Interval::Months(x * rhs), } } } impl Interval { fn into_date_time(self, base: Dt, ts: Option) -> Option
{ match self { Interval::Seconds(secs) => { // since numbers of seconds _is a timespec_, we don't add the timespec on top // eg now + 15m shouldn't then process 12pm after it. // Ideally Interval::Seconds should be part of timespec. base.offset_seconds(secs as i64) } Interval::Days(days) => { let (tz, date, time) = base.split(); let date = date.offset_days(days as i64)?; if let Some(ts) = ts { ts.into_date_time(tz, date) } else { Some(Dt::new(tz, date, time)) } } Interval::Months(months) => { let (tz, date, _) = base.split(); let date = date.offset_months(months)?; if let Some(ts) = ts { ts.into_date_time(tz, date) } else { let time = ::from_hms(0, 0, 0)?; Some(Dt::new(tz, date, time)) } } } } } #[derive(Debug, Clone)] pub enum DateSpec { Absolute(AbsDate), // Y M D (e.g. 2018-06-02, 4 July 2017) Relative(Interval), // n U (e.g. 2min, 3 years ago, -2d) FromName(ByName, Direction), // (e.g. 'next fri', 'jul') } impl DateSpec { pub fn into_date_time( self, base: Dt, ts: Option, dialect: Dialect, ) -> Option
{ match self { DateSpec::Absolute(ad) => match ts { Some(ts) => ts.into_date_time(base.split().0, ad.into_date()?), None => Some(Dt::new( base.split().0, ad.into_date::()?, ::from_hms(0, 0, 0)?, )), }, DateSpec::Relative(skip) => skip.into_date_time(base, ts), DateSpec::FromName(byname, direct) => byname.into_date_time(base, ts, dialect, direct), } } } #[derive(Debug, Clone)] pub struct TimeSpec { pub hour: u32, pub min: u32, pub sec: u32, pub microsec: u32, pub offset: Option, } impl TimeSpec { pub const fn new(hour: u32, min: u32, sec: u32, microsec: u32) -> Self { Self { hour, min, sec, microsec, offset: None, } } pub fn with_offset(mut self, offset: i64) -> Self { self.offset = Some(offset); self } pub fn into_date_time(self, tz: Dt::TimeZone, date: Dt::Date) -> Option
{ let date = date.offset_days((self.hour / 24) as i64)?; let time = ::from_hms(self.hour % 24, self.min, self.sec)? .with_micros(self.microsec)?; if let Some(offs) = self.offset { Dt::new(tz, date, time).with_offset(offs) } else { Some(Dt::new(tz, date, time)) } } } #[derive(Debug, Clone)] pub struct DateTimeSpec { pub date: Option, pub time: Option, } #[derive(PartialEq, Eq, Clone, Copy)] pub(crate) struct Lowercase([u8; 16]); impl Lowercase { pub(crate) const fn literal(s: &str) -> Self { assert!(s.len() < 16); let mut i = 0; let mut out = [0; 16]; while i < s.len() { assert!(s.as_bytes()[i].is_ascii_lowercase()); out[i] = s.as_bytes()[i]; i += 1; } Self(out) } fn truncate(mut self, n: usize) -> Self { self.0[n..].fill(0); self } } impl From<&str> for Lowercase { fn from(value: &str) -> Self { if value.len() > 16 { // some value that will never be equal to a literal return Self(*b"AAAAAAAAAAAAAAAA"); } let mut out = [0; 16]; out[..value.len()].copy_from_slice(value.as_bytes()); out.make_ascii_lowercase(); Self(out) } } // same as chrono's 'count days from monday' convention pub(crate) fn week_day(s: Lowercase) -> Option { const SUN: Lowercase = Lowercase::literal("sun"); const MON: Lowercase = Lowercase::literal("mon"); const TUE: Lowercase = Lowercase::literal("tue"); const WED: Lowercase = Lowercase::literal("wed"); const THU: Lowercase = Lowercase::literal("thu"); const FRI: Lowercase = Lowercase::literal("fri"); const SAT: Lowercase = Lowercase::literal("sat"); match s.truncate(3) { SUN => Some(6), MON => Some(0), TUE => Some(1), WED => Some(2), THU => Some(3), FRI => Some(4), SAT => Some(5), _ => None, } } pub(crate) fn month_name(s: Lowercase) -> Option { const JAN: Lowercase = Lowercase::literal("jan"); const FEB: Lowercase = Lowercase::literal("feb"); const MAR: Lowercase = Lowercase::literal("mar"); const APR: Lowercase = Lowercase::literal("apr"); const MAY: Lowercase = Lowercase::literal("may"); const JUN: Lowercase = Lowercase::literal("jun"); const JUL: Lowercase = Lowercase::literal("jul"); const AUG: Lowercase = Lowercase::literal("aug"); const SEP: Lowercase = Lowercase::literal("sep"); const OCT: Lowercase = Lowercase::literal("oct"); const NOV: Lowercase = Lowercase::literal("nov"); const DEC: Lowercase = Lowercase::literal("dec"); match s.truncate(3) { JAN => Some(1), FEB => Some(2), MAR => Some(3), APR => Some(4), MAY => Some(5), JUN => Some(6), JUL => Some(7), AUG => Some(8), SEP => Some(9), OCT => Some(10), NOV => Some(11), DEC => Some(12), _ => None, } } pub(crate) fn time_unit(input: Lowercase) -> Option { if input == Lowercase::literal("s") || input.0.starts_with(b"se") { Some(Interval::Seconds(1)) } else if input == Lowercase::literal("m") || input.0.starts_with(b"mi") { Some(Interval::Seconds(60)) } else if input == Lowercase::literal("h") || input.0.starts_with(b"ho") { Some(Interval::Seconds(60 * 60)) } else if input == Lowercase::literal("d") || input.0.starts_with(b"da") { Some(Interval::Days(1)) } else if input == Lowercase::literal("w") || input.0.starts_with(b"we") { Some(Interval::Days(7)) } else if input.0.starts_with(b"mo") { Some(Interval::Months(1)) } else if input == Lowercase::literal("y") || input.0.starts_with(b"ye") { Some(Interval::Months(12)) } else { None } }