49 Commits

Author SHA1 Message Date
Sylvestre Ledru f68a01699e prepare release 0.14.0 2026-02-14 18:55:26 +01:00
Sylvestre Ledru 73b4b0a50c improve the code coverage 2026-02-14 18:19:31 +01:00
Sylvestre Ledru 002ca41008 tests: add edge case tests for 0 years and 0 months
Add tests to verify that adding 0 years or 0 months returns the
original date unchanged, including edge cases with Feb 29.
2026-02-14 18:19:31 +01:00
Sylvestre Ledru 818cfa4ad0 date: add more tests for cross-TZ-mishandled test - embedded timezone
NOTE: This commit only adds tests as the implementation was already
working correctly. These tests document and verify the expected
behavior for the embedded timezone edge case referenced in Debian
bug #851934.
2026-02-14 18:19:31 +01:00
Sylvestre Ledru 5cf8a6fbdd date: add more tests leap year overflow 2026-02-14 18:19:31 +01:00
Sylvestre Ledru 9688db08a8 Fix the coverage (#260) 2026-02-14 17:53:31 +01:00
cerdelen f10a950e23 parse_datetime: better variable name 2026-02-14 11:21:39 +01:00
cerdelen 14bbd4e848 parse_datetime: relative Year overflow for leap years 2026-02-14 11:21:39 +01:00
cerdelen dbfdea454f parse_datetime: Remove unnessecary import 2026-02-14 11:21:39 +01:00
cerdelen 7e528ad609 parse_datetime: Implement GNU style overflow/underflow
In GNU if using date with relative Months with the -d flag and the target Month does not have the source day (for example 31) GNU overflows the diff in days.
2026-02-14 11:21:39 +01:00
cerdelen 6306843888 parse_datetime: add regression tests 2026-02-14 11:21:39 +01:00
cerdelen d5c441bd3f parse_datetime: fix overflow issue 2026-02-14 11:21:39 +01:00
cerdelen 62cb7d9687 parse_datetime: Fix relative date adjustments for months using the actual days 2026-02-14 11:21:39 +01:00
Chris Dryden 642bbbf214 Merge pull request #255 from ChrisDryden/fix-tz-prefix-with-base-date
fix: TZ prefix should override base date timezone
2026-02-12 09:29:36 +01:00
renovate[bot] 6396c07d72 chore(deps): update rust crate jiff to v0.2.20 2026-02-12 07:06:17 +01:00
renovate[bot] 377ba0e040 chore(deps): update rust crate libfuzzer-sys to v0.4.12 2026-02-11 07:03:20 +01:00
Daniel Hofstetter ffb1ba35c5 Merge pull request #256 from uutils/renovate/jiff-0.x-lockfile
chore(deps): update rust crate jiff to v0.2.19
2026-02-06 07:52:19 +01:00
renovate[bot] eeffa242a1 chore(deps): update rust crate jiff to v0.2.19 2026-02-06 00:33:10 +00:00
Daniel Hofstetter 3a1edfb177 Merge pull request #251 from uutils/renovate/jiff-0.x-lockfile
chore(deps): update rust crate jiff to v0.2.18
2026-01-06 07:09:04 +01:00
renovate[bot] 866b6b446f chore(deps): update rust crate jiff to v0.2.18 2026-01-06 05:15:24 +00:00
Daniel Hofstetter 13e038a503 Merge pull request #250 from uutils/renovate/jiff-0.x-lockfile
chore(deps): update rust crate jiff to v0.2.17
2025-12-25 07:09:32 +01:00
renovate[bot] 7030df6cf0 chore(deps): update rust crate jiff to v0.2.17 2025-12-24 21:35:09 +00:00
Daniel Hofstetter cfbb973215 Merge pull request #249 from uutils/renovate/winnow-0.x-lockfile
chore(deps): update rust crate winnow to v0.7.14
2025-11-27 06:53:55 +01:00
renovate[bot] 88a4a718b0 chore(deps): update rust crate winnow to v0.7.14 2025-11-26 19:44:53 +00:00
Daniel Hofstetter 6462d410d8 Merge pull request #248 from cakebaker/bump_version_to_0_13_3
chore: bump version to `0.13.3`
2025-11-21 10:12:43 +01:00
Daniel Hofstetter 92d095b53b chore: bump version to 0.13.3 2025-11-21 09:52:19 +01:00
Daniel Hofstetter ea143af391 Merge pull request #247 from cakebaker/bump_dependencies
chore: update dependencies
2025-11-21 09:49:15 +01:00
Daniel Hofstetter 835e46e6f1 chore: update dependencies 2025-11-21 09:44:58 +01:00
Daniel Hofstetter 31b8e58cc5 Merge pull request #246 from 3v1n0/fix-parsing-relative-times
relative: Support parsing floating relative values with spaces
2025-11-21 09:16:18 +01:00
Daniel Hofstetter 9de30f5fc3 Merge pull request #245 from uutils/renovate/actions-checkout-6.x
chore(deps): update actions/checkout action to v6
2025-11-21 06:58:39 +01:00
Marco Trevisan (Treviño) d6ab892c29 relative: Add more tests for parsing signed values with spaces 2025-11-21 06:37:09 +01:00
Marco Trevisan (Treviño) 1e4fc30f7b relative: Support parsing floating relative values with spaces
In case a string such as "now + 1.5 seconds" was parsed we were failing.

This happened because after processing now, the parser was getting
to the point in which the string contained "+ 1.5", and once the sign
was processed, the remaining " 1.5" string conained a space that was
causing sec_and_nsec to fail.

Instead of failing at this point, just strip the spaces after the sign has
been processed.

Note in fact that "0+0.0 seconds" was working fine

Closes: https://github.com/uutils/coreutils/issues/8618
2025-11-21 06:37:09 +01:00
renovate[bot] 6ce151fb58 chore(deps): update actions/checkout action to v6 2025-11-20 18:43:38 +00:00
Daniel Hofstetter 9f6605f20d Merge pull request #244 from cakebaker/bump_version_to_0_13_2
chore: bump version to `0.13.2`
2025-11-09 18:27:31 +01:00
Daniel Hofstetter a025a04d72 chore: bump version to 0.13.2 2025-11-09 17:49:27 +01:00
Sylvestre Ledru dbae631ff0 Merge pull request #241 from cakebaker/literal3
feat: support formats `Nov-14-2024` and `November-14-2024`
2025-11-09 15:03:15 +01:00
Daniel Hofstetter 83278f0ddc Merge pull request #243 from uutils/renovate/jiff-0.x-lockfile
chore(deps): update rust crate jiff to v0.2.16
2025-11-07 17:39:07 +01:00
renovate[bot] ae518cac37 chore(deps): update rust crate jiff to v0.2.16 2025-11-07 15:41:39 +00:00
Daniel Hofstetter bc5d201b68 Merge pull request #242 from cakebaker/update_dependencies
chore: update dependencies
2025-10-30 09:29:46 +01:00
Daniel Hofstetter 8c56c5cb13 chore: update dependencies 2025-10-29 11:13:04 +01:00
Daniel Hofstetter 9c95c9549b feat: support formats Nov-14-2024/November-14-2024 2025-10-29 10:40:22 +01:00
Daniel Hofstetter ba69fb8772 Merge pull request #239 from cakebaker/bump_version_to_0_13_1
chore: bump version to 0.13.1
2025-10-04 17:27:00 +02:00
Daniel Hofstetter 912bc4e706 chore: bump version to 0.13.1 2025-10-04 17:19:18 +02:00
Daniel Hofstetter c239ed0b36 Merge pull request #238 from stillbeingnick/bugfix/issue/coreutils-date-invalid
Changing ErrMode::Cut to ErrMode::Backtrack to allow alt parser flow (Fix for uutils/coreutils issue 8754)
2025-10-04 17:00:00 +02:00
Nicholas Still 3074fe3baa Change ErrMode::Cut to ErrMode::Backtrack to allow alt parser flow 2025-10-04 16:55:43 +02:00
Daniel Hofstetter 29e368662c Merge pull request #237 from yuankunzhang/add-comments-for-build-function
docs: add comments for the build function
2025-09-28 15:21:47 +02:00
yuankunzhang 1e757df39b docs: add comments for the build function 2025-09-27 21:34:09 +08:00
Daniel Hofstetter 7a32b1ee3d Merge pull request #236 from yuankunzhang/remove-regex-dependency
chore: remove regex crate as dependency
2025-09-27 14:29:14 +02:00
yuankunzhang c898cd84de chore: remove regex crate as dependency 2025-09-27 20:10:25 +08:00
11 changed files with 848 additions and 342 deletions
+11 -9
View File
@@ -14,7 +14,7 @@ jobs:
matrix:
os: [ubuntu-latest, macOS-latest, windows-latest]
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
- run: cargo check
@@ -25,7 +25,7 @@ jobs:
matrix:
os: [ubuntu-latest, macOS-latest, windows-latest]
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
- run: cargo test
@@ -33,7 +33,7 @@ jobs:
name: cargo fmt --all -- --check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
- run: rustup component add rustfmt
- run: cargo fmt --all -- --check
@@ -45,7 +45,7 @@ jobs:
matrix:
os: [ubuntu-latest, macOS-latest, windows-latest]
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
- run: rustup component add clippy
- run: cargo clippy --all-targets -- -D warnings
@@ -57,7 +57,7 @@ jobs:
matrix:
os: [ubuntu-latest, macOS-latest, windows-latest]
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_MIN_SRV }}
@@ -74,7 +74,7 @@ jobs:
- { os: macos-latest , features: macos }
- { os: windows-latest , features: windows }
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Initialize workflow variables
id: vars
shell: bash
@@ -134,17 +134,19 @@ jobs:
# display coverage files
grcov . --binary-path="${COVERAGE_REPORT_DIR}" --output-type files --ignore build.rs --ignore "vendor/*" --ignore "/*" --ignore "[a-zA-Z]:/*" --excl-br-line "^\s*((debug_)?assert(_eq|_ne)?!|#\[derive\()" | sort --unique
# generate coverage report
grcov . --binary-path="${COVERAGE_REPORT_DIR}" --output-type lcov --output-path "${COVERAGE_REPORT_FILE}" --branch --ignore build.rs --ignore "vendor/*" --ignore "/*" --ignore "[a-zA-Z]:/*" --excl-br-line "^\s*((debug_)?assert(_eq|_ne)?!|#\[derive\()"
grcov . --binary-path="${COVERAGE_REPORT_DIR}" --output-type lcov --output-path "${COVERAGE_REPORT_FILE}" --branch --ignore build.rs --ignore "vendor/*" --ignore "/*" --ignore "[a-zA-Z]:/*" --excl-br-line "^\s*((debug_)?assert(_eq|_ne)?!|#\[derive\()" --source-dir .
echo "report=${COVERAGE_REPORT_FILE}" >> $GITHUB_OUTPUT
- name: Upload coverage results (to Codecov.io)
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ${{ steps.coverage.outputs.report }}
## flags: IntegrationTests, UnitTests, ${{ steps.vars.outputs.CODECOV_FLAGS }}
flags: ${{ steps.vars.outputs.CODECOV_FLAGS }}
name: codecov-umbrella
fail_ci_if_error: false
verbose: true
directory: ./
root_dir: ./
fuzz:
name: Run the fuzzers
@@ -152,7 +154,7 @@ jobs:
env:
RUN_FOR: 60
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@nightly
- name: Install `cargo-fuzz`
run: cargo install cargo-fuzz
Generated
+66 -121
View File
@@ -4,9 +4,9 @@ version = 3
[[package]]
name = "aho-corasick"
version = "1.1.3"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
dependencies = [
"memchr",
]
@@ -19,9 +19,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "cfg-if"
version = "1.0.3"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "equivalent"
@@ -80,15 +80,15 @@ checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
[[package]]
name = "hashbrown"
version = "0.15.5"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
[[package]]
name = "indexmap"
version = "2.11.1"
version = "2.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "206a8042aec68fa4a62e8d3f7aa4ceb508177d9324faf261e1959e495b7a1921"
checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5"
dependencies = [
"equivalent",
"hashbrown",
@@ -96,24 +96,24 @@ dependencies = [
[[package]]
name = "jiff"
version = "0.2.15"
version = "0.2.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49"
checksum = "c867c356cc096b33f4981825ab281ecba3db0acefe60329f044c1789d94c6543"
dependencies = [
"jiff-static",
"jiff-tzdb-platform",
"log",
"portable-atomic",
"portable-atomic-util",
"serde",
"serde_core",
"windows-sys",
]
[[package]]
name = "jiff-static"
version = "0.2.15"
version = "0.2.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4"
checksum = "f7946b4325269738f270bb55b3c19ab5c5040525f83fd625259422a9d25d9be5"
dependencies = [
"proc-macro2",
"quote",
@@ -143,9 +143,9 @@ checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
[[package]]
name = "memchr"
version = "2.7.5"
version = "2.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
[[package]]
name = "num-traits"
@@ -158,11 +158,10 @@ dependencies = [
[[package]]
name = "parse_datetime"
version = "0.13.0"
version = "0.14.0"
dependencies = [
"jiff",
"num-traits",
"regex",
"rstest",
"winnow",
]
@@ -196,36 +195,36 @@ dependencies = [
[[package]]
name = "proc-macro-crate"
version = "3.3.0"
version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35"
checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983"
dependencies = [
"toml_edit",
]
[[package]]
name = "proc-macro2"
version = "1.0.101"
version = "1.0.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de"
checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.40"
version = "1.0.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
dependencies = [
"proc-macro2",
]
[[package]]
name = "regex"
version = "1.11.3"
version = "1.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b5288124840bee7b386bc413c487869b360b2b4ec421ea56425128692f2a82c"
checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4"
dependencies = [
"aho-corasick",
"memchr",
@@ -235,9 +234,9 @@ dependencies = [
[[package]]
name = "regex-automata"
version = "0.4.11"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "833eb9ce86d40ef33cb1306d8accf7bc8ec2bfea4355cbdebb3df68b40925cad"
checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c"
dependencies = [
"aho-corasick",
"memchr",
@@ -246,9 +245,9 @@ dependencies = [
[[package]]
name = "regex-syntax"
version = "0.8.6"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001"
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
[[package]]
name = "relative-path"
@@ -296,33 +295,24 @@ dependencies = [
[[package]]
name = "semver"
version = "1.0.26"
version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0"
[[package]]
name = "serde"
version = "1.0.221"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "341877e04a22458705eb4e131a1508483c877dca2792b3781d4e5d8a6019ec43"
dependencies = [
"serde_core",
]
checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
[[package]]
name = "serde_core"
version = "1.0.221"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c459bc0a14c840cb403fc14b148620de1e0778c96ecd6e0c8c3cacb6d8d00fe"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.221"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6185cf75117e20e62b1ff867b9518577271e58abe0037c40bb4794969355ab0"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
@@ -337,9 +327,9 @@ checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589"
[[package]]
name = "syn"
version = "2.0.106"
version = "2.0.110"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6"
checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea"
dependencies = [
"proc-macro2",
"quote",
@@ -348,105 +338,60 @@ dependencies = [
[[package]]
name = "toml_datetime"
version = "0.6.11"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
checksum = "a197c0ec7d131bfc6f7e82c8442ba1595aeab35da7adbf05b6b73cd06a16b6be"
dependencies = [
"serde_core",
]
[[package]]
name = "toml_edit"
version = "0.22.27"
version = "0.23.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
checksum = "c2ad0b7ae9cfeef5605163839cb9221f453399f15cfb5c10be9885fcf56611f9"
dependencies = [
"indexmap",
"toml_datetime",
"toml_parser",
"winnow",
]
[[package]]
name = "toml_parser"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b551886f449aa90d4fe2bdaa9f4a2577ad2dde302c61ecf262d80b116db95c10"
dependencies = [
"winnow",
]
[[package]]
name = "unicode-ident"
version = "1.0.19"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d"
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.59.0"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-targets",
"windows-link",
]
[[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"
[[package]]
name = "winnow"
version = "0.7.13"
version = "0.7.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf"
checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829"
dependencies = [
"memchr",
]
+1 -2
View File
@@ -1,7 +1,7 @@
[package]
name = "parse_datetime"
description = "parsing human-readable time strings and converting them to a DateTime"
version = "0.13.0"
version = "0.14.0"
edition = "2021"
license = "MIT"
repository = "https://github.com/uutils/parse_datetime"
@@ -9,7 +9,6 @@ readme = "README.md"
rust-version = "1.71.1"
[dependencies]
regex = "1.10.4"
winnow = "0.7.10"
num-traits = "0.2.19"
jiff = { version = "0.2.15", default-features = false, features = ["tz-system", "tzdb-bundle-platform", "tzdb-zoneinfo"] }
+46 -161
View File
@@ -2,15 +2,6 @@
# It is not intended for manual editing.
version = 4
[[package]]
name = "aho-corasick"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
dependencies = [
"memchr",
]
[[package]]
name = "arbitrary"
version = "1.4.2"
@@ -25,9 +16,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "cc"
version = "1.2.37"
version = "1.2.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65193589c6404eb80b450d618eaf9a2cafaaafd57ecce47370519ef674a7bd44"
checksum = "cd405d82c84ff7f35739f175f67d8b9fb7687a0e84ccdc78bd3568839827cf07"
dependencies = [
"find-msvc-tools",
"jobserver",
@@ -37,15 +28,15 @@ dependencies = [
[[package]]
name = "cfg-if"
version = "1.0.3"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "find-msvc-tools"
version = "0.1.1"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fd99930f64d146689264c637b5af2f0233a933bef0d8570e2526bf9e083192d"
checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844"
[[package]]
name = "fuzz_parse_datetime"
@@ -57,36 +48,36 @@ dependencies = [
[[package]]
name = "getrandom"
version = "0.3.3"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"wasi",
"wasip2",
]
[[package]]
name = "jiff"
version = "0.2.15"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49"
checksum = "49cce2b81f2098e7e3efc35bc2e0a6b7abec9d34128283d7a26fa8f32a6dbb35"
dependencies = [
"jiff-static",
"jiff-tzdb-platform",
"log",
"portable-atomic",
"portable-atomic-util",
"serde",
"serde_core",
"windows-sys",
]
[[package]]
name = "jiff-static"
version = "0.2.15"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4"
checksum = "980af8b43c3ad5d8d349ace167ec8170839f753a42d233ba19e08afe1850fa69"
dependencies = [
"proc-macro2",
"quote",
@@ -120,15 +111,15 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.175"
version = "0.2.177"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
[[package]]
name = "libfuzzer-sys"
version = "0.4.10"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5037190e1f70cbeef565bd267599242926f724d3b8a9f510fd7e0b540cfa4404"
checksum = "f12a681b7dd8ce12bff52488013ba614b869148d54dd79836ab85aafdd53f08d"
dependencies = [
"arbitrary",
"cc",
@@ -142,9 +133,9 @@ checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
[[package]]
name = "memchr"
version = "2.7.5"
version = "2.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
[[package]]
name = "num-traits"
@@ -157,11 +148,10 @@ dependencies = [
[[package]]
name = "parse_datetime"
version = "0.11.0"
version = "0.13.3"
dependencies = [
"jiff",
"num-traits",
"regex",
"winnow",
]
@@ -182,18 +172,18 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.101"
version = "1.0.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de"
checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.40"
version = "1.0.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
dependencies = [
"proc-macro2",
]
@@ -204,58 +194,20 @@ version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "regex"
version = "1.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001"
[[package]]
name = "serde"
version = "1.0.221"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "341877e04a22458705eb4e131a1508483c877dca2792b3781d4e5d8a6019ec43"
dependencies = [
"serde_core",
]
[[package]]
name = "serde_core"
version = "1.0.221"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c459bc0a14c840cb403fc14b148620de1e0778c96ecd6e0c8c3cacb6d8d00fe"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.221"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6185cf75117e20e62b1ff867b9518577271e58abe0037c40bb4794969355ab0"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
@@ -270,9 +222,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "syn"
version = "2.0.106"
version = "2.0.110"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6"
checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea"
dependencies = [
"proc-macro2",
"quote",
@@ -281,101 +233,34 @@ dependencies = [
[[package]]
name = "unicode-ident"
version = "1.0.19"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d"
[[package]]
name = "wasi"
version = "0.14.5+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4494f6290a82f5fe584817a676a34b9d6763e8d9d18204009fb31dceca98fd4"
dependencies = [
"wasip2",
]
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
[[package]]
name = "wasip2"
version = "1.0.0+wasi-0.2.4"
version = "1.0.1+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03fa2761397e5bd52002cd7e73110c71af2109aca4e521a9f40473fe685b0a24"
checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7"
dependencies = [
"wit-bindgen",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.59.0"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-targets",
"windows-link",
]
[[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"
[[package]]
name = "winnow"
version = "0.7.13"
@@ -387,6 +272,6 @@ dependencies = [
[[package]]
name = "wit-bindgen"
version = "0.45.1"
version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c573471f125075647d03df72e026074b7203790d41351cd6edc96f46bcccd36"
checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
+223 -35
View File
@@ -1,7 +1,7 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
use jiff::{civil, Span, Zoned};
use jiff::{civil, Span, ToSpan, Zoned};
use super::{date, epoch, error, offset, relative, time, weekday, year, Item};
@@ -160,31 +160,63 @@ impl DateTimeBuilder {
self.set_time(time)
}
/// Build a `Zoned` object from the pieces accumulated in this builder.
///
/// Resolution order (mirrors GNU `date` semantics):
///
/// 1. Base instant.
/// - a. If `self.base` is provided, start with it.
/// - b. Else if a `timezone` rule is present, start with "now" in that
/// timezone.
/// - c. Else start with current system local time.
///
/// 2. Absolute timestamp override.
/// - a. If `self.timestamp` is set, it fully determines the result.
///
/// 3. Time of day truncation.
/// - a. If any of date, time, weekday, offset, timezone is set, zero the
/// time of day to 00:00:00 before applying fields.
///
/// 4. Fieldwise resolution (applied to the base instant).
/// - a. Apply date. If year is absent in the parsed date, inherit the year
/// from the base instant.
/// - b. Apply time. If time carries an explicit numeric offset, apply the
/// offset before setting time.
/// - c. Apply weekday (e.g., "next Friday" or "last Monday").
/// - d. Apply relative adjustments (e.g., "+3 days", "-2 months").
/// - e. Apply final fixed offset if present.
pub(super) fn build(self) -> Result<Zoned, error::Error> {
let base = self.base.unwrap_or(if let Some(tz) = &self.timezone {
jiff::Timestamp::now().to_zoned(tz.clone())
} else {
Zoned::now()
});
// If a timestamp is set, we use it to build the `Zoned` object.
if let Some(ts) = self.timestamp {
return Ok(jiff::Timestamp::try_from(ts)?.to_zoned(base.offset().to_time_zone()));
}
// If any of the following items are set, we truncate the time portion
// of the base date to zero; otherwise, we use the base date as is.
let mut dt = if self.date.is_none()
&& self.time.is_none()
&& self.weekday.is_none()
&& self.offset.is_none()
&& self.timezone.is_none()
{
base
} else {
base.with().time(civil::time(0, 0, 0, 0)).build()?
// 1. Choose the base instant.
// If a TZ="..." prefix was parsed, it should override the base's timezone
// while keeping the base's timestamp for relative date calculations.
let has_timezone = self.timezone.is_some();
let base = match (self.base, self.timezone) {
(Some(b), Some(tz)) => b.timestamp().to_zoned(tz),
(Some(b), None) => b,
(None, Some(tz)) => jiff::Timestamp::now().to_zoned(tz),
(None, None) => Zoned::now(),
};
// 2. Absolute timestamp override everything else.
if let Some(ts) = self.timestamp {
let ts = jiff::Timestamp::try_from(ts)?;
return Ok(ts.to_zoned(base.offset().to_time_zone()));
}
// 3. Determine whether to truncate the time of day.
let need_midnight = self.date.is_some()
|| self.time.is_some()
|| self.weekday.is_some()
|| self.offset.is_some()
|| has_timezone;
let mut dt = if need_midnight {
base.with().time(civil::time(0, 0, 0, 0)).build()?
} else {
base
};
// 4a. Apply date.
if let Some(date) = self.date {
let d: civil::Date = if date.year.is_some() {
date.try_into()?
@@ -194,6 +226,7 @@ impl DateTimeBuilder {
dt = dt.with().date(d).build()?;
}
// 4b. Apply time.
if let Some(time) = self.time.clone() {
if let Some(offset) = &time.offset {
dt = dt.datetime().to_zoned(offset.try_into()?)?;
@@ -203,13 +236,13 @@ impl DateTimeBuilder {
dt = dt.with().time(t).build()?;
}
if let Some(weekday::Weekday { offset, day }) = self.weekday {
// 4c. Apply weekday.
if let Some(weekday::Weekday { mut offset, day }) = self.weekday {
if self.time.is_none() {
dt = dt.with().time(civil::time(0, 0, 0, 0)).build()?;
}
let mut offset = offset;
let day = day.into();
let target = day.into();
// If the current day is not the target day, we need to adjust
// the x value to ensure we find the correct day.
@@ -217,7 +250,7 @@ impl DateTimeBuilder {
// Consider this:
// Assuming today is Monday, next Friday is actually THIS Friday;
// but next Monday is indeed NEXT Monday.
if dt.date().weekday() != day && offset > 0 {
if dt.date().weekday() != target && offset > 0 {
offset -= 1;
}
@@ -237,7 +270,7 @@ impl DateTimeBuilder {
//
// Example 4: next Thursday (x = 1, day = Thursday)
// delta = (3 - 3) % 7 + (1) * 7 = 7
let delta = (day.since(civil::Weekday::Monday) as i32
let delta = (target.since(civil::Weekday::Monday) as i32
- dt.date().weekday().since(civil::Weekday::Monday) as i32)
.rem_euclid(7)
+ offset.checked_mul(7).ok_or("multiplication overflow")?;
@@ -245,16 +278,28 @@ impl DateTimeBuilder {
dt = dt.checked_add(Span::new().try_days(delta)?)?;
}
// 4d. Apply relative adjustments.
for rel in self.relative {
dt = dt.checked_add::<Span>(if let relative::Relative::Months(x) = rel {
// *NOTE* This is done in this way to conform to GNU behavior.
let days = dt.date().last_of_month().day() as i32;
Span::new().try_days(days.checked_mul(x).ok_or("multiplication overflow")?)?
} else {
rel.try_into()?
})?;
dt = match rel {
relative::Relative::Years(_) | relative::Relative::Months(_) => {
// GNU way of calculating relative months and years
// GNU changes the month and then checks if the target month has
// this day. If this day does not exist in the target month it overflows
// the difference
let original_day_of_month = dt.day();
dt = dt.checked_add::<Span>(rel.try_into()?)?;
if original_day_of_month != dt.day() {
dt = dt.checked_add(
(original_day_of_month.checked_sub(dt.day()).unwrap_or(0)).days(),
)?;
}
dt
}
_ => dt.checked_add::<Span>(rel.try_into()?)?,
}
}
// 4e. Apply final fixed offset.
if let Some(offset) = self.offset {
let (offset, hour_adjustment) = offset.normalize();
dt = dt.checked_add(Span::new().hours(hour_adjustment))?;
@@ -288,3 +333,146 @@ impl TryFrom<Vec<Item>> for DateTimeBuilder {
Ok(builder)
}
}
#[cfg(test)]
mod tests {
use super::*;
// Helper functions to create test items by parsing
fn timestamp() -> epoch::Timestamp {
let mut input = "@1234567890";
epoch::parse(&mut input).unwrap()
}
fn date() -> date::Date {
let mut input = "2023-06-15";
date::parse(&mut input).unwrap()
}
fn time() -> time::Time {
let mut input = "12:30:00";
time::parse(&mut input).unwrap()
}
fn time_with_offset() -> time::Time {
let mut input = "12:30:00+05:00";
time::parse(&mut input).unwrap()
}
fn weekday() -> weekday::Weekday {
let mut input = "monday";
weekday::parse(&mut input).unwrap()
}
fn offset() -> offset::Offset {
let mut input = "+05:00";
offset::timezone_offset(&mut input).unwrap()
}
fn relative() -> relative::Relative {
let mut input = "1 day";
relative::parse(&mut input).unwrap()
}
fn timezone() -> jiff::tz::TimeZone {
jiff::tz::TimeZone::UTC
}
#[test]
fn test_duplicate_items_errors() {
let test_cases = vec![
(
vec![Item::TimeZone(timezone()), Item::TimeZone(timezone())],
"timezone rule cannot appear more than once",
),
(
vec![Item::Timestamp(timestamp()), Item::Timestamp(timestamp())],
"timestamp cannot appear more than once",
),
(
vec![Item::Date(date()), Item::Date(date())],
"date cannot appear more than once",
),
(
vec![Item::Time(time()), Item::Time(time())],
"time cannot appear more than once",
),
(
vec![Item::Weekday(weekday()), Item::Weekday(weekday())],
"weekday cannot appear more than once",
),
(
vec![Item::Offset(offset()), Item::Offset(offset())],
"time offset cannot appear more than once",
),
];
for (items, expected_err) in test_cases {
let result = DateTimeBuilder::try_from(items);
assert_eq!(result.unwrap_err(), expected_err);
}
}
#[test]
fn test_timestamp_cannot_be_combined_with_other_items() {
let test_cases = vec![
vec![Item::Date(date()), Item::Timestamp(timestamp())],
vec![Item::Time(time()), Item::Timestamp(timestamp())],
vec![Item::Weekday(weekday()), Item::Timestamp(timestamp())],
vec![Item::Offset(offset()), Item::Timestamp(timestamp())],
vec![Item::Relative(relative()), Item::Timestamp(timestamp())],
vec![Item::Timestamp(timestamp()), Item::Date(date())],
vec![Item::Timestamp(timestamp()), Item::Time(time())],
vec![Item::Timestamp(timestamp()), Item::Weekday(weekday())],
vec![Item::Timestamp(timestamp()), Item::Relative(relative())],
vec![Item::Timestamp(timestamp()), Item::Offset(offset())],
vec![Item::Timestamp(timestamp()), Item::Pure("2023".to_string())],
];
for items in test_cases {
let result = DateTimeBuilder::try_from(items);
assert_eq!(
result.unwrap_err(),
"timestamp cannot be combined with other date/time items"
);
}
}
#[test]
fn test_time_offset_conflicts() {
// Time with offset followed by separate Offset item
let items1 = vec![Item::Time(time_with_offset()), Item::Offset(offset())];
assert_eq!(
DateTimeBuilder::try_from(items1).unwrap_err(),
"time offset cannot appear more than once"
);
// Offset item followed by Time with offset
let items2 = vec![Item::Offset(offset()), Item::Time(time_with_offset())];
assert_eq!(
DateTimeBuilder::try_from(items2).unwrap_err(),
"time offset and timezone are mutually exclusive"
);
}
#[test]
fn test_valid_combination_date_time() {
let items = vec![Item::Date(date()), Item::Time(time())];
let result = DateTimeBuilder::try_from(items);
assert!(result.is_ok());
}
#[test]
fn test_valid_combination_date_weekday() {
let items = vec![Item::Date(date()), Item::Weekday(weekday())];
let result = DateTimeBuilder::try_from(items);
assert!(result.is_ok());
}
#[test]
fn test_valid_timestamp_alone() {
let items = vec![Item::Timestamp(timestamp())];
let result = DateTimeBuilder::try_from(items);
assert!(result.is_ok());
}
}
+48 -12
View File
@@ -128,7 +128,7 @@ impl TryFrom<Date> for jiff::civil::Date {
}
pub(super) fn parse(input: &mut &str) -> ModalResult<Date> {
alt((iso1, iso2, us, literal1, literal2)).parse_next(input)
alt((iso1, iso2, us, literal1, literal2, literal3)).parse_next(input)
}
/// Parse `[year]-[month]-[day]`
@@ -138,9 +138,10 @@ pub(super) fn iso1(input: &mut &str) -> ModalResult<Date> {
let (year, _, month, _, day) =
(year_str, s('-'), s(dec_uint), s('-'), s(dec_uint)).parse_next(input)?;
// Map err to Backtrack instead of Cut to avoid early termination of parsing
(year, month, day)
.try_into()
.map_err(|e| ErrMode::Cut(ctx_err(e)))
.map_err(|e| ErrMode::Backtrack(ctx_err(e)))
}
/// Parse `[year][month][day]`
@@ -156,7 +157,7 @@ pub(super) fn iso2(input: &mut &str) -> ModalResult<Date> {
(year, month, day)
.try_into()
.map_err(|e| ErrMode::Cut(ctx_err(e)))
.map_err(|e| ErrMode::Backtrack(ctx_err(e)))
}
/// Parse `[year]/[month]/[day]` or `[month]/[day]/[year]` or `[month]/[day]`.
@@ -178,19 +179,21 @@ fn us(input: &mut &str) -> ModalResult<Date> {
let day = day_from_str(s2)?;
(s1, n, day)
.try_into()
.map_err(|e| ErrMode::Cut(ctx_err(e)))
.map_err(|e| ErrMode::Backtrack(ctx_err(e)))
}
Some(s2) => {
// [month]/[day]/[year]
let month = month_from_str(s1)?;
(s2, month, n)
.try_into()
.map_err(|e| ErrMode::Cut(ctx_err(e)))
.map_err(|e| ErrMode::Backtrack(ctx_err(e)))
}
None => {
// [month]/[day]
let month = month_from_str(s1)?;
(month, n).try_into().map_err(|e| ErrMode::Cut(ctx_err(e)))
(month, n)
.try_into()
.map_err(|e| ErrMode::Backtrack(ctx_err(e)))
}
}
}
@@ -213,10 +216,10 @@ fn literal1(input: &mut &str) -> ModalResult<Date> {
match year {
Some(year) => (year, month, day)
.try_into()
.map_err(|e| ErrMode::Cut(ctx_err(e))),
.map_err(|e| ErrMode::Backtrack(ctx_err(e))),
None => (month, day)
.try_into()
.map_err(|e| ErrMode::Cut(ctx_err(e))),
.map_err(|e| ErrMode::Backtrack(ctx_err(e))),
}
}
@@ -242,13 +245,25 @@ fn literal2(input: &mut &str) -> ModalResult<Date> {
match year {
Some(year) => (year, month, day)
.try_into()
.map_err(|e| ErrMode::Cut(ctx_err(e))),
.map_err(|e| ErrMode::Backtrack(ctx_err(e))),
None => (month, day)
.try_into()
.map_err(|e| ErrMode::Cut(ctx_err(e))),
.map_err(|e| ErrMode::Backtrack(ctx_err(e))),
}
}
/// Parse `November-14-2022` and `Nov-14-2022`. Unlike `literal2`, the year is mandatory
/// to match the behavior of GNU `date`.
fn literal3(input: &mut &str) -> ModalResult<Date> {
let (month, _, day, _, year) =
(s(literal_month), s('-'), s(dec_uint), s('-'), year_str).parse_next(input)?;
// Map err to Backtrack instead of Cut to avoid early termination of parsing
(year, month, day)
.try_into()
.map_err(|e| ErrMode::Backtrack(ctx_err(e)))
}
/// Parse the name of a month (case-insensitive)
fn literal_month(input: &mut &str) -> ModalResult<u8> {
s(alpha1)
@@ -274,12 +289,12 @@ fn literal_month(input: &mut &str) -> ModalResult<u8> {
fn month_from_str(s: &str) -> ModalResult<u8> {
s.parse::<u8>()
.map_err(|_| ErrMode::Cut(ctx_err("month must be a valid u8 number")))
.map_err(|_| ErrMode::Backtrack(ctx_err("month must be a valid u8 number")))
}
fn day_from_str(s: &str) -> ModalResult<u8> {
s.parse::<u8>()
.map_err(|_| ErrMode::Cut(ctx_err("day must be a valid u8 number")))
.map_err(|_| ErrMode::Backtrack(ctx_err("day must be a valid u8 number")))
}
#[cfg(test)]
@@ -546,6 +561,27 @@ mod tests {
assert_eq!(s, ", 2022a");
}
#[test]
fn literal3() {
let reference = Date {
year: Some(2022),
month: 11,
day: 14,
};
for mut s in [
"november-14-2022",
"november----14----2022",
"november - 14 - 2022",
"nov-14-2022",
"nov---14---2022",
"nov - 14 - 2022",
] {
let old_s = s.to_owned();
assert_eq!(parse(&mut s).unwrap(), reference, "Format string: {old_s}");
}
}
#[test]
fn with_year() {
let reference = Date {
+61
View File
@@ -142,3 +142,64 @@ pub(super) fn ctx_err(reason: &'static str) -> ContextError {
err.push(StrContext::Expected(StrContextValue::Description(reason)));
err
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_dec_int() {
for (input, expected) in [
("123", 123), // positive without sign
("+123", 123), // positive with '+' sign
("-123", -123), // negative with '-' sign
("0", 0), // zero
("+0", 0), // zero with '+' sign
("-0", 0), // zero with '-' sign (parses as 0)
("012", 12), // zero-prefixed (the main reason for this function)
("+012", 12), // zero-prefixed with '+' sign
("-012", -12), // zero-prefixed with '-' sign
("00123", 123), // multiple leading zeros
("2147483647", 2147483647), // i32::MAX
("-2147483648", -2147483648), // i32::MIN
] {
let mut s = input;
assert_eq!(
dec_int::<ContextError>(&mut s).unwrap(),
expected,
"{input}"
);
}
for input in [
"", // empty string
"+", // sign without digits
"-", // sign without digits
"abc", // non-numeric
"12a", // starts with digits but has non-digit after (but should parse "12" successfully)
] {
let mut s = input;
let result = dec_int::<ContextError>(&mut s);
// Note: "12a" will actually succeed and parse "12", leaving "a" unparsed
if input == "12a" {
assert_eq!(result.unwrap(), 12, "{input}");
assert_eq!(s, "a", "Should leave 'a' unparsed");
} else {
assert!(result.is_err(), "{input} should fail");
}
}
// Test overflow cases
for input in [
"2147483648", // i32::MAX + 1
"-2147483649", // i32::MIN - 1
"99999999999", // way too large
] {
let mut s = input;
assert!(
dec_int::<ContextError>(&mut s).is_err(),
"{input} should overflow"
);
}
}
}
+15 -2
View File
@@ -83,7 +83,7 @@ pub(super) fn parse(input: &mut &str) -> ModalResult<Relative> {
fn seconds(input: &mut &str) -> ModalResult<Relative> {
(
opt(alt((s('+').value(1), s('-').value(-1)))),
sec_and_nsec,
s(sec_and_nsec),
s(alpha1).verify(|s: &str| matches!(s, "seconds" | "second" | "sec" | "secs")),
ago,
)
@@ -138,11 +138,13 @@ mod tests {
("secs", Relative::Seconds(1, 0)),
("second ago", Relative::Seconds(-1, 0)),
("3 seconds", Relative::Seconds(3, 0)),
("+ 3 seconds", Relative::Seconds(3, 0)),
("3.5 seconds", Relative::Seconds(3, 500_000_000)),
("-3.5 seconds", Relative::Seconds(-4, 500_000_000)),
("+3.5 seconds", Relative::Seconds(3, 500_000_000)),
("+ 3.5 seconds", Relative::Seconds(3, 500_000_000)),
("3.5 seconds ago", Relative::Seconds(-4, 500_000_000)),
("-3.5 seconds ago", Relative::Seconds(3, 500_000_000)),
("- 3.5 seconds ago", Relative::Seconds(3, 500_000_000)),
// Minutes
("minute", Relative::Minutes(1)),
("minutes", Relative::Minutes(1)),
@@ -150,29 +152,40 @@ mod tests {
("mins", Relative::Minutes(1)),
("10 minutes", Relative::Minutes(10)),
("-10 minutes", Relative::Minutes(-10)),
("- 10 minutes", Relative::Minutes(-10)),
("10 minutes ago", Relative::Minutes(-10)),
("-10 minutes ago", Relative::Minutes(10)),
("- 10 minutes ago", Relative::Minutes(10)),
("-10 minutes ago", Relative::Minutes(10)),
("- 10 minutes ago", Relative::Minutes(10)),
// Hours
("hour", Relative::Hours(1)),
("hours", Relative::Hours(1)),
("10 hours", Relative::Hours(10)),
("+10 hours", Relative::Hours(10)),
("+ 10 hours", Relative::Hours(10)),
("-10 hours", Relative::Hours(-10)),
("- 10 hours", Relative::Hours(-10)),
("10 hours ago", Relative::Hours(-10)),
("-10 hours ago", Relative::Hours(10)),
("- 10 hours ago", Relative::Hours(10)),
// Days
("day", Relative::Days(1)),
("days", Relative::Days(1)),
("10 days", Relative::Days(10)),
("+10 days", Relative::Days(10)),
("+ 10 days", Relative::Days(10)),
("-10 days", Relative::Days(-10)),
("- 10 days", Relative::Days(-10)),
("10 days ago", Relative::Days(-10)),
("-10 days ago", Relative::Days(10)),
("- 10 days ago", Relative::Days(10)),
// Multiple days
("fortnight", Relative::Days(14)),
("fortnights", Relative::Days(14)),
("2 fortnights ago", Relative::Days(-28)),
("+2 fortnights ago", Relative::Days(-28)),
("+ 2 fortnights ago", Relative::Days(-28)),
("week", Relative::Days(7)),
("weeks", Relative::Days(7)),
("2 weeks ago", Relative::Days(-14)),
+36
View File
@@ -307,6 +307,42 @@ mod tests {
assert!(parse_datetime(relative_time).is_ok());
}
}
#[test]
fn integer_seconds_offset() {
let dt = "0 + 0 seconds";
assert!(parse_datetime(dt).is_ok());
}
#[test]
fn integer_seconds_offset_spaceless() {
let dt = "0+0 seconds";
assert!(parse_datetime(dt).is_ok());
}
#[test]
fn floating_seconds_offset() {
let dt = "0 + 0.0 seconds";
assert!(parse_datetime(dt).is_ok());
}
#[test]
fn floating_seconds_offset_spaceless() {
let dt = "0+0.0 seconds";
assert!(parse_datetime(dt).is_ok());
}
#[test]
fn floating_seconds_offset_from_now() {
let dt = "now + 1.5 seconds";
assert!(parse_datetime(dt).is_ok());
}
#[test]
fn floating_seconds_offset_from_tomorrow_spaceless() {
let dt = "tomorrow+1.5 seconds";
assert!(parse_datetime(dt).is_ok());
}
}
#[cfg(test)]
+190
View File
@@ -95,3 +95,193 @@ fn test_date_omitting_year(#[case] input: &str, #[case] year: u32, #[case] expec
.unwrap();
check_relative(now, input, expected);
}
#[rstest]
#[case::tz_prefix_est5("TZ=\"EST5\" 1970-01-01 00:00", "1970-01-01 00:00:00-05:00")]
#[case::tz_prefix_pst8("TZ=\"PST8\" 1970-01-01 00:00", "1970-01-01 00:00:00-08:00")]
#[case::tz_prefix_utc("TZ=\"UTC\" 1970-01-01 12:00", "1970-01-01 12:00:00+00:00")]
#[case::tz_prefix_europe_paris(
r#"TZ="Europe/Paris" 2025-01-02 03:04:05"#,
"2025-01-02 03:04:05+01:00"
)]
fn test_tz_prefix_with_base_date(#[case] input: &str, #[case] expected: &str) {
let base = "2020-06-15 12:00:00"
.parse::<DateTime>()
.unwrap()
.to_zoned(TimeZone::UTC)
.unwrap();
check_relative(base, input, expected);
}
// Test leap year overflow: Feb 29 + years → non-leap year should overflow to March 1
// This matches GNU date behavior
#[rstest]
#[case::feb29_1996_plus_1year("1996-02-29 00:00:00", "1 year", "1997-03-01 00:00:00+00:00")]
#[case::feb29_2020_plus_1year("2020-02-29 00:00:00", "1 year", "2021-03-01 00:00:00+00:00")]
#[case::feb29_2000_plus_1year("2000-02-29 00:00:00", "1 year", "2001-03-01 00:00:00+00:00")]
// Edge case: 0 years should return the same date
#[case::zero_years("2024-01-15 12:30:45", "0 years", "2024-01-15 12:30:45+00:00")]
#[case::zero_years_feb29("2020-02-29 00:00:00", "0 years", "2020-02-29 00:00:00+00:00")]
fn test_leap_year_overflow(#[case] base: &str, #[case] input: &str, #[case] expected: &str) {
let now = base
.parse::<DateTime>()
.unwrap()
.to_zoned(TimeZone::UTC)
.unwrap();
check_relative(now, input, expected);
}
// Test month arithmetic with day overflow
// Matches GNU date behavior: when adding months causes day clamping,
// overflow to next month (e.g., Jan 31 + 1 month = March 2/3, not Feb 28/29)
#[rstest]
#[case::jan31_plus_1month_leap("2024-01-31 00:00:00", "1 month", "2024-03-02 00:00:00+00:00")]
#[case::jan31_plus_1month_nonleap("2023-01-31 00:00:00", "1 month", "2023-03-03 00:00:00+00:00")]
#[case::mar31_plus_1month("2024-03-31 00:00:00", "1 month", "2024-05-01 00:00:00+00:00")]
#[case::may31_plus_1month("2024-05-31 00:00:00", "1 month", "2024-07-01 00:00:00+00:00")]
#[case::rel_2b("1997-01-19 08:17:48", "7 months ago", "1996-06-19 08:17:48+00:00")]
// Edge case: 0 months should return the same date
#[case::zero_months("2024-01-31 12:30:45", "0 months", "2024-01-31 12:30:45+00:00")]
#[case::zero_months_feb29("2020-02-29 00:00:00", "0 months", "2020-02-29 00:00:00+00:00")]
fn test_month_overflow(#[case] base: &str, #[case] input: &str, #[case] expected: &str) {
let now = base
.parse::<DateTime>()
.unwrap()
.to_zoned(TimeZone::UTC)
.unwrap();
check_relative(now, input, expected);
}
// Test negative year operations with leap year edge cases
#[rstest]
#[case::feb29_minus_1year("2020-02-29 00:00:00", "1 year ago", "2019-03-01 00:00:00+00:00")]
#[case::feb29_minus_4years("2020-02-29 00:00:00", "4 years ago", "2016-02-29 00:00:00+00:00")]
#[case::march1_minus_1year("2021-03-01 00:00:00", "1 year ago", "2020-03-01 00:00:00+00:00")]
fn test_negative_year_operations(#[case] base: &str, #[case] input: &str, #[case] expected: &str) {
let now = base
.parse::<DateTime>()
.unwrap()
.to_zoned(TimeZone::UTC)
.unwrap();
check_relative(now, input, expected);
}
// Test negative month operations with day overflow
#[rstest]
#[case::march31_minus_1month("2024-03-31 00:00:00", "1 month ago", "2024-03-02 00:00:00+00:00")]
#[case::march31_minus_1month_nonleap(
"2023-03-31 00:00:00",
"1 month ago",
"2023-03-03 00:00:00+00:00"
)]
#[case::may31_minus_1month("2024-05-31 00:00:00", "1 month ago", "2024-05-01 00:00:00+00:00")]
#[case::jan31_minus_1month("2024-01-31 00:00:00", "1 month ago", "2023-12-31 00:00:00+00:00")]
fn test_negative_month_operations(#[case] base: &str, #[case] input: &str, #[case] expected: &str) {
let now = base
.parse::<DateTime>()
.unwrap()
.to_zoned(TimeZone::UTC)
.unwrap();
check_relative(now, input, expected);
}
// Test chained operations (multiple relative adjustments in one parse)
// These ensure that year and month overflow logic works correctly when combined
#[rstest]
// Feb 29, 2020 + 1 year = March 1, 2021; + 1 month = April 1, 2021
#[case::feb29_plus_year_plus_month(
"2020-02-29 00:00:00",
"1 year 1 month",
"2021-04-01 00:00:00+00:00"
)]
// Jan 31, 2024 + 1 month = March 2, 2024; + 1 year = March 2, 2025
#[case::jan31_plus_month_plus_year(
"2024-01-31 00:00:00",
"1 month 1 year",
"2025-03-02 00:00:00+00:00"
)]
// Jan 31 + 2 months + 1 day
#[case::jan31_plus_2months_1day(
"2024-01-31 00:00:00",
"2 months 1 day",
"2024-04-01 00:00:00+00:00"
)]
// Feb 29 - 1 year + 1 month (March 1, 2019 + 1 month = April 1, 2019)
#[case::feb29_minus_year_plus_month(
"2020-02-29 00:00:00",
"1 year ago 1 month",
"2019-04-01 00:00:00+00:00"
)]
// Multiple operations with days
#[case::complex_chain(
"2024-01-31 12:30:45",
"1 year 2 months 3 days 4 hours",
"2025-04-03 16:30:45+00:00"
)]
fn test_chained_operations(#[case] base: &str, #[case] input: &str, #[case] expected: &str) {
let now = base
.parse::<DateTime>()
.unwrap()
.to_zoned(TimeZone::UTC)
.unwrap();
check_relative(now, input, expected);
}
// Test multiple month additions
// Verifies correct handling when adding multiple months at once
#[rstest]
// Jan 31 + 2 months: Jan 31 -> March 31 (no clamping, month has 31 days)
#[case::jan31_plus_2months("2024-01-31 00:00:00", "2 months", "2024-03-31 00:00:00+00:00")]
// Jan 31 + 3 months: Jan 31 -> April 30 (clamps), overflow to May 1
#[case::jan31_plus_3months("2024-01-31 00:00:00", "3 months", "2024-05-01 00:00:00+00:00")]
// Jan 31 + 6 months: Jan 31 -> July 31 (no overflow)
#[case::jan31_plus_6months("2024-01-31 00:00:00", "6 months", "2024-07-31 00:00:00+00:00")]
// Jan 31 + 7 months: Jan 31 -> Aug 31 (no overflow)
#[case::jan31_plus_7months("2024-01-31 00:00:00", "7 months", "2024-08-31 00:00:00+00:00")]
// Aug 31 + 6 months: Aug 31 -> Feb 28 (2025 non-leap), overflow to March 3
#[case::aug31_plus_6months("2024-08-31 00:00:00", "6 months", "2025-03-03 00:00:00+00:00")]
// May 31 - 3 months: May 31 -> Feb 29 (2024 leap), overflow to March 2
#[case::may31_minus_3months_leap(
"2024-05-31 00:00:00",
"3 months ago",
"2024-03-02 00:00:00+00:00"
)]
// Oct 31 - 8 months: Oct 31 -> Feb 29 (2024 leap), overflow to March 2
#[case::oct31_minus_8months_leap(
"2024-10-31 00:00:00",
"8 months ago",
"2024-03-02 00:00:00+00:00"
)]
fn test_multiple_month_skip(#[case] base: &str, #[case] input: &str, #[case] expected: &str) {
let now = base
.parse::<DateTime>()
.unwrap()
.to_zoned(TimeZone::UTC)
.unwrap();
check_relative(now, input, expected);
}
// Test embedded timezone handling (cross-TZ-mishandled)
// When TZ="..." is specified in input with a base date, apply the timezone to the base
// https://bugs.debian.org/851934#10
//
// NOTE: These tests were added without implementation changes.
// The timezone handling was already working correctly from previous commits.
// These tests document and verify the expected behavior for this edge case.
#[rstest]
#[case::utc_explicit(r#"TZ="UTC0" 1970-01-01 00:00"#, "1970-01-01 00:00:00+00:00")]
#[case::with_time(r#"TZ="EST5" 1970-01-01 12:30:45"#, "1970-01-01 12:30:45-05:00")]
#[case::iana_timezone(
r#"TZ="America/New_York" 1970-01-01 00:00"#,
"1970-01-01 00:00:00-05:00"
)]
// Bug #851934: timezone conversion case
// Parse date in Australia/Perth (AWST, UTC+8) and output should reflect that timezone
// Input: 2016-08-15 07:00 in Australia/Perth -> expected: 2016-08-15 07:00:00+08:00
#[case::perth_to_london(
r#"TZ="Australia/Perth" 2016-08-15 07:00"#,
"2016-08-15 07:00:00+08:00"
)]
fn test_embedded_timezone(#[case] input: &str, #[case] expected: &str) {
check_absolute(input, expected);
}
+151
View File
@@ -128,3 +128,154 @@ fn test_time_invalid(#[case] input: &str) {
"Input string '{input}' did not produce an error when parsing"
);
}
#[rstest]
#[case::months_ago_0("2026-01-12 0 Months ago", "2026-01-12")]
#[case::months_ago_negative_1("2026-01-12 -1 Months ago", "2026-02-12")]
#[case::months_ago_1("2026-01-12 1 Months ago", "2025-12-12")]
#[case::months_ago_2("2026-01-12 2 Months ago", "2025-11-12")]
#[case::months_ago_3("2026-01-12 3 Months ago", "2025-10-12")]
#[case::months_ago_4("2026-01-12 4 Months ago", "2025-09-12")]
#[case::months_ago_5("2026-01-12 5 Months ago", "2025-08-12")]
#[case::months_ago_6("2026-01-12 6 Months ago", "2025-07-12")]
#[case::months_ago_7("2026-01-12 7 Months ago", "2025-06-12")]
#[case::months_ago_8("2026-01-12 8 Months ago", "2025-05-12")]
#[case::months_ago_9("2026-01-12 9 Months ago", "2025-04-12")]
#[case::months_ago_10("2026-01-12 10 Months ago", "2025-03-12")]
#[case::months_ago_11("2026-01-12 11 Months ago", "2025-02-12")]
#[case::months_ago_12("2026-01-12 12 Months ago", "2025-01-12")]
#[case::months_ago_24("2026-01-12 24 Months ago", "2024-01-12")]
#[case::months_ago_36("2026-01-12 36 Months ago", "2023-01-12")]
#[case::months_ago_120("2026-01-12 120 Months ago", "2016-01-12")]
#[case::months_ago_240("2026-01-12 240 Months ago", "2006-01-12")]
fn test_time_months_ago(#[case] input: &str, #[case] expected: &str) {
let now = "2026-01-12"
.parse::<DateTime>()
.unwrap()
.to_zoned(TimeZone::UTC)
.unwrap();
check_time(input, expected, "%Y-%m-%d", Some(now));
}
#[rstest]
#[case::from_28_feb_in_1_month("2026-02-28 1 months", "2026-03-28")]
#[case::from_28_feb_1_month_ago("2026-02-28 1 months ago", "2026-01-28")]
#[case::from_31_jan_in_1_month("2026-01-31 1 months", "2026-03-03")]
#[case::from_31_march_1_month_ago("2026-03-31 1 months ago", "2026-03-03")]
#[case::from_15_march_1_month_ago("2026-03-15 1 months ago", "2026-02-15")]
#[case::from_15_jan_in_1_month("2026-01-15 1 months", "2026-02-15")]
fn test_relative_month_time_non_leap_year(#[case] input: &str, #[case] expected: &str) {
let now = "2026-01-12"
.parse::<DateTime>()
.unwrap()
.to_zoned(TimeZone::UTC)
.unwrap();
check_time(input, expected, "%Y-%m-%d", Some(now));
}
#[rstest]
#[case::from_31_dec_1_month_ago("2026-12-31 1 months ago", "2026-12-01")]
#[case::from_31_dec_3_month_ago("2026-12-31 3 months ago", "2026-10-01")]
#[case::from_31_mar_in_1_month("2026-03-31 1 months", "2026-05-01")]
#[case::from_31_mar_in_3_month("2026-03-31 3 months", "2026-07-01")]
fn test_relative_month_time_dest_month_does_not_have_the_day(
#[case] input: &str,
#[case] expected: &str,
) {
let now = "2026-01-12"
.parse::<DateTime>()
.unwrap()
.to_zoned(TimeZone::UTC)
.unwrap();
check_time(input, expected, "%Y-%m-%d", Some(now));
}
#[rstest]
#[case::from_29_feb_in_1_year("2024-02-29 1 year", "2025-03-01")]
#[case::from_29_feb_1_year_ago("2024-02-29 1 year ago", "2023-03-01")]
fn test_relative_year_time_leap_year(#[case] input: &str, #[case] expected: &str) {
let now = "2026-01-12"
.parse::<DateTime>()
.unwrap()
.to_zoned(TimeZone::UTC)
.unwrap();
check_time(input, expected, "%Y-%m-%d", Some(now));
}
#[rstest]
#[case::from_29_feb_in_1_month("2024-02-29 1 months", "2024-03-29")]
#[case::from_29_feb_1_month_ago("2024-02-29 1 months ago", "2024-01-29")]
#[case::from_31_jan_in_1_month("2024-01-31 1 months", "2024-03-02")]
#[case::from_31_march_1_month_ago("2024-03-31 1 months ago", "2024-03-02")]
#[case::from_15_march_1_month_ago("2024-03-15 1 months ago", "2024-02-15")]
#[case::from_15_jan_in_1_month("2024-01-15 1 months", "2024-02-15")]
fn test_relative_month_time_leap_year(#[case] input: &str, #[case] expected: &str) {
let now = "2026-01-12"
.parse::<DateTime>()
.unwrap()
.to_zoned(TimeZone::UTC)
.unwrap();
check_time(input, expected, "%Y-%m-%d", Some(now));
}
#[rstest]
#[case::months_in_0_months("2026-01-12 0 months", "2026-01-12")]
#[case::months_in_negative_1_months("2026-01-12 -1 months", "2025-12-12")]
#[case::months_in_1_months("2026-01-12 1 months", "2026-02-12")]
#[case::months_in_2_months("2026-01-12 2 months", "2026-03-12")]
#[case::months_in_3_months("2026-01-12 3 months", "2026-04-12")]
#[case::months_in_4_months("2026-01-12 4 months", "2026-05-12")]
#[case::months_in_5_months("2026-01-12 5 months", "2026-06-12")]
#[case::months_in_6_months("2026-01-12 6 months", "2026-07-12")]
#[case::months_in_7_months("2026-01-12 7 months", "2026-08-12")]
#[case::months_in_8_months("2026-01-12 8 months", "2026-09-12")]
#[case::months_in_9_months("2026-01-12 9 months", "2026-10-12")]
#[case::months_in_10_months("2026-01-12 10 months", "2026-11-12")]
#[case::months_in_11_months("2026-01-12 11 months", "2026-12-12")]
#[case::months_in_12_months("2026-01-12 12 months", "2027-01-12")]
#[case::months_in_24_months("2026-01-12 24 months", "2028-01-12")]
#[case::months_in_36_months("2026-01-12 36 months", "2029-01-12")]
#[case::months_in_120_months("2026-01-12 120 months", "2036-01-12")]
#[case::months_in_240_months("2026-01-12 240 months", "2046-01-12")]
fn test_time_in_months(#[case] input: &str, #[case] expected: &str) {
let now = "2026-01-12"
.parse::<DateTime>()
.unwrap()
.to_zoned(TimeZone::UTC)
.unwrap();
check_time(input, expected, "%Y-%m-%d", Some(now));
}
#[rstest]
#[case::decimal_1_whole("1.123456789 seconds ago")]
#[case::decimal_2_whole("12.123456789 seconds ago")]
#[case::decimal_3_whole("123.123456789 seconds ago")]
#[case::decimal_4_whole("1234.123456789 seconds ago")]
#[case::decimal_5_whole("12345.123456789 seconds ago")]
#[case::decimal_6_whole("123456.123456789 seconds ago")]
#[case::decimal_7_whole("1234567.123456789 seconds ago")]
#[case::decimal_8_whole("12345678.123456789 seconds ago")]
#[case::decimal_9_whole("123456789.123456789 seconds ago")]
#[case::decimal_10_whole("1234567891.123456789 seconds ago")]
#[case::decimal_11_whole("12345678912.123456789 seconds ago")]
#[case::decimal_12_whole("123456789123.123456789 seconds ago")]
fn test_time_seconds_ago(#[case] input: &str) {
let result = parse_datetime::parse_datetime(input);
assert!(
result.is_ok(),
"Input string '{input}', produced {result:?}, instead of Ok(Zoned)"
);
}
#[rstest]
#[case::decimal_13_whole("1234567891234.123456789 seconds ago")]
#[case::decimal_14_whole("12345678912345.123456789 seconds ago")]
#[case::decimal_15_whole("123456789123456.123456789 seconds ago")]
fn test_time_seconds_ago_invalid(#[case] input: &str) {
let result = parse_datetime::parse_datetime(input);
assert_eq!(
result,
Err(parse_datetime::ParseDateTimeError::InvalidInput),
"Input string '{input}' did not produce an error when parsing"
);
}