diff --git a/Cargo.lock b/Cargo.lock index 9b462c6..4430b6d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -166,6 +166,30 @@ dependencies = [ "hashbrown", ] +[[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", +] + +[[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 = "js-sys" version = "0.3.77" @@ -214,6 +238,7 @@ name = "parse_datetime" version = "0.11.0" dependencies = [ "chrono", + "jiff", "num-traits", "regex", "rstest", @@ -232,6 +257,21 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "portable-atomic" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +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-macro-crate" version = "3.3.0" @@ -344,6 +384,26 @@ version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" +[[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" diff --git a/Cargo.toml b/Cargo.toml index b76ffd2..9f9ed3e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ 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"] } [dev-dependencies] rstest = "0.26" diff --git a/src/items/builder.rs b/src/items/builder.rs index f11d94c..021fae8 100644 --- a/src/items/builder.rs +++ b/src/items/builder.rs @@ -196,8 +196,8 @@ impl DateTimeBuilder { 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, + month as u32, + day as u32, dt.hour(), dt.minute(), dt.second(), diff --git a/src/items/date.rs b/src/items/date.rs index 50f3ec4..0c3be11 100644 --- a/src/items/date.rs +++ b/src/items/date.rs @@ -41,13 +41,13 @@ use super::{ }; #[derive(PartialEq, Eq, Clone, Debug, Default)] -pub struct Date { - pub day: u32, - pub month: u32, - pub year: Option, +pub(crate) struct Date { + pub(crate) day: u8, + pub(crate) month: u8, + pub(crate) year: Option, } -impl TryFrom<(&str, u32, u32)> for Date { +impl TryFrom<(&str, u8, u8)> for Date { type Error = &'static str; /// Create a `Date` from a tuple of `(year, month, day)`. @@ -55,7 +55,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 { + fn try_from(value: (&str, u8, u8)) -> Result { let (year_str, month, day) = value; let year = year_from_str(year_str)?; @@ -80,11 +80,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 { + fn try_from((month, day): (u8, u8)) -> Result { if !(1..=12).contains(&month) { return Err("month must be between 1 and 12"); } @@ -104,14 +104,27 @@ impl TryFrom<(u32, u32)> for Date { } } -pub fn parse(input: &mut &str) -> ModalResult { +impl TryFrom for jiff::civil::Date { + type Error = &'static str; + + fn try_from(date: Date) -> Result { + 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 { 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 { +pub(super) fn iso1(input: &mut &str) -> ModalResult { let (year, _, month, _, day) = (year_str, s('-'), s(dec_uint), s('-'), s(dec_uint)).parse_next(input)?; @@ -123,19 +136,13 @@ pub fn iso1(input: &mut &str) -> ModalResult { /// Parse `[year][month][day]` /// /// This is also used by [`combined`](super::combined). -pub fn iso2(input: &mut &str) -> ModalResult { +pub(super) fn iso2(input: &mut &str) -> ModalResult { 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::() - .map_err(|_| ErrMode::Cut(ctx_err("month must be a valid number")))?; - - let day = date_str[len - 2..] - .parse::() - .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 +165,21 @@ fn us(input: &mut &str) -> ModalResult { // // GNU quirk: interpret as [year]/[month]/[day] if the first part is at // least 4 characters long. - let day = s2 - .parse::() - .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::() - .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::() - .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 +240,7 @@ fn literal2(input: &mut &str) -> ModalResult { } /// Parse the name of a month (case-insensitive) -fn literal_month(input: &mut &str) -> ModalResult { +fn literal_month(input: &mut &str) -> ModalResult { s(alpha1) .verify_map(|s: &str| { Some(match s { @@ -261,6 +262,16 @@ fn literal_month(input: &mut &str) -> ModalResult { .parse_next(input) } +fn month_from_str(s: &str) -> ModalResult { + s.parse::() + .map_err(|_| ErrMode::Cut(ctx_err("month must be a valid u8 number"))) +} + +fn day_from_str(s: &str) -> ModalResult { + s.parse::() + .map_err(|_| ErrMode::Cut(ctx_err("day must be a valid u8 number"))) +} + #[cfg(test)] mod tests { use super::{parse, Date}; diff --git a/src/items/year.rs b/src/items/year.rs index 412779c..564d12d 100644 --- a/src/items/year.rs +++ b/src/items/year.rs @@ -15,10 +15,10 @@ use winnow::{stream::AsChar, token::take_while, ModalResult, Parser}; use super::primitive::s; // TODO: Leverage `TryFrom` trait. -pub(super) fn year_from_str(year_str: &str) -> Result { +pub(super) fn year_from_str(year_str: &str) -> Result { let mut year = year_str - .parse::() - .map_err(|_| "year must be a valid number")?; + .parse::() + .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. @@ -57,16 +57,16 @@ mod tests { #[test] fn test_year() { // 2-characters are converted to 19XX/20XX - assert_eq!(year_from_str("10").unwrap(), 2010u32); - assert_eq!(year_from_str("68").unwrap(), 2068u32); - assert_eq!(year_from_str("69").unwrap(), 1969u32); - assert_eq!(year_from_str("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!(year_from_str("468").unwrap(), 468u32); - assert_eq!(year_from_str("469").unwrap(), 469u32); - assert_eq!(year_from_str("1568").unwrap(), 1568u32); - assert_eq!(year_from_str("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!(year_from_str("10000").is_err());