diff --git a/src/items/builder.rs b/src/items/builder.rs index cb87bea..060be9b 100644 --- a/src/items/builder.rs +++ b/src/items/builder.rs @@ -3,7 +3,7 @@ use jiff::{civil, Span, Zoned}; -use super::{date, epoch, error, relative, time, timezone, weekday, year}; +use super::{date, epoch, error, relative, time, timezone, weekday, year, Item}; /// The builder is used to construct a DateTime object from various components. /// The parser creates a `DateTimeBuilder` object with the parsed components, @@ -248,3 +248,26 @@ impl DateTimeBuilder { Ok(dt) } } + +impl TryFrom> for DateTimeBuilder { + type Error = &'static str; + + fn try_from(items: Vec) -> Result { + let mut builder = DateTimeBuilder::new(); + + for item in items { + builder = match item { + Item::Timestamp(ts) => builder.set_timestamp(ts)?, + Item::DateTime(dt) => builder.set_date(dt.date)?.set_time(dt.time)?, + Item::Date(d) => builder.set_date(d)?, + Item::Time(t) => builder.set_time(t)?, + Item::Weekday(weekday) => builder.set_weekday(weekday)?, + Item::TimeZone(tz) => builder.set_timezone(tz)?, + Item::Relative(rel) => builder.push_relative(rel)?, + Item::Pure(pure) => builder.set_pure(pure)?, + } + } + + Ok(builder) + } +} diff --git a/src/items/mod.rs b/src/items/mod.rs index b3da0c9..30818a2 100644 --- a/src/items/mod.rs +++ b/src/items/mod.rs @@ -49,7 +49,7 @@ pub(crate) mod error; use jiff::Zoned; use primitive::space; use winnow::{ - combinator::{alt, eof, terminated, trace}, + combinator::{alt, eof, preceded, repeat_till, terminated, trace}, error::{AddContext, ContextError, ErrMode, StrContext, StrContextValue}, stream::Stream, ModalResult, Parser, @@ -70,7 +70,8 @@ enum Item { Pure(String), } -/// Parse a date and time string based on a specific date. +/// Parse a date and time string and build a `Zoned` object. The parsed result +/// is resolved against the given base date and time. pub(crate) fn parse_at_date + Clone>(base: Zoned, input: S) -> Result { let input = input.as_ref().to_ascii_lowercase(); match parse(&mut input.as_str()) { @@ -79,11 +80,12 @@ pub(crate) fn parse_at_date + Clone>(base: Zoned, input: S) -> Res } } -/// Parse a date and time string based on the current local time. +/// Parse a date and time string and build a `Zoned` object. The parsed result +/// is resolved against the current local date and time. pub(crate) fn parse_at_local + Clone>(input: S) -> Result { let input = input.as_ref().to_ascii_lowercase(); match parse(&mut input.as_str()) { - Ok(builder) => builder.build(), + Ok(builder) => builder.build(), // the builder uses current local date and time if no base is given. Err(e) => Err(e.into()), } } @@ -196,7 +198,7 @@ fn parse(input: &mut &str) -> ModalResult { fn parse_timestamp(input: &mut &str) -> ModalResult { trace( "parse_timestamp", - terminated(epoch::parse.map(Item::Timestamp), eof), + terminated(epoch::parse.map(Item::Timestamp), preceded(space, eof)), ) .verify_map(|ts: Item| { if let Item::Timestamp(ts) = ts { @@ -210,59 +212,13 @@ fn parse_timestamp(input: &mut &str) -> ModalResult { /// Parse a sequence of items. fn parse_items(input: &mut &str) -> ModalResult { - let mut builder = DateTimeBuilder::new(); - - loop { - match parse_item.parse_next(input) { - Ok(item) => match item { - Item::Timestamp(ts) => { - builder = builder - .set_timestamp(ts) - .map_err(|e| expect_error(input, e))?; - } - Item::DateTime(dt) => { - builder = builder - .set_date(dt.date) - .map_err(|e| expect_error(input, e))? - .set_time(dt.time) - .map_err(|e| expect_error(input, e))?; - } - Item::Date(d) => { - builder = builder.set_date(d).map_err(|e| expect_error(input, e))?; - } - Item::Time(t) => { - builder = builder.set_time(t).map_err(|e| expect_error(input, e))?; - } - Item::Weekday(weekday) => { - builder = builder - .set_weekday(weekday) - .map_err(|e| expect_error(input, e))?; - } - Item::TimeZone(tz) => { - builder = builder - .set_timezone(tz) - .map_err(|e| expect_error(input, e))?; - } - Item::Relative(rel) => { - builder = builder - .push_relative(rel) - .map_err(|e| expect_error(input, e))?; - } - Item::Pure(pure) => { - builder = builder.set_pure(pure).map_err(|e| expect_error(input, e))?; - } - }, - Err(ErrMode::Backtrack(_)) => break, - Err(e) => return Err(e), - } - } - - space.parse_next(input)?; - if !input.is_empty() { - return Err(expect_error(input, "unexpected input")); - } + let (items, _): (Vec, _) = trace( + "parse_items", + repeat_till(0.., parse_item, preceded(space, eof)), + ) + .parse_next(input)?; - Ok(builder) + items.try_into().map_err(|e| expect_error(input, e)) } /// Parse an item. @@ -346,6 +302,11 @@ mod tests { test_eq_fmt("%Y-%m-%dT%H:%M:%S%:z", "@1690466034") ); + assert_eq!( + "2023-07-27T13:53:54+00:00", + test_eq_fmt("%Y-%m-%dT%H:%M:%S%:z", " @1690466034 ") + ); + // https://github.com/uutils/coreutils/issues/6398 // TODO: make this work // assert_eq!("1111 1111 00", test_eq_fmt("%m%d %H%M %S", "11111111")); @@ -371,6 +332,12 @@ mod tests { ); } + #[test] + fn empty() { + let result = parse(&mut ""); + assert!(result.is_ok()); + } + #[test] fn invalid() { let result = parse(&mut "2025-05-19 2024-05-20 06:14:49"); @@ -396,7 +363,6 @@ mod tests { let result = parse(&mut "2025-05-19 +00:00 +01:00"); assert!(result.is_err()); - assert!(result.unwrap_err().to_string().contains("unexpected input")); let result = parse(&mut "m1y"); assert!(result.is_err()); @@ -407,15 +373,12 @@ mod tests { let result = parse(&mut "2025-05-19 abcdef"); assert!(result.is_err()); - assert!(result.unwrap_err().to_string().contains("unexpected input")); let result = parse(&mut "@1690466034 2025-05-19"); assert!(result.is_err()); - assert!(result.unwrap_err().to_string().contains("unexpected input")); let result = parse(&mut "2025-05-19 @1690466034"); assert!(result.is_err()); - assert!(result.unwrap_err().to_string().contains("unexpected input")); // Pure number as year (too large). let result = parse(&mut "jul 18 12:30 10000");