diff --git a/src/items/date.rs b/src/items/date.rs index 97cfdef..1f1389f 100644 --- a/src/items/date.rs +++ b/src/items/date.rs @@ -27,7 +27,7 @@ //! > ‘September’. use winnow::{ - ascii::{alpha1, multispace1}, + ascii::alpha1, combinator::{alt, eof, opt, preceded, terminated}, error::ErrMode, stream::AsChar, @@ -36,7 +36,7 @@ use winnow::{ }; use super::{ - primitive::{ctx_err, dec_uint, s}, + primitive::{ctx_err, dec_uint, multispace1, s}, year::{year_from_str, year_str}, }; diff --git a/src/items/mod.rs b/src/items/mod.rs index d89e23f..6870a6b 100644 --- a/src/items/mod.rs +++ b/src/items/mod.rs @@ -391,6 +391,12 @@ mod tests { assert!(result.is_ok()); } + #[test] + fn whitespace_only() { + let result = parse(&mut "\x0B \x0C"); + assert!(result.is_ok()); + } + #[test] fn invalid() { let result = parse(&mut "2025-05-19 2024-05-20 06:14:49"); diff --git a/src/items/primitive.rs b/src/items/primitive.rs index ba7d32e..42042a8 100644 --- a/src/items/primitive.rs +++ b/src/items/primitive.rs @@ -6,7 +6,7 @@ use std::str::FromStr; use winnow::{ - ascii::{digit1, multispace0, Uint}, + ascii::{digit1, Uint}, combinator::{alt, delimited, not, opt, peek, preceded, repeat, separated}, error::{ContextError, ParserError, StrContext, StrContextValue}, stream::AsChar, @@ -26,6 +26,23 @@ where preceded(space, p) } +/// Same as winnow's `multispace0`, but also accepting `\v` and `\f`, like +/// the whitespace GNU `date` accepts between tokens +fn multispace0<'a, E>(input: &mut &'a str) -> winnow::Result<&'a str, E> +where + E: ParserError<&'a str>, +{ + take_while(0.., (' ', '\t', '\n', '\x0B', '\x0C', '\r')).parse_next(input) +} + +/// Same as [`multispace0`], but requiring at least one character +pub(super) fn multispace1<'a, E>(input: &mut &'a str) -> winnow::Result<&'a str, E> +where + E: ParserError<&'a str>, +{ + take_while(1.., (' ', '\t', '\n', '\x0B', '\x0C', '\r')).parse_next(input) +} + /// Parse the space in-between tokens /// /// You probably want to use the [`s`] combinator instead. diff --git a/tests/date.rs b/tests/date.rs index 1428adb..af83322 100644 --- a/tests/date.rs +++ b/tests/date.rs @@ -75,6 +75,18 @@ fn test_absolute_date_numeric(#[case] input: &str, #[case] expected: &str) { check_absolute(input, expected); } +// Any run of ASCII whitespace may separate items, like in GNU date. The +// expected values were verified with GNU date (`date -d 'input'`). +#[rstest] +#[case::vertical_tab_between_date_and_year("Jan 23\x0B2026 1:00AM", "2026-01-23 01:00:00+00:00")] +#[case::form_feed_between_date_and_year("Jan 23\x0C2026 1:00AM", "2026-01-23 01:00:00+00:00")] +#[case::vertical_tab_after_comma("Jan 23,\x0B2026", "2026-01-23 00:00:00+00:00")] +#[case::vertical_tab_before_time("14 nov 2026\x0B1:00AM", "2026-11-14 01:00:00+00:00")] +#[case::vertical_tab_as_iso_separator("2026-01-23\x0B01:00:00", "2026-01-23 01:00:00+00:00")] +fn test_whitespace_between_items(#[case] input: &str, #[case] expected: &str) { + check_absolute(input, expected); +} + #[rstest] #[case::us_style("11/14", 2022, "2022-11-14 00:00:00+00:00")] #[case::alphabetical_full_month_in_front("november 14", 2022, "2022-11-14 00:00:00+00:00")]