You've already forked parse_datetime
mirror of
https://github.com/uutils/parse_datetime.git
synced 2026-06-10 16:13:15 -07:00
Compare commits
60 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5662a1b47f | |||
| f75ec46d63 | |||
| 2dc4fa3ffb | |||
| 69649bf6c6 | |||
| 583eafe62c | |||
| 6815ba1406 | |||
| a4c29bafcb | |||
| e70d066e03 | |||
| e5f9894b11 | |||
| 1f4ebd93c6 | |||
| 74dd77db53 | |||
| 6f15b90e36 | |||
| 4db8bf4b22 | |||
| 84d27e6386 | |||
| a0f1376d2f | |||
| 59eac72c44 | |||
| d6bde134df | |||
| 43842ebf8e | |||
| c31711d351 | |||
| ea5d61b3eb | |||
| c55bea1d8b | |||
| 6baf7e301d | |||
| 795d7e6c43 | |||
| 3f4bd53eba | |||
| ec30e0e1f2 | |||
| 70055eaa21 | |||
| 871cae962e | |||
| 91692a17e0 | |||
| e225cf0557 | |||
| 8e2816d187 | |||
| 84d4e294df | |||
| c87e103745 | |||
| 7d0c3e4500 | |||
| ae25053473 | |||
| 17847a8d9c | |||
| 6fce02949d | |||
| cc5029c177 | |||
| 1b96dd5707 | |||
| b5439d8cc2 | |||
| d2e6e7819d | |||
| a8e26ab539 | |||
| 920cfa5f1b | |||
| a79ce1fc31 | |||
| c87b6f19eb | |||
| 25f9e48e55 | |||
| 14b8a9447c | |||
| 692f17526d | |||
| 5c686bcfdf | |||
| c86b137e6f | |||
| f075ba8d8d | |||
| 5ca5586523 | |||
| 2bc18c52b7 | |||
| 3eb58ab00a | |||
| 628011ae97 | |||
| 42d09dadf7 | |||
| 908356f3a3 | |||
| 65965a85d1 | |||
| 551985ef91 | |||
| 8fa8928a6a | |||
| fa90303836 |
@@ -14,7 +14,7 @@ jobs:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macOS-latest, windows-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- 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@v4
|
||||
- uses: actions/checkout@v5
|
||||
- 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@v4
|
||||
- uses: actions/checkout@v5
|
||||
- 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@v4
|
||||
- uses: actions/checkout@v5
|
||||
- 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@v4
|
||||
- uses: actions/checkout@v5
|
||||
- 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@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: Initialize workflow variables
|
||||
id: vars
|
||||
shell: bash
|
||||
@@ -152,7 +152,7 @@ jobs:
|
||||
env:
|
||||
RUN_FOR: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- uses: dtolnay/rust-toolchain@nightly
|
||||
- name: Install `cargo-fuzz`
|
||||
run: cargo install cargo-fuzz
|
||||
|
||||
Generated
+145
-210
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "parse_datetime"
|
||||
description = "parsing human-readable time strings and converting them to a DateTime"
|
||||
version = "0.11.0"
|
||||
version = "0.12.0"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/uutils/parse_datetime"
|
||||
@@ -10,9 +10,9 @@ rust-version = "1.71.1"
|
||||
|
||||
[dependencies]
|
||||
regex = "1.10.4"
|
||||
chrono = { version="0.4.38", default-features=false, features=["std", "alloc", "clock"] }
|
||||
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"] }
|
||||
|
||||
[dev-dependencies]
|
||||
rstest = "0.26"
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
[](https://github.com/uutils/parse_datetime/blob/main/LICENSE)
|
||||
[](https://codecov.io/gh/uutils/parse_datetime)
|
||||
|
||||
A Rust crate for parsing human-readable relative time strings and human-readable datetime strings and converting them to a `DateTime`.
|
||||
A Rust crate for parsing human-readable relative time strings and human-readable datetime strings and converting them to a jiff's `Zoned` object.
|
||||
|
||||
## Features
|
||||
|
||||
@@ -12,7 +12,7 @@ A Rust crate for parsing human-readable relative time strings and human-readable
|
||||
- Supports positive and negative durations.
|
||||
- Allows for chaining time units (e.g., "1 hour 2 minutes" or "2 days 2 hours ago").
|
||||
- Calculate durations relative to a specified date.
|
||||
- Relies on Chrono
|
||||
- Relies on Jiff
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -25,26 +25,26 @@ cargo add parse_datetime
|
||||
Then, import the crate and use the `parse_datetime_at_date` function:
|
||||
|
||||
```rs
|
||||
use chrono::{Duration, Local};
|
||||
use jiff::{ToSpan, Zoned};
|
||||
use parse_datetime::parse_datetime_at_date;
|
||||
|
||||
let now = Local::now();
|
||||
let after = parse_datetime_at_date(now, "+3 days");
|
||||
let now = Zoned::now();
|
||||
let after = parse_datetime_at_date(now.clone(), "+3 days");
|
||||
|
||||
assert_eq!(
|
||||
(now + Duration::days(3)).naive_utc(),
|
||||
after.unwrap().naive_utc()
|
||||
now.checked_add(3.days()).unwrap(),
|
||||
after.unwrap()
|
||||
);
|
||||
```
|
||||
|
||||
For DateTime parsing, import the `parse_datetime` function:
|
||||
|
||||
```rs
|
||||
use jiff::{civil::{date, time} ,Zoned};
|
||||
use parse_datetime::parse_datetime;
|
||||
use chrono::{Local, TimeZone};
|
||||
|
||||
let dt = parse_datetime("2021-02-14 06:37:47");
|
||||
assert_eq!(dt.unwrap(), Local.with_ymd_and_hms(2021, 2, 14, 6, 37, 47).unwrap());
|
||||
assert_eq!(dt.unwrap(), Zoned::now().with().date(date(2021, 2, 14)).time(time(6, 37, 47, 0)).build().unwrap());
|
||||
```
|
||||
|
||||
### Supported Formats
|
||||
@@ -69,7 +69,7 @@ The `parse_datetime` and `parse_datetime_at_date` functions support absolute dat
|
||||
|
||||
The `parse_datetime` and `parse_datetime_at_date` function return:
|
||||
|
||||
- `Ok(DateTime<FixedOffset>)` - If the input string can be parsed as a datetime
|
||||
- `Ok(Zoned)` - If the input string can be parsed as a `Zoned` object
|
||||
- `Err(ParseDateTimeError::InvalidInput)` - If the input string cannot be parsed
|
||||
|
||||
## Fuzzer
|
||||
|
||||
Generated
+153
-158
@@ -4,52 +4,32 @@ version = 4
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.0.1"
|
||||
version = "1.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04"
|
||||
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "android-tzdata"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
|
||||
|
||||
[[package]]
|
||||
name = "android_system_properties"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arbitrary"
|
||||
version = "1.3.0"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2d098ff73c1ca148721f37baad5ea6a465a13f9573aba8641fbbbae8164a54e"
|
||||
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.1.0"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1"
|
||||
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.10"
|
||||
version = "1.2.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13208fcbb66eaeffe09b99fffbe1af420f00a7b35aa99ad683dfc1aa76145229"
|
||||
checksum = "65193589c6404eb80b450d618eaf9a2cafaaafd57ecce47370519ef674a7bd44"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"jobserver",
|
||||
"libc",
|
||||
"shlex",
|
||||
@@ -57,84 +37,92 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9"
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.41"
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d"
|
||||
dependencies = [
|
||||
"android-tzdata",
|
||||
"iana-time-zone",
|
||||
"num-traits",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
|
||||
checksum = "7fd99930f64d146689264c637b5af2f0233a933bef0d8570e2526bf9e083192d"
|
||||
|
||||
[[package]]
|
||||
name = "fuzz_parse_datetime"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"libfuzzer-sys",
|
||||
"parse_datetime",
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.61"
|
||||
name = "getrandom"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220"
|
||||
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"core-foundation-sys",
|
||||
"iana-time-zone-haiku",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
"windows-core",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone-haiku"
|
||||
version = "0.1.2"
|
||||
name = "jiff"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||
checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"jiff-static",
|
||||
"jiff-tzdb-platform",
|
||||
"log",
|
||||
"portable-atomic",
|
||||
"portable-atomic-util",
|
||||
"serde",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jiff-static"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jiff-tzdb"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1283705eb0a21404d2bfd6eef2a7593d240bc42a0bdb39db0ad6fa2ec026524"
|
||||
|
||||
[[package]]
|
||||
name = "jiff-tzdb-platform"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8"
|
||||
dependencies = [
|
||||
"jiff-tzdb",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jobserver"
|
||||
version = "0.1.32"
|
||||
version = "0.1.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0"
|
||||
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.70"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a"
|
||||
dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.142"
|
||||
version = "0.2.175"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a987beff54b60ffa6d51982e1aa1146bc42f19bd26be28b0586f252fccf5317"
|
||||
checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
|
||||
|
||||
[[package]]
|
||||
name = "libfuzzer-sys"
|
||||
@@ -148,15 +136,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.18"
|
||||
version = "0.4.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "518ef76f2f87365916b142844c16d8fefd85039bc5699050210a7778ee1cd1de"
|
||||
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.6.2"
|
||||
version = "2.7.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5486aed0026218e61b8a01d5fbd5a0a134649abb71a0e53b7bc088529dced86e"
|
||||
checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
@@ -167,45 +155,60 @@ dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3"
|
||||
|
||||
[[package]]
|
||||
name = "parse_datetime"
|
||||
version = "0.10.0"
|
||||
version = "0.11.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"jiff",
|
||||
"num-traits",
|
||||
"regex",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.59"
|
||||
name = "portable-atomic"
|
||||
version = "1.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6aeca18b86b413c660b781aa319e4e2648a3e6f9eadc9b47e9038e6fe9f3451b"
|
||||
checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483"
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic-util"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507"
|
||||
dependencies = [
|
||||
"portable-atomic",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.101"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.28"
|
||||
version = "1.0.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488"
|
||||
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.11.1"
|
||||
name = "r-efi"
|
||||
version = "5.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
|
||||
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",
|
||||
@@ -215,9 +218,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.8"
|
||||
version = "0.4.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3"
|
||||
checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
@@ -226,9 +229,38 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.5"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
||||
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"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c459bc0a14c840cb403fc14b148620de1e0778c96ecd6e0c8c3cacb6d8d00fe"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.221"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6185cf75117e20e62b1ff867b9518577271e58abe0037c40bb4794969355ab0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
@@ -238,9 +270,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.18"
|
||||
version = "2.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e"
|
||||
checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -249,80 +281,37 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.9"
|
||||
version = "1.0.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0"
|
||||
checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.93"
|
||||
name = "wasi"
|
||||
version = "0.14.5+wasi-0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5"
|
||||
checksum = "a4494f6290a82f5fe584817a676a34b9d6763e8d9d18204009fb31dceca98fd4"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"wasm-bindgen-macro",
|
||||
"wasip2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-backend"
|
||||
version = "0.2.93"
|
||||
name = "wasip2"
|
||||
version = "1.0.0+wasi-0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b"
|
||||
checksum = "03fa2761397e5bd52002cd7e73110c71af2109aca4e521a9f40473fe685b0a24"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"log",
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-shared",
|
||||
"wit-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.93"
|
||||
name = "windows-sys"
|
||||
version = "0.59.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.93"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.93"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484"
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
|
||||
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3"
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.6"
|
||||
@@ -389,9 +378,15 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.7.10"
|
||||
version = "0.7.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec"
|
||||
checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.45.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c573471f125075647d03df72e026074b7203790d41351cd6edc96f46bcccd36"
|
||||
|
||||
@@ -8,8 +8,6 @@ cargo-fuzz = true
|
||||
|
||||
[dependencies]
|
||||
libfuzzer-sys = "0.4.7"
|
||||
regex = "1.10.4"
|
||||
chrono = { version="0.4", default-features=false, features=["std", "alloc", "clock"] }
|
||||
|
||||
[dependencies.parse_datetime]
|
||||
path = "../"
|
||||
|
||||
+155
-202
@@ -1,9 +1,9 @@
|
||||
// For the full copyright and license information, please view the LICENSE
|
||||
// file that was distributed with this source code.
|
||||
|
||||
use chrono::{DateTime, Datelike, FixedOffset, NaiveDate, TimeZone, Timelike};
|
||||
use jiff::{civil, Span, Zoned};
|
||||
|
||||
use super::{date, relative, time, weekday};
|
||||
use super::{date, epoch, error, relative, time, timezone, weekday, year, Item};
|
||||
|
||||
/// The builder is used to construct a DateTime object from various components.
|
||||
/// The parser creates a `DateTimeBuilder` object with the parsed components,
|
||||
@@ -11,13 +11,13 @@ use super::{date, relative, time, weekday};
|
||||
/// date and time using the `set_base()` method before calling `build()`, or
|
||||
/// leave it unset to use the current date and time as the base.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct DateTimeBuilder {
|
||||
base: Option<DateTime<FixedOffset>>,
|
||||
timestamp: Option<f64>,
|
||||
pub(crate) struct DateTimeBuilder {
|
||||
base: Option<Zoned>,
|
||||
timestamp: Option<epoch::Timestamp>,
|
||||
date: Option<date::Date>,
|
||||
time: Option<time::Time>,
|
||||
weekday: Option<weekday::Weekday>,
|
||||
timezone: Option<time::Offset>,
|
||||
timezone: Option<timezone::Offset>,
|
||||
relative: Vec<relative::Relative>,
|
||||
}
|
||||
|
||||
@@ -28,83 +28,133 @@ impl DateTimeBuilder {
|
||||
|
||||
/// Sets the base date and time for the builder. If not set, the current
|
||||
/// date and time will be used.
|
||||
pub(super) fn set_base(mut self, base: DateTime<FixedOffset>) -> Self {
|
||||
pub(super) fn set_base(mut self, base: Zoned) -> Self {
|
||||
self.base = Some(base);
|
||||
self
|
||||
}
|
||||
|
||||
/// Timestamp value is exclusive to other date/time components. Caller of
|
||||
/// the builder must ensure that it is not combined with other items.
|
||||
pub(super) fn set_timestamp(mut self, ts: f64) -> Result<Self, &'static str> {
|
||||
/// Sets a timestamp value. Timestamp values are exclusive to other date/time
|
||||
/// items (date, time, weekday, timezone, relative adjustments).
|
||||
pub(super) fn set_timestamp(mut self, ts: epoch::Timestamp) -> Result<Self, &'static str> {
|
||||
if self.timestamp.is_some() {
|
||||
return Err("timestamp cannot appear more than once");
|
||||
} else if self.date.is_some()
|
||||
|| self.time.is_some()
|
||||
|| self.weekday.is_some()
|
||||
|| self.timezone.is_some()
|
||||
|| !self.relative.is_empty()
|
||||
{
|
||||
return Err("timestamp cannot be combined with other date/time items");
|
||||
}
|
||||
|
||||
self.timestamp = Some(ts);
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub(super) fn set_year(mut self, year: u32) -> Result<Self, &'static str> {
|
||||
if let Some(date) = self.date.as_mut() {
|
||||
if date.year.is_some() {
|
||||
Err("year cannot appear more than once")
|
||||
} else {
|
||||
date.year = Some(year);
|
||||
Ok(self)
|
||||
}
|
||||
} else {
|
||||
self.date = Some(date::Date {
|
||||
day: 1,
|
||||
month: 1,
|
||||
year: Some(year),
|
||||
});
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn set_date(mut self, date: date::Date) -> Result<Self, &'static str> {
|
||||
if self.date.is_some() || self.timestamp.is_some() {
|
||||
Err("date cannot appear more than once")
|
||||
} else {
|
||||
self.date = Some(date);
|
||||
Ok(self)
|
||||
if self.timestamp.is_some() {
|
||||
return Err("timestamp cannot be combined with other date/time items");
|
||||
} else if self.date.is_some() {
|
||||
return Err("date cannot appear more than once");
|
||||
}
|
||||
|
||||
self.date = Some(date);
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub(super) fn set_time(mut self, time: time::Time) -> Result<Self, &'static str> {
|
||||
if self.time.is_some() || self.timestamp.is_some() {
|
||||
Err("time cannot appear more than once")
|
||||
if self.timestamp.is_some() {
|
||||
return Err("timestamp cannot be combined with other date/time items");
|
||||
} else if self.time.is_some() {
|
||||
return Err("time cannot appear more than once");
|
||||
} else if self.timezone.is_some() && time.offset.is_some() {
|
||||
Err("time offset and timezone are mutually exclusive")
|
||||
} else {
|
||||
self.time = Some(time);
|
||||
Ok(self)
|
||||
return Err("time offset and timezone are mutually exclusive");
|
||||
}
|
||||
|
||||
self.time = Some(time);
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub(super) fn set_weekday(mut self, weekday: weekday::Weekday) -> Result<Self, &'static str> {
|
||||
if self.weekday.is_some() {
|
||||
Err("weekday cannot appear more than once")
|
||||
} else {
|
||||
self.weekday = Some(weekday);
|
||||
Ok(self)
|
||||
if self.timestamp.is_some() {
|
||||
return Err("timestamp cannot be combined with other date/time items");
|
||||
} else if self.weekday.is_some() {
|
||||
return Err("weekday cannot appear more than once");
|
||||
}
|
||||
|
||||
self.weekday = Some(weekday);
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub(super) fn set_timezone(mut self, timezone: time::Offset) -> Result<Self, &'static str> {
|
||||
if self.timezone.is_some() {
|
||||
Err("timezone cannot appear more than once")
|
||||
pub(super) fn set_timezone(mut self, timezone: timezone::Offset) -> Result<Self, &'static str> {
|
||||
if self.timestamp.is_some() {
|
||||
return Err("timestamp cannot be combined with other date/time items");
|
||||
} else if self.timezone.is_some() {
|
||||
return Err("timezone cannot appear more than once");
|
||||
} else if self.time.as_ref().and_then(|t| t.offset.as_ref()).is_some() {
|
||||
Err("time offset and timezone are mutually exclusive")
|
||||
} else {
|
||||
self.timezone = Some(timezone);
|
||||
Ok(self)
|
||||
return Err("time offset and timezone are mutually exclusive");
|
||||
}
|
||||
|
||||
self.timezone = Some(timezone);
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub(super) fn push_relative(mut self, relative: relative::Relative) -> Self {
|
||||
pub(super) fn push_relative(
|
||||
mut self,
|
||||
relative: relative::Relative,
|
||||
) -> Result<Self, &'static str> {
|
||||
if self.timestamp.is_some() {
|
||||
return Err("timestamp cannot be combined with other date/time items");
|
||||
}
|
||||
|
||||
self.relative.push(relative);
|
||||
self
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub(super) fn build(self) -> Option<DateTime<FixedOffset>> {
|
||||
let base = self.base.unwrap_or_else(|| chrono::Local::now().into());
|
||||
/// Sets a pure number that can be interpreted as either a year or time
|
||||
/// depending on the current state of the builder.
|
||||
///
|
||||
/// If a date is already set but lacks a year, the number is interpreted as
|
||||
/// a year. Otherwise, it's interpreted as a time in HHMM, HMM, HH, or H
|
||||
/// format.
|
||||
pub(super) fn set_pure(mut self, pure: String) -> Result<Self, &'static str> {
|
||||
if self.timestamp.is_some() {
|
||||
return Err("timestamp cannot be combined with other date/time items");
|
||||
}
|
||||
|
||||
if let Some(date) = self.date.as_mut() {
|
||||
if date.year.is_none() {
|
||||
date.year = Some(year::year_from_str(&pure)?);
|
||||
return Ok(self);
|
||||
}
|
||||
}
|
||||
|
||||
let (mut hour_str, mut minute_str) = match pure.len() {
|
||||
1..=2 => (pure.as_str(), "0"),
|
||||
3..=4 => pure.split_at(pure.len() - 2),
|
||||
_ => {
|
||||
return Err("pure number must be 1-4 digits when interpreted as time");
|
||||
}
|
||||
};
|
||||
|
||||
let hour = time::hour24(&mut hour_str).map_err(|_| "invalid hour in pure number")?;
|
||||
let minute = time::minute(&mut minute_str).map_err(|_| "invalid minute in pure number")?;
|
||||
|
||||
let time = time::Time {
|
||||
hour,
|
||||
minute,
|
||||
..Default::default()
|
||||
};
|
||||
self.set_time(time)
|
||||
}
|
||||
|
||||
pub(super) fn build(self) -> Result<Zoned, error::Error> {
|
||||
let base = self.base.unwrap_or(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.
|
||||
@@ -116,79 +166,30 @@ impl DateTimeBuilder {
|
||||
{
|
||||
base
|
||||
} else {
|
||||
new_date(
|
||||
base.year(),
|
||||
base.month(),
|
||||
base.day(),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
*base.offset(),
|
||||
)?
|
||||
base.with().time(civil::time(0, 0, 0, 0)).build()?
|
||||
};
|
||||
|
||||
if let Some(ts) = self.timestamp {
|
||||
// TODO: How to make the fract -> nanosecond conversion more precise?
|
||||
// Maybe considering using the
|
||||
// [rust_decimal](https://crates.io/crates/rust_decimal) crate?
|
||||
match chrono::Utc.timestamp_opt(ts as i64, (ts.fract() * 10f64.powi(9)).round() as u32)
|
||||
{
|
||||
chrono::MappedLocalTime::Single(t) => {
|
||||
// If the timestamp is valid, we can use it directly.
|
||||
dt = t.with_timezone(&dt.timezone());
|
||||
}
|
||||
chrono::MappedLocalTime::Ambiguous(earliest, _latest) => {
|
||||
// TODO: When there is a fold in the local time, which value
|
||||
// do we choose? For now, we use the earliest one.
|
||||
dt = earliest.with_timezone(&dt.timezone());
|
||||
}
|
||||
chrono::MappedLocalTime::None => {
|
||||
return None; // Invalid timestamp
|
||||
}
|
||||
if let Some(date) = self.date {
|
||||
let d: civil::Date = if date.year.is_some() {
|
||||
date.try_into()?
|
||||
} else {
|
||||
date.with_year(dt.date().year() as u16).try_into()?
|
||||
};
|
||||
dt = dt.with().date(d).build()?;
|
||||
}
|
||||
|
||||
if let Some(time) = self.time.clone() {
|
||||
if let Some(offset) = &time.offset {
|
||||
dt = dt.datetime().to_zoned(offset.try_into()?)?;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(date::Date { year, month, day }) = self.date {
|
||||
dt = new_date(
|
||||
year.map(|x| x as i32).unwrap_or(dt.year()),
|
||||
month,
|
||||
day,
|
||||
dt.hour(),
|
||||
dt.minute(),
|
||||
dt.second(),
|
||||
dt.nanosecond(),
|
||||
*dt.offset(),
|
||||
)?;
|
||||
}
|
||||
|
||||
if let Some(time::Time {
|
||||
hour,
|
||||
minute,
|
||||
second,
|
||||
ref offset,
|
||||
}) = self.time
|
||||
{
|
||||
let offset = offset
|
||||
.clone()
|
||||
.and_then(|o| chrono::FixedOffset::try_from(o).ok())
|
||||
.unwrap_or(*dt.offset());
|
||||
|
||||
dt = new_date(
|
||||
dt.year(),
|
||||
dt.month(),
|
||||
dt.day(),
|
||||
hour,
|
||||
minute,
|
||||
second as u32,
|
||||
(second.fract() * 10f64.powi(9)).round() as u32,
|
||||
offset,
|
||||
)?;
|
||||
let t: civil::Time = time.try_into()?;
|
||||
dt = dt.with().time(t).build()?;
|
||||
}
|
||||
|
||||
if let Some(weekday::Weekday { offset, day }) = self.weekday {
|
||||
if self.time.is_none() {
|
||||
dt = new_date(dt.year(), dt.month(), dt.day(), 0, 0, 0, 0, *dt.offset())?;
|
||||
dt = dt.with().time(civil::time(0, 0, 0, 0)).build()?;
|
||||
}
|
||||
|
||||
let mut offset = offset;
|
||||
@@ -200,7 +201,7 @@ impl DateTimeBuilder {
|
||||
// Consider this:
|
||||
// Assuming today is Monday, next Friday is actually THIS Friday;
|
||||
// but next Monday is indeed NEXT Monday.
|
||||
if dt.weekday() != day && offset > 0 {
|
||||
if dt.date().weekday() != day && offset > 0 {
|
||||
offset -= 1;
|
||||
}
|
||||
|
||||
@@ -220,101 +221,53 @@ impl DateTimeBuilder {
|
||||
//
|
||||
// Example 4: next Thursday (x = 1, day = Thursday)
|
||||
// delta = (3 - 3) % 7 + (1) * 7 = 7
|
||||
let delta = (day.num_days_from_monday() as i32
|
||||
- dt.weekday().num_days_from_monday() as i32)
|
||||
let delta = (day.since(civil::Weekday::Monday) as i32
|
||||
- dt.date().weekday().since(civil::Weekday::Monday) as i32)
|
||||
.rem_euclid(7)
|
||||
+ offset.checked_mul(7)?;
|
||||
+ offset.checked_mul(7).ok_or("multiplication overflow")?;
|
||||
|
||||
dt = if delta < 0 {
|
||||
dt.checked_sub_days(chrono::Days::new((-delta) as u64))?
|
||||
} else {
|
||||
dt.checked_add_days(chrono::Days::new(delta as u64))?
|
||||
}
|
||||
dt = dt.checked_add(Span::new().try_days(delta)?)?;
|
||||
}
|
||||
|
||||
for rel in self.relative {
|
||||
// TODO: Handle potential overflows in the addition operations.
|
||||
match rel {
|
||||
relative::Relative::Years(x) => {
|
||||
dt = dt.with_year(dt.year() + x)?;
|
||||
}
|
||||
relative::Relative::Months(x) => {
|
||||
// *NOTE* This is done in this way to conform to
|
||||
// GNU behavior.
|
||||
let days = last_day_of_month(dt.year(), dt.month());
|
||||
if x >= 0 {
|
||||
dt += dt
|
||||
.date_naive()
|
||||
.checked_add_days(chrono::Days::new((days * x as u32) as u64))?
|
||||
.signed_duration_since(dt.date_naive());
|
||||
} else {
|
||||
dt += dt
|
||||
.date_naive()
|
||||
.checked_sub_days(chrono::Days::new((days * -x as u32) as u64))?
|
||||
.signed_duration_since(dt.date_naive());
|
||||
}
|
||||
}
|
||||
relative::Relative::Days(x) => dt += chrono::Duration::days(x.into()),
|
||||
relative::Relative::Hours(x) => dt += chrono::Duration::hours(x.into()),
|
||||
relative::Relative::Minutes(x) => {
|
||||
dt += chrono::Duration::try_minutes(x.into())?;
|
||||
}
|
||||
// Seconds are special because they can be given as a float.
|
||||
relative::Relative::Seconds(x) => {
|
||||
dt += chrono::Duration::try_seconds(x as i64)?;
|
||||
}
|
||||
}
|
||||
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()?
|
||||
})?;
|
||||
}
|
||||
|
||||
if let Some(offset) = self.timezone {
|
||||
dt = with_timezone_restore(offset, dt)?;
|
||||
let (offset, hour_adjustment) = offset.normalize();
|
||||
dt = dt.checked_add(Span::new().hours(hour_adjustment))?;
|
||||
dt = dt.datetime().to_zoned((&offset).try_into()?)?;
|
||||
}
|
||||
|
||||
Some(dt)
|
||||
Ok(dt)
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn new_date(
|
||||
year: i32,
|
||||
month: u32,
|
||||
day: u32,
|
||||
hour: u32,
|
||||
minute: u32,
|
||||
second: u32,
|
||||
nano: u32,
|
||||
offset: FixedOffset,
|
||||
) -> Option<DateTime<FixedOffset>> {
|
||||
let newdate = NaiveDate::from_ymd_opt(year, month, day)
|
||||
.and_then(|naive| naive.and_hms_nano_opt(hour, minute, second, nano))?;
|
||||
impl TryFrom<Vec<Item>> for DateTimeBuilder {
|
||||
type Error = &'static str;
|
||||
|
||||
Some(DateTime::<FixedOffset>::from_local(newdate, offset))
|
||||
}
|
||||
fn try_from(items: Vec<Item>) -> Result<Self, Self::Error> {
|
||||
let mut builder = DateTimeBuilder::new();
|
||||
|
||||
/// Restores year, month, day, etc after applying the timezone
|
||||
/// returns None if timezone overflows the date
|
||||
fn with_timezone_restore(
|
||||
offset: time::Offset,
|
||||
at: DateTime<FixedOffset>,
|
||||
) -> Option<DateTime<FixedOffset>> {
|
||||
let offset: FixedOffset = chrono::FixedOffset::try_from(offset).ok()?;
|
||||
let copy = at;
|
||||
let x = at
|
||||
.with_timezone(&offset)
|
||||
.with_day(copy.day())?
|
||||
.with_month(copy.month())?
|
||||
.with_year(copy.year())?
|
||||
.with_hour(copy.hour())?
|
||||
.with_minute(copy.minute())?
|
||||
.with_second(copy.second())?
|
||||
.with_nanosecond(copy.nanosecond())?;
|
||||
Some(x)
|
||||
}
|
||||
for item in items {
|
||||
builder = match item {
|
||||
Item::Timestamp(ts) => builder.set_timestamp(ts)?,
|
||||
Item::DateTime(dt) => builder.set_date(dt.date)?.set_time(dt.time)?,
|
||||
Item::Date(d) => builder.set_date(d)?,
|
||||
Item::Time(t) => builder.set_time(t)?,
|
||||
Item::Weekday(weekday) => builder.set_weekday(weekday)?,
|
||||
Item::TimeZone(tz) => builder.set_timezone(tz)?,
|
||||
Item::Relative(rel) => builder.push_relative(rel)?,
|
||||
Item::Pure(pure) => builder.set_pure(pure)?,
|
||||
}
|
||||
}
|
||||
|
||||
fn last_day_of_month(year: i32, month: u32) -> u32 {
|
||||
NaiveDate::from_ymd_opt(year, month + 1, 1)
|
||||
.unwrap_or(NaiveDate::from_ymd_opt(year + 1, 1, 1).unwrap())
|
||||
.pred_opt()
|
||||
.unwrap()
|
||||
.day()
|
||||
Ok(builder)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,16 +21,12 @@ use winnow::{
|
||||
|
||||
use crate::items::space;
|
||||
|
||||
use super::{
|
||||
date::{self, Date},
|
||||
primitive::s,
|
||||
time::{self, Time},
|
||||
};
|
||||
use super::{date, primitive::s, time};
|
||||
|
||||
#[derive(PartialEq, Debug, Clone, Default)]
|
||||
pub(crate) struct DateTime {
|
||||
pub(crate) date: Date,
|
||||
pub(crate) time: Time,
|
||||
pub(crate) date: date::Date,
|
||||
pub(crate) time: time::Time,
|
||||
}
|
||||
|
||||
pub(crate) fn parse(input: &mut &str) -> ModalResult<DateTime> {
|
||||
@@ -59,7 +55,8 @@ mod tests {
|
||||
time: Time {
|
||||
hour: 10,
|
||||
minute: 10,
|
||||
second: 55.0,
|
||||
second: 55,
|
||||
nanosecond: 0,
|
||||
offset: None,
|
||||
},
|
||||
});
|
||||
|
||||
+50
-29
@@ -41,13 +41,23 @@ use super::{
|
||||
};
|
||||
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Default)]
|
||||
pub struct Date {
|
||||
pub day: u32,
|
||||
pub month: u32,
|
||||
pub year: Option<u32>,
|
||||
pub(crate) struct Date {
|
||||
pub(crate) day: u8,
|
||||
pub(crate) month: u8,
|
||||
pub(crate) year: Option<u16>,
|
||||
}
|
||||
|
||||
impl TryFrom<(&str, u32, u32)> for Date {
|
||||
impl Date {
|
||||
pub(super) fn with_year(self, year: u16) -> Self {
|
||||
Date {
|
||||
day: self.day,
|
||||
month: self.month,
|
||||
year: Some(year),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<(&str, u8, u8)> for Date {
|
||||
type Error = &'static str;
|
||||
|
||||
/// Create a `Date` from a tuple of `(year, month, day)`.
|
||||
@@ -55,7 +65,7 @@ impl TryFrom<(&str, u32, u32)> for Date {
|
||||
/// Note: The `year` is represented as a `&str` to handle a specific GNU
|
||||
/// compatibility quirk. See the comment in [`year`](super::year) for more
|
||||
/// details.
|
||||
fn try_from(value: (&str, u32, u32)) -> Result<Self, Self::Error> {
|
||||
fn try_from(value: (&str, u8, u8)) -> Result<Self, Self::Error> {
|
||||
let (year_str, month, day) = value;
|
||||
let year = year_from_str(year_str)?;
|
||||
|
||||
@@ -80,11 +90,11 @@ impl TryFrom<(&str, u32, u32)> for Date {
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<(u32, u32)> for Date {
|
||||
impl TryFrom<(u8, u8)> for Date {
|
||||
type Error = &'static str;
|
||||
|
||||
/// Create a `Date` from a tuple of `(month, day)`.
|
||||
fn try_from((month, day): (u32, u32)) -> Result<Self, Self::Error> {
|
||||
fn try_from((month, day): (u8, u8)) -> Result<Self, Self::Error> {
|
||||
if !(1..=12).contains(&month) {
|
||||
return Err("month must be between 1 and 12");
|
||||
}
|
||||
@@ -104,14 +114,27 @@ impl TryFrom<(u32, u32)> for Date {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse(input: &mut &str) -> ModalResult<Date> {
|
||||
impl TryFrom<Date> for jiff::civil::Date {
|
||||
type Error = &'static str;
|
||||
|
||||
fn try_from(date: Date) -> Result<Self, Self::Error> {
|
||||
jiff::civil::Date::new(
|
||||
date.year.unwrap_or(0) as i16,
|
||||
date.month as i8,
|
||||
date.day as i8,
|
||||
)
|
||||
.map_err(|_| "date is not valid")
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn parse(input: &mut &str) -> ModalResult<Date> {
|
||||
alt((iso1, iso2, us, literal1, literal2)).parse_next(input)
|
||||
}
|
||||
|
||||
/// Parse `[year]-[month]-[day]`
|
||||
///
|
||||
/// This is also used by [`combined`](super::combined).
|
||||
pub fn iso1(input: &mut &str) -> ModalResult<Date> {
|
||||
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)?;
|
||||
|
||||
@@ -123,19 +146,13 @@ pub fn iso1(input: &mut &str) -> ModalResult<Date> {
|
||||
/// Parse `[year][month][day]`
|
||||
///
|
||||
/// This is also used by [`combined`](super::combined).
|
||||
pub fn iso2(input: &mut &str) -> ModalResult<Date> {
|
||||
pub(super) fn iso2(input: &mut &str) -> ModalResult<Date> {
|
||||
let date_str = take_while(5.., AsChar::is_dec_digit).parse_next(input)?;
|
||||
let len = date_str.len();
|
||||
|
||||
let year = &date_str[..len - 4];
|
||||
|
||||
let month = date_str[len - 4..len - 2]
|
||||
.parse::<u32>()
|
||||
.map_err(|_| ErrMode::Cut(ctx_err("month must be a valid number")))?;
|
||||
|
||||
let day = date_str[len - 2..]
|
||||
.parse::<u32>()
|
||||
.map_err(|_| ErrMode::Cut(ctx_err("day must be a valid number")))?;
|
||||
let month = month_from_str(&date_str[len - 4..len - 2])?;
|
||||
let day = day_from_str(&date_str[len - 2..])?;
|
||||
|
||||
(year, month, day)
|
||||
.try_into()
|
||||
@@ -158,27 +175,21 @@ fn us(input: &mut &str) -> ModalResult<Date> {
|
||||
//
|
||||
// GNU quirk: interpret as [year]/[month]/[day] if the first part is at
|
||||
// least 4 characters long.
|
||||
let day = s2
|
||||
.parse::<u32>()
|
||||
.map_err(|_| ErrMode::Cut(ctx_err("day must be a valid number")))?;
|
||||
let day = day_from_str(s2)?;
|
||||
(s1, n, day)
|
||||
.try_into()
|
||||
.map_err(|e| ErrMode::Cut(ctx_err(e)))
|
||||
}
|
||||
Some(s2) => {
|
||||
// [month]/[day]/[year]
|
||||
let month = s1
|
||||
.parse::<u32>()
|
||||
.map_err(|_| ErrMode::Cut(ctx_err("month must be a valid number")))?;
|
||||
let month = month_from_str(s1)?;
|
||||
(s2, month, n)
|
||||
.try_into()
|
||||
.map_err(|e| ErrMode::Cut(ctx_err(e)))
|
||||
}
|
||||
None => {
|
||||
// [month]/[day]
|
||||
let month = s1
|
||||
.parse::<u32>()
|
||||
.map_err(|_| ErrMode::Cut(ctx_err("month must be a valid number")))?;
|
||||
let month = month_from_str(s1)?;
|
||||
(month, n).try_into().map_err(|e| ErrMode::Cut(ctx_err(e)))
|
||||
}
|
||||
}
|
||||
@@ -239,7 +250,7 @@ fn literal2(input: &mut &str) -> ModalResult<Date> {
|
||||
}
|
||||
|
||||
/// Parse the name of a month (case-insensitive)
|
||||
fn literal_month(input: &mut &str) -> ModalResult<u32> {
|
||||
fn literal_month(input: &mut &str) -> ModalResult<u8> {
|
||||
s(alpha1)
|
||||
.verify_map(|s: &str| {
|
||||
Some(match s {
|
||||
@@ -261,6 +272,16 @@ fn literal_month(input: &mut &str) -> ModalResult<u32> {
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
fn month_from_str(s: &str) -> ModalResult<u8> {
|
||||
s.parse::<u8>()
|
||||
.map_err(|_| ErrMode::Cut(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")))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{parse, Date};
|
||||
|
||||
+120
-18
@@ -1,35 +1,137 @@
|
||||
// For the full copyright and license information, please view the LICENSE
|
||||
// file that was distributed with this source code.
|
||||
|
||||
use winnow::{combinator::preceded, ModalResult, Parser};
|
||||
//! Parse a timestamp item.
|
||||
//!
|
||||
//! From the GNU docs:
|
||||
//!
|
||||
//! > If you precede a number with ‘@’, it represents an internal timestamp as
|
||||
//! > a count of seconds. The number can contain an internal decimal point
|
||||
//! > (either ‘.’ or ‘,’); any excess precision not supported by the internal
|
||||
//! > representation is truncated toward minus infinity. Such a number cannot
|
||||
//! > be combined with any other date item, as it specifies a complete
|
||||
//! > timestamp.
|
||||
//! >
|
||||
//! > On most hosts, these counts ignore the presence of leap seconds. For
|
||||
//! > example, on most hosts ‘@1483228799’ represents 2016-12-31 23:59:59 UTC,
|
||||
//! > ‘@1483228800’ represents 2017-01-01 00:00:00 UTC, and there is no way to
|
||||
//! > represent the intervening leap second 2016-12-31 23:59:60 UTC.
|
||||
|
||||
use super::primitive::{float, s};
|
||||
use winnow::{
|
||||
ascii::digit1,
|
||||
combinator::{opt, preceded},
|
||||
token::one_of,
|
||||
ModalResult, Parser,
|
||||
};
|
||||
|
||||
/// Parse a timestamp in the form of `@1234567890`.
|
||||
pub fn parse(input: &mut &str) -> ModalResult<f64> {
|
||||
s(preceded("@", float)).parse_next(input)
|
||||
use super::primitive::{dec_uint, plus_or_minus, s};
|
||||
|
||||
/// Represents a timestamp with nanosecond accuracy.
|
||||
///
|
||||
/// # Invariants
|
||||
///
|
||||
/// - `nanosecond` is always in the range of `0..1_000_000_000`.
|
||||
/// - Negative timestamps are represented by a negative `second` value and a
|
||||
/// positive `nanosecond` value.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub(super) struct Timestamp {
|
||||
second: i64,
|
||||
nanosecond: u32,
|
||||
}
|
||||
|
||||
impl TryFrom<Timestamp> for jiff::Timestamp {
|
||||
type Error = &'static str;
|
||||
|
||||
fn try_from(ts: Timestamp) -> Result<Self, Self::Error> {
|
||||
jiff::Timestamp::new(
|
||||
ts.second,
|
||||
i32::try_from(ts.nanosecond).map_err(|_| "nanosecond in timestamp exceeds i32::MAX")?,
|
||||
)
|
||||
.map_err(|_| "timestamp value is out of valid range")
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a timestamp in the form of `@1234567890` or `@-1234567890.12345` or
|
||||
/// `@1234567890,12345`.
|
||||
pub(super) fn parse(input: &mut &str) -> ModalResult<Timestamp> {
|
||||
(s("@"), opt(plus_or_minus), s(sec_and_nsec))
|
||||
.verify_map(|(_, sign, (sec, nsec))| {
|
||||
let sec = i64::try_from(sec).ok()?;
|
||||
let (second, nanosecond) = match (sign, nsec) {
|
||||
(Some('-'), 0) => (-sec, 0),
|
||||
// Truncate towards minus infinity.
|
||||
(Some('-'), _) => ((-sec).checked_sub(1)?, 1_000_000_000 - nsec),
|
||||
_ => (sec, nsec),
|
||||
};
|
||||
Some(Timestamp { second, nanosecond })
|
||||
})
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
/// Parse a second value in the form of `1234567890` or `1234567890.12345` or
|
||||
/// `1234567890,12345`.
|
||||
///
|
||||
/// The first part represents whole seconds. The optional second part represents
|
||||
/// fractional seconds, parsed as a nanosecond value from up to 9 digits
|
||||
/// (padded with zeros on the right if fewer digits are present). If the second
|
||||
/// part is omitted, it defaults to 0 nanoseconds.
|
||||
pub(super) fn sec_and_nsec(input: &mut &str) -> ModalResult<(u64, u32)> {
|
||||
(dec_uint, opt(preceded(one_of(['.', ',']), digit1)))
|
||||
.verify_map(|(sec, opt_nsec_str)| match opt_nsec_str {
|
||||
Some(nsec_str) if nsec_str.len() >= 9 => Some((sec, nsec_str[..9].parse().ok()?)),
|
||||
Some(nsec_str) => {
|
||||
let multiplier = 10_u32.pow(9 - nsec_str.len() as u32);
|
||||
Some((sec, nsec_str.parse::<u32>().ok()?.checked_mul(multiplier)?))
|
||||
}
|
||||
None => Some((sec, 0)),
|
||||
})
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::parse;
|
||||
use super::*;
|
||||
|
||||
fn float_eq(a: f64, b: f64) -> bool {
|
||||
(a - b).abs() < f64::EPSILON
|
||||
fn ts(second: i64, nanosecond: u32) -> Timestamp {
|
||||
Timestamp { second, nanosecond }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn float() {
|
||||
let mut input = "@1234567890";
|
||||
assert!(float_eq(parse(&mut input).unwrap(), 1234567890.0));
|
||||
fn parse_sec_and_nsec() {
|
||||
for (input, expected) in [
|
||||
("1234567890", (1234567890, 0)), // only seconds
|
||||
("1234567890.12345", (1234567890, 123450000)), // seconds and nanoseconds, '.' as floating point
|
||||
("1234567890,12345", (1234567890, 123450000)), // seconds and nanoseconds, ',' as floating point
|
||||
("1234567890.1234567890123", (1234567890, 123456789)), // nanoseconds with more than 9 digits, truncated
|
||||
] {
|
||||
let mut s = input;
|
||||
assert_eq!(sec_and_nsec(&mut s).unwrap(), expected, "{input}");
|
||||
}
|
||||
|
||||
let mut input = "@1234567890.12345";
|
||||
assert!(float_eq(parse(&mut input).unwrap(), 1234567890.12345));
|
||||
for input in [
|
||||
".1234567890", // invalid: no leading seconds
|
||||
"-1234567890", // invalid: negative input not allowed
|
||||
] {
|
||||
let mut s = input;
|
||||
assert!(sec_and_nsec(&mut s).is_err(), "{input}");
|
||||
}
|
||||
}
|
||||
|
||||
let mut input = "@1234567890,12345";
|
||||
assert!(float_eq(parse(&mut input).unwrap(), 1234567890.12345));
|
||||
|
||||
let mut input = "@-1234567890.12345";
|
||||
assert_eq!(parse(&mut input).unwrap(), -1234567890.12345);
|
||||
#[test]
|
||||
fn timestamp() {
|
||||
for (input, expected) in [
|
||||
("@1234567890", ts(1234567890, 0)), // positive seconds, no nanoseconds
|
||||
("@ 1234567890", ts(1234567890, 0)), // space after '@', positive seconds, no nanoseconds
|
||||
("@-1234567890", ts(-1234567890, 0)), // negative seconds, no nanoseconds
|
||||
("@ -1234567890", ts(-1234567890, 0)), // space after '@', negative seconds, no nanoseconds
|
||||
("@ - 1234567890", ts(-1234567890, 0)), // space after '@' and after '-', negative seconds, no nanoseconds
|
||||
("@1234567890.12345", ts(1234567890, 123450000)), // positive seconds with nanoseconds, '.' as floating point
|
||||
("@1234567890,12345", ts(1234567890, 123450000)), // positive seconds with nanoseconds, ',' as floating point
|
||||
("@-1234567890.12345", ts(-1234567891, 876550000)), // negative seconds with nanoseconds, '.' as floating point
|
||||
("@1234567890.1234567890123", ts(1234567890, 123456789)), // nanoseconds with more than 9 digits, truncated
|
||||
] {
|
||||
let mut s = input;
|
||||
assert_eq!(parse(&mut s).unwrap(), expected, "{input}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
use std::fmt;
|
||||
|
||||
use winnow::error::{ContextError, ErrMode};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum Error {
|
||||
ParseError(String),
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Error::ParseError(reason) => {
|
||||
write!(f, "{reason}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&'static str> for Error {
|
||||
fn from(reason: &'static str) -> Self {
|
||||
Error::ParseError(reason.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ErrMode<ContextError>> for Error {
|
||||
fn from(err: ErrMode<ContextError>) -> Self {
|
||||
Error::ParseError(err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<jiff::Error> for Error {
|
||||
fn from(err: jiff::Error) -> Self {
|
||||
Error::ParseError(err.to_string())
|
||||
}
|
||||
}
|
||||
+160
-131
@@ -21,18 +21,18 @@
|
||||
//! - [`combined`]
|
||||
//! - [`date`]
|
||||
//! - [`epoch`]
|
||||
//! - [`pure`]
|
||||
//! - [`relative`]
|
||||
//! - [`time`]
|
||||
//! - [`timezone`]
|
||||
//! - [`weekday`]
|
||||
//! - [`year`]
|
||||
|
||||
#![allow(deprecated)]
|
||||
|
||||
// date and time items
|
||||
mod combined;
|
||||
mod date;
|
||||
mod epoch;
|
||||
mod pure;
|
||||
mod relative;
|
||||
mod time;
|
||||
mod timezone;
|
||||
@@ -44,47 +44,50 @@ mod builder;
|
||||
mod ordinal;
|
||||
mod primitive;
|
||||
|
||||
use builder::DateTimeBuilder;
|
||||
use chrono::{DateTime, FixedOffset};
|
||||
pub(crate) mod error;
|
||||
|
||||
use jiff::Zoned;
|
||||
use primitive::space;
|
||||
use winnow::{
|
||||
combinator::{alt, eof, terminated, trace},
|
||||
combinator::{alt, eof, preceded, repeat_till, terminated, trace},
|
||||
error::{AddContext, ContextError, ErrMode, StrContext, StrContextValue},
|
||||
stream::Stream,
|
||||
ModalResult, Parser,
|
||||
};
|
||||
|
||||
use crate::ParseDateTimeError;
|
||||
use builder::DateTimeBuilder;
|
||||
use error::Error;
|
||||
|
||||
#[derive(PartialEq, Debug)]
|
||||
pub(crate) enum Item {
|
||||
Timestamp(f64),
|
||||
Year(u32),
|
||||
enum Item {
|
||||
Timestamp(epoch::Timestamp),
|
||||
DateTime(combined::DateTime),
|
||||
Date(date::Date),
|
||||
Time(time::Time),
|
||||
Weekday(weekday::Weekday),
|
||||
Relative(relative::Relative),
|
||||
TimeZone(time::Offset),
|
||||
TimeZone(timezone::Offset),
|
||||
Pure(String),
|
||||
}
|
||||
|
||||
/// Build a `DateTime<FixedOffset>` from a `DateTimeBuilder` and a base date.
|
||||
pub(crate) fn at_date(
|
||||
builder: DateTimeBuilder,
|
||||
base: DateTime<FixedOffset>,
|
||||
) -> Result<DateTime<FixedOffset>, ParseDateTimeError> {
|
||||
builder
|
||||
.set_base(base)
|
||||
.build()
|
||||
.ok_or(ParseDateTimeError::InvalidInput)
|
||||
/// Parse a date and time string and build a `Zoned` object. The parsed result
|
||||
/// is resolved against the given base date and time.
|
||||
pub(crate) fn parse_at_date<S: AsRef<str> + Clone>(base: Zoned, input: S) -> Result<Zoned, Error> {
|
||||
let input = input.as_ref().to_ascii_lowercase();
|
||||
match parse(&mut input.as_str()) {
|
||||
Ok(builder) => builder.set_base(base).build(),
|
||||
Err(e) => Err(e.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a `DateTime<FixedOffset>` from a `DateTimeBuilder` and the current
|
||||
/// time.
|
||||
pub(crate) fn at_local(
|
||||
builder: DateTimeBuilder,
|
||||
) -> Result<DateTime<FixedOffset>, ParseDateTimeError> {
|
||||
builder.build().ok_or(ParseDateTimeError::InvalidInput)
|
||||
/// Parse a date and time string and build a `Zoned` object. The parsed result
|
||||
/// is resolved against the current local date and time.
|
||||
pub(crate) fn parse_at_local<S: AsRef<str> + Clone>(input: S) -> Result<Zoned, Error> {
|
||||
let input = input.as_ref().to_ascii_lowercase();
|
||||
match parse(&mut input.as_str()) {
|
||||
Ok(builder) => builder.build(), // the builder uses current local date and time if no base is given.
|
||||
Err(e) => Err(e.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a date and time string.
|
||||
@@ -97,7 +100,7 @@ pub(crate) fn at_local(
|
||||
/// timestamp = "@" , float ;
|
||||
///
|
||||
/// items = item , { item } ;
|
||||
/// item = datetime | date | time | relative | weekday | timezone | year ;
|
||||
/// item = datetime | date | time | relative | weekday | timezone | pure ;
|
||||
///
|
||||
/// datetime = date , [ "t" | whitespace ] , iso_time ;
|
||||
///
|
||||
@@ -178,9 +181,11 @@ pub(crate) fn at_local(
|
||||
///
|
||||
/// timezone = named_zone , [ time_offset ] ;
|
||||
///
|
||||
/// pure = { digit }
|
||||
///
|
||||
/// optional_whitespace = { whitespace } ;
|
||||
/// ```
|
||||
pub(crate) fn parse(input: &mut &str) -> ModalResult<DateTimeBuilder> {
|
||||
fn parse(input: &mut &str) -> ModalResult<DateTimeBuilder> {
|
||||
trace("parse", alt((parse_timestamp, parse_items))).parse_next(input)
|
||||
}
|
||||
|
||||
@@ -193,7 +198,7 @@ pub(crate) fn parse(input: &mut &str) -> ModalResult<DateTimeBuilder> {
|
||||
fn parse_timestamp(input: &mut &str) -> ModalResult<DateTimeBuilder> {
|
||||
trace(
|
||||
"parse_timestamp",
|
||||
terminated(epoch::parse.map(Item::Timestamp), eof),
|
||||
terminated(epoch::parse.map(Item::Timestamp), preceded(space, eof)),
|
||||
)
|
||||
.verify_map(|ts: Item| {
|
||||
if let Item::Timestamp(ts) = ts {
|
||||
@@ -207,57 +212,13 @@ fn parse_timestamp(input: &mut &str) -> ModalResult<DateTimeBuilder> {
|
||||
|
||||
/// Parse a sequence of items.
|
||||
fn parse_items(input: &mut &str) -> ModalResult<DateTimeBuilder> {
|
||||
let mut builder = DateTimeBuilder::new();
|
||||
let (items, _): (Vec<Item>, _) = trace(
|
||||
"parse_items",
|
||||
repeat_till(0.., parse_item, preceded(space, eof)),
|
||||
)
|
||||
.parse_next(input)?;
|
||||
|
||||
loop {
|
||||
match parse_item.parse_next(input) {
|
||||
Ok(item) => match item {
|
||||
Item::Timestamp(ts) => {
|
||||
builder = builder
|
||||
.set_timestamp(ts)
|
||||
.map_err(|e| expect_error(input, e))?;
|
||||
}
|
||||
Item::Year(year) => {
|
||||
builder = builder.set_year(year).map_err(|e| expect_error(input, e))?;
|
||||
}
|
||||
Item::DateTime(dt) => {
|
||||
builder = builder
|
||||
.set_date(dt.date)
|
||||
.map_err(|e| expect_error(input, e))?
|
||||
.set_time(dt.time)
|
||||
.map_err(|e| expect_error(input, e))?;
|
||||
}
|
||||
Item::Date(d) => {
|
||||
builder = builder.set_date(d).map_err(|e| expect_error(input, e))?;
|
||||
}
|
||||
Item::Time(t) => {
|
||||
builder = builder.set_time(t).map_err(|e| expect_error(input, e))?;
|
||||
}
|
||||
Item::Weekday(weekday) => {
|
||||
builder = builder
|
||||
.set_weekday(weekday)
|
||||
.map_err(|e| expect_error(input, e))?;
|
||||
}
|
||||
Item::TimeZone(tz) => {
|
||||
builder = builder
|
||||
.set_timezone(tz)
|
||||
.map_err(|e| expect_error(input, e))?;
|
||||
}
|
||||
Item::Relative(rel) => {
|
||||
builder = builder.push_relative(rel);
|
||||
}
|
||||
},
|
||||
Err(ErrMode::Backtrack(_)) => break,
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
space.parse_next(input)?;
|
||||
if !input.is_empty() {
|
||||
return Err(expect_error(input, "unexpected input"));
|
||||
}
|
||||
|
||||
Ok(builder)
|
||||
items.try_into().map_err(|e| expect_error(input, e))
|
||||
}
|
||||
|
||||
/// Parse an item.
|
||||
@@ -271,7 +232,7 @@ fn parse_item(input: &mut &str) -> ModalResult<Item> {
|
||||
relative::parse.map(Item::Relative),
|
||||
weekday::parse.map(Item::Weekday),
|
||||
timezone::parse.map(Item::TimeZone),
|
||||
year::parse.map(Item::Year),
|
||||
pure::parse.map(Item::Pure),
|
||||
)),
|
||||
)
|
||||
.parse_next(input)
|
||||
@@ -288,13 +249,16 @@ fn expect_error(input: &mut &str, reason: &'static str) -> ErrMode<ContextError>
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{at_date, parse, DateTimeBuilder};
|
||||
use chrono::{
|
||||
DateTime, FixedOffset, NaiveDate, NaiveDateTime, NaiveTime, TimeZone, Timelike, Utc,
|
||||
};
|
||||
use jiff::{civil::DateTime, tz::TimeZone, ToSpan, Zoned};
|
||||
|
||||
fn at_utc(builder: DateTimeBuilder) -> DateTime<FixedOffset> {
|
||||
at_date(builder, Utc::now().fixed_offset()).unwrap()
|
||||
use super::{parse, DateTimeBuilder};
|
||||
|
||||
fn at_date(builder: DateTimeBuilder, base: Zoned) -> Zoned {
|
||||
builder.set_base(base).build().unwrap()
|
||||
}
|
||||
|
||||
fn at_utc(builder: DateTimeBuilder) -> Zoned {
|
||||
at_date(builder, Zoned::now().with_time_zone(TimeZone::UTC))
|
||||
}
|
||||
|
||||
fn test_eq_fmt(fmt: &str, input: &str) -> String {
|
||||
@@ -303,7 +267,7 @@ mod tests {
|
||||
.map(at_utc)
|
||||
.map_err(|e| eprintln!("TEST FAILED AT:\n{e}"))
|
||||
.expect("parsing failed during tests")
|
||||
.format(fmt)
|
||||
.strftime(fmt)
|
||||
.to_string()
|
||||
}
|
||||
|
||||
@@ -338,6 +302,11 @@ mod tests {
|
||||
test_eq_fmt("%Y-%m-%dT%H:%M:%S%:z", "@1690466034")
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
"2023-07-27T13:53:54+00:00",
|
||||
test_eq_fmt("%Y-%m-%dT%H:%M:%S%:z", " @1690466034 ")
|
||||
);
|
||||
|
||||
// https://github.com/uutils/coreutils/issues/6398
|
||||
// TODO: make this work
|
||||
// assert_eq!("1111 1111 00", test_eq_fmt("%m%d %H%M %S", "11111111"));
|
||||
@@ -363,6 +332,12 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty() {
|
||||
let result = parse(&mut "");
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid() {
|
||||
let result = parse(&mut "2025-05-19 2024-05-20 06:14:49");
|
||||
@@ -386,16 +361,8 @@ mod tests {
|
||||
.to_string()
|
||||
.contains("time cannot appear more than once"));
|
||||
|
||||
let result = parse(&mut "2025-05-19 2024");
|
||||
assert!(result.is_err());
|
||||
assert!(result
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("year cannot appear more than once"));
|
||||
|
||||
let result = parse(&mut "2025-05-19 +00:00 +01:00");
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().to_string().contains("unexpected input"));
|
||||
|
||||
let result = parse(&mut "m1y");
|
||||
assert!(result.is_err());
|
||||
@@ -406,96 +373,158 @@ mod tests {
|
||||
|
||||
let result = parse(&mut "2025-05-19 abcdef");
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().to_string().contains("unexpected input"));
|
||||
|
||||
let result = parse(&mut "@1690466034 2025-05-19");
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().to_string().contains("unexpected input"));
|
||||
|
||||
let result = parse(&mut "2025-05-19 @1690466034");
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().to_string().contains("unexpected input"));
|
||||
|
||||
// Pure number as year (too large).
|
||||
let result = parse(&mut "jul 18 12:30 10000");
|
||||
assert!(result.is_err());
|
||||
assert!(result
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("year must be no greater than 9999"));
|
||||
|
||||
// Pure number as time (too long).
|
||||
let result = parse(&mut "01:02 12345");
|
||||
assert!(result.is_err());
|
||||
assert!(result
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("pure number must be 1-4 digits when interpreted as time"));
|
||||
|
||||
// Pure number as time (repeated time).
|
||||
let result = parse(&mut "01:02 1234");
|
||||
assert!(result.is_err());
|
||||
assert!(result
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("time cannot appear more than once"));
|
||||
|
||||
// Pure number as time (invalid hour).
|
||||
let result = parse(&mut "jul 18 2025 2400");
|
||||
assert!(result.is_err());
|
||||
assert!(result
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("invalid hour in pure number"));
|
||||
|
||||
// Pure number as time (invalid minute).
|
||||
let result = parse(&mut "jul 18 2025 2360");
|
||||
assert!(result.is_err());
|
||||
assert!(result
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("invalid minute in pure number"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_weekday() {
|
||||
// Jan 1 2025 is a Wed
|
||||
let now = Utc
|
||||
.from_utc_datetime(&NaiveDateTime::new(
|
||||
NaiveDate::from_ymd_opt(2025, 1, 1).unwrap(),
|
||||
NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
|
||||
))
|
||||
.fixed_offset();
|
||||
let now = "2025-01-01 00:00:00"
|
||||
.parse::<DateTime>()
|
||||
.unwrap()
|
||||
.to_zoned(TimeZone::UTC)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
at_date(parse(&mut "last wed").unwrap(), now).unwrap(),
|
||||
now - chrono::Duration::days(7)
|
||||
at_date(parse(&mut "last wed").unwrap(), now.clone()),
|
||||
now.checked_sub(7.days()).unwrap()
|
||||
);
|
||||
assert_eq!(at_date(parse(&mut "this wed").unwrap(), now).unwrap(), now);
|
||||
assert_eq!(at_date(parse(&mut "this wed").unwrap(), now.clone()), now);
|
||||
assert_eq!(
|
||||
at_date(parse(&mut "next wed").unwrap(), now).unwrap(),
|
||||
now + chrono::Duration::days(7)
|
||||
at_date(parse(&mut "next wed").unwrap(), now.clone()),
|
||||
now.checked_add(7.days()).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
at_date(parse(&mut "last thu").unwrap(), now).unwrap(),
|
||||
now - chrono::Duration::days(6)
|
||||
at_date(parse(&mut "last thu").unwrap(), now.clone()),
|
||||
now.checked_sub(6.days()).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
at_date(parse(&mut "this thu").unwrap(), now).unwrap(),
|
||||
now + chrono::Duration::days(1)
|
||||
at_date(parse(&mut "this thu").unwrap(), now.clone()),
|
||||
now.checked_add(1.days()).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
at_date(parse(&mut "next thu").unwrap(), now).unwrap(),
|
||||
now + chrono::Duration::days(1)
|
||||
at_date(parse(&mut "next thu").unwrap(), now.clone()),
|
||||
now.checked_add(1.days()).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
at_date(parse(&mut "1 wed").unwrap(), now).unwrap(),
|
||||
now + chrono::Duration::days(7)
|
||||
at_date(parse(&mut "1 wed").unwrap(), now.clone()),
|
||||
now.checked_add(7.days()).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
at_date(parse(&mut "1 thu").unwrap(), now).unwrap(),
|
||||
now + chrono::Duration::days(1)
|
||||
at_date(parse(&mut "1 thu").unwrap(), now.clone()),
|
||||
now.checked_add(1.days()).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
at_date(parse(&mut "2 wed").unwrap(), now).unwrap(),
|
||||
now + chrono::Duration::days(14)
|
||||
at_date(parse(&mut "2 wed").unwrap(), now.clone()),
|
||||
now.checked_add(14.days()).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
at_date(parse(&mut "2 thu").unwrap(), now).unwrap(),
|
||||
now + chrono::Duration::days(8)
|
||||
at_date(parse(&mut "2 thu").unwrap(), now.clone()),
|
||||
now.checked_add(8.days()).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_date_time() {
|
||||
let now = Utc::now().fixed_offset();
|
||||
let now = Zoned::now().with_time_zone(TimeZone::UTC);
|
||||
|
||||
let result = at_date(parse(&mut "2 days ago").unwrap(), now).unwrap();
|
||||
assert_eq!(result, now - chrono::Duration::days(2));
|
||||
let result = at_date(parse(&mut "2 days ago").unwrap(), now.clone());
|
||||
assert_eq!(result, now.checked_sub(2.days()).unwrap());
|
||||
assert_eq!(result.hour(), now.hour());
|
||||
assert_eq!(result.minute(), now.minute());
|
||||
assert_eq!(result.second(), now.second());
|
||||
|
||||
let result = at_date(parse(&mut "2 days 3 days ago").unwrap(), now).unwrap();
|
||||
assert_eq!(result, now - chrono::Duration::days(1));
|
||||
let result = at_date(parse(&mut "2 days 3 days ago").unwrap(), now.clone());
|
||||
assert_eq!(result, now.checked_sub(1.days()).unwrap());
|
||||
assert_eq!(result.hour(), now.hour());
|
||||
assert_eq!(result.minute(), now.minute());
|
||||
assert_eq!(result.second(), now.second());
|
||||
|
||||
let result = at_date(parse(&mut "2025-01-01 2 days ago").unwrap(), now).unwrap();
|
||||
let result = at_date(parse(&mut "2025-01-01 2 days ago").unwrap(), now.clone());
|
||||
assert_eq!(result.hour(), 0);
|
||||
assert_eq!(result.minute(), 0);
|
||||
assert_eq!(result.second(), 0);
|
||||
|
||||
let result = at_date(parse(&mut "3 weeks").unwrap(), now).unwrap();
|
||||
assert_eq!(result, now + chrono::Duration::days(21));
|
||||
let result = at_date(parse(&mut "3 weeks").unwrap(), now.clone());
|
||||
assert_eq!(result, now.checked_add(21.days()).unwrap());
|
||||
assert_eq!(result.hour(), now.hour());
|
||||
assert_eq!(result.minute(), now.minute());
|
||||
assert_eq!(result.second(), now.second());
|
||||
|
||||
let result = at_date(parse(&mut "2025-01-01 3 weeks").unwrap(), now).unwrap();
|
||||
let result = at_date(parse(&mut "2025-01-01 3 weeks").unwrap(), now);
|
||||
assert_eq!(result.hour(), 0);
|
||||
assert_eq!(result.minute(), 0);
|
||||
assert_eq!(result.second(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pure() {
|
||||
let now = Zoned::now().with_time_zone(TimeZone::UTC);
|
||||
|
||||
// Pure number as year.
|
||||
let result = at_date(parse(&mut "jul 18 12:30 2025").unwrap(), now.clone());
|
||||
assert_eq!(result.year(), 2025);
|
||||
|
||||
// Pure number as time.
|
||||
let result = at_date(parse(&mut "1230").unwrap(), now.clone());
|
||||
assert_eq!(result.hour(), 12);
|
||||
assert_eq!(result.minute(), 30);
|
||||
|
||||
let result = at_date(parse(&mut "123").unwrap(), now.clone());
|
||||
assert_eq!(result.hour(), 1);
|
||||
assert_eq!(result.minute(), 23);
|
||||
|
||||
let result = at_date(parse(&mut "12").unwrap(), now.clone());
|
||||
assert_eq!(result.hour(), 12);
|
||||
assert_eq!(result.minute(), 0);
|
||||
|
||||
let result = at_date(parse(&mut "1").unwrap(), now.clone());
|
||||
assert_eq!(result.hour(), 1);
|
||||
assert_eq!(result.minute(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
+25
-21
@@ -3,8 +3,10 @@
|
||||
|
||||
//! Primitive combinators.
|
||||
|
||||
use std::str::FromStr;
|
||||
|
||||
use winnow::{
|
||||
ascii::{digit1, multispace0},
|
||||
ascii::{digit1, multispace0, Uint},
|
||||
combinator::{alt, delimited, not, opt, peek, preceded, repeat, separated},
|
||||
error::{ContextError, ParserError, StrContext, StrContextValue},
|
||||
stream::AsChar,
|
||||
@@ -100,36 +102,38 @@ where
|
||||
///
|
||||
/// See the rationale for `dec_int` for why we don't use
|
||||
/// `winnow::ascii::dec_uint`.
|
||||
pub(super) fn dec_uint<'a, E>(input: &mut &'a str) -> winnow::Result<u32, E>
|
||||
pub(super) fn dec_uint<'a, O, E>(input: &mut &'a str) -> winnow::Result<O, E>
|
||||
where
|
||||
O: Uint + FromStr,
|
||||
E: ParserError<&'a str>,
|
||||
{
|
||||
digit1
|
||||
.void()
|
||||
.take()
|
||||
dec_uint_str
|
||||
.verify_map(|s: &str| s.parse().ok())
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
/// Parse a float number.
|
||||
///
|
||||
/// Rationale for not using `winnow::ascii::float`: the `float` parser provided
|
||||
/// by winnow accepts E-notation numbers (e.g., `1.23e4`), whereas GNU date
|
||||
/// rejects such numbers. To remain compatible with GNU date, we provide a
|
||||
/// custom implementation that only accepts inputs like [+-]?[0-9]+(\.[0-9]+)?.
|
||||
pub(super) fn float<'a, E>(input: &mut &'a str) -> winnow::Result<f64, E>
|
||||
/// Parse an unsigned decimal integer as a string slice.
|
||||
pub(super) fn dec_uint_str<'a, E>(input: &mut &'a str) -> winnow::Result<&'a str, E>
|
||||
where
|
||||
E: ParserError<&'a str>,
|
||||
{
|
||||
(
|
||||
opt(one_of(['+', '-'])),
|
||||
digit1,
|
||||
opt(preceded(one_of(['.', ',']), digit1)),
|
||||
)
|
||||
.void()
|
||||
.take()
|
||||
.verify_map(|s: &str| s.replace(",", ".").parse().ok())
|
||||
.parse_next(input)
|
||||
digit1.void().take().parse_next(input)
|
||||
}
|
||||
|
||||
/// Parse a colon preceded by whitespace.
|
||||
pub(super) fn colon<'a, E>(input: &mut &'a str) -> winnow::Result<(), E>
|
||||
where
|
||||
E: ParserError<&'a str>,
|
||||
{
|
||||
s(':').void().parse_next(input)
|
||||
}
|
||||
|
||||
/// Parse a plus or minus character optionally preceeded by whitespace.
|
||||
pub(super) fn plus_or_minus<'a, E>(input: &mut &'a str) -> winnow::Result<char, E>
|
||||
where
|
||||
E: ParserError<&'a str>,
|
||||
{
|
||||
s(alt(('+', '-'))).parse_next(input)
|
||||
}
|
||||
|
||||
/// Create a context error with a reason.
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
// For the full copyright and license information, please view the LICENSE
|
||||
// file that was distributed with this source code.
|
||||
|
||||
//! Parse a pure number string.
|
||||
//!
|
||||
//! From the GNU docs:
|
||||
//!
|
||||
//! > The precise interpretation of a pure decimal number depends on the
|
||||
//! > context in the date string.
|
||||
//! >
|
||||
//! > If the decimal number is of the form YYYYMMDD and no other calendar
|
||||
//! > date item (*note Calendar date items::) appears before it in the date
|
||||
//! > string, then YYYY is read as the year, MM as the month number and DD as
|
||||
//! > the day of the month, for the specified calendar date.
|
||||
//! >
|
||||
//! > If the decimal number is of the form HHMM and no other time of day
|
||||
//! > item appears before it in the date string, then HH is read as the hour
|
||||
//! > of the day and MM as the minute of the hour, for the specified time of
|
||||
//! > day. MM can also be omitted.
|
||||
//! >
|
||||
//! > If both a calendar date and a time of day appear to the left of a
|
||||
//! > number in the date string, but no relative item, then the number
|
||||
//! > overrides the year.
|
||||
|
||||
use winnow::{ModalResult, Parser};
|
||||
|
||||
use super::primitive::{dec_uint_str, s};
|
||||
|
||||
/// Parse a pure number string and return it as an owned `String`. We return a
|
||||
/// `String` here because the interpretation of the number depends on the
|
||||
/// parsing context in which it appears. The interpretation is deferred to the
|
||||
/// result building phase.
|
||||
pub(super) fn parse(input: &mut &str) -> ModalResult<String> {
|
||||
s(dec_uint_str)
|
||||
.map(|s: &str| s.to_owned())
|
||||
.parse_next(input)
|
||||
}
|
||||
+60
-53
@@ -37,38 +37,37 @@ use winnow::{
|
||||
ModalResult, Parser,
|
||||
};
|
||||
|
||||
use super::{
|
||||
ordinal::ordinal,
|
||||
primitive::{float, s},
|
||||
};
|
||||
use super::{epoch::sec_and_nsec, ordinal::ordinal, primitive::s};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Relative {
|
||||
pub(crate) enum Relative {
|
||||
Years(i32),
|
||||
Months(i32),
|
||||
Days(i32),
|
||||
Hours(i32),
|
||||
Minutes(i32),
|
||||
// Seconds are special because they can be given as a float
|
||||
Seconds(f64),
|
||||
Seconds(i64, u32),
|
||||
}
|
||||
|
||||
impl Relative {
|
||||
// TODO: determine how to handle multiplication overflows,
|
||||
// using saturating_mul for now.
|
||||
fn mul(self, n: i32) -> Self {
|
||||
match self {
|
||||
Self::Years(x) => Self::Years(n.saturating_mul(x)),
|
||||
Self::Months(x) => Self::Months(n.saturating_mul(x)),
|
||||
Self::Days(x) => Self::Days(n.saturating_mul(x)),
|
||||
Self::Hours(x) => Self::Hours(n.saturating_mul(x)),
|
||||
Self::Minutes(x) => Self::Minutes(n.saturating_mul(x)),
|
||||
Self::Seconds(x) => Self::Seconds(f64::from(n) * x),
|
||||
impl TryFrom<Relative> for jiff::Span {
|
||||
type Error = &'static str;
|
||||
|
||||
fn try_from(relative: Relative) -> Result<Self, Self::Error> {
|
||||
match relative {
|
||||
Relative::Years(years) => jiff::Span::new().try_years(years),
|
||||
Relative::Months(months) => jiff::Span::new().try_months(months),
|
||||
Relative::Days(days) => jiff::Span::new().try_days(days),
|
||||
Relative::Hours(hours) => jiff::Span::new().try_hours(hours),
|
||||
Relative::Minutes(minutes) => jiff::Span::new().try_minutes(minutes),
|
||||
Relative::Seconds(seconds, nanoseconds) => jiff::Span::new()
|
||||
.try_seconds(seconds)
|
||||
.and_then(|span| span.try_nanoseconds(nanoseconds)),
|
||||
}
|
||||
.map_err(|_| "relative value is invalid")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse(input: &mut &str) -> ModalResult<Relative> {
|
||||
pub(super) fn parse(input: &mut &str) -> ModalResult<Relative> {
|
||||
alt((
|
||||
s("tomorrow").value(Relative::Days(1)),
|
||||
s("yesterday").value(Relative::Days(-1)),
|
||||
@@ -76,24 +75,48 @@ pub fn parse(input: &mut &str) -> ModalResult<Relative> {
|
||||
s("today").value(Relative::Days(0)),
|
||||
s("now").value(Relative::Days(0)),
|
||||
seconds,
|
||||
other,
|
||||
displacement,
|
||||
))
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
fn seconds(input: &mut &str) -> ModalResult<Relative> {
|
||||
(
|
||||
opt(alt((s(float), ordinal.map(|x| x as f64)))),
|
||||
opt(alt((s('+').value(1), s('-').value(-1)))),
|
||||
sec_and_nsec,
|
||||
s(alpha1).verify(|s: &str| matches!(s, "seconds" | "second" | "sec" | "secs")),
|
||||
ago,
|
||||
)
|
||||
.map(|(n, _, ago)| Relative::Seconds(n.unwrap_or(1.0) * if ago { -1.0 } else { 1.0 }))
|
||||
.verify_map(|(sign, (sec, nsec), _, ago)| {
|
||||
let sec = i64::try_from(sec).ok()?;
|
||||
let sign = sign.unwrap_or(1) * if ago { -1 } else { 1 };
|
||||
let (second, nanosecond) = match (sign, nsec) {
|
||||
(-1, 0) => (-sec, 0),
|
||||
// Truncate towards minus infinity.
|
||||
(-1, _) => ((-sec).checked_sub(1)?, 1_000_000_000 - nsec),
|
||||
_ => (sec, nsec),
|
||||
};
|
||||
Some(Relative::Seconds(second, nanosecond))
|
||||
})
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
fn other(input: &mut &str) -> ModalResult<Relative> {
|
||||
(opt(ordinal), integer_unit, ago)
|
||||
.map(|(n, unit, ago)| unit.mul(n.unwrap_or(1) * if ago { -1 } else { 1 }))
|
||||
fn displacement(input: &mut &str) -> ModalResult<Relative> {
|
||||
(opt(ordinal), s(alpha1), ago)
|
||||
.verify_map(|(n, unit, ago): (Option<i32>, &str, bool)| {
|
||||
let multiplier = n.unwrap_or(1) * if ago { -1 } else { 1 };
|
||||
Some(match unit.strip_suffix('s').unwrap_or(unit) {
|
||||
"year" => Relative::Years(multiplier),
|
||||
"month" => Relative::Months(multiplier),
|
||||
"fortnight" => Relative::Days(multiplier.checked_mul(14)?),
|
||||
"week" => Relative::Days(multiplier.checked_mul(7)?),
|
||||
"day" => Relative::Days(multiplier),
|
||||
"hour" => Relative::Hours(multiplier),
|
||||
"minute" | "min" => Relative::Minutes(multiplier),
|
||||
"second" | "sec" => Relative::Seconds(multiplier as i64, 0),
|
||||
_ => return None,
|
||||
})
|
||||
})
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
@@ -101,23 +124,6 @@ fn ago(input: &mut &str) -> ModalResult<bool> {
|
||||
opt(s("ago")).map(|o| o.is_some()).parse_next(input)
|
||||
}
|
||||
|
||||
fn integer_unit(input: &mut &str) -> ModalResult<Relative> {
|
||||
s(alpha1)
|
||||
.verify_map(|s: &str| {
|
||||
Some(match s.strip_suffix('s').unwrap_or(s) {
|
||||
"year" => Relative::Years(1),
|
||||
"month" => Relative::Months(1),
|
||||
"fortnight" => Relative::Days(14),
|
||||
"week" => Relative::Days(7),
|
||||
"day" => Relative::Days(1),
|
||||
"hour" => Relative::Hours(1),
|
||||
"minute" | "min" => Relative::Minutes(1),
|
||||
_ => return None,
|
||||
})
|
||||
})
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{parse, Relative};
|
||||
@@ -126,16 +132,17 @@ mod tests {
|
||||
fn all() {
|
||||
for (s, rel) in [
|
||||
// Seconds
|
||||
("second", Relative::Seconds(1.0)),
|
||||
("sec", Relative::Seconds(1.0)),
|
||||
("seconds", Relative::Seconds(1.0)),
|
||||
("secs", Relative::Seconds(1.0)),
|
||||
("second ago", Relative::Seconds(-1.0)),
|
||||
("3 seconds", Relative::Seconds(3.0)),
|
||||
("3.5 seconds", Relative::Seconds(3.5)),
|
||||
// ("+3.5 seconds", Relative::Seconds(3.5)),
|
||||
("3.5 seconds ago", Relative::Seconds(-3.5)),
|
||||
("-3.5 seconds ago", Relative::Seconds(3.5)),
|
||||
("second", Relative::Seconds(1, 0)),
|
||||
("sec", Relative::Seconds(1, 0)),
|
||||
("seconds", Relative::Seconds(1, 0)),
|
||||
("secs", Relative::Seconds(1, 0)),
|
||||
("second ago", Relative::Seconds(-1, 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 ago", Relative::Seconds(-4, 500_000_000)),
|
||||
("-3.5 seconds ago", Relative::Seconds(3, 500_000_000)),
|
||||
// Minutes
|
||||
("minute", Relative::Minutes(1)),
|
||||
("minutes", Relative::Minutes(1)),
|
||||
@@ -181,7 +188,7 @@ mod tests {
|
||||
("now", Relative::Days(0)),
|
||||
// This something
|
||||
("this day", Relative::Days(0)),
|
||||
("this second", Relative::Seconds(0.0)),
|
||||
("this second", Relative::Seconds(0, 0)),
|
||||
("this year", Relative::Years(0)),
|
||||
// Weird stuff
|
||||
("next week ago", Relative::Days(-7)),
|
||||
|
||||
+112
-533
File diff suppressed because it is too large
Load Diff
+462
-4
@@ -1,10 +1,468 @@
|
||||
// For the full copyright and license information, please view the LICENSE
|
||||
// file that was distributed with this source code.
|
||||
|
||||
use winnow::ModalResult;
|
||||
//! Parse a timezone item.
|
||||
//!
|
||||
//! From the GNU docs:
|
||||
//!
|
||||
//! > A “time zone item” specifies an international time zone, indicated by a
|
||||
//! > small set of letters, e.g., ‘UTC’ or ‘Z’ for Coordinated Universal Time.
|
||||
//! > Any included periods are ignored. By following a non-daylight-saving
|
||||
//! > time zone by the string ‘DST’ in a separate word (that is, separated by
|
||||
//! > some white space), the corresponding daylight saving time zone may be
|
||||
//! > specified. Alternatively, a non-daylight-saving time zone can be
|
||||
//! > followed by a time zone correction, to add the two values. This is
|
||||
//! > normally done only for ‘UTC’; for example, ‘UTC+05:30’ is equivalent to
|
||||
//! > ‘+05:30’.
|
||||
//! >
|
||||
//! > Time zone items other than ‘UTC’ and ‘Z’ are obsolescent and are not
|
||||
//! > recommended, because they are ambiguous; for example, ‘EST’ has a
|
||||
//! > different meaning in Australia than in the United States, and ‘A’ has
|
||||
//! > different meaning as a military time zone than as an obsolete RFC 822
|
||||
//! > time zone. Instead, it's better to use unambiguous numeric time zone
|
||||
//! > corrections like ‘-0500’, as described in the previous section.
|
||||
//! >
|
||||
//! > If neither a time zone item nor a time zone correction is supplied,
|
||||
//! > timestamps are interpreted using the rules of the default time zone.
|
||||
|
||||
use super::time;
|
||||
use std::fmt::Display;
|
||||
|
||||
pub(crate) fn parse(input: &mut &str) -> ModalResult<time::Offset> {
|
||||
time::timezone(input)
|
||||
use winnow::{
|
||||
combinator::{alt, peek},
|
||||
error::{ContextError, ErrMode},
|
||||
stream::{AsChar, Stream},
|
||||
token::take_while,
|
||||
ModalResult, Parser,
|
||||
};
|
||||
|
||||
use super::{
|
||||
primitive::{colon, ctx_err, dec_uint, dec_uint_str, plus_or_minus, s},
|
||||
relative,
|
||||
};
|
||||
|
||||
/// Represents a time zone offset from UTC.
|
||||
///
|
||||
/// This struct is used to represent a time zone offset in hours and minutes,
|
||||
/// with a boolean indicating whether the offset is negative (i.e., west of
|
||||
/// UTC).
|
||||
#[derive(PartialEq, Debug, Clone, Default)]
|
||||
pub(super) struct Offset {
|
||||
negative: bool,
|
||||
hours: u8,
|
||||
minutes: u8,
|
||||
}
|
||||
|
||||
impl Offset {
|
||||
/// Merge two timezone offsets.
|
||||
///
|
||||
/// Note: when parsing an offset from a string (e.g., "+08:00"), the hours
|
||||
/// and minutes are validated to ensure they fall within valid bounds. In
|
||||
/// contrast, merging two offsets does not perform such validation. This
|
||||
/// behavior is intentional to match GNU date.
|
||||
fn merge(self, offset: Offset) -> Offset {
|
||||
fn combine(a: u16, neg_a: bool, b: u16, neg_b: bool) -> (u16, bool) {
|
||||
if neg_a == neg_b {
|
||||
(a + b, neg_a)
|
||||
} else if a > b {
|
||||
(a - b, neg_a)
|
||||
} else {
|
||||
(b - a, neg_b)
|
||||
}
|
||||
}
|
||||
let (total_minutes, negative) = combine(
|
||||
(self.hours as u16) * 60 + (self.minutes as u16),
|
||||
self.negative,
|
||||
(offset.hours as u16) * 60 + (offset.minutes as u16),
|
||||
offset.negative,
|
||||
);
|
||||
let hours = (total_minutes / 60) as u8;
|
||||
let minutes = (total_minutes % 60) as u8;
|
||||
|
||||
Offset {
|
||||
negative,
|
||||
hours,
|
||||
minutes,
|
||||
}
|
||||
}
|
||||
|
||||
/// Normalize the offset so that the hour field is within the accepted range.
|
||||
///
|
||||
/// - If the hour field is less than 24, or exactly 24 with a zero minute,
|
||||
/// the offset is already normalized, and the function returns the offset
|
||||
/// itself along with a zero hour adjustment.
|
||||
/// - Otherwise, the hour field is reduced to 23 while preserving the minute
|
||||
/// field, and the function returns the normalized offset along with the
|
||||
/// hour adjustment needed to reach the original offset.
|
||||
pub(super) fn normalize(self) -> (Offset, i8) {
|
||||
if self.hours < 24 || (self.hours == 24 && self.minutes == 0) {
|
||||
return (self, 0);
|
||||
}
|
||||
|
||||
let hour_adjustment = (self.hours as i8 - 23) * if self.negative { 1 } else { -1 };
|
||||
(
|
||||
Offset {
|
||||
negative: self.negative,
|
||||
hours: 23,
|
||||
minutes: self.minutes,
|
||||
},
|
||||
hour_adjustment,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<(bool, u8, u8)> for Offset {
|
||||
type Error = &'static str;
|
||||
|
||||
fn try_from((negative, hours, minutes): (bool, u8, u8)) -> Result<Self, Self::Error> {
|
||||
if hours > 24 {
|
||||
return Err("timezone hour must be between 0 and 24");
|
||||
}
|
||||
if minutes > 60 || (hours == 24 && minutes != 0) {
|
||||
return Err("timezone minute must be between 0 and 60");
|
||||
}
|
||||
|
||||
Ok(Offset {
|
||||
negative,
|
||||
hours,
|
||||
minutes,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&Offset> for jiff::tz::TimeZone {
|
||||
type Error = &'static str;
|
||||
|
||||
fn try_from(
|
||||
Offset {
|
||||
negative,
|
||||
hours,
|
||||
minutes,
|
||||
}: &Offset,
|
||||
) -> Result<Self, Self::Error> {
|
||||
let secs = (*hours as i32) * 3600 + (*minutes as i32) * 60;
|
||||
let secs = if *negative { -secs } else { secs };
|
||||
|
||||
let offset = jiff::tz::Offset::from_seconds(secs).map_err(|_| "offset is invalid")?;
|
||||
Ok(jiff::tz::TimeZone::fixed(offset))
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Offset {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
|
||||
write!(
|
||||
fmt,
|
||||
"{}{:02}:{:02}",
|
||||
if self.negative { "-" } else { "+" },
|
||||
self.hours,
|
||||
self.minutes
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn parse(input: &mut &str) -> ModalResult<Offset> {
|
||||
timezone_name_offset.parse_next(input)
|
||||
}
|
||||
|
||||
/// Parse a timezone starting with `+` or `-`.
|
||||
pub(super) fn timezone_offset(input: &mut &str) -> ModalResult<Offset> {
|
||||
// Strings like "+8 years" are ambiguous, they can either be parsed as a
|
||||
// timezone offset "+8" and a relative time "years", or just a relative time
|
||||
// "+8 years". GNU date parses them the second way, so we do the same here.
|
||||
//
|
||||
// Return early if the input can be parsed as a relative time.
|
||||
if peek(relative::parse).parse_next(input).is_ok() {
|
||||
return Err(ErrMode::Backtrack(ContextError::new()));
|
||||
}
|
||||
|
||||
alt((timezone_offset_colon, timezone_offset_colonless)).parse_next(input)
|
||||
}
|
||||
|
||||
/// Parse a timezone by name, with an optional numeric offset appended.
|
||||
fn timezone_name_offset(input: &mut &str) -> ModalResult<Offset> {
|
||||
/// I'm assuming there are no timezone abbreviations with more
|
||||
/// than 6 charactres
|
||||
const MAX_TZ_SIZE: usize = 6;
|
||||
let nextword = s(take_while(1..=MAX_TZ_SIZE, AsChar::is_alpha)).parse_next(input)?;
|
||||
let tz = timezone_name_to_offset(nextword)?;
|
||||
|
||||
// Strings like "UTC +8 years" are ambiguous, they can either be parsed as
|
||||
// "UTC+8" and "years", or "UTC" and "+8 years". GNU date parses them the
|
||||
// second way, so we do the same here.
|
||||
//
|
||||
// Only process if the input cannot be parsed as a relative time.
|
||||
if peek(relative::parse).parse_next(input).is_err() {
|
||||
let start = input.checkpoint();
|
||||
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)
|
||||
}
|
||||
|
||||
/// Parse a timezone offset with a colon separating hours and minutes, e.g.,
|
||||
/// `+08:00`, `+8:00`, `+8:0`.
|
||||
fn timezone_offset_colon(input: &mut &str) -> ModalResult<Offset> {
|
||||
(plus_or_minus, s(dec_uint), s(colon), s(dec_uint))
|
||||
.parse_next(input)
|
||||
.and_then(|(sign, hours, _, minutes)| {
|
||||
(sign == '-', hours, minutes)
|
||||
.try_into()
|
||||
.map_err(|e| ErrMode::Cut(ctx_err(e)))
|
||||
})
|
||||
}
|
||||
|
||||
/// Parse a timezone offset without colon, e.g., `+0800`, `+800`, `+08`, `+8`.
|
||||
fn timezone_offset_colonless(input: &mut &str) -> ModalResult<Offset> {
|
||||
(plus_or_minus, s(dec_uint_str))
|
||||
.verify_map(|(sign, s)| {
|
||||
// GNU date accepts numeric offset strings with leading zeroes. For
|
||||
// example, `+000000110` is valid. In such cases, the string is
|
||||
// truncated to the last four characters. Thus, `+000000110` becomes
|
||||
// `+0110` (note that one leading zero is kept).
|
||||
let s = if s.len() > 4 && s.trim_start_matches('0').len() <= 4 {
|
||||
&s[s.len() - 4..]
|
||||
} else {
|
||||
s
|
||||
};
|
||||
|
||||
// Hour and minute values are dependent on the length of the string.
|
||||
// For example:
|
||||
//
|
||||
// - "5" -> 05:00
|
||||
// - "05" -> 05:00
|
||||
// - "530" -> 05:30 (the minute is the last two characters here)
|
||||
// - "0530"-> 05:30
|
||||
// - "0000530" -> 05:30
|
||||
let (h_str, m_str) = match s.len() {
|
||||
1 | 2 => (s, "0"),
|
||||
3 => s.split_at(1),
|
||||
4 => s.split_at(2),
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
let hours = h_str.parse::<u8>().ok()?;
|
||||
let minutes = m_str.parse::<u8>().ok()?;
|
||||
Some((sign, hours, minutes))
|
||||
})
|
||||
.parse_next(input)
|
||||
.and_then(|(sign, hours, minutes)| {
|
||||
(sign == '-', hours, minutes)
|
||||
.try_into()
|
||||
.map_err(|e| ErrMode::Cut(ctx_err(e)))
|
||||
})
|
||||
}
|
||||
|
||||
/// Named timezone list.
|
||||
///
|
||||
/// The full list of timezones can be extracted from
|
||||
/// https://www.timeanddate.com/time/zones/. GNU date only supports a subset of
|
||||
/// these. We support the same subset as GNU date.
|
||||
fn timezone_name_to_offset(input: &str) -> ModalResult<Offset> {
|
||||
let mut offset_str = match input {
|
||||
"z" => Ok("+0"),
|
||||
"y" => Ok("-12"),
|
||||
"x" => Ok("-11"),
|
||||
"wet" => Ok("+0"),
|
||||
"west" => Ok("+1"),
|
||||
"wat" => Ok("+1"),
|
||||
"w" => Ok("-10"),
|
||||
"v" => Ok("-9"),
|
||||
"utc" => Ok("+0"),
|
||||
"u" => Ok("-8"),
|
||||
"t" => Ok("-7"),
|
||||
"sst" => Ok("-11"),
|
||||
"sgt" => Ok("+8"),
|
||||
"sast" => Ok("+2"),
|
||||
"s" => Ok("-6"),
|
||||
"r" => Ok("-5"),
|
||||
"q" => Ok("-4"),
|
||||
"pst" => Ok("-8"),
|
||||
"pdt" => Ok("-7"),
|
||||
"p" => Ok("-3"),
|
||||
"o" => Ok("-2"),
|
||||
"nzst" => Ok("+12"),
|
||||
"nzdt" => Ok("+13"),
|
||||
"nst" => Ok("-3:30"),
|
||||
"ndt" => Ok("-2:30"),
|
||||
"n" => Ok("-1"),
|
||||
"mst" => Ok("-7"),
|
||||
"msk" => Ok("+3"),
|
||||
"msd" => Ok("+4"),
|
||||
"mdt" => Ok("-6"),
|
||||
"m" => Ok("+12"),
|
||||
"l" => Ok("+11"),
|
||||
"k" => Ok("+10"),
|
||||
"jst" => Ok("+9"),
|
||||
"ist" => Ok("+5:30"),
|
||||
"i" => Ok("+9"),
|
||||
"hst" => Ok("-10"),
|
||||
"h" => Ok("+8"),
|
||||
"gst" => Ok("+4"),
|
||||
"gmt" => Ok("+0"),
|
||||
"g" => Ok("+7"),
|
||||
"f" => Ok("+6"),
|
||||
"est" => Ok("-5"),
|
||||
"eet" => Ok("+2"),
|
||||
"eest" => Ok("+3"),
|
||||
"edt" => Ok("-4"),
|
||||
"eat" => Ok("+3"),
|
||||
"e" => Ok("+5"),
|
||||
"d" => Ok("+4"),
|
||||
"cst" => Ok("-6"),
|
||||
"clt" => Ok("-4"),
|
||||
"clst" => Ok("-3"),
|
||||
"cet" => Ok("+1"),
|
||||
"cest" => Ok("+2"),
|
||||
"cdt" => Ok("-5"),
|
||||
"cat" => Ok("+2"),
|
||||
"c" => Ok("+3"),
|
||||
"bst" => Ok("+6"),
|
||||
"brt" => Ok("-3"),
|
||||
"brst" => Ok("-2"),
|
||||
"b" => Ok("+2"),
|
||||
"ast" => Ok("-3"),
|
||||
"art" => Ok("-3"),
|
||||
"akst" => Ok("-9"),
|
||||
"akdt" => Ok("-8"),
|
||||
"adt" => Ok("+4"),
|
||||
"a" => Ok("+1"),
|
||||
_ => Err(ErrMode::Backtrack(ContextError::new())),
|
||||
}?;
|
||||
|
||||
timezone_offset(&mut offset_str)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn off(negative: bool, hours: u8, minutes: u8) -> Offset {
|
||||
Offset {
|
||||
negative,
|
||||
hours,
|
||||
minutes,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn timezone_offset_with_colon() {
|
||||
for (input, expected) in [
|
||||
("+00:00", off(false, 0, 0)), // UTC
|
||||
("-00:00", off(true, 0, 0)), // UTC
|
||||
("+01:00", off(false, 1, 0)), // positive offset
|
||||
("-06:00", off(true, 6, 0)), // negative offset
|
||||
("+05:30", off(false, 5, 30)), // positive offset with non-zero minutes
|
||||
("-03:30", off(true, 3, 30)), // negative offset with non-zero minutes
|
||||
("- 06:00", off(true, 6, 0)), // space after sign
|
||||
("- 06 : 00", off(true, 6, 0)), // space around colon
|
||||
("+5:3", off(false, 5, 3)), // single-digit hours and single-digit minutes
|
||||
("+5:03", off(false, 5, 3)), // single-digit hours
|
||||
("+05:3", off(false, 5, 3)), // single-digit minutes
|
||||
("+00005:00030", off(false, 5, 30)), // leading zeroes in hours and minutes
|
||||
("+00:00abc", off(false, 0, 0)), // space separator can be ignored if immediately followed by alphas (GNU date behavior)
|
||||
] {
|
||||
let mut s = input;
|
||||
assert_eq!(timezone_offset(&mut s).unwrap(), expected, "{input}");
|
||||
}
|
||||
|
||||
for input in [
|
||||
"+25:00", // invalid: hours > 24
|
||||
"-23:61", // invalid: minutes > 60
|
||||
"+24:01", // invalid: minutes > 0 when hours == 24
|
||||
] {
|
||||
let mut s = input;
|
||||
assert!(timezone_offset(&mut s).is_err(), "{input}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn timezone_offset_without_colon() {
|
||||
for (input, expected) in [
|
||||
("+0000", off(false, 0, 0)), // UTC
|
||||
("-0000", off(true, 0, 0)), // UTC
|
||||
("+0100", off(false, 1, 0)), // positive offset
|
||||
("-0600", off(true, 6, 0)), // negative offset
|
||||
("+0530", off(false, 5, 30)), // positive offset with non-zero minutes
|
||||
("-0330", off(true, 3, 30)), // negative offset with non-zero minutes
|
||||
("- 0330", off(true, 3, 30)), // space after sign
|
||||
("+530", off(false, 5, 30)), // single-digit hours
|
||||
("+05", off(false, 5, 0)), // double-digit hours and no minutes
|
||||
("+5", off(false, 5, 0)), // single-digit hours and no minutes
|
||||
("+00000530", off(false, 5, 30)), // leading zeroes
|
||||
("+0000abc", off(false, 0, 0)), // space separator can be ignored if immediately followed by alphas (GNU date behavior)
|
||||
] {
|
||||
let mut s = input;
|
||||
assert_eq!(timezone_offset(&mut s).unwrap(), expected, "{input}");
|
||||
}
|
||||
|
||||
for input in [
|
||||
"+2500", // invalid: hours > 24
|
||||
"-2361", // invalid: minutes > 60
|
||||
"+2401", // invalid: minutes > 0 when hours == 24
|
||||
"+23 days", // invalid: ambiguous with relative time parsing
|
||||
] {
|
||||
let mut s = input;
|
||||
assert!(timezone_offset(&mut s).is_err(), "{input}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn timezone_name_without_offset() {
|
||||
for (input, expected) in [
|
||||
("utc", off(false, 0, 0)), // UTC
|
||||
("gmt", off(false, 0, 0)), // UTC
|
||||
("z", off(false, 0, 0)), // UTC
|
||||
("west", off(false, 1, 0)), // positive offset
|
||||
("cst", off(true, 6, 0)), // negative offset
|
||||
("ist", off(false, 5, 30)), // positive offset with non-zero minutes
|
||||
("nst", off(true, 3, 30)), // negative offset with non-zero minutes
|
||||
("z123", off(false, 0, 0)), // space separator can be ignored if immediately followed by digits (GNU date behavior)
|
||||
] {
|
||||
let mut s = input;
|
||||
assert_eq!(timezone_name_offset(&mut s).unwrap(), expected, "{input}");
|
||||
}
|
||||
|
||||
for input in [
|
||||
"abc", // invalid: non-existent timezone
|
||||
"utcabc", // invalid: non-existent timezone
|
||||
] {
|
||||
let mut s = input;
|
||||
assert!(timezone_name_offset(&mut s).is_err(), "{input}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn timezone_name_with_offset() {
|
||||
for (input, expected) in [
|
||||
("utc+5:30", off(false, 5, 30)), // UTC with possitive offset
|
||||
("utc-5:30", off(true, 5, 30)), // UTC with negative offset
|
||||
("utc +5:30", off(false, 5, 30)), // space after timezone name
|
||||
("utc + 5 : 30", off(false, 5, 30)), // spaces
|
||||
("a+5:30", off(false, 6, 30)), // merge two positive offsets (a=+1)
|
||||
("a-5:30", off(true, 4, 30)), // merge positive and negative offsets (a=+1)
|
||||
("n-5:30", off(true, 6, 30)), // merge two negative offsets (n=-1)
|
||||
("n+5:30", off(false, 4, 30)), // merge negative and positive offsets (n=-1)
|
||||
("m+24", off(false, 36, 0)), // maximum possible positive offset (m=+12)
|
||||
("y-24", off(true, 36, 0)), // maximum possible negative offset (y=-12)
|
||||
] {
|
||||
let mut s = input;
|
||||
assert_eq!(timezone_name_offset(&mut s).unwrap(), expected, "{input}");
|
||||
}
|
||||
|
||||
for input in [
|
||||
"abc+08:00", // invalid: non-existent timezone
|
||||
"utc+25", // invalid: invalid offset
|
||||
"utc+23 days", // invalid: ambiguous with relative time parsing
|
||||
] {
|
||||
let mut s = input;
|
||||
assert!(
|
||||
timezone_name_offset(&mut s).is_err() || !s.is_empty(),
|
||||
"{input}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,21 +41,21 @@ pub(crate) enum Day {
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Debug)]
|
||||
pub struct Weekday {
|
||||
pub(crate) struct Weekday {
|
||||
pub(crate) offset: i32,
|
||||
pub(crate) day: Day,
|
||||
}
|
||||
|
||||
impl From<Day> for chrono::Weekday {
|
||||
impl From<Day> for jiff::civil::Weekday {
|
||||
fn from(value: Day) -> Self {
|
||||
match value {
|
||||
Day::Monday => chrono::Weekday::Mon,
|
||||
Day::Tuesday => chrono::Weekday::Tue,
|
||||
Day::Wednesday => chrono::Weekday::Wed,
|
||||
Day::Thursday => chrono::Weekday::Thu,
|
||||
Day::Friday => chrono::Weekday::Fri,
|
||||
Day::Saturday => chrono::Weekday::Sat,
|
||||
Day::Sunday => chrono::Weekday::Sun,
|
||||
Day::Monday => jiff::civil::Weekday::Monday,
|
||||
Day::Tuesday => jiff::civil::Weekday::Tuesday,
|
||||
Day::Wednesday => jiff::civil::Weekday::Wednesday,
|
||||
Day::Thursday => jiff::civil::Weekday::Thursday,
|
||||
Day::Friday => jiff::civil::Weekday::Friday,
|
||||
Day::Saturday => jiff::civil::Weekday::Saturday,
|
||||
Day::Sunday => jiff::civil::Weekday::Sunday,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+15
-19
@@ -10,19 +10,15 @@
|
||||
//! strings. For example, `"00"` is interpreted as `2000`, whereas `"0"`,
|
||||
//! `"000"`, or `"0000"` are interpreted as `0`.
|
||||
|
||||
use winnow::{error::ErrMode, stream::AsChar, token::take_while, ModalResult, Parser};
|
||||
use winnow::{stream::AsChar, token::take_while, ModalResult, Parser};
|
||||
|
||||
use super::primitive::{ctx_err, s};
|
||||
|
||||
pub(super) fn parse(input: &mut &str) -> ModalResult<u32> {
|
||||
year_from_str(year_str(input)?).map_err(|e| ErrMode::Cut(ctx_err(e)))
|
||||
}
|
||||
use super::primitive::s;
|
||||
|
||||
// TODO: Leverage `TryFrom` trait.
|
||||
pub(super) fn year_from_str(year_str: &str) -> Result<u32, &'static str> {
|
||||
pub(super) fn year_from_str(year_str: &str) -> Result<u16, &'static str> {
|
||||
let mut year = year_str
|
||||
.parse::<u32>()
|
||||
.map_err(|_| "year must be a valid number")?;
|
||||
.parse::<u16>()
|
||||
.map_err(|_| "year must be a valid u16 number")?;
|
||||
|
||||
// If year is 68 or smaller, then 2000 is added to it; otherwise, if year
|
||||
// is less than 100, then 1900 is added to it.
|
||||
@@ -56,23 +52,23 @@ pub(super) fn year_str<'a>(input: &mut &'a str) -> ModalResult<&'a str> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::parse;
|
||||
use super::year_from_str;
|
||||
|
||||
#[test]
|
||||
fn test_year() {
|
||||
// 2-characters are converted to 19XX/20XX
|
||||
assert_eq!(parse(&mut "10").unwrap(), 2010u32);
|
||||
assert_eq!(parse(&mut "68").unwrap(), 2068u32);
|
||||
assert_eq!(parse(&mut "69").unwrap(), 1969u32);
|
||||
assert_eq!(parse(&mut "99").unwrap(), 1999u32);
|
||||
assert_eq!(year_from_str("10").unwrap(), 2010u16);
|
||||
assert_eq!(year_from_str("68").unwrap(), 2068u16);
|
||||
assert_eq!(year_from_str("69").unwrap(), 1969u16);
|
||||
assert_eq!(year_from_str("99").unwrap(), 1999u16);
|
||||
|
||||
// 3,4-characters are converted verbatim
|
||||
assert_eq!(parse(&mut "468").unwrap(), 468u32);
|
||||
assert_eq!(parse(&mut "469").unwrap(), 469u32);
|
||||
assert_eq!(parse(&mut "1568").unwrap(), 1568u32);
|
||||
assert_eq!(parse(&mut "1569").unwrap(), 1569u32);
|
||||
assert_eq!(year_from_str("468").unwrap(), 468u16);
|
||||
assert_eq!(year_from_str("469").unwrap(), 469u16);
|
||||
assert_eq!(year_from_str("1568").unwrap(), 1568u16);
|
||||
assert_eq!(year_from_str("1569").unwrap(), 1569u16);
|
||||
|
||||
// years greater than 9999 are not accepted
|
||||
assert!(parse(&mut "10000").is_err());
|
||||
assert!(year_from_str("10000").is_err());
|
||||
}
|
||||
}
|
||||
|
||||
+186
-202
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user