Merge pull request #213 from yuankunzhang/replace-chrono-with-jiff-6

(jiff series #6) refactor: replace chrono with jiff
This commit is contained in:
Daniel Hofstetter
2025-08-20 17:18:32 +02:00
committed by GitHub
14 changed files with 479 additions and 681 deletions
Generated
+50 -184
View File
@@ -11,66 +11,18 @@ dependencies = [
"memchr",
]
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]]
name = "autocfg"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "bumpalo"
version = "3.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
[[package]]
name = "cc"
version = "1.2.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32db95edf998450acc7881c932f94cd9b05c87b4b2599e8bab064753da4acfd1"
dependencies = [
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
version = "0.4.41"
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.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "equivalent"
version = "1.0.2"
@@ -132,30 +84,6 @@ version = "0.15.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5"
[[package]]
name = "iana-time-zone"
version = "0.1.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"log",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "indexmap"
version = "2.9.0"
@@ -173,10 +101,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49"
dependencies = [
"jiff-static",
"jiff-tzdb-platform",
"log",
"portable-atomic",
"portable-atomic-util",
"serde",
"windows-sys",
]
[[package]]
@@ -191,20 +121,19 @@ dependencies = [
]
[[package]]
name = "js-sys"
version = "0.3.77"
name = "jiff-tzdb"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
dependencies = [
"once_cell",
"wasm-bindgen",
]
checksum = "c1283705eb0a21404d2bfd6eef2a7593d240bc42a0bdb39db0ad6fa2ec026524"
[[package]]
name = "libc"
version = "0.2.172"
name = "jiff-tzdb-platform"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8"
dependencies = [
"jiff-tzdb",
]
[[package]]
name = "log"
@@ -227,17 +156,10 @@ dependencies = [
"autocfg",
]
[[package]]
name = "once_cell"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "parse_datetime"
version = "0.11.0"
dependencies = [
"chrono",
"jiff",
"num-traits",
"regex",
@@ -372,12 +294,6 @@ dependencies = [
"semver",
]
[[package]]
name = "rustversion"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
[[package]]
name = "semver"
version = "1.0.26"
@@ -404,12 +320,6 @@ dependencies = [
"syn",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "slab"
version = "0.4.11"
@@ -451,121 +361,77 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
[[package]]
name = "wasm-bindgen"
version = "0.2.100"
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
"windows-targets",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.100"
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"bumpalo",
"log",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.100"
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.100"
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
dependencies = [
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.100"
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
dependencies = [
"unicode-ident",
]
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows-core"
version = "0.61.0"
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link",
"windows-result",
"windows-strings",
]
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows-implement"
version = "0.60.0"
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows-interface"
version = "0.59.1"
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows-link"
version = "0.1.1"
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows-result"
version = "0.3.2"
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97"
dependencies = [
"windows-link",
]
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
+1 -2
View File
@@ -10,10 +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 = ["std"] }
jiff = { version = "0.2.15", default-features = false, features = ["tz-system", "tzdb-bundle-platform", "tzdb-zoneinfo"] }
[dev-dependencies]
rstest = "0.26"
+10 -10
View File
@@ -4,7 +4,7 @@
[![License](http://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/uutils/parse_datetime/blob/main/LICENSE)
[![CodeCov](https://codecov.io/gh/uutils/parse_datetime/branch/main/graph/badge.svg)](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
+80 -11
View File
@@ -112,6 +112,31 @@ dependencies = [
"cc",
]
[[package]]
name = "jiff"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49"
dependencies = [
"jiff-static",
"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 = "jobserver"
version = "0.1.32"
@@ -148,9 +173,9 @@ dependencies = [
[[package]]
name = "log"
version = "0.4.18"
version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "518ef76f2f87365916b142844c16d8fefd85039bc5699050210a7778ee1cd1de"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
[[package]]
name = "memchr"
@@ -175,28 +200,43 @@ 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.97"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d61789d7719defeb74ea5fe81f2fdfdbd28a803847077cecce2ff14e1472f6f1"
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",
]
@@ -230,6 +270,26 @@ version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "serde"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "shlex"
version = "1.3.0"
@@ -238,9 +298,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",
@@ -323,6 +383,15 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3"
[[package]]
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
+43 -150
View File
@@ -1,7 +1,7 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
use chrono::{DateTime, Datelike, FixedOffset, NaiveDate, TimeZone, Timelike};
use jiff::{civil, Span, Zoned};
use super::{date, epoch, relative, time, timezone, weekday, year};
@@ -12,7 +12,7 @@ use super::{date, epoch, relative, time, timezone, weekday, year};
/// leave it unset to use the current date and time as the base.
#[derive(Debug, Default)]
pub(crate) struct DateTimeBuilder {
base: Option<DateTime<FixedOffset>>,
base: Option<Zoned>,
timestamp: Option<epoch::Timestamp>,
date: Option<date::Date>,
time: Option<time::Time>,
@@ -28,7 +28,7 @@ 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
}
@@ -148,27 +148,20 @@ impl DateTimeBuilder {
self.set_time(time)
}
fn build_from_timestamp(
ts: epoch::Timestamp,
tz: &FixedOffset,
) -> Option<DateTime<FixedOffset>> {
match chrono::Utc.timestamp_opt(ts.second, ts.nanosecond) {
chrono::MappedLocalTime::Single(t) => Some(t.with_timezone(tz)),
chrono::MappedLocalTime::Ambiguous(earliest, _latest) => {
// When there is a fold in the local time, we use the earliest
// one.
Some(earliest.with_timezone(tz))
}
chrono::MappedLocalTime::None => None, // Invalid timestamp
}
fn build_from_timestamp(ts: epoch::Timestamp, tz: jiff::tz::TimeZone) -> Option<Zoned> {
Some(
jiff::Timestamp::new(ts.second, ts.nanosecond as i32)
.ok()?
.to_zoned(tz),
)
}
pub(super) fn build(self) -> Option<DateTime<FixedOffset>> {
let base = self.base.unwrap_or_else(|| chrono::Local::now().into());
pub(super) fn build(self) -> Option<Zoned> {
let base = self.base.unwrap_or(Zoned::now());
// If a timestamp is set, we use it to build the DateTime object.
// If a timestamp is set, we use it to build the `Zoned` object.
if let Some(ts) = self.timestamp {
return Self::build_from_timestamp(ts, base.offset());
return Self::build_from_timestamp(ts, base.offset().to_time_zone());
}
// If any of the following items are set, we truncate the time portion
@@ -181,59 +174,30 @@ impl DateTimeBuilder {
{
base
} else {
new_date(
base.year(),
base.month(),
base.day(),
0,
0,
0,
0,
*base.offset(),
)?
base.with().time(civil::time(0, 0, 0, 0)).build().ok()?
};
if let Some(date::Date { year, month, day }) = self.date {
dt = new_date(
year.map(|x| x as i32).unwrap_or(dt.year()),
month as u32,
day as u32,
dt.hour(),
dt.minute(),
dt.second(),
dt.nanosecond(),
*dt.offset(),
)?;
if let Some(date) = self.date {
let d: civil::Date = if date.year.is_some() {
date.try_into().ok()?
} else {
date.with_year(dt.date().year() as u16).try_into().ok()?
};
dt = dt.with().date(d).build().ok()?;
}
if let Some(time::Time {
hour,
minute,
second,
nanosecond,
ref offset,
}) = self.time
{
let offset = offset
.clone()
.and_then(|o| chrono::FixedOffset::try_from(o).ok())
.unwrap_or(*dt.offset());
if let Some(time) = self.time.clone() {
if let Some(offset) = &time.offset {
dt = dt.datetime().to_zoned(offset.try_into().ok()?).ok()?;
}
dt = new_date(
dt.year(),
dt.month(),
dt.day(),
hour as u32,
minute as u32,
second as u32,
nanosecond,
offset,
)?;
let t: civil::Time = time.try_into().ok()?;
dt = dt.with().time(t).build().ok()?;
}
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().ok()?;
}
let mut offset = offset;
@@ -245,7 +209,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;
}
@@ -265,101 +229,30 @@ 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)?;
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).ok()?).ok()?;
}
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())?;
}
relative::Relative::Seconds(s, ns) => {
dt += chrono::Duration::seconds(s);
dt += chrono::Duration::nanoseconds(ns.into());
}
}
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()?
} else {
rel.try_into().ok()?
})
.ok()?;
}
if let Some(offset) = self.timezone {
dt = with_timezone_restore(offset, dt)?;
if let Some(offset) = &self.timezone {
dt = dt.datetime().to_zoned(offset.try_into().ok()?).ok()?;
}
Some(dt)
}
}
#[allow(clippy::too_many_arguments, deprecated)]
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))?;
Some(DateTime::<FixedOffset>::from_local(newdate, offset))
}
/// Restores year, month, day, etc after applying the timezone
/// returns None if timezone overflows the date
fn with_timezone_restore(
offset: timezone::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)
}
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()
}
+10
View File
@@ -47,6 +47,16 @@ pub(crate) struct Date {
pub(crate) year: Option<u16>,
}
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;
+55 -60
View File
@@ -45,7 +45,7 @@ mod ordinal;
mod primitive;
use builder::DateTimeBuilder;
use chrono::{DateTime, FixedOffset};
use jiff::Zoned;
use primitive::space;
use winnow::{
combinator::{alt, eof, terminated, trace},
@@ -68,22 +68,17 @@ pub(crate) enum Item {
Pure(String),
}
/// Build a `DateTime<FixedOffset>` from a `DateTimeBuilder` and a base date.
pub(crate) fn at_date(
builder: DateTimeBuilder,
base: DateTime<FixedOffset>,
) -> Result<DateTime<FixedOffset>, ParseDateTimeError> {
/// Build a `Zoned` object from a `DateTimeBuilder` and a base `Zoned` object.
pub(crate) fn at_date(builder: DateTimeBuilder, base: Zoned) -> Result<Zoned, ParseDateTimeError> {
builder
.set_base(base)
.build()
.ok_or(ParseDateTimeError::InvalidInput)
}
/// Build a `DateTime<FixedOffset>` from a `DateTimeBuilder` and the current
/// time.
pub(crate) fn at_local(
builder: DateTimeBuilder,
) -> Result<DateTime<FixedOffset>, ParseDateTimeError> {
/// Build a `Zoned` object from a `DateTimeBuilder` and a default `Zoned` object
/// (the current time in the local timezone).
pub(crate) fn at_local(builder: DateTimeBuilder) -> Result<Zoned, ParseDateTimeError> {
builder.build().ok_or(ParseDateTimeError::InvalidInput)
}
@@ -292,14 +287,12 @@ fn expect_error(input: &mut &str, reason: &'static str) -> ErrMode<ContextError>
#[cfg(test)]
mod tests {
use super::{at_date, parse, DateTimeBuilder};
use chrono::{
DateTime, Datelike, FixedOffset, NaiveDate, NaiveDateTime, NaiveTime, TimeZone, Timelike,
Utc,
};
use jiff::{civil::DateTime, tz::TimeZone, ToSpan, Zoned};
fn at_utc(builder: DateTimeBuilder) -> DateTime<FixedOffset> {
at_date(builder, Utc::now().fixed_offset()).unwrap()
use super::{at_date, parse, DateTimeBuilder};
fn at_utc(builder: DateTimeBuilder) -> Zoned {
at_date(builder, Zoned::now().with_time_zone(TimeZone::UTC)).unwrap()
}
fn test_eq_fmt(fmt: &str, input: &str) -> String {
@@ -308,7 +301,7 @@ mod tests {
.map(at_utc)
.map_err(|e| eprintln!("TEST FAILED AT:\n{e}"))
.expect("parsing failed during tests")
.format(fmt)
.strftime(fmt)
.to_string()
}
@@ -458,75 +451,77 @@ mod tests {
#[test]
fn relative_weekday() {
// Jan 1 2025 is a Wed
let now = Utc
.from_utc_datetime(&NaiveDateTime::new(
NaiveDate::from_ymd_opt(2025, 1, 1).unwrap(),
NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
))
.fixed_offset();
let now = "2025-01-01 00:00:00"
.parse::<DateTime>()
.unwrap()
.to_zoned(TimeZone::UTC)
.unwrap();
assert_eq!(
at_date(parse(&mut "last wed").unwrap(), now).unwrap(),
now - chrono::Duration::days(7)
);
assert_eq!(at_date(parse(&mut "this wed").unwrap(), now).unwrap(), now);
assert_eq!(
at_date(parse(&mut "next wed").unwrap(), now).unwrap(),
now + chrono::Duration::days(7)
at_date(parse(&mut "last wed").unwrap(), now.clone()).unwrap(),
now.checked_sub(7.days()).unwrap()
);
assert_eq!(
at_date(parse(&mut "last thu").unwrap(), now).unwrap(),
now - chrono::Duration::days(6)
at_date(parse(&mut "this wed").unwrap(), now.clone()).unwrap(),
now
);
assert_eq!(
at_date(parse(&mut "this thu").unwrap(), now).unwrap(),
now + chrono::Duration::days(1)
at_date(parse(&mut "next wed").unwrap(), now.clone()).unwrap(),
now.checked_add(7.days()).unwrap()
);
assert_eq!(
at_date(parse(&mut "next thu").unwrap(), now).unwrap(),
now + chrono::Duration::days(1)
at_date(parse(&mut "last thu").unwrap(), now.clone()).unwrap(),
now.checked_sub(6.days()).unwrap()
);
assert_eq!(
at_date(parse(&mut "1 wed").unwrap(), now).unwrap(),
now + chrono::Duration::days(7)
at_date(parse(&mut "this thu").unwrap(), now.clone()).unwrap(),
now.checked_add(1.days()).unwrap()
);
assert_eq!(
at_date(parse(&mut "1 thu").unwrap(), now).unwrap(),
now + chrono::Duration::days(1)
at_date(parse(&mut "next thu").unwrap(), now.clone()).unwrap(),
now.checked_add(1.days()).unwrap()
);
assert_eq!(
at_date(parse(&mut "2 wed").unwrap(), now).unwrap(),
now + chrono::Duration::days(14)
at_date(parse(&mut "1 wed").unwrap(), now.clone()).unwrap(),
now.checked_add(7.days()).unwrap()
);
assert_eq!(
at_date(parse(&mut "2 thu").unwrap(), now).unwrap(),
now + chrono::Duration::days(8)
at_date(parse(&mut "1 thu").unwrap(), now.clone()).unwrap(),
now.checked_add(1.days()).unwrap()
);
assert_eq!(
at_date(parse(&mut "2 wed").unwrap(), now.clone()).unwrap(),
now.checked_add(14.days()).unwrap()
);
assert_eq!(
at_date(parse(&mut "2 thu").unwrap(), now.clone()).unwrap(),
now.checked_add(8.days()).unwrap()
);
}
#[test]
fn relative_date_time() {
let now = Utc::now().fixed_offset();
let now = Zoned::now().with_time_zone(TimeZone::UTC);
let result = at_date(parse(&mut "2 days ago").unwrap(), now).unwrap();
assert_eq!(result, now - chrono::Duration::days(2));
let result = at_date(parse(&mut "2 days ago").unwrap(), now.clone()).unwrap();
assert_eq!(result, now.checked_sub(2.days()).unwrap());
assert_eq!(result.hour(), now.hour());
assert_eq!(result.minute(), now.minute());
assert_eq!(result.second(), now.second());
let result = at_date(parse(&mut "2 days 3 days ago").unwrap(), now).unwrap();
assert_eq!(result, now - chrono::Duration::days(1));
let result = at_date(parse(&mut "2 days 3 days ago").unwrap(), now.clone()).unwrap();
assert_eq!(result, now.checked_sub(1.days()).unwrap());
assert_eq!(result.hour(), now.hour());
assert_eq!(result.minute(), now.minute());
assert_eq!(result.second(), now.second());
let result = at_date(parse(&mut "2025-01-01 2 days ago").unwrap(), now).unwrap();
let result = at_date(parse(&mut "2025-01-01 2 days ago").unwrap(), now.clone()).unwrap();
assert_eq!(result.hour(), 0);
assert_eq!(result.minute(), 0);
assert_eq!(result.second(), 0);
let result = at_date(parse(&mut "3 weeks").unwrap(), now).unwrap();
assert_eq!(result, now + chrono::Duration::days(21));
let result = at_date(parse(&mut "3 weeks").unwrap(), now.clone()).unwrap();
assert_eq!(result, now.checked_add(21.days()).unwrap());
assert_eq!(result.hour(), now.hour());
assert_eq!(result.minute(), now.minute());
assert_eq!(result.second(), now.second());
@@ -539,26 +534,26 @@ mod tests {
#[test]
fn pure() {
let now = Utc::now().fixed_offset();
let now = Zoned::now().with_time_zone(TimeZone::UTC);
// Pure number as year.
let result = at_date(parse(&mut "jul 18 12:30 2025").unwrap(), now).unwrap();
let result = at_date(parse(&mut "jul 18 12:30 2025").unwrap(), now.clone()).unwrap();
assert_eq!(result.year(), 2025);
// Pure number as time.
let result = at_date(parse(&mut "1230").unwrap(), now).unwrap();
let result = at_date(parse(&mut "1230").unwrap(), now.clone()).unwrap();
assert_eq!(result.hour(), 12);
assert_eq!(result.minute(), 30);
let result = at_date(parse(&mut "123").unwrap(), now).unwrap();
let result = at_date(parse(&mut "123").unwrap(), now.clone()).unwrap();
assert_eq!(result.hour(), 1);
assert_eq!(result.minute(), 23);
let result = at_date(parse(&mut "12").unwrap(), now).unwrap();
let result = at_date(parse(&mut "12").unwrap(), now.clone()).unwrap();
assert_eq!(result.hour(), 12);
assert_eq!(result.minute(), 0);
let result = at_date(parse(&mut "1").unwrap(), now).unwrap();
let result = at_date(parse(&mut "1").unwrap(), now.clone()).unwrap();
assert_eq!(result.hour(), 1);
assert_eq!(result.minute(), 0);
}
+15 -10
View File
@@ -37,6 +37,8 @@ use winnow::{
ModalResult, Parser,
};
use crate::ParseDateTimeError;
use super::{epoch::sec_and_nsec, ordinal::ordinal, primitive::s};
#[derive(Clone, Copy, Debug, PartialEq)]
@@ -49,18 +51,21 @@ pub(crate) enum Relative {
Seconds(i64, u32),
}
impl From<Relative> for jiff::Span {
fn from(relative: Relative) -> Self {
impl TryFrom<Relative> for jiff::Span {
type Error = ParseDateTimeError;
fn try_from(relative: Relative) -> Result<Self, Self::Error> {
match relative {
Relative::Years(years) => jiff::Span::new().years(years),
Relative::Months(months) => jiff::Span::new().months(months),
Relative::Days(days) => jiff::Span::new().days(days),
Relative::Hours(hours) => jiff::Span::new().hours(hours),
Relative::Minutes(minutes) => jiff::Span::new().minutes(minutes),
Relative::Seconds(seconds, nanoseconds) => {
jiff::Span::new().seconds(seconds).nanoseconds(nanoseconds)
}
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(|_| ParseDateTimeError::InvalidInput)
}
}
+9 -29
View File
@@ -27,11 +27,10 @@
use std::fmt::Display;
use chrono::FixedOffset;
use winnow::{
combinator::{alt, peek, seq},
error::{ContextError, ErrMode},
stream::AsChar,
stream::{AsChar, Stream},
token::take_while,
ModalResult, Parser,
};
@@ -78,7 +77,7 @@ impl Offset {
}
}
impl TryFrom<Offset> for chrono::FixedOffset {
impl TryFrom<&Offset> for jiff::tz::TimeZone {
type Error = ParseDateTimeError;
fn try_from(
@@ -86,37 +85,16 @@ impl TryFrom<Offset> for chrono::FixedOffset {
negative,
hours,
minutes,
}: Offset,
}: &Offset,
) -> Result<Self, Self::Error> {
let secs = (hours as i32) * 3600 + (minutes as i32) * 60;
let offset = if negative {
FixedOffset::west_opt(secs).ok_or(ParseDateTimeError::InvalidInput)?
} else {
FixedOffset::east_opt(secs).ok_or(ParseDateTimeError::InvalidInput)?
};
Ok(offset)
}
}
impl TryFrom<Offset> for jiff::tz::Offset {
type Error = ParseDateTimeError;
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 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(|_| ParseDateTimeError::InvalidInput)?;
let tz = jiff::tz::TimeZone::fixed(offset);
Ok(offset)
Ok(tz)
}
}
@@ -217,11 +195,13 @@ fn timezone_name_offset(input: &mut &str) -> ModalResult<Offset> {
//
// 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_num.parse_next(input) {
let newtz = tz.merge(other_tz);
return Ok(newtz);
};
input.reset(&start);
}
Ok(tz)
-14
View File
@@ -46,20 +46,6 @@ pub(crate) struct Weekday {
pub(crate) day: Day,
}
impl From<Day> for chrono::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,
}
}
}
impl From<Day> for jiff::civil::Weekday {
fn from(value: Day) -> Self {
match value {
+178 -193
View File
File diff suppressed because it is too large Load Diff
+10 -6
View File
@@ -3,7 +3,7 @@
use std::env;
use chrono::{DateTime, FixedOffset};
use jiff::Zoned;
use parse_datetime::{parse_datetime, parse_datetime_at_date};
pub fn check_absolute(input: &str, expected: &str) {
@@ -15,19 +15,23 @@ pub fn check_absolute(input: &str, expected: &str) {
};
assert_eq!(
&parsed.to_rfc3339().replace("T", " "),
parsed.strftime("%Y-%m-%d %H:%M:%S%:z").to_string(),
expected,
"Input value: {input}"
);
}
pub fn check_relative(now: DateTime<FixedOffset>, input: &str, expected: &str) {
pub fn check_relative(now: Zoned, input: &str, expected: &str) {
env::set_var("TZ", "UTC0");
let parsed = match parse_datetime_at_date(now.into(), input) {
let parsed = match parse_datetime_at_date(now, input) {
Ok(v) => v,
Err(e) => panic!("Failed to parse date from value '{input}': {e}"),
};
let expected_parsed = DateTime::parse_from_rfc3339(expected).unwrap();
assert_eq!(parsed, expected_parsed, "Input value: {input}");
assert_eq!(
parsed.strftime("%Y-%m-%d %H:%M:%S%:z").to_string(),
expected,
"Input value: {input}"
);
}
+7 -5
View File
@@ -1,10 +1,11 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
use jiff::{civil::DateTime, tz::TimeZone};
use rstest::rstest;
mod common;
use common::check_absolute;
use common::{check_absolute, check_relative};
// The expected values are produced by GNU date version 8.32
// export LC_TIME=en_US.UTF-8
@@ -87,9 +88,10 @@ fn test_absolute_date_numeric(#[case] input: &str, #[case] expected: &str) {
#[case::alphabetical_long_month_at_back_hyphen("14-november", 2022, "2022-11-14 00:00:00+00:00")]
#[case::alphabetical_short_month_at_back_hyphen("14-nov", 2022, "2022-11-14 00:00:00+00:00")]
fn test_date_omitting_year(#[case] input: &str, #[case] year: u32, #[case] expected: &str) {
use chrono::DateTime;
use common::check_relative;
let now = DateTime::parse_from_rfc3339(&format!("{year}-06-01T00:00:00+00:00")).unwrap();
let now = format!("{year}-06-01 00:00:00")
.parse::<DateTime>()
.unwrap()
.to_zoned(TimeZone::UTC)
.unwrap();
check_relative(now, input, expected);
}
+11 -7
View File
@@ -1,7 +1,7 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
use chrono::{DateTime, Local};
use jiff::{civil::DateTime, tz::TimeZone, Zoned};
use parse_datetime::parse_datetime_at_date;
use rstest::rstest;
@@ -13,17 +13,17 @@ use rstest::rstest;
// Documentation for the date format can be found at:
// https://www.gnu.org/software/coreutils/manual/html_node/Time-of-day-items.html
pub fn check_time(input: &str, expected: &str, format: &str, base: Option<DateTime<Local>>) {
pub fn check_time(input: &str, expected: &str, format: &str, base: Option<Zoned>) {
std::env::set_var("TZ", "UTC0");
let now = base.unwrap_or_else(|| std::time::SystemTime::now().into());
let now = base.unwrap_or(Zoned::now());
let parsed = match parse_datetime_at_date(now, input) {
Ok(v) => v,
Err(e) => panic!("Failed to parse time from value '{input}': {e}"),
}
.to_utc();
.with_time_zone(TimeZone::UTC);
assert_eq!(
&format!("{}", parsed.format(format)),
format!("{}", parsed.strftime(format)),
expected,
"Input value: {input}"
);
@@ -97,8 +97,12 @@ fn test_time_correction(#[case] input: &str, #[case] expected: &str) {
#[case::minus_13("12:34:56-13:00", "2022-06-11 01:34:56")]
*/
fn test_time_correction_with_overflow(#[case] input: &str, #[case] expected: &str) {
let now = DateTime::parse_from_rfc3339("2022-06-10T00:00:00+00:00").unwrap();
check_time(input, expected, "%Y-%m-%d %H:%M:%S", Some(now.into()));
let now = "2022-06-10 00:00:00"
.parse::<DateTime>()
.unwrap()
.to_zoned(TimeZone::UTC)
.unwrap();
check_time(input, expected, "%Y-%m-%d %H:%M:%S", Some(now));
}
#[rstest]