diff --git a/crates/winnow-datetime/CHANGELOG.md b/crates/winnow-datetime/CHANGELOG.md index 5bf742b..70eb2d7 100644 --- a/crates/winnow-datetime/CHANGELOG.md +++ b/crates/winnow-datetime/CHANGELOG.md @@ -1,3 +1,6 @@ +## 0.2.2 - 2015-05-14 +* Added convert::jiff for jiff support of date and time + ## 0.2.1 - 2015-05-11 * Made the convert modules public so that they can be used outside of the crate. * Added TryInto for time::OffsetDateTime diff --git a/crates/winnow-datetime/Cargo.toml b/crates/winnow-datetime/Cargo.toml index dbc190e..ae6ca69 100644 --- a/crates/winnow-datetime/Cargo.toml +++ b/crates/winnow-datetime/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "winnow_datetime" -version = "0.2.1" +version = "0.2.2" description = "Parsing dates using winnow" keywords = [ "iso8601", "date-time", "parser", "winnow" ] categories = [ "parser-implementations", "date-and-time" ] @@ -15,6 +15,7 @@ edition = "2021" [dependencies] winnow = "0.7" chrono = { version = "0.4", default-features = false, optional = true } +jiff = { version = "0.2.13", optional = true } time = { version = "0.3.37", default-features = false, optional = true } num-traits = { version = "0.2", optional = true } paste = "1.0.15" @@ -24,5 +25,6 @@ serde = { version = "1.0", features = ["derive"], optional = true } default = ["std"] std = ["winnow/std"] chrono = ["dep:chrono", "dep:num-traits"] +jiff = ["dep:jiff", "dep:num-traits"] time = ["dep:time", "dep:num-traits"] serde = ["dep:serde"] diff --git a/crates/winnow-datetime/README.md b/crates/winnow-datetime/README.md index 3b67972..5743260 100644 --- a/crates/winnow-datetime/README.md +++ b/crates/winnow-datetime/README.md @@ -29,6 +29,12 @@ is the most common format used on the internet. dates, times, durations, and intervals. [ISO8601](https://en.wikipedia.org/wiki/ISO_8601) is a very ambitious format that can represent a wide range of date and time concepts. +### Conversion +[winnow-datetime] provides a set of TryInto implementations to convert to common rust date/time libraries. Currently +chrono, jiff, and time are supported. Each have a feature flag of the same name as the lib to enable support for the +conversions. The TryInto implementations are available with the features and so try_into() could be called to convert to +any of the compatible types. + ## Parsing Something Strange Despite there being countless specifications some people will still come up with their own way to poetically express a datetime. So if you are looking to parse those you can build the provided structs with any combination of the pieces diff --git a/crates/winnow-datetime/src/convert/chrono.rs b/crates/winnow-datetime/src/convert/chrono.rs index 98bcaf8..e7d6670 100644 --- a/crates/winnow-datetime/src/convert/chrono.rs +++ b/crates/winnow-datetime/src/convert/chrono.rs @@ -7,8 +7,8 @@ use num_traits::FromPrimitive; impl TryFrom for chrono::NaiveDate { type Error = (); - fn try_from(iso: crate::Date) -> Result { - let maybe = match iso { + fn try_from(d: crate::Date) -> Result { + let maybe = match d { crate::Date::YMD { year, month, day } => { chrono::NaiveDate::from_ymd_opt(year, month, day) } @@ -37,12 +37,12 @@ mod test_date { #[test] fn naivedate_from_ymd() { - let iso = crate::Date::YMD { + let d = crate::Date::YMD { year: 2023, month: 2, day: 8, }; - let naive = chrono::NaiveDate::try_from(iso).unwrap(); + let naive = chrono::NaiveDate::try_from(d).unwrap(); assert_eq!(naive.year(), 2023); assert_eq!(naive.month(), 2); assert_eq!(naive.day(), 8); @@ -50,12 +50,12 @@ mod test_date { #[test] fn naivedate_from_ywd() { - let iso = Date::Week { + let d = Date::Week { year: 2023, week: 6, day: 2, }; - let naive = chrono::NaiveDate::try_from(iso).unwrap(); + let naive = chrono::NaiveDate::try_from(d).unwrap(); assert_eq!(naive.year(), 2023); assert_eq!(naive.month(), 2); assert_eq!(naive.day(), 8); @@ -63,11 +63,11 @@ mod test_date { #[test] fn naivedate_from_ordinal() { - let iso = crate::Date::Ordinal { + let d = crate::Date::Ordinal { year: 2023, day: 39, }; - let naive = chrono::NaiveDate::try_from(iso).unwrap(); + let naive = chrono::NaiveDate::try_from(d).unwrap(); assert_eq!(naive.year(), 2023); assert_eq!(naive.month(), 2); assert_eq!(naive.day(), 8); @@ -76,8 +76,8 @@ mod test_date { impl TryFrom for chrono::NaiveTime { type Error = (); - fn try_from(iso: crate::Time) -> Result { - chrono::NaiveTime::from_hms_opt(iso.hour, iso.minute, iso.second).ok_or(()) + fn try_from(t: crate::Time) -> Result { + chrono::NaiveTime::from_hms_opt(t.hour, t.minute, t.second).ok_or(()) } } @@ -91,8 +91,8 @@ impl crate::Time { impl TryFrom for chrono::DateTime { type Error = (); - fn try_from(iso: crate::DateTime) -> Result { - let offset = iso.time.offset.unwrap_or(crate::Offset { + fn try_from(dt: crate::DateTime) -> Result { + let offset = dt.time.offset.unwrap_or(crate::Offset { offset_hours: 0, offset_minutes: 0, }); @@ -100,8 +100,8 @@ impl TryFrom for chrono::DateTime { let offset_minutes = offset.offset_hours * 3600 + offset.offset_minutes; let offset = chrono::FixedOffset::east_opt(offset_minutes).ok_or(())?; - let naive_time = chrono::NaiveTime::try_from(iso.time)?; - let naive_date_time = chrono::NaiveDate::try_from(iso.date)?.and_time(naive_time); + let naive_time = chrono::NaiveTime::try_from(dt.time)?; + let naive_date_time = chrono::NaiveDate::try_from(dt.date)?.and_time(naive_time); offset .from_local_datetime(&naive_date_time) @@ -129,7 +129,7 @@ mod test_datetime { #[test] fn datetime_from_iso_ymd_offset() { - let iso = crate::DateTime { + let dt = crate::DateTime { date: crate::Date::YMD { year: 2023, month: 2, @@ -146,7 +146,7 @@ mod test_datetime { }), }, }; - let datetime = chrono::DateTime::try_from(iso).unwrap(); + let datetime = chrono::DateTime::try_from(dt).unwrap(); assert_eq!(datetime.year(), 2023); assert_eq!(datetime.month(), 2); @@ -159,7 +159,7 @@ mod test_datetime { #[test] fn datetime_from_iso_ymd_utc() { - let iso = crate::DateTime { + let dt = crate::DateTime { date: crate::Date::YMD { year: 2023, month: 2, @@ -176,7 +176,7 @@ mod test_datetime { }), }, }; - let datetime = chrono::DateTime::try_from(iso).unwrap(); + let datetime = chrono::DateTime::try_from(dt).unwrap(); assert_eq!(datetime.year(), 2023); assert_eq!(datetime.month(), 2); @@ -189,7 +189,7 @@ mod test_datetime { #[test] fn datetime_from_iso_ymd_no_offset() { - let iso = crate::DateTime { + let dt = crate::DateTime { date: crate::Date::YMD { year: 2023, month: 2, @@ -206,7 +206,7 @@ mod test_datetime { }), }, }; - let datetime = chrono::DateTime::try_from(iso).unwrap(); + let datetime = chrono::DateTime::try_from(dt).unwrap(); assert_eq!(datetime.year(), 2023); assert_eq!(datetime.month(), 2); @@ -219,7 +219,7 @@ mod test_datetime { #[test] fn datetime_from_iso_ywd() { - let iso = crate::DateTime { + let dt = crate::DateTime { date: crate::Date::Week { year: 2023, week: 6, @@ -236,7 +236,7 @@ mod test_datetime { }), }, }; - let datetime = chrono::DateTime::try_from(iso).unwrap(); + let datetime = chrono::DateTime::try_from(dt).unwrap(); assert_eq!(datetime.year(), 2023); assert_eq!(datetime.month(), 2); diff --git a/crates/winnow-datetime/src/convert/jiff.rs b/crates/winnow-datetime/src/convert/jiff.rs new file mode 100644 index 0000000..0044bf9 --- /dev/null +++ b/crates/winnow-datetime/src/convert/jiff.rs @@ -0,0 +1,215 @@ +use core::convert::TryFrom; + +impl TryFrom for jiff::civil::Time { + type Error = jiff::Error; + + fn try_from(t: crate::Time) -> Result { + jiff::civil::Time::new( + t.hour.try_into().unwrap(), + t.minute.try_into().unwrap(), + t.second.try_into().unwrap(), + t.millisecond.try_into().unwrap(), + ) + } +} + +impl crate::Time { + /// create a [`jiff::civil::Time`] if possible + pub fn into_civil_time(self) -> Option { + jiff::civil::Time::try_from(self).ok() + } +} + +impl TryFrom for jiff::civil::Date { + type Error = jiff::Error; + + fn try_from(d: crate::Date) -> Result { + match d { + crate::Date::YMD { year, month, day } => { + jiff::civil::Date::new(year as i16, month as i8, day.try_into().unwrap()) + } + + crate::Date::Week { year, week, day } => { + let wd = match day { + 1 => jiff::civil::Weekday::Monday, + 2 => jiff::civil::Weekday::Tuesday, + 3 => jiff::civil::Weekday::Wednesday, + 4 => jiff::civil::Weekday::Thursday, + 5 => jiff::civil::Weekday::Friday, + 6 => jiff::civil::Weekday::Saturday, + 7 => jiff::civil::Weekday::Sunday, + _ => panic!("Invalid day of week"), + }; + + Ok(jiff::civil::ISOWeekDate::new( + year.try_into().unwrap(), + week.try_into().unwrap(), + wd, + )? + .into()) + } + + crate::Date::Ordinal { year, day } => { + unimplemented!( + "Ordinal date conversion for {}-{} not implemented for jiff", + year, + day + ); + } + } + } +} + +impl crate::Date { + /// create a [`jeff::civil::Date`] if possible + pub fn into_civil_date(self) -> Option { + jiff::civil::Date::try_from(self).ok() + } +} + +impl TryFrom for jiff::civil::DateTime { + type Error = jiff::Error; + + fn try_from(dt: crate::DateTime) -> Result { + let naive_date = jiff::civil::Date::try_from(dt.date)?; + let naive_time = jiff::civil::Time::try_from(dt.time)?; + + Ok(naive_date.to_datetime(naive_time)) + } +} + +impl TryFrom for jiff::Zoned { + type Error = jiff::Error; + + fn try_from(dt: crate::DateTime) -> Result { + let naive_date = jiff::civil::Date::try_from(dt.date)?; + let naive_time = jiff::civil::Time::try_from(dt.time)?; + let naive_datetime = naive_date.to_datetime(naive_time); + + let offset = jiff::tz::Offset::from_seconds(match dt.time.offset { + Some(o) => (o.offset_hours * 3600 + o.offset_minutes * 60) + .try_into() + .unwrap(), + None => 0, + })?; + + let tz = jiff::tz::TimeZone::fixed(offset); + naive_datetime.to_zoned(tz) + } +} + +impl crate::DateTime { + /// create a [`jiff::civil::DateTime`] if possible + pub fn into_datetime(self) -> Option { + jiff::civil::DateTime::try_from(self).ok() + } + + pub fn into_zoned(self) -> Option { + jiff::Zoned::try_from(self).ok() + } +} + +impl TryFrom for jiff::Span { + type Error = jiff::Error; + + fn try_from(d: crate::Duration) -> Result { + let ms = d.milliseconds.unwrap_or(0.0).trunc(); + let ns = (d.milliseconds.unwrap_or(0.0).fract() * 1_000_000.0).round(); + + Ok(jiff::Span::new() + .years(d.years) + .months(d.months) + .weeks(d.weeks) + .days(d.days) + .hours(d.hours) + .minutes(d.minutes) + .seconds(d.seconds) + .milliseconds(ms as i64) + .nanoseconds(ns as i64)) + } +} + +#[cfg(test)] +mod date_and_time { + use core::convert::TryFrom; + + #[test] + fn time_from_hms() { + let iso = crate::Time { + hour: 23, + minute: 40, + second: 0, + millisecond: 0, + offset: Default::default(), + }; + let time = jiff::civil::Time::try_from(iso).unwrap(); + assert_eq!(time.hour(), 23); + assert_eq!(time.minute(), 40); + assert_eq!(time.second(), 0); + } + + #[test] + fn date_from_ymd() { + let iso = crate::Date::YMD { + year: 2023, + month: 2, + day: 8, + }; + + let date = jiff::civil::Date::try_from(iso).unwrap(); + assert_eq!(date.year(), 2023); + assert_eq!(date.month() as u8, 2); + assert_eq!(date.day(), 8); + } + + #[test] + fn datetime_from_ymd_hms() { + let dt = crate::DateTime { + date: crate::Date::YMD { + year: 2024, + month: 3, + day: 9, + }, + time: crate::Time { + hour: 23, + minute: 40, + second: 0, + millisecond: 0, + offset: Default::default(), + }, + }; + + let datetime = time::PrimitiveDateTime::try_from(dt).unwrap(); + assert_eq!(datetime.year(), 2024); + assert_eq!(datetime.month() as u8, 3); + assert_eq!(datetime.day(), 9); + assert_eq!(datetime.hour(), 23); + assert_eq!(datetime.minute(), 40); + assert_eq!(datetime.second(), 0); + } + + #[test] + fn span_from_duration() { + let d = crate::Duration { + years: 5, + months: 4, + weeks: 3, + hours: 2, + days: 1, + minutes: 30, + seconds: 15, + milliseconds: Some(500.544), + }; + + let s = jiff::Span::try_from(d).unwrap(); + + assert_eq!(s.get_years(), 5); + assert_eq!(s.get_months(), 4); + assert_eq!(s.get_weeks(), 3); + assert_eq!(s.get_hours(), 2); + assert_eq!(s.get_minutes(), 30); + assert_eq!(s.get_seconds(), 15); + assert_eq!(s.get_milliseconds(), 500); + assert_eq!(s.get_nanoseconds(), 544006); + } +} diff --git a/crates/winnow-datetime/src/convert/mod.rs b/crates/winnow-datetime/src/convert/mod.rs index 776051f..5428cc9 100644 --- a/crates/winnow-datetime/src/convert/mod.rs +++ b/crates/winnow-datetime/src/convert/mod.rs @@ -1,5 +1,8 @@ -#[cfg(feature = "time")] -pub mod time; - #[cfg(feature = "chrono")] pub mod chrono; + +#[cfg(feature = "jiff")] +pub mod jiff; + +#[cfg(feature = "time")] +pub mod time; diff --git a/crates/winnow-datetime/src/convert/time.rs b/crates/winnow-datetime/src/convert/time.rs index bd3c5e7..79d9646 100644 --- a/crates/winnow-datetime/src/convert/time.rs +++ b/crates/winnow-datetime/src/convert/time.rs @@ -3,11 +3,11 @@ use core::convert::TryFrom; impl TryFrom for time::Time { type Error = (); - fn try_from(iso: crate::Time) -> Result { + fn try_from(t: crate::Time) -> Result { time::Time::from_hms( - iso.hour.try_into().unwrap(), - iso.minute.try_into().unwrap(), - iso.second.try_into().unwrap(), + t.hour.try_into().unwrap(), + t.minute.try_into().unwrap(), + t.second.try_into().unwrap(), ) .or(Err(())) } @@ -23,8 +23,8 @@ impl crate::Time { impl TryFrom for time::Date { type Error = (); - fn try_from(iso: crate::Date) -> Result { - match iso { + fn try_from(d: crate::Date) -> Result { + match d { crate::Date::YMD { year, month, day } => time::Date::from_calendar_date( year, time::Month::try_from(u8::try_from(month).unwrap()).unwrap(), @@ -64,9 +64,9 @@ impl crate::Date { impl TryFrom for time::PrimitiveDateTime { type Error = (); - fn try_from(iso: crate::DateTime) -> Result { - let naive_date = time::Date::try_from(iso.date)?; - let naive_time = time::Time::try_from(iso.time)?; + fn try_from(dt: crate::DateTime) -> Result { + let naive_date = time::Date::try_from(dt.date)?; + let naive_time = time::Time::try_from(dt.time)?; Ok(naive_date.with_time(naive_time)) } } @@ -74,11 +74,11 @@ impl TryFrom for time::PrimitiveDateTime { impl TryFrom for time::OffsetDateTime { type Error = (); - fn try_from(iso: crate::DateTime) -> Result { - let naive_date = time::Date::try_from(iso.date)?; - let naive_time = time::Time::try_from(iso.time)?; + fn try_from(dt: crate::DateTime) -> Result { + let naive_date = time::Date::try_from(dt.date)?; + let naive_time = time::Time::try_from(dt.time)?; - if let Some(o) = iso.time.offset { + if let Some(o) = dt.time.offset { if o.offset_hours == 0 && o.offset_minutes == 0 { Ok(time::OffsetDateTime::new_utc(naive_date, naive_time)) } else {