mirror of
https://github.com/uutils/parse_datetime.git
synced 2026-10-16 03:25:04 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c2ab28fcb | ||
|
|
0510844e44 | ||
|
|
eb444cc124 | ||
|
|
f75711a932 | ||
|
|
de62667844 | ||
|
|
701f5a0200 | ||
|
|
2df367552d | ||
|
|
3fc6d60400 | ||
|
|
1c92c879ed | ||
|
|
5bc4b0a902 | ||
|
|
a066a83ca4 | ||
|
|
ed95575362 | ||
|
|
c6bfe2d15a | ||
|
|
59336d8db1 | ||
|
|
dabd1eab31 | ||
|
|
f1523281a0 | ||
|
|
5b0b578a4c | ||
|
|
2788bf4e3d | ||
|
|
04fa426eff | ||
|
|
fef8bf0cfc | ||
|
|
e25bf1530c | ||
|
|
fdd87ce6ad | ||
|
|
f451f94912 | ||
|
|
d4bace0d65 |
No files matched your search
@@ -147,22 +147,25 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
RUN_FOR: 60
|
RUN_FOR: 60
|
||||||
|
# cargo-fuzz uses nightly features
|
||||||
|
RUSTC_BOOTSTRAP: 1
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v7
|
||||||
- name: emulate nightly toolchain
|
|
||||||
run: echo "RUSTC_BOOTSTRAP=1" >> "${GITHUB_ENV}"
|
|
||||||
- name: Install `cargo-fuzz`
|
- name: Install `cargo-fuzz`
|
||||||
run: cargo install cargo-fuzz --locked
|
uses: taiki-e/install-action@v2
|
||||||
|
with:
|
||||||
|
tool: cargo-fuzz
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
- name: Run fuzz_parse_datetime for XX seconds
|
- name: Run fuzz_parse_datetime for XX seconds
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
## Run it
|
## Run it
|
||||||
cd fuzz
|
cd fuzz
|
||||||
cargo fuzz run fuzz_parse_datetime -- -max_total_time=${{ env.RUN_FOR }} -detect_leaks=0
|
# https://github.com/rust-fuzz/cargo-fuzz/issues/398
|
||||||
|
cargo fuzz run --target host-tuple fuzz_parse_datetime -- -max_total_time=${{ env.RUN_FOR }} -detect_leaks=0
|
||||||
- name: Run fuzz_large_year for XX seconds
|
- name: Run fuzz_large_year for XX seconds
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
## Run it
|
## Run it
|
||||||
cd fuzz
|
cd fuzz
|
||||||
cargo fuzz run fuzz_large_year -- -max_total_time=${{ env.RUN_FOR }} -detect_leaks=0
|
cargo fuzz run --target host-tuple fuzz_large_year -- -max_total_time=${{ env.RUN_FOR }} -detect_leaks=0
|
||||||
Generated
+146
-211
File diff suppressed because it is too large.
Load diff
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "parse_datetime"
|
name = "parse_datetime"
|
||||||
description = "parsing human-readable time strings and converting them to a Zoned datetime"
|
description = "parsing human-readable time strings and converting them to a Zoned datetime"
|
||||||
version = "0.16.0"
|
version = "0.17.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://github.com/uutils/parse_datetime"
|
repository = "https://github.com/uutils/parse_datetime"
|
||||||
@@ -14,7 +14,7 @@ num-traits = "0.2.19"
|
|||||||
jiff = { version = "0.2.15", default-features = false, features = ["tz-system", "tzdb-bundle-platform", "tzdb-zoneinfo"] }
|
jiff = { version = "0.2.15", default-features = false, features = ["tz-system", "tzdb-bundle-platform", "tzdb-zoneinfo"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
rstest = "0.26"
|
rstest = "0.27"
|
||||||
|
|
||||||
[dev-dependencies.codspeed-criterion-compat]
|
[dev-dependencies.codspeed-criterion-compat]
|
||||||
version = "5.0"
|
version = "5.0"
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ The `parse_datetime` and `parse_datetime_at_date` functions support absolute dat
|
|||||||
- "now" or "today"
|
- "now" or "today"
|
||||||
- "yesterday"
|
- "yesterday"
|
||||||
- "tomorrow"
|
- "tomorrow"
|
||||||
- use "ago" for the past
|
- use "ago" for the past or "hence" for the future
|
||||||
- use "next" or "last" with `unit` (e.g., "next week", "last year")
|
- use "next" or "last" with `unit` (e.g., "next week", "last year")
|
||||||
- unix timestamps (for example "@0" "@1344000")
|
- unix timestamps (for example "@0" "@1344000")
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,16 @@ fn bench_relative_keywords(c: &mut Criterion) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn bench_weekday(c: &mut Criterion) {
|
fn bench_weekday(c: &mut Criterion) {
|
||||||
c.bench_function("parse_weekday", |b| b.iter(|| parse_datetime("wednesday")));
|
// Use a fixed reference date: the cost of resolving a weekday depends on
|
||||||
|
// how many days separate it from "today", so relying on the current date
|
||||||
|
// makes the measurement vary with the day the benchmark runs.
|
||||||
|
// 2000-01-03 is a Monday, so "wednesday" resolves two days ahead.
|
||||||
|
let base = jiff::civil::DateTime::from(jiff::civil::date(2000, 1, 3))
|
||||||
|
.to_zoned(jiff::tz::TimeZone::UTC)
|
||||||
|
.unwrap();
|
||||||
|
c.bench_function("parse_weekday", |b| {
|
||||||
|
b.iter(|| parse_datetime_at_date(base.clone(), "wednesday"))
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn bench_timezone_offset(c: &mut Criterion) {
|
fn bench_timezone_offset(c: &mut Criterion) {
|
||||||
|
|||||||
Generated
+76
-103
@@ -4,9 +4,9 @@ version = 4
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "arbitrary"
|
name = "arbitrary"
|
||||||
version = "1.4.2"
|
version = "1.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
|
checksum = "3bc62ac97cc33321f50863d514c3bc38a453947a8f9e781137e47c7401020aed"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"derive_arbitrary",
|
"derive_arbitrary",
|
||||||
]
|
]
|
||||||
@@ -25,9 +25,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.2.62"
|
version = "1.8.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98"
|
checksum = "6651c9ed80effdc7db0ff72512157f901af5e3549e341e24b1dd4887d836d838"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"find-msvc-tools",
|
"find-msvc-tools",
|
||||||
"jobserver",
|
"jobserver",
|
||||||
@@ -37,15 +37,15 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cfg-if"
|
name = "cfg-if"
|
||||||
version = "1.0.4"
|
version = "1.0.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "defmt"
|
name = "defmt"
|
||||||
version = "1.1.0"
|
version = "1.1.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a6e524506490a1953d237cb87b1cfc1e46f88c18f10a22dfe0f507dc6bfc7f7f"
|
checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"defmt-macros",
|
"defmt-macros",
|
||||||
@@ -53,15 +53,14 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "defmt-macros"
|
name = "defmt-macros"
|
||||||
version = "1.1.0"
|
version = "1.1.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f0a27770e9c8f719a79d8b638281f4d828f77d8fd61e0bd94451b9b85e576a0b"
|
checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"defmt-parser",
|
"defmt-parser",
|
||||||
"proc-macro-error2",
|
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 2.0.119",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -75,20 +74,20 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "derive_arbitrary"
|
name = "derive_arbitrary"
|
||||||
version = "1.4.2"
|
version = "1.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
|
checksum = "1b034bd7d5f032402a2479444dcc6f74e36a03f31854d41680fb240ef682a1ac"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 3.0.8",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "find-msvc-tools"
|
name = "find-msvc-tools"
|
||||||
version = "0.1.9"
|
version = "0.1.14"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fuzz_parse_datetime"
|
name = "fuzz_parse_datetime"
|
||||||
@@ -102,21 +101,20 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "getrandom"
|
name = "getrandom"
|
||||||
version = "0.3.4"
|
version = "0.4.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"libc",
|
"libc",
|
||||||
"r-efi",
|
"r-efi",
|
||||||
"wasip2",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jiff"
|
name = "jiff"
|
||||||
version = "0.2.35"
|
version = "0.2.38"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "668b7183bd07af9a4885f5c35b0cc5c83c4607a913c16b7e17291832910d2dcc"
|
checksum = "b2b005715dcbeb0089a3c0dab99f2ff1cc3b2525323552703d648585d342a383"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"defmt",
|
"defmt",
|
||||||
"jiff-core",
|
"jiff-core",
|
||||||
@@ -131,30 +129,31 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jiff-core"
|
name = "jiff-core"
|
||||||
version = "0.1.0"
|
version = "0.1.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7feca88439efe53da3754500c1851dedf3cb36c524dd5cf8225cc0794de95d09"
|
checksum = "5e52fe76043ccecc9005d2305ebaadf7d7fc0cc89ca6baa10a94d6bc68c7128c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"defmt",
|
"defmt",
|
||||||
|
"log",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jiff-static"
|
name = "jiff-static"
|
||||||
version = "0.2.35"
|
version = "0.2.38"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3a69dcb3a21cfb32ce1cd056169337ca284af0766dd766e7878819b251a49204"
|
checksum = "2cc9817253cf7c7ee4684451bd327e88d6f3658014e54a29198625590650695c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"jiff-core",
|
"jiff-core",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 2.0.119",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jiff-tzdb"
|
name = "jiff-tzdb"
|
||||||
version = "0.1.6"
|
version = "0.1.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c900ef84826f1338a557697dc8fc601df9ca9af4ac137c7fb61d4c6f2dfd3076"
|
checksum = "fa8377070c6bae868759445e5a77f66d84f0b72f3a054bfb00e6d038b8282da7"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jiff-tzdb-platform"
|
name = "jiff-tzdb-platform"
|
||||||
@@ -167,9 +166,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jobserver"
|
name = "jobserver"
|
||||||
version = "0.1.34"
|
version = "0.1.35"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
|
checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom",
|
"getrandom",
|
||||||
"libc",
|
"libc",
|
||||||
@@ -177,9 +176,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.186"
|
version = "0.2.190"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libfuzzer-sys"
|
name = "libfuzzer-sys"
|
||||||
@@ -193,15 +192,15 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "log"
|
name = "log"
|
||||||
version = "0.4.29"
|
version = "0.4.34"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "memchr"
|
||||||
version = "2.8.0"
|
version = "2.8.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-traits"
|
name = "num-traits"
|
||||||
@@ -214,7 +213,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parse_datetime"
|
name = "parse_datetime"
|
||||||
version = "0.15.0"
|
version = "0.17.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"jiff",
|
"jiff",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
@@ -223,96 +222,85 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "portable-atomic"
|
name = "portable-atomic"
|
||||||
version = "1.13.1"
|
version = "1.15.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
|
checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "portable-atomic-util"
|
name = "portable-atomic-util"
|
||||||
version = "0.2.7"
|
version = "0.2.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618"
|
checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"portable-atomic",
|
"portable-atomic",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "proc-macro-error-attr2"
|
|
||||||
version = "2.0.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "proc-macro-error2"
|
|
||||||
version = "2.0.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro-error-attr2",
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.106"
|
version = "1.0.107"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.45"
|
version = "1.0.47"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "r-efi"
|
name = "r-efi"
|
||||||
version = "5.3.0"
|
version = "6.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_core"
|
name = "serde_core"
|
||||||
version = "1.0.228"
|
version = "1.0.229"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_derive"
|
name = "serde_derive"
|
||||||
version = "1.0.228"
|
version = "1.0.229"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 3.0.8",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "shlex"
|
name = "shlex"
|
||||||
version = "1.3.0"
|
version = "2.0.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.117"
|
version = "2.0.119"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "3.0.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "01016da373cd8f7ef12624f796309f5c31ba8d646dd08856c02cd741d823c622"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -321,38 +309,29 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "2.0.18"
|
version = "2.0.21"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
|
checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"thiserror-impl",
|
"thiserror-impl",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror-impl"
|
name = "thiserror-impl"
|
||||||
version = "2.0.18"
|
version = "2.0.21"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
|
checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 3.0.8",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.24"
|
version = "1.0.26"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "wasip2"
|
|
||||||
version = "1.0.3+wasi-0.2.9"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6"
|
|
||||||
dependencies = [
|
|
||||||
"wit-bindgen",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-link"
|
name = "windows-link"
|
||||||
@@ -362,15 +341,9 @@ checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winnow"
|
name = "winnow"
|
||||||
version = "1.0.3"
|
version = "1.0.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1"
|
checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "wit-bindgen"
|
|
||||||
version = "0.57.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
|
|
||||||
+13
-6
@@ -212,10 +212,15 @@ impl DateTimeBuilder {
|
|||||||
/// from the base instant.
|
/// from the base instant.
|
||||||
/// - b. Apply time. If time carries an explicit numeric offset, apply the
|
/// - b. Apply time. If time carries an explicit numeric offset, apply the
|
||||||
/// offset before setting time.
|
/// offset before setting time.
|
||||||
/// - c. Apply weekday (e.g., "next Friday" or "last Monday").
|
/// - c. Apply weekday (e.g., "next Friday" or "last Monday"). A weekday
|
||||||
|
/// is ignored when an explicit calendar date is given.
|
||||||
/// - d. Apply fixed offset if present (anchors the instant).
|
/// - d. Apply fixed offset if present (anchors the instant).
|
||||||
/// - e. Apply relative adjustments (e.g., "+3 days", "-2 months").
|
/// - e. Apply relative adjustments (e.g., "+3 days", "-2 months").
|
||||||
pub(super) fn build(self) -> Result<ParsedDateTime, error::Error> {
|
pub(super) fn build(mut self) -> Result<ParsedDateTime, error::Error> {
|
||||||
|
if self.date.is_some() {
|
||||||
|
self.weekday = None;
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(date) = self.date.as_ref() {
|
if let Some(date) = self.date.as_ref() {
|
||||||
if date.year.unwrap_or(0) > 9999 {
|
if date.year.unwrap_or(0) > 9999 {
|
||||||
return self.build_extended();
|
return self.build_extended();
|
||||||
@@ -581,11 +586,11 @@ mod tests {
|
|||||||
|
|
||||||
fn date() -> date::Date {
|
fn date() -> date::Date {
|
||||||
let mut input = "2023-06-15";
|
let mut input = "2023-06-15";
|
||||||
date::parse(&mut input).unwrap()
|
date::iso1(&mut input).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn date_large(mut input: &str) -> date::Date {
|
fn date_large(mut input: &str) -> date::Date {
|
||||||
date::parse(&mut input).unwrap()
|
date::iso1(&mut input).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn time() -> time::Time {
|
fn time() -> time::Time {
|
||||||
@@ -902,7 +907,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn build_extended_applies_relative_units_and_weekday() {
|
fn build_extended_applies_relative_units_and_ignores_weekday_with_date() {
|
||||||
let base = "2000-01-01 00:00:00"
|
let base = "2000-01-01 00:00:00"
|
||||||
.parse::<DateTime>()
|
.parse::<DateTime>()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
@@ -918,7 +923,9 @@ mod tests {
|
|||||||
])
|
])
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let dt = expect_extended_datetime(builder.set_base(base).build().unwrap());
|
let dt = expect_extended_datetime(builder.set_base(base).build().unwrap());
|
||||||
assert_eq!((dt.year, dt.month, dt.day), (10000, 1, 4));
|
// The weekday is ignored because a date is given, so only the relative
|
||||||
|
// items move it (GNU: `next mon 10000-01-01 +1 day` is 10000-01-02).
|
||||||
|
assert_eq!((dt.year, dt.month, dt.day), (10000, 1, 2));
|
||||||
assert_eq!((dt.hour, dt.minute, dt.second), (2, 3, 4));
|
assert_eq!((dt.hour, dt.minute, dt.second), (2, 3, 4));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+12
-4
@@ -37,8 +37,9 @@ fn remaining_starts_with_meridiem(input: &str) -> bool {
|
|||||||
|| trimmed.starts_with("p.m.")
|
|| trimmed.starts_with("p.m.")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn parse(input: &mut &str) -> ModalResult<DateTime> {
|
/// Parse the separator and time of a combined item whose ISO date has already
|
||||||
let date = trace("iso_date", alt((date::iso1, date::iso2))).parse_next(input)?;
|
/// been parsed.
|
||||||
|
pub(crate) fn parse_time_after_date(input: &mut &str, date: date::Date) -> ModalResult<DateTime> {
|
||||||
// Note: the `T` is lowercased by the main parse function
|
// Note: the `T` is lowercased by the main parse function
|
||||||
alt((s('t').void(), (' ', space).void())).parse_next(input)?;
|
alt((s('t').void(), (' ', space).void())).parse_next(input)?;
|
||||||
|
|
||||||
@@ -59,8 +60,15 @@ pub(crate) fn parse(input: &mut &str) -> ModalResult<DateTime> {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{parse, DateTime};
|
use winnow::{combinator::alt, ModalResult, Parser};
|
||||||
use crate::items::{date::Date, time::Time};
|
|
||||||
|
use super::{parse_time_after_date, DateTime};
|
||||||
|
use crate::items::{date, date::Date, time::Time};
|
||||||
|
|
||||||
|
fn parse(input: &mut &str) -> ModalResult<DateTime> {
|
||||||
|
let date = alt((date::iso1, date::iso2)).parse_next(input)?;
|
||||||
|
parse_time_after_date(input, date)
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn some_date() {
|
fn some_date() {
|
||||||
|
|||||||
+11
-3
@@ -127,8 +127,10 @@ impl TryFrom<Date> for jiff::civil::Date {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn parse(input: &mut &str) -> ModalResult<Date> {
|
/// Parse a date that is not in ISO format. ISO dates are parsed by [`iso1`]
|
||||||
alt((iso1, iso2, us, literal1, literal2, literal3)).parse_next(input)
|
/// and [`iso2`], which the caller tries first.
|
||||||
|
pub(super) fn parse_non_iso(input: &mut &str) -> ModalResult<Date> {
|
||||||
|
alt((us, literal1, literal2, literal3)).parse_next(input)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse `[year]-[month]-[day]`
|
/// Parse `[year]-[month]-[day]`
|
||||||
@@ -299,7 +301,13 @@ fn day_from_str(s: &str) -> ModalResult<u8> {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{parse, Date};
|
use winnow::{combinator::alt, ModalResult, Parser};
|
||||||
|
|
||||||
|
use super::Date;
|
||||||
|
|
||||||
|
fn parse(input: &mut &str) -> ModalResult<Date> {
|
||||||
|
alt((super::iso1, super::iso2, super::parse_non_iso)).parse_next(input)
|
||||||
|
}
|
||||||
|
|
||||||
// Test cases from the GNU docs:
|
// Test cases from the GNU docs:
|
||||||
//
|
//
|
||||||
|
|||||||
+65
-5
@@ -48,6 +48,8 @@ mod primitive;
|
|||||||
|
|
||||||
pub(crate) mod error;
|
pub(crate) mod error;
|
||||||
|
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
use crate::ParsedDateTime;
|
use crate::ParsedDateTime;
|
||||||
use jiff::Zoned;
|
use jiff::Zoned;
|
||||||
use primitive::space;
|
use primitive::space;
|
||||||
@@ -159,7 +161,7 @@ pub(crate) fn parse_at_local<S: AsRef<str> + Clone>(input: S) -> Result<ParsedDa
|
|||||||
///
|
///
|
||||||
/// time_offset = ( "+" | "-" ) , dec_uint , [ ":" , dec_uint ] ;
|
/// time_offset = ( "+" | "-" ) , dec_uint , [ ":" , dec_uint ] ;
|
||||||
///
|
///
|
||||||
/// relative = [ numeric_ordinal ] , unit , [ "ago" ] | day_shift ;
|
/// relative = [ numeric_ordinal ] , unit , [ "ago" | "hence" ] | day_shift ;
|
||||||
///
|
///
|
||||||
/// unit = "year" | "years"
|
/// unit = "year" | "years"
|
||||||
/// | "month" | "months"
|
/// | "month" | "months"
|
||||||
@@ -236,8 +238,12 @@ fn parse_items(input: &mut &str) -> ModalResult<DateTimeBuilder> {
|
|||||||
let tz = timezone::parse(input).map(Item::TimeZone);
|
let tz = timezone::parse(input).map(Item::TimeZone);
|
||||||
|
|
||||||
// Convert input to lowercase for case-insensitive parsing.
|
// Convert input to lowercase for case-insensitive parsing.
|
||||||
let lower = input.to_ascii_lowercase();
|
let lower: Cow<str> = if input.bytes().any(|b| b.is_ascii_uppercase()) {
|
||||||
let input = &mut lower.as_str();
|
input.to_ascii_lowercase().into()
|
||||||
|
} else {
|
||||||
|
(*input).into()
|
||||||
|
};
|
||||||
|
let input = &mut lower.as_ref();
|
||||||
|
|
||||||
let (mut items, _): (Vec<Item>, _) = trace(
|
let (mut items, _): (Vec<Item>, _) = trace(
|
||||||
"parse_items",
|
"parse_items",
|
||||||
@@ -255,11 +261,26 @@ fn parse_items(input: &mut &str) -> ModalResult<DateTimeBuilder> {
|
|||||||
|
|
||||||
/// Parse an item.
|
/// Parse an item.
|
||||||
fn parse_item(input: &mut &str) -> ModalResult<Item> {
|
fn parse_item(input: &mut &str) -> ModalResult<Item> {
|
||||||
|
// An ISO date is either the start of a combined date and time item or a
|
||||||
|
// date item on its own. Parse it once instead of once per alternative.
|
||||||
|
let start = input.checkpoint();
|
||||||
|
if let Ok(date) = alt((date::iso1, date::iso2)).parse_next(input) {
|
||||||
|
let after_date = input.checkpoint();
|
||||||
|
match combined::parse_time_after_date(input, date.clone()) {
|
||||||
|
Ok(dt) => return Ok(Item::DateTime(dt)),
|
||||||
|
Err(ErrMode::Backtrack(_)) => {
|
||||||
|
input.reset(&after_date);
|
||||||
|
return Ok(Item::Date(date));
|
||||||
|
}
|
||||||
|
Err(e) => return Err(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
input.reset(&start);
|
||||||
|
|
||||||
trace(
|
trace(
|
||||||
"parse_item",
|
"parse_item",
|
||||||
alt((
|
alt((
|
||||||
combined::parse.map(Item::DateTime),
|
date::parse_non_iso.map(Item::Date),
|
||||||
date::parse.map(Item::Date),
|
|
||||||
time::parse.map(Item::Time),
|
time::parse.map(Item::Time),
|
||||||
relative::parse.map(Item::Relative),
|
relative::parse.map(Item::Relative),
|
||||||
weekday::parse.map(Item::Weekday),
|
weekday::parse.map(Item::Weekday),
|
||||||
@@ -743,6 +764,45 @@ mod tests {
|
|||||||
assert_eq!(result.second(), 0);
|
assert_eq!(result.second(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn offset_followed_by_relative() {
|
||||||
|
let base = "2000-01-01 12:00:00"
|
||||||
|
.parse::<DateTime>()
|
||||||
|
.unwrap()
|
||||||
|
.to_zoned(TimeZone::UTC)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Both read "+8 years" as a relative item, not as an offset of +8.
|
||||||
|
for input in ["UTC +8 years", "+8 years"] {
|
||||||
|
let result = parse_at_date(base.clone(), input).unwrap();
|
||||||
|
assert_eq!(result.to_string(), "2008-01-01 12:00:00+00:00", "{input}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn comments_and_ignored_signs() {
|
||||||
|
let base = "2000-06-15 12:34:56"
|
||||||
|
.parse::<DateTime>()
|
||||||
|
.unwrap()
|
||||||
|
.to_zoned(TimeZone::UTC)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
for (input, expected) in [
|
||||||
|
("1997-01-01 (foo) 10:00", "1997-01-01 10:00:00+00:00"),
|
||||||
|
("1997-01-01 (a (b)) 10:00", "1997-01-01 10:00:00+00:00"),
|
||||||
|
("(foo) 1997-01-01", "1997-01-01 00:00:00+00:00"),
|
||||||
|
("+ (c) 1997-01-01", "1997-01-01 00:00:00+00:00"),
|
||||||
|
("- (c) 1997-01-01", "1997-01-01 00:00:00+00:00"),
|
||||||
|
("1997-01-01 (x) + 1 day", "1997-01-02 00:00:00+00:00"),
|
||||||
|
] {
|
||||||
|
let result = parse_at_date(base.clone(), input).unwrap();
|
||||||
|
assert_eq!(result.to_string(), expected, "{input}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Like GNU, a hyphen followed by a digit is a sign, not ignored.
|
||||||
|
assert!(parse_at_date(base.clone(), "- 1997-01-01").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn pure() {
|
fn pure() {
|
||||||
let now = Zoned::now().with_time_zone(TimeZone::UTC);
|
let now = Zoned::now().with_time_zone(TimeZone::UTC);
|
||||||
|
|||||||
+123
-97
@@ -193,16 +193,21 @@ pub(super) fn parse_local(input: &mut &str) -> ModalResult<()> {
|
|||||||
|
|
||||||
/// Parse a timezone starting with `+` or `-`.
|
/// Parse a timezone starting with `+` or `-`.
|
||||||
pub(super) fn timezone_offset(input: &mut &str) -> ModalResult<Offset> {
|
pub(super) fn timezone_offset(input: &mut &str) -> ModalResult<Offset> {
|
||||||
// Strings like "+8 years" are ambiguous, they can either be parsed as a
|
// Like GNU, read "+8 years" as a relative item, not as "+8" and "years".
|
||||||
// timezone offset "+8" and a relative time "years", or just a relative time
|
// The costlier relative check only runs once an offset has parsed.
|
||||||
// "+8 years". GNU date parses them the second way, so we do the same here.
|
let start = input.checkpoint();
|
||||||
//
|
let result = alt((timezone_offset_colon, timezone_offset_colonless)).parse_next(input);
|
||||||
// Return early if the input can be parsed as a relative time.
|
if matches!(result, Err(ErrMode::Backtrack(_))) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
let end = input.checkpoint();
|
||||||
|
input.reset(&start);
|
||||||
if peek(relative::parse).parse_next(input).is_ok() {
|
if peek(relative::parse).parse_next(input).is_ok() {
|
||||||
return Err(ErrMode::Backtrack(ContextError::new()));
|
return Err(ErrMode::Backtrack(ContextError::new()));
|
||||||
}
|
}
|
||||||
|
input.reset(&end);
|
||||||
alt((timezone_offset_colon, timezone_offset_colonless)).parse_next(input)
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse a timezone by name, with an optional numeric offset appended.
|
/// Parse a timezone by name, with an optional numeric offset appended.
|
||||||
@@ -210,20 +215,13 @@ fn timezone_name_offset(input: &mut &str) -> ModalResult<Offset> {
|
|||||||
let nextword = s(take_while(1..=MAX_TZ_SIZE, AsChar::is_alpha)).parse_next(input)?;
|
let nextword = s(take_while(1..=MAX_TZ_SIZE, AsChar::is_alpha)).parse_next(input)?;
|
||||||
let tz = timezone_name_to_offset(nextword)?;
|
let tz = timezone_name_to_offset(nextword)?;
|
||||||
|
|
||||||
// Strings like "UTC +8 years" are ambiguous, they can either be parsed as
|
// Like GNU, read "UTC +8 years" as "UTC" and "+8 years", not as "UTC+8"
|
||||||
// "UTC+8" and "years", or "UTC" and "+8 years". GNU date parses them the
|
// and "years". `timezone_offset` rejects input that parses as relative.
|
||||||
// second way, so we do the same here.
|
let start = input.checkpoint();
|
||||||
//
|
if let Ok(other_tz) = timezone_offset.parse_next(input) {
|
||||||
// Only process if the input cannot be parsed as a relative time.
|
return Ok(tz.merge(other_tz));
|
||||||
if peek(relative::parse).parse_next(input).is_err() {
|
};
|
||||||
let start = input.checkpoint();
|
input.reset(&start);
|
||||||
if let Ok(other_tz) = timezone_offset.parse_next(input) {
|
|
||||||
let new_tz = tz.merge(other_tz);
|
|
||||||
|
|
||||||
return Ok(new_tz);
|
|
||||||
};
|
|
||||||
input.reset(&start);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(tz)
|
Ok(tz)
|
||||||
}
|
}
|
||||||
@@ -287,83 +285,87 @@ fn timezone_offset_colonless(input: &mut &str) -> ModalResult<Offset> {
|
|||||||
/// https://www.timeanddate.com/time/zones/. GNU date only supports a subset of
|
/// https://www.timeanddate.com/time/zones/. GNU date only supports a subset of
|
||||||
/// these. We support the same subset as GNU date.
|
/// these. We support the same subset as GNU date.
|
||||||
fn timezone_name_to_offset(input: &str) -> ModalResult<Offset> {
|
fn timezone_name_to_offset(input: &str) -> ModalResult<Offset> {
|
||||||
let mut offset_str = match input {
|
let (negative, hours, minutes) = match input {
|
||||||
"z" => Ok("+0"),
|
"z" => (false, 0, 0),
|
||||||
"y" => Ok("-12"),
|
"y" => (true, 12, 0),
|
||||||
"x" => Ok("-11"),
|
"x" => (true, 11, 0),
|
||||||
"wet" => Ok("+0"),
|
"wet" => (false, 0, 0),
|
||||||
"west" => Ok("+1"),
|
"west" => (false, 1, 0),
|
||||||
"wat" => Ok("+1"),
|
"wat" => (false, 1, 0),
|
||||||
"w" => Ok("-10"),
|
"w" => (true, 10, 0),
|
||||||
"v" => Ok("-9"),
|
"v" => (true, 9, 0),
|
||||||
"utc" => Ok("+0"),
|
"utc" => (false, 0, 0),
|
||||||
"ut" => Ok("+0"),
|
"ut" => (false, 0, 0),
|
||||||
"u" => Ok("-8"),
|
"u" => (true, 8, 0),
|
||||||
"t" => Ok("-7"),
|
"t" => (true, 7, 0),
|
||||||
"sst" => Ok("-11"),
|
"sst" => (true, 12, 0),
|
||||||
"sgt" => Ok("+8"),
|
"sgt" => (false, 8, 0),
|
||||||
"sast" => Ok("+2"),
|
"sast" => (false, 2, 0),
|
||||||
"s" => Ok("-6"),
|
"s" => (true, 6, 0),
|
||||||
"r" => Ok("-5"),
|
"r" => (true, 5, 0),
|
||||||
"q" => Ok("-4"),
|
"q" => (true, 4, 0),
|
||||||
"pst" => Ok("-8"),
|
"pst" => (true, 8, 0),
|
||||||
"pdt" => Ok("-7"),
|
"pdt" => (true, 7, 0),
|
||||||
"p" => Ok("-3"),
|
"p" => (true, 3, 0),
|
||||||
"o" => Ok("-2"),
|
"o" => (true, 2, 0),
|
||||||
"nzst" => Ok("+12"),
|
"nzst" => (false, 12, 0),
|
||||||
"nzdt" => Ok("+13"),
|
"nzdt" => (false, 13, 0),
|
||||||
"nst" => Ok("-3:30"),
|
"nst" => (true, 3, 30),
|
||||||
"ndt" => Ok("-2:30"),
|
"ndt" => (true, 2, 30),
|
||||||
"n" => Ok("-1"),
|
"n" => (true, 1, 0),
|
||||||
"mst" => Ok("-7"),
|
"mst" => (true, 7, 0),
|
||||||
"msk" => Ok("+3"),
|
"msk" => (false, 3, 0),
|
||||||
"msd" => Ok("+4"),
|
"msd" => (false, 4, 0),
|
||||||
"mez" => Ok("+1"),
|
"mez" => (false, 1, 0),
|
||||||
"mesz" => Ok("+2"),
|
"mesz" => (false, 2, 0),
|
||||||
"mest" => Ok("+2"),
|
"mest" => (false, 2, 0),
|
||||||
"mdt" => Ok("-6"),
|
"mdt" => (true, 6, 0),
|
||||||
"m" => Ok("+12"),
|
"m" => (false, 12, 0),
|
||||||
"l" => Ok("+11"),
|
"l" => (false, 11, 0),
|
||||||
"kst" => Ok("+9"),
|
"kst" => (false, 9, 0),
|
||||||
"k" => Ok("+10"),
|
"k" => (false, 10, 0),
|
||||||
"jst" => Ok("+9"),
|
"jst" => (false, 9, 0),
|
||||||
"ist" => Ok("+5:30"),
|
"ist" => (false, 5, 30),
|
||||||
"i" => Ok("+9"),
|
"i" => (false, 9, 0),
|
||||||
"hst" => Ok("-10"),
|
"hst" => (true, 10, 0),
|
||||||
"h" => Ok("+8"),
|
"h" => (false, 8, 0),
|
||||||
"gst" => Ok("+4"),
|
"gst" => (false, 10, 0),
|
||||||
"gmt" => Ok("+0"),
|
"gmt" => (false, 0, 0),
|
||||||
"g" => Ok("+7"),
|
"g" => (false, 7, 0),
|
||||||
"f" => Ok("+6"),
|
"f" => (false, 6, 0),
|
||||||
"est" => Ok("-5"),
|
"est" => (true, 5, 0),
|
||||||
"eet" => Ok("+2"),
|
"eet" => (false, 2, 0),
|
||||||
"eest" => Ok("+3"),
|
"eest" => (false, 3, 0),
|
||||||
"edt" => Ok("-4"),
|
"edt" => (true, 4, 0),
|
||||||
"eat" => Ok("+3"),
|
"eat" => (false, 3, 0),
|
||||||
"e" => Ok("+5"),
|
"e" => (false, 5, 0),
|
||||||
"d" => Ok("+4"),
|
"d" => (false, 4, 0),
|
||||||
"cst" => Ok("-6"),
|
"cst" => (true, 6, 0),
|
||||||
"clt" => Ok("-4"),
|
"clt" => (true, 4, 0),
|
||||||
"clst" => Ok("-3"),
|
"clst" => (true, 3, 0),
|
||||||
"cet" => Ok("+1"),
|
"cet" => (false, 1, 0),
|
||||||
"cest" => Ok("+2"),
|
"cest" => (false, 2, 0),
|
||||||
"cdt" => Ok("-5"),
|
"cdt" => (true, 5, 0),
|
||||||
"cat" => Ok("+2"),
|
"cat" => (false, 2, 0),
|
||||||
"c" => Ok("+3"),
|
"c" => (false, 3, 0),
|
||||||
"bst" => Ok("+6"),
|
"bst" => (false, 1, 0),
|
||||||
"brt" => Ok("-3"),
|
"brt" => (true, 3, 0),
|
||||||
"brst" => Ok("-2"),
|
"brst" => (true, 2, 0),
|
||||||
"b" => Ok("+2"),
|
"b" => (false, 2, 0),
|
||||||
"ast" => Ok("-3"),
|
"ast" => (true, 4, 0),
|
||||||
"art" => Ok("-3"),
|
"art" => (true, 3, 0),
|
||||||
"akst" => Ok("-9"),
|
"akst" => (true, 9, 0),
|
||||||
"akdt" => Ok("-8"),
|
"akdt" => (true, 8, 0),
|
||||||
"adt" => Ok("+4"),
|
"adt" => (true, 3, 0),
|
||||||
"a" => Ok("+1"),
|
"a" => (false, 1, 0),
|
||||||
_ => Err(ErrMode::Backtrack(ContextError::new())),
|
_ => return Err(ErrMode::Backtrack(ContextError::new())),
|
||||||
}?;
|
};
|
||||||
|
|
||||||
timezone_offset(&mut offset_str)
|
Ok(Offset {
|
||||||
|
negative,
|
||||||
|
hours,
|
||||||
|
minutes,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -457,6 +459,12 @@ mod tests {
|
|||||||
("mesz", off(false, 2, 0)),
|
("mesz", off(false, 2, 0)),
|
||||||
("mest", off(false, 2, 0)),
|
("mest", off(false, 2, 0)),
|
||||||
("kst", off(false, 9, 0)),
|
("kst", off(false, 9, 0)),
|
||||||
|
// Corrected to the offsets GNU date assigns these abbreviations.
|
||||||
|
("adt", off(true, 3, 0)), // Atlantic Daylight, was +4
|
||||||
|
("ast", off(true, 4, 0)), // Atlantic Standard, was -3
|
||||||
|
("bst", off(false, 1, 0)), // British Summer, was +6
|
||||||
|
("gst", off(false, 10, 0)), // Guam Standard, was +4
|
||||||
|
("sst", off(true, 12, 0)), // was -11
|
||||||
("z123", off(false, 0, 0)), // space separator can be ignored if immediately followed by digits (GNU date behavior)
|
("z123", off(false, 0, 0)), // space separator can be ignored if immediately followed by digits (GNU date behavior)
|
||||||
] {
|
] {
|
||||||
let mut s = input;
|
let mut s = input;
|
||||||
@@ -503,6 +511,24 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn offset_followed_by_relative() {
|
||||||
|
// "+8 years" is a relative item, so it is not an offset.
|
||||||
|
let mut s = "+8 years";
|
||||||
|
assert!(timezone_offset(&mut s).is_err());
|
||||||
|
assert_eq!(s, "+8 years");
|
||||||
|
|
||||||
|
// "utc +8 years" is "utc" followed by the relative item "+8 years".
|
||||||
|
let mut s = "utc +8 years";
|
||||||
|
assert_eq!(timezone_name_offset(&mut s).unwrap(), off(false, 0, 0));
|
||||||
|
assert_eq!(s, " +8 years");
|
||||||
|
|
||||||
|
// Without a relative item, the offset is still merged.
|
||||||
|
let mut s = "utc +8";
|
||||||
|
assert_eq!(timezone_name_offset(&mut s).unwrap(), off(false, 8, 0));
|
||||||
|
assert!(s.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn total_seconds() {
|
fn total_seconds() {
|
||||||
assert_eq!(off(false, 0, 0).total_seconds(), 0);
|
assert_eq!(off(false, 0, 0).total_seconds(), 0);
|
||||||
|
|||||||
+37
-1
@@ -35,7 +35,7 @@ fn text_ordinal(input: &mut &str) -> ModalResult<i32> {
|
|||||||
"fifth" => 5,
|
"fifth" => 5,
|
||||||
"sixth" => 6,
|
"sixth" => 6,
|
||||||
"seventh" => 7,
|
"seventh" => 7,
|
||||||
"eight" => 8,
|
"eighth" => 8,
|
||||||
"ninth" => 9,
|
"ninth" => 9,
|
||||||
"tenth" => 10,
|
"tenth" => 10,
|
||||||
"eleventh" => 11,
|
"eleventh" => 11,
|
||||||
@@ -45,3 +45,39 @@ fn text_ordinal(input: &mut &str) -> ModalResult<i32> {
|
|||||||
})
|
})
|
||||||
.parse_next(input)
|
.parse_next(input)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::ordinal;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn text_ordinal() {
|
||||||
|
for (s, expected) in [
|
||||||
|
("last", -1),
|
||||||
|
("this", 0),
|
||||||
|
("next", 1),
|
||||||
|
("first", 1),
|
||||||
|
("third", 3),
|
||||||
|
("fourth", 4),
|
||||||
|
("fifth", 5),
|
||||||
|
("sixth", 6),
|
||||||
|
("seventh", 7),
|
||||||
|
("eighth", 8),
|
||||||
|
("ninth", 9),
|
||||||
|
("tenth", 10),
|
||||||
|
("eleventh", 11),
|
||||||
|
("twelfth", 12),
|
||||||
|
] {
|
||||||
|
let mut input = s;
|
||||||
|
assert_eq!(ordinal(&mut input).unwrap(), expected, "{s}");
|
||||||
|
assert!(input.is_empty(), "{s}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn invalid_text_ordinal() {
|
||||||
|
for mut s in ["eight", "second", "foo"] {
|
||||||
|
assert!(ordinal(&mut s).is_err(), "{s}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+35
-1
@@ -32,7 +32,13 @@ fn multispace0<'a, E>(input: &mut &'a str) -> winnow::Result<&'a str, E>
|
|||||||
where
|
where
|
||||||
E: ParserError<&'a str>,
|
E: ParserError<&'a str>,
|
||||||
{
|
{
|
||||||
take_while(0.., (' ', '\t', '\n', '\x0B', '\x0C', '\r')).parse_next(input)
|
let len = input
|
||||||
|
.bytes()
|
||||||
|
.take_while(|b| matches!(b, b' ' | b'\t' | b'\n' | b'\x0B' | b'\x0C' | b'\r'))
|
||||||
|
.count();
|
||||||
|
let (ws, rest) = input.split_at(len);
|
||||||
|
*input = rest;
|
||||||
|
Ok(ws)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Same as [`multispace0`], but requiring at least one character
|
/// Same as [`multispace0`], but requiring at least one character
|
||||||
@@ -50,6 +56,11 @@ pub(super) fn space<'a, E>(input: &mut &'a str) -> winnow::Result<(), E>
|
|||||||
where
|
where
|
||||||
E: ParserError<&'a str>,
|
E: ParserError<&'a str>,
|
||||||
{
|
{
|
||||||
|
// Fast path: most calls see no comment or ignorable sign
|
||||||
|
multispace0.parse_next(input)?;
|
||||||
|
if !input.starts_with(['(', '-', '+']) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
separated(0.., multispace0, alt((comment, ignored_hyphen_or_plus))).parse_next(input)
|
separated(0.., multispace0, alt((comment, ignored_hyphen_or_plus))).parse_next(input)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,4 +230,27 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn space_skips_comments_and_ignored_signs() {
|
||||||
|
for (input, rest) in [
|
||||||
|
("", ""),
|
||||||
|
("abc", "abc"),
|
||||||
|
(" \t abc", "abc"),
|
||||||
|
("(foo) abc", "abc"),
|
||||||
|
(" (a (b)) abc", "abc"),
|
||||||
|
("- abc", "abc"),
|
||||||
|
("+ (c) abc", "abc"),
|
||||||
|
("- (c) 12", "12"),
|
||||||
|
// A sign followed by a digit is not ignored.
|
||||||
|
("- 12", "- 12"),
|
||||||
|
(" +12", "+12"),
|
||||||
|
// An unbalanced comment is not skipped.
|
||||||
|
("(foo abc", "(foo abc"),
|
||||||
|
] {
|
||||||
|
let mut s = input;
|
||||||
|
space::<ContextError>(&mut s).unwrap();
|
||||||
|
assert_eq!(s, rest, "{input:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+19
-7
@@ -59,9 +59,19 @@ impl TryFrom<Relative> for jiff::Span {
|
|||||||
Relative::Days(days) => jiff::Span::new().try_days(days),
|
Relative::Days(days) => jiff::Span::new().try_days(days),
|
||||||
Relative::Hours(hours) => jiff::Span::new().try_hours(hours),
|
Relative::Hours(hours) => jiff::Span::new().try_hours(hours),
|
||||||
Relative::Minutes(minutes) => jiff::Span::new().try_minutes(minutes),
|
Relative::Minutes(minutes) => jiff::Span::new().try_minutes(minutes),
|
||||||
Relative::Seconds(seconds, nanoseconds) => jiff::Span::new()
|
Relative::Seconds(seconds, nanoseconds) => {
|
||||||
.try_seconds(seconds)
|
// -0.25 is stored as (-1, 750_000_000), but a `jiff::Span` needs
|
||||||
.and_then(|span| span.try_nanoseconds(nanoseconds)),
|
// both fields with one sign, so move the fraction onto it.
|
||||||
|
let (seconds, nanoseconds) = if seconds < 0 && nanoseconds > 0 {
|
||||||
|
(seconds + 1, -i64::from(1_000_000_000 - nanoseconds))
|
||||||
|
} else {
|
||||||
|
(seconds, i64::from(nanoseconds))
|
||||||
|
};
|
||||||
|
|
||||||
|
jiff::Span::new()
|
||||||
|
.try_seconds(seconds)
|
||||||
|
.and_then(|span| span.try_nanoseconds(nanoseconds))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.map_err(|_| "relative value is invalid")
|
.map_err(|_| "relative value is invalid")
|
||||||
}
|
}
|
||||||
@@ -85,7 +95,7 @@ fn seconds(input: &mut &str) -> ModalResult<Relative> {
|
|||||||
opt(alt((s('+').value(1), s('-').value(-1)))),
|
opt(alt((s('+').value(1), s('-').value(-1)))),
|
||||||
s(sec_and_nsec),
|
s(sec_and_nsec),
|
||||||
s(alpha1).verify(|s: &str| matches!(s, "seconds" | "second" | "sec" | "secs")),
|
s(alpha1).verify(|s: &str| matches!(s, "seconds" | "second" | "sec" | "secs")),
|
||||||
ago,
|
direction,
|
||||||
)
|
)
|
||||||
.verify_map(|(sign, (sec, nsec), _, ago)| {
|
.verify_map(|(sign, (sec, nsec), _, ago)| {
|
||||||
let sec = i64::try_from(sec).ok()?;
|
let sec = i64::try_from(sec).ok()?;
|
||||||
@@ -102,7 +112,7 @@ fn seconds(input: &mut &str) -> ModalResult<Relative> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn displacement(input: &mut &str) -> ModalResult<Relative> {
|
fn displacement(input: &mut &str) -> ModalResult<Relative> {
|
||||||
(opt(ordinal), s(alpha1), ago)
|
(opt(ordinal), s(alpha1), direction)
|
||||||
.verify_map(|(n, unit, ago): (Option<i32>, &str, bool)| {
|
.verify_map(|(n, unit, ago): (Option<i32>, &str, bool)| {
|
||||||
let multiplier = n.unwrap_or(1) * if ago { -1 } else { 1 };
|
let multiplier = n.unwrap_or(1) * if ago { -1 } else { 1 };
|
||||||
Some(match unit.strip_suffix('s').unwrap_or(unit) {
|
Some(match unit.strip_suffix('s').unwrap_or(unit) {
|
||||||
@@ -120,8 +130,10 @@ fn displacement(input: &mut &str) -> ModalResult<Relative> {
|
|||||||
.parse_next(input)
|
.parse_next(input)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ago(input: &mut &str) -> ModalResult<bool> {
|
fn direction(input: &mut &str) -> ModalResult<bool> {
|
||||||
opt(s("ago")).map(|o| o.is_some()).parse_next(input)
|
opt(alt((s("ago").value(true), s("hence").value(false))))
|
||||||
|
.map(|backward| backward.unwrap_or(false))
|
||||||
|
.parse_next(input)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ pub(super) fn parse(input: &mut &str) -> ModalResult<Weekday> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn day(input: &mut &str) -> ModalResult<Day> {
|
fn day(input: &mut &str) -> ModalResult<Day> {
|
||||||
s(alpha1)
|
s((alpha1, opt('.')).take())
|
||||||
.verify_map(|s: &str| {
|
.verify_map(|s: &str| {
|
||||||
Some(match s {
|
Some(match s {
|
||||||
"monday" | "mon" | "mon." => Day::Monday,
|
"monday" | "mon" | "mon." => Day::Monday,
|
||||||
|
|||||||
+67
@@ -521,6 +521,49 @@ mod tests {
|
|||||||
"2023-03-05 00:00:00 000000000"
|
"2023-03-05 00:00:00 000000000"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_weekday_with_text_ordinal() {
|
||||||
|
let date = "2023-02-28 10:12:03"
|
||||||
|
.parse::<DateTime>()
|
||||||
|
.unwrap()
|
||||||
|
.to_zoned(TimeZone::system())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
get_formatted_date(&date, "eighth monday"),
|
||||||
|
"2023-04-24 00:00:00 000000000"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_weekday_abbreviation_with_period() {
|
||||||
|
let date = "2023-02-28 10:12:03"
|
||||||
|
.parse::<DateTime>()
|
||||||
|
.unwrap()
|
||||||
|
.to_zoned(TimeZone::system())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
for (weekday, expected) in [
|
||||||
|
("mon.", "2023-03-06 00:00:00 000000000"),
|
||||||
|
("tue.", "2023-02-28 00:00:00 000000000"),
|
||||||
|
("wed.", "2023-03-01 00:00:00 000000000"),
|
||||||
|
("thu.", "2023-03-02 00:00:00 000000000"),
|
||||||
|
("fri.", "2023-03-03 00:00:00 000000000"),
|
||||||
|
("sat.", "2023-03-04 00:00:00 000000000"),
|
||||||
|
("sun.", "2023-03-05 00:00:00 000000000"),
|
||||||
|
] {
|
||||||
|
assert_eq!(get_formatted_date(&date, weekday), expected, "{weekday}");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (input, expected) in [
|
||||||
|
("wed., 2024-01-03", "2024-01-03 00:00:00"),
|
||||||
|
("mon., 2024-01-03", "2024-01-03 00:00:00"),
|
||||||
|
] {
|
||||||
|
let actual = crate::parse_datetime(input).unwrap().expect_in_range();
|
||||||
|
assert_eq!(actual.strftime("%F %T").to_string(), expected, "{input}");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -784,6 +827,30 @@ mod tests {
|
|||||||
mod test_relative {
|
mod test_relative {
|
||||||
use crate::parse_datetime;
|
use crate::parse_datetime;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_hence_suffix() {
|
||||||
|
let now: jiff::Zoned = "2026-01-15T12:00:00Z[UTC]".parse().unwrap();
|
||||||
|
for relative in [
|
||||||
|
"2 months",
|
||||||
|
"year",
|
||||||
|
"-2 days",
|
||||||
|
"next week",
|
||||||
|
"0 hours",
|
||||||
|
"1.5 seconds",
|
||||||
|
] {
|
||||||
|
let expected = crate::parse_datetime_at_date(now.clone(), relative).unwrap();
|
||||||
|
for suffix in ["hence", "HENCE"] {
|
||||||
|
assert_eq!(
|
||||||
|
crate::parse_datetime_at_date(now.clone(), format!("{relative} {suffix}"))
|
||||||
|
.unwrap(),
|
||||||
|
expected,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert!(crate::parse_datetime_at_date(now.clone(), "hence").is_err());
|
||||||
|
assert!(crate::parse_datetime_at_date(now, "2 days henceforth").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_month() {
|
fn test_month() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|||||||
@@ -298,6 +298,19 @@ fn test_embedded_timezone(#[case] input: &str, #[case] expected: &str) {
|
|||||||
check_absolute(input, expected);
|
check_absolute(input, expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Zone abbreviations must resolve to the offsets GNU date assigns them,
|
||||||
|
// not to other real meanings of the same letters, e.g. BST as Bangladesh
|
||||||
|
// Standard Time rather than British Summer Time.
|
||||||
|
#[rstest]
|
||||||
|
#[case::bst("2026-06-15 12:00 BST", "2026-06-15 12:00:00+01:00")]
|
||||||
|
#[case::gst("2026-06-15 12:00 GST", "2026-06-15 12:00:00+10:00")]
|
||||||
|
#[case::ast("2026-06-15 12:00 AST", "2026-06-15 12:00:00-04:00")]
|
||||||
|
#[case::adt("2026-06-15 12:00 ADT", "2026-06-15 12:00:00-03:00")]
|
||||||
|
#[case::sst("2026-06-15 12:00 SST", "2026-06-15 12:00:00-12:00")]
|
||||||
|
fn test_zone_abbreviation_offsets(#[case] input: &str, #[case] expected: &str) {
|
||||||
|
check_absolute(input, expected);
|
||||||
|
}
|
||||||
|
|
||||||
// Regression test for uutils/coreutils#12555.
|
// Regression test for uutils/coreutils#12555.
|
||||||
// A fixed offset (e.g. the "UTC" keyword) must anchor the instant *before*
|
// A fixed offset (e.g. the "UTC" keyword) must anchor the instant *before*
|
||||||
// relative adjustments are applied. Otherwise, when the base zone observes DST,
|
// relative adjustments are applied. Otherwise, when the base zone observes DST,
|
||||||
@@ -434,3 +447,48 @@ fn test_zone_item_keeps_time_of_day_with_relative(#[case] input: &str, #[case] e
|
|||||||
"`{input}` should resolve to {expected} in its own zone"
|
"`{input}` should resolve to {expected} in its own zone"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A weekday is ignored when an explicit date is given, as in GNU date
|
||||||
|
// (checked against coreutils 9.12, base Thursday 2026-10-01).
|
||||||
|
#[rstest]
|
||||||
|
#[case::mismatching_named_month("Wednesday August 17 2026", "2026-08-17 00:00:00")]
|
||||||
|
#[case::matching_named_month("Monday August 17 2026", "2026-08-17 00:00:00")]
|
||||||
|
#[case::abbreviated_with_comma("Wed, Aug 17 2026", "2026-08-17 00:00:00")]
|
||||||
|
#[case::day_month_with_comma("Wednesday, 17 August 2026", "2026-08-17 00:00:00")]
|
||||||
|
#[case::named_month_without_year("Wed Aug 17", "2026-08-17 00:00:00")]
|
||||||
|
#[case::iso_date("Wed 2026-08-17", "2026-08-17 00:00:00")]
|
||||||
|
#[case::iso_date_then_weekday("2026-08-17 Wed", "2026-08-17 00:00:00")]
|
||||||
|
#[case::us_date("Wed 08/17/2026", "2026-08-17 00:00:00")]
|
||||||
|
#[case::date_and_time("Wed 2026-08-17 10:30", "2026-08-17 10:30:00")]
|
||||||
|
#[case::time_before_date("Wed 10:30 2026-08-17", "2026-08-17 10:30:00")]
|
||||||
|
#[case::iso_datetime("2026-08-17T10:30:00 Wed", "2026-08-17 10:30:00")]
|
||||||
|
#[case::next_weekday("next Friday Sep 25 2026", "2026-09-25 00:00:00")]
|
||||||
|
#[case::next_other_weekday("next Sunday Sep 25 2026", "2026-09-25 00:00:00")]
|
||||||
|
#[case::last_weekday("last Wed 2026-08-17", "2026-08-17 00:00:00")]
|
||||||
|
#[case::ordinal_weekday("third Wed 2026-08-17", "2026-08-17 00:00:00")]
|
||||||
|
#[case::numeric_ordinal_weekday("2 Wed 2026-08-17", "2026-08-17 00:00:00")]
|
||||||
|
#[case::date_output_format("Sun Sep 25 12:00:00 AM UTC 2026", "2026-09-25 00:00:00")]
|
||||||
|
// Relative items still apply on top of the given date.
|
||||||
|
#[case::relative_day("Wed 2026-08-17 +1 day", "2026-08-18 00:00:00")]
|
||||||
|
#[case::relative_week("Wed 2026-08-17 next week", "2026-08-24 00:00:00")]
|
||||||
|
// Without a date, the weekday is still honoured.
|
||||||
|
#[case::weekday_alone("Wed", "2026-10-07 00:00:00")]
|
||||||
|
#[case::weekday_and_time("Wed 10:30", "2026-10-07 10:30:00")]
|
||||||
|
#[case::weekday_and_relative("Wed +1 day", "2026-10-08 00:00:00")]
|
||||||
|
fn test_weekday_ignored_with_explicit_date(#[case] input: &str, #[case] expected: &str) {
|
||||||
|
let base = "2026-10-01 12:00:00"
|
||||||
|
.parse::<DateTime>()
|
||||||
|
.unwrap()
|
||||||
|
.to_zoned(TimeZone::UTC)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let parsed = parse_datetime::parse_datetime_at_date(base, input)
|
||||||
|
.unwrap()
|
||||||
|
.expect_in_range();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
parsed.datetime().to_string(),
|
||||||
|
expected.parse::<DateTime>().unwrap().to_string(),
|
||||||
|
"`{input}` should resolve to {expected}"
|
||||||
|
);
|
||||||
|
}
|
||||||
+37
-1
@@ -5,7 +5,7 @@ use jiff::{civil::DateTime, tz::TimeZone, Zoned};
|
|||||||
use parse_datetime::parse_datetime_at_date;
|
use parse_datetime::parse_datetime_at_date;
|
||||||
use rstest::rstest;
|
use rstest::rstest;
|
||||||
|
|
||||||
// The expected values are produced by GNU date version 8.32
|
// The expected values are produced by GNU date version 9.11
|
||||||
// export LC_TIME=en_US.UTF-8
|
// export LC_TIME=en_US.UTF-8
|
||||||
// export TZ=UTC
|
// export TZ=UTC
|
||||||
// date date --date="12:34:56+09:00" +"%H:%M:%S.%N"
|
// date date --date="12:34:56+09:00" +"%H:%M:%S.%N"
|
||||||
@@ -280,3 +280,39 @@ fn test_time_seconds_ago_invalid(#[case] input: &str) {
|
|||||||
"Input string '{input}' did not produce an error when parsing"
|
"Input string '{input}' did not produce an error when parsing"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fractional relative seconds, checked with
|
||||||
|
// TZ=UTC date --date="2026-08-27 12:00:00 +0000 <input>" +"%H:%M:%S.%N"
|
||||||
|
#[rstest]
|
||||||
|
#[case::plus_half("+0.5 sec", "12:00:00.500000000")]
|
||||||
|
#[case::minus_half("-0.5 sec", "11:59:59.500000000")]
|
||||||
|
#[case::plus_one_and_a_half("+1.5 sec", "12:00:01.500000000")]
|
||||||
|
#[case::minus_one_and_a_half("-1.5 sec", "11:59:58.500000000")]
|
||||||
|
#[case::plus_quarter("+0.25 sec", "12:00:00.250000000")]
|
||||||
|
#[case::minus_quarter("-0.25 sec", "11:59:59.750000000")]
|
||||||
|
#[case::plus_one_and_three_quarters("+1.75 sec", "12:00:01.750000000")]
|
||||||
|
#[case::minus_one_and_three_quarters("-1.75 sec", "11:59:58.250000000")]
|
||||||
|
#[case::plus_two_and_an_eighth("+2.125 sec", "12:00:02.125000000")]
|
||||||
|
#[case::minus_two_and_an_eighth("-2.125 sec", "11:59:57.875000000")]
|
||||||
|
#[case::minus_nine_nanoseconds("-0.000000009 sec", "11:59:59.999999991")]
|
||||||
|
#[case::minus_almost_one("-0.999999999 sec", "11:59:59.000000001")]
|
||||||
|
#[case::plus_half_ago("+0.5 sec ago", "11:59:59.500000000")]
|
||||||
|
#[case::minus_half_ago("-0.5 sec ago", "12:00:00.500000000")]
|
||||||
|
#[case::plus_one_and_a_half_ago("+1.5 sec ago", "11:59:58.500000000")]
|
||||||
|
#[case::unsigned_half_ago("0.5 sec ago", "11:59:59.500000000")]
|
||||||
|
#[case::unsigned_half("0.5 sec", "12:00:00.500000000")]
|
||||||
|
#[case::plus_zero_fraction("+0.0 sec", "12:00:00.000000000")]
|
||||||
|
#[case::minus_zero_fraction("-0.0 sec", "12:00:00.000000000")]
|
||||||
|
#[case::minus_whole_with_fraction_zero("-1.0 sec", "11:59:59.000000000")]
|
||||||
|
#[case::minus_whole("-1 second ago", "12:00:01.000000000")]
|
||||||
|
#[case::plural_minus_half("-0.5 seconds", "11:59:59.500000000")]
|
||||||
|
#[case::abbreviated_minus_half("-0.5 secs", "11:59:59.500000000")]
|
||||||
|
fn test_relative_fractional_seconds(#[case] input: &str, #[case] expected: &str) {
|
||||||
|
let base = "2026-08-27 12:00:00"
|
||||||
|
.parse::<DateTime>()
|
||||||
|
.unwrap()
|
||||||
|
.to_zoned(TimeZone::UTC)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
check_time(input, expected, "%H:%M:%S.%N", Some(base));
|
||||||
|
}
|
||||||
Reference in new issue
Block a user