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
68 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| eb610870e1 | |||
| 9db7a54926 | |||
| e43652c0ee | |||
| d9c2ed2b11 | |||
| 6a16e61ab1 | |||
| f0c499593e | |||
| 5453d6777a | |||
| 0c09d5bd17 | |||
| 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.13.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 = "../"
|
||||
|
||||
+178
-208
@@ -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, offset, relative, time, 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,14 @@ 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>,
|
||||
offset: Option<offset::Offset>,
|
||||
timezone: Option<jiff::tz::TimeZone>,
|
||||
relative: Vec<relative::Relative>,
|
||||
}
|
||||
|
||||
@@ -28,167 +29,183 @@ 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 the timezone rule for the builder.
|
||||
///
|
||||
/// By default, the builder uses the time zone rules indicated by the `TZ`
|
||||
/// environment variable, or the system default rules if `TZ` is not set.
|
||||
/// This method allows overriding the time zone rules.
|
||||
fn set_timezone(mut self, tz: jiff::tz::TimeZone) -> Result<Self, &'static str> {
|
||||
if self.timezone.is_some() {
|
||||
return Err("timezone rule cannot appear more than once");
|
||||
}
|
||||
|
||||
self.timezone = Some(tz);
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
/// 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.offset.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)
|
||||
fn set_date(mut self, date: date::Date) -> Result<Self, &'static str> {
|
||||
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_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)
|
||||
fn set_time(mut self, time: time::Time) -> Result<Self, &'static str> {
|
||||
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.offset.is_some() && time.offset.is_some() {
|
||||
return Err("time offset and timezone are mutually exclusive");
|
||||
}
|
||||
|
||||
self.time = Some(time);
|
||||
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")
|
||||
} 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)
|
||||
fn set_weekday(mut self, weekday: weekday::Weekday) -> Result<Self, &'static str> {
|
||||
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_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)
|
||||
fn set_offset(mut self, timezone: offset::Offset) -> Result<Self, &'static str> {
|
||||
if self.timestamp.is_some() {
|
||||
return Err("timestamp cannot be combined with other date/time items");
|
||||
} else if self.offset.is_some()
|
||||
|| self.time.as_ref().and_then(|t| t.offset.as_ref()).is_some()
|
||||
{
|
||||
return Err("time offset cannot appear more than once");
|
||||
}
|
||||
|
||||
self.offset = Some(timezone);
|
||||
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")
|
||||
} 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)
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn push_relative(mut self, relative: relative::Relative) -> Self {
|
||||
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.
|
||||
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(if let Some(tz) = &self.timezone {
|
||||
jiff::Timestamp::now().to_zoned(tz.clone())
|
||||
} else {
|
||||
Zoned::now()
|
||||
});
|
||||
|
||||
// If a timestamp is set, we use it to build the `Zoned` object.
|
||||
if let Some(ts) = self.timestamp {
|
||||
return Ok(jiff::Timestamp::try_from(ts)?.to_zoned(base.offset().to_time_zone()));
|
||||
}
|
||||
|
||||
// If any of the following items are set, we truncate the time portion
|
||||
// of the base date to zero; otherwise, we use the base date as is.
|
||||
let mut dt = if self.timestamp.is_none()
|
||||
&& self.date.is_none()
|
||||
let mut dt = if self.date.is_none()
|
||||
&& self.time.is_none()
|
||||
&& self.weekday.is_none()
|
||||
&& self.offset.is_none()
|
||||
&& self.timezone.is_none()
|
||||
{
|
||||
base
|
||||
} else {
|
||||
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 +217,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 +237,54 @@ 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)?;
|
||||
if let Some(offset) = self.offset {
|
||||
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::Offset(offset) => builder.set_offset(offset)?,
|
||||
Item::Relative(rel) => builder.push_relative(rel)?,
|
||||
Item::TimeZone(tz) => builder.set_timezone(tz)?,
|
||||
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())
|
||||
}
|
||||
}
|
||||
+219
-144
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,468 @@
|
||||
// For the full copyright and license information, please view the LICENSE
|
||||
// file that was distributed with this source code.
|
||||
|
||||
//! Parse an offset 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 std::fmt::Display;
|
||||
|
||||
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}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
+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
+334
-4
@@ -1,10 +1,340 @@
|
||||
// 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. The timezone item must be at the beginning of the
|
||||
//! input string and in the `TZ="..."` format.
|
||||
//!
|
||||
//! From the GNU docs:
|
||||
//!
|
||||
//! > Normally, dates are interpreted using the rules of the current time zone,
|
||||
//! > which in turn are specified by the ‘TZ’ environment variable, or by a
|
||||
//! > system default if ‘TZ’ is not set. To specify a different set of default
|
||||
//! > time zone rules that apply just to one date, start the date with a string
|
||||
//! > of the form ‘TZ="RULE"’. The two quote characters (‘"’) must be present in
|
||||
//! > the date, and any quotes or backslashes within RULE must be escaped by a
|
||||
//! > backslash.
|
||||
|
||||
use super::time;
|
||||
use jiff::tz::{Offset, TimeZone};
|
||||
use winnow::{
|
||||
combinator::{alt, delimited, opt, preceded, repeat},
|
||||
stream::AsChar,
|
||||
token::{one_of, take_while},
|
||||
ModalResult, Parser,
|
||||
};
|
||||
|
||||
pub(crate) fn parse(input: &mut &str) -> ModalResult<time::Offset> {
|
||||
time::timezone(input)
|
||||
use super::primitive::{dec_uint, plus_or_minus};
|
||||
|
||||
pub(super) fn parse(input: &mut &str) -> ModalResult<TimeZone> {
|
||||
delimited("TZ=\"", preceded(opt(':'), alt((posix, iana))), '"').parse_next(input)
|
||||
}
|
||||
|
||||
/// Parse a posix (proleptic) timezone string (e.g., "UTC7", "JST-9").
|
||||
///
|
||||
/// TODO: This implementation is incomplete. It currently only parses the
|
||||
/// `STDOFFSET` part of the format.
|
||||
///
|
||||
/// From the GNU docs:
|
||||
///
|
||||
/// > The proleptic format is:
|
||||
/// >
|
||||
/// > STDOFFSET[DST[OFFSET][,START[/TIME],END[/TIME]]]
|
||||
/// >
|
||||
/// > The STD string specifies the time zone abbreviation, which must be at
|
||||
/// > least three bytes long. ...
|
||||
/// >
|
||||
/// > The OFFSET specifies the time value you must add to the local time to
|
||||
/// > get a UTC value. It has syntax like:
|
||||
/// >
|
||||
/// > [+|-]HH[:MM[:SS]]
|
||||
/// >
|
||||
/// > This is positive if the local time zone is west of the Prime Meridian
|
||||
/// > and negative if it is east; this is opposite from the usual convention
|
||||
/// > that positive time zone offsets are east of the Prime Meridian. The
|
||||
/// > hour HH must be between 0 and 24 and may be a single digit, and the
|
||||
/// > minutes MM and seconds SS, if present, must be between 0 and 59.
|
||||
fn posix(input: &mut &str) -> ModalResult<TimeZone> {
|
||||
(take_while(3.., AsChar::is_alpha), posix_offset)
|
||||
.verify_map(|(_, offset)| Offset::from_seconds(offset).ok().map(|o| o.to_time_zone()))
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
/// Parse an IANA (geographical) timezone string (e.g., "Europe/Paris"). If the
|
||||
/// string is not a valid IANA timezone name, the UTC timezone is returned.
|
||||
///
|
||||
/// Compatibility notes:
|
||||
///
|
||||
/// - The implementation uses `jiff::tz::TimeZone::get()` to resolve time zones.
|
||||
/// Only canonical/aliased IANA names are accepted. Absolute file paths are
|
||||
/// not supported.
|
||||
/// - GNU `date` resolves time zones from the tzdata files under
|
||||
/// `/usr/share/zoneinfo` (respecting `TZDIR`) and also accepts an absolute
|
||||
/// path when the string starts with `/`.
|
||||
///
|
||||
/// From the GNU docs:
|
||||
///
|
||||
/// > If the format's CHARACTERS begin with ‘/’ it is an absolute file
|
||||
/// > name; otherwise the library looks for the file
|
||||
/// > ‘/usr/share/zoneinfo/CHARACTERS’. The ‘zoneinfo’ directory contains
|
||||
/// > data files describing time zone rulesets in many different parts of the
|
||||
/// > world. The names represent major cities, with subdirectories for
|
||||
/// > geographical areas; for example, ‘America/New_York’, ‘Europe/London’,
|
||||
/// > ‘Asia/Tokyo’. These data files are installed by the system
|
||||
/// > administrator, who also sets ‘/etc/localtime’ to point to the data file
|
||||
/// > for the local time zone ruleset.
|
||||
fn iana(input: &mut &str) -> ModalResult<TimeZone> {
|
||||
repeat(
|
||||
0..,
|
||||
alt((
|
||||
preceded('\\', one_of(['\\', '"'])).map(|c: char| c.to_string()),
|
||||
take_while(1, |c| c != '"' && c != '\\').map(str::to_string),
|
||||
)),
|
||||
)
|
||||
.map(|parts: Vec<String>| parts.concat())
|
||||
.map(|s| TimeZone::get(&s).unwrap_or(TimeZone::UTC))
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
fn posix_offset(input: &mut &str) -> ModalResult<i32> {
|
||||
let uint = dec_uint::<u32, _>;
|
||||
|
||||
(
|
||||
opt(plus_or_minus),
|
||||
alt((
|
||||
(uint, preceded(':', uint), preceded(':', uint)).map(|(h, m, s)| (h, m, s)),
|
||||
(uint, preceded(':', uint)).map(|(h, m)| (h, m, 0)),
|
||||
uint.map(|h| (h, 0, 0)),
|
||||
)),
|
||||
)
|
||||
.map(|(sign, (h, m, s))| {
|
||||
// The sign is opposite from the usual convention:
|
||||
// - Positive offsets are west of UTC.
|
||||
// - Negative offsets are east of UTC.
|
||||
let sign = if sign == Some('-') { 1 } else { -1 };
|
||||
|
||||
// - If hour is greater than 24, clamp it to 24.
|
||||
// - If minute is greater than 59, clamp it to 59.
|
||||
// - If second is greater than 59, clamp it to 59.
|
||||
let h = h.min(24) as i32;
|
||||
let m = m.min(59) as i32;
|
||||
let s = s.min(59) as i32;
|
||||
|
||||
sign * (h * 3600 + m * 60 + s)
|
||||
})
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn tz_rule() {
|
||||
// empty string
|
||||
for (input, expected) in [
|
||||
(r#"TZ="""#, "UTC"),
|
||||
(r#"TZ=":""#, "UTC"),
|
||||
(r#"TZ=" ""#, "UTC"),
|
||||
(r#"TZ=": ""#, "UTC"),
|
||||
] {
|
||||
let mut s = input;
|
||||
assert_eq!(
|
||||
parse(&mut s).unwrap().iana_name(),
|
||||
Some(expected),
|
||||
"{input}"
|
||||
);
|
||||
}
|
||||
|
||||
// iana
|
||||
for (input, expected) in [
|
||||
(r#"TZ="Etc/Zulu""#, "Etc/Zulu"),
|
||||
(r#"TZ=":Etc/Zulu""#, "Etc/Zulu"),
|
||||
(r#"TZ="America/New_York""#, "America/New_York"),
|
||||
(r#"TZ=":America/New_York""#, "America/New_York"),
|
||||
(r#"TZ="Asia/Tokyo""#, "Asia/Tokyo"),
|
||||
(r#"TZ=":Asia/Tokyo""#, "Asia/Tokyo"),
|
||||
(r#"TZ="Unknown/Timezone""#, "UTC"),
|
||||
(r#"TZ=":Unknown/Timezone""#, "UTC"),
|
||||
] {
|
||||
let mut s = input;
|
||||
assert_eq!(
|
||||
parse(&mut s).unwrap().iana_name(),
|
||||
Some(expected),
|
||||
"{input}"
|
||||
);
|
||||
}
|
||||
|
||||
// posix
|
||||
for (input, expected) in [
|
||||
(r#"TZ="UTC0""#, 0),
|
||||
(r#"TZ=":UTC0""#, 0),
|
||||
(r#"TZ="UTC+5""#, -5 * 3600),
|
||||
(r#"TZ=":UTC+5""#, -5 * 3600),
|
||||
(r#"TZ="UTC-5""#, 5 * 3600),
|
||||
(r#"TZ=":UTC-5""#, 5 * 3600),
|
||||
(r#"TZ="UTC+5:20""#, -(5 * 3600 + 20 * 60)),
|
||||
(r#"TZ=":UTC+5:20""#, -(5 * 3600 + 20 * 60)),
|
||||
(r#"TZ="UTC-5:20""#, 5 * 3600 + 20 * 60),
|
||||
(r#"TZ=":UTC-5:20""#, 5 * 3600 + 20 * 60),
|
||||
(r#"TZ="UTC+5:20:15""#, -(5 * 3600 + 20 * 60 + 15)),
|
||||
(r#"TZ=":UTC+5:20:15""#, -(5 * 3600 + 20 * 60 + 15)),
|
||||
(r#"TZ="UTC-5:20:15""#, 5 * 3600 + 20 * 60 + 15),
|
||||
(r#"TZ=":UTC-5:20:15""#, 5 * 3600 + 20 * 60 + 15),
|
||||
] {
|
||||
let mut s = input;
|
||||
assert_eq!(
|
||||
parse(&mut s).unwrap().to_fixed_offset().unwrap().seconds(),
|
||||
expected,
|
||||
"{input}"
|
||||
);
|
||||
}
|
||||
|
||||
// invalid
|
||||
for input in [
|
||||
r#"UTC"#, // missing "TZ="
|
||||
r#"tz="UTC""#, // lowercase "tz"
|
||||
r#"TZ=UTC"#, // missing quotes
|
||||
] {
|
||||
let mut s = input;
|
||||
assert!(parse(&mut s).is_err(), "{input}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_iana() {
|
||||
for (input, expected) in [
|
||||
("UTC", "UTC"), // utc timezone
|
||||
("Etc/Zulu", "Etc/Zulu"), // etc timezone
|
||||
("America/New_York", "America/New_York"), // named timezone
|
||||
("Asia/Tokyo", "Asia/Tokyo"), // named timezone
|
||||
("Unknown/Timezone", "UTC"), // unknown timezone
|
||||
] {
|
||||
let mut s = input;
|
||||
assert_eq!(iana(&mut s).unwrap().iana_name(), Some(expected), "{input}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_posix() {
|
||||
let to_seconds = |input: &str| {
|
||||
let mut s = input;
|
||||
posix(&mut s).unwrap().to_fixed_offset().unwrap().seconds()
|
||||
};
|
||||
|
||||
// hour
|
||||
for (input, expected) in [
|
||||
("UTC0", 0),
|
||||
("UTC+0", 0),
|
||||
("UTC-0", 0),
|
||||
("UTC000", 0),
|
||||
("UTC+5", -5 * 3600),
|
||||
("UTC-5", 5 * 3600),
|
||||
("ABC0", 0),
|
||||
("ABC+5", -5 * 3600),
|
||||
("ABC-5", 5 * 3600),
|
||||
] {
|
||||
assert_eq!(to_seconds(input), expected, "{input}");
|
||||
}
|
||||
|
||||
// hour:minute
|
||||
for (input, expected) in [
|
||||
("UTC0:0", 0),
|
||||
("UTC+0:0", 0),
|
||||
("UTC-0:0", 0),
|
||||
("UTC00:00", 0),
|
||||
("UTC+5:20", -(5 * 3600 + 20 * 60)),
|
||||
("UTC-5:20", 5 * 3600 + 20 * 60),
|
||||
("ABC0:0", 0),
|
||||
("ABC+5:20", -(5 * 3600 + 20 * 60)),
|
||||
("ABC-5:20", 5 * 3600 + 20 * 60),
|
||||
] {
|
||||
assert_eq!(to_seconds(input), expected, "{input}");
|
||||
}
|
||||
|
||||
// hour:minute:second
|
||||
for (input, expected) in [
|
||||
("UTC0:0:0", 0),
|
||||
("UTC+0:0:0", 0),
|
||||
("UTC-0:0:0", 0),
|
||||
("UTC00:00:00", 0),
|
||||
("UTC+5:20:15", -(5 * 3600 + 20 * 60 + 15)),
|
||||
("UTC-5:20:15", 5 * 3600 + 20 * 60 + 15),
|
||||
("ABC0:0:0", 0),
|
||||
("ABC+5:20:15", -(5 * 3600 + 20 * 60 + 15)),
|
||||
("ABC-5:20:15", 5 * 3600 + 20 * 60 + 15),
|
||||
] {
|
||||
assert_eq!(to_seconds(input), expected, "{input}");
|
||||
}
|
||||
|
||||
// invalid
|
||||
for input in [
|
||||
"AB", // too short
|
||||
"A1C", // not just letters
|
||||
] {
|
||||
let mut s = input;
|
||||
assert!(posix(&mut s).is_err(), "{input}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_posix_offset() {
|
||||
// hour
|
||||
for (input, expected) in [
|
||||
("0", 0), // zero hour
|
||||
("00", 0), // zero hour, two digits
|
||||
("000", 0), // zero hour, three digits
|
||||
("+0", 0), // zero hour, explicit plus
|
||||
("-0", 0), // zero hour, explicit minus
|
||||
("5", -5 * 3600), // positive hour
|
||||
("-5", 5 * 3600), // negative hour
|
||||
("005", -5 * 3600), // positive hour with leading zeros
|
||||
("-05", 5 * 3600), // negative hour with leading zeros
|
||||
("25", -24 * 3600), // hour > 24, clamps to 24
|
||||
("-25", 24 * 3600), // hour > 24, clamps to 24
|
||||
] {
|
||||
let mut s = input;
|
||||
assert_eq!(posix_offset(&mut s).unwrap(), expected, "{input}");
|
||||
}
|
||||
|
||||
// hour:minute
|
||||
for (input, expected) in [
|
||||
("0:0", 0), // zero hour and minute
|
||||
("00:00", 0), // zero hour and minute, two digits
|
||||
("000:000", 0), // zero hour and minute, three digits
|
||||
("+0:0", 0), // zero hour and minute, explicit plus
|
||||
("-0:0", 0), // zero hour and minute, explicit minus
|
||||
("5:20", -(5 * 3600 + 20 * 60)), // positive hour and minute
|
||||
("-5:20", 5 * 3600 + 20 * 60), // negative hour and minute
|
||||
("005:020", -(5 * 3600 + 20 * 60)), // positive hour and minute with leading zeros
|
||||
("-05:20", 5 * 3600 + 20 * 60), // negative hour and minute with leading zeros
|
||||
("25:20", -(24 * 3600 + 20 * 60)), // hour > 24, clamps to 24
|
||||
("-25:20", 24 * 3600 + 20 * 60), // hour > 24, clamps to 24
|
||||
("5:60", -(5 * 3600 + 59 * 60)), // minute > 59, clamps to 59
|
||||
("-5:60", 5 * 3600 + 59 * 60), // minute > 59, clamps to 59
|
||||
] {
|
||||
let mut s = input;
|
||||
assert_eq!(posix_offset(&mut s).unwrap(), expected, "{input}");
|
||||
}
|
||||
|
||||
// hour:minute:second
|
||||
for (input, expected) in [
|
||||
("0:0:0", 0), // zero hour, minute, and second
|
||||
("00:00:00", 0), // zero hour, minute, and second, two digits
|
||||
("000:000:000", 0), // zero hour, minute, and second, three digits
|
||||
("+0:0:0", 0), // zero hour, minute, and second, explicit plus
|
||||
("-0:0:0", 0), // zero hour, minute, and second, explicit minus
|
||||
("5:20:15", -(5 * 3600 + 20 * 60 + 15)), // positive hour, minute, and second
|
||||
("-5:20:15", 5 * 3600 + 20 * 60 + 15), // negative hour, minute, and second
|
||||
("005:020:015", -(5 * 3600 + 20 * 60 + 15)), // positive hour, minute, and second with leading zeros
|
||||
("-05:20:15", 5 * 3600 + 20 * 60 + 15), // negative hour, minute, and second with leading zeros
|
||||
("25:20:15", -(24 * 3600 + 20 * 60 + 15)), // hour > 24, clamps to 24
|
||||
("-25:20:15", 24 * 3600 + 20 * 60 + 15), // hour > 24, clamps to 24
|
||||
("5:60:15", -(5 * 3600 + 59 * 60 + 15)), // minute > 59, clamps to 59
|
||||
("-5:60:15", 5 * 3600 + 59 * 60 + 15), // minute > 59, clamps to 59
|
||||
("5:20:60", -(5 * 3600 + 20 * 60 + 59)), // second > 59, clamps to 59
|
||||
("-5:20:60", 5 * 3600 + 20 * 60 + 59), // second > 59, clamps to 59
|
||||
] {
|
||||
let mut s = input;
|
||||
assert_eq!(posix_offset(&mut s).unwrap(), expected, "{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());
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user