diff --git a/src/items/builder.rs b/src/items/builder.rs index c6860ad..02c8974 100644 --- a/src/items/builder.rs +++ b/src/items/builder.rs @@ -33,13 +33,11 @@ impl DateTimeBuilder { 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: i32) -> Result { - if self.timestamp.is_some() { - Err("timestamp cannot appear more than once") - } else { - self.timestamp = Some(ts); - Ok(self) - } + self.timestamp = Some(ts); + Ok(self) } pub(super) fn set_year(mut self, year: u32) -> Result { diff --git a/src/items/epoch.rs b/src/items/epoch.rs index 4936339..338de8a 100644 --- a/src/items/epoch.rs +++ b/src/items/epoch.rs @@ -5,6 +5,13 @@ use winnow::{combinator::preceded, ModalResult, Parser}; use super::primitive::{dec_int, s}; +/// Parse a timestamp in the form of `@1234567890`. +/// +/// Grammar: +/// +/// ```ebnf +/// timestamp = "@" dec_int ; +/// ``` pub fn parse(input: &mut &str) -> ModalResult { s(preceded("@", dec_int)).parse_next(input) } diff --git a/src/items/mod.rs b/src/items/mod.rs index 8b483de..fcf829b 100644 --- a/src/items/mod.rs +++ b/src/items/mod.rs @@ -45,7 +45,7 @@ use builder::DateTimeBuilder; use chrono::{DateTime, FixedOffset}; use primitive::space; use winnow::{ - combinator::{alt, trace}, + combinator::{alt, eof, terminated, trace}, error::{AddContext, ContextError, ErrMode, StrContext, StrContextValue}, stream::Stream, ModalResult, Parser, @@ -65,19 +65,30 @@ pub enum Item { TimeZone(time::Offset), } +fn expect_error(input: &mut &str, reason: &'static str) -> ErrMode { + ErrMode::Cut(ContextError::new()).add_context( + input, + &input.checkpoint(), + StrContext::Expected(StrContextValue::Description(reason)), + ) +} + /// Parse an item. -/// TODO: timestamp values are exclusive with other items. See -/// https://github.com/uutils/parse_datetime/issues/156 -pub fn parse_one(input: &mut &str) -> ModalResult { +/// +/// Grammar: +/// +/// ```ebnf +/// item = combined | date | time | relative | weekday | timezone | year ; +/// ``` +fn parse_item(input: &mut &str) -> ModalResult { trace( - "parse_one", + "parse_item", alt(( combined::parse.map(Item::DateTime), date::parse.map(Item::Date), time::parse.map(Item::Time), relative::parse.map(Item::Relative), weekday::parse.map(Item::Weekday), - epoch::parse.map(Item::Timestamp), timezone::parse.map(Item::TimeZone), date::year.map(Item::Year), )), @@ -85,19 +96,18 @@ pub fn parse_one(input: &mut &str) -> ModalResult { .parse_next(input) } -fn expect_error(input: &mut &str, reason: &'static str) -> ErrMode { - ErrMode::Cut(ContextError::new()).add_context( - input, - &input.checkpoint(), - StrContext::Expected(StrContextValue::Description(reason)), - ) -} - -pub fn parse(input: &mut &str) -> ModalResult { +/// Parse a sequence of items. +/// +/// Grammar: +/// +/// ```ebnf +/// items = item, { space, item } ; +/// ``` +fn parse_items(input: &mut &str) -> ModalResult { let mut builder = DateTimeBuilder::new(); loop { - match parse_one.parse_next(input) { + match parse_item.parse_next(input) { Ok(item) => match item { Item::Timestamp(ts) => { builder = builder @@ -147,6 +157,38 @@ pub fn parse(input: &mut &str) -> ModalResult { Ok(builder) } +/// Parse a timestamp. +/// +/// From the GNU docs: +/// +/// (Timestamp) Such a number cannot be combined with any other date item, as it +/// specifies a complete timestamp. +fn parse_timestamp(input: &mut &str) -> ModalResult { + trace( + "parse_timestamp", + terminated(epoch::parse.map(Item::Timestamp), eof), + ) + .verify_map(|ts: Item| { + if let Item::Timestamp(ts) = ts { + DateTimeBuilder::new().set_timestamp(ts).ok() + } else { + None + } + }) + .parse_next(input) +} + +/// Parse a date and time string. +/// +/// Grammar: +/// +/// ```ebnf +/// date_time = timestamp | items ; +/// ``` +pub(crate) fn parse(input: &mut &str) -> ModalResult { + trace("parse", alt((parse_timestamp, parse_items))).parse_next(input) +} + pub(crate) fn at_date( builder: DateTimeBuilder, base: DateTime, @@ -287,6 +329,14 @@ 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")); } #[test]