Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 14 additions & 11 deletions src/items/date.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,9 +138,10 @@ pub(super) fn iso1(input: &mut &str) -> ModalResult<Date> {
let (year, _, month, _, day) =
(year_str, s('-'), s(dec_uint), s('-'), s(dec_uint)).parse_next(input)?;

// Map err to Backtrack instead of Cut to avoid early termination of parsing
(year, month, day)
.try_into()
.map_err(|e| ErrMode::Cut(ctx_err(e)))
.map_err(|e| ErrMode::Backtrack(ctx_err(e)))
}

/// Parse `[year][month][day]`
Expand All @@ -156,7 +157,7 @@ pub(super) fn iso2(input: &mut &str) -> ModalResult<Date> {

(year, month, day)
.try_into()
.map_err(|e| ErrMode::Cut(ctx_err(e)))
.map_err(|e| ErrMode::Backtrack(ctx_err(e)))
}

/// Parse `[year]/[month]/[day]` or `[month]/[day]/[year]` or `[month]/[day]`.
Expand All @@ -178,19 +179,21 @@ fn us(input: &mut &str) -> ModalResult<Date> {
let day = day_from_str(s2)?;
(s1, n, day)
.try_into()
.map_err(|e| ErrMode::Cut(ctx_err(e)))
.map_err(|e| ErrMode::Backtrack(ctx_err(e)))
}
Some(s2) => {
// [month]/[day]/[year]
let month = month_from_str(s1)?;
(s2, month, n)
.try_into()
.map_err(|e| ErrMode::Cut(ctx_err(e)))
.map_err(|e| ErrMode::Backtrack(ctx_err(e)))
}
None => {
// [month]/[day]
let month = month_from_str(s1)?;
(month, n).try_into().map_err(|e| ErrMode::Cut(ctx_err(e)))
(month, n)
.try_into()
.map_err(|e| ErrMode::Backtrack(ctx_err(e)))
}
}
}
Expand All @@ -213,10 +216,10 @@ fn literal1(input: &mut &str) -> ModalResult<Date> {
match year {
Some(year) => (year, month, day)
.try_into()
.map_err(|e| ErrMode::Cut(ctx_err(e))),
.map_err(|e| ErrMode::Backtrack(ctx_err(e))),
None => (month, day)
.try_into()
.map_err(|e| ErrMode::Cut(ctx_err(e))),
.map_err(|e| ErrMode::Backtrack(ctx_err(e))),
}
}

Expand All @@ -242,10 +245,10 @@ fn literal2(input: &mut &str) -> ModalResult<Date> {
match year {
Some(year) => (year, month, day)
.try_into()
.map_err(|e| ErrMode::Cut(ctx_err(e))),
.map_err(|e| ErrMode::Backtrack(ctx_err(e))),
None => (month, day)
.try_into()
.map_err(|e| ErrMode::Cut(ctx_err(e))),
.map_err(|e| ErrMode::Backtrack(ctx_err(e))),
}
}

Expand Down Expand Up @@ -274,12 +277,12 @@ fn literal_month(input: &mut &str) -> ModalResult<u8> {

fn month_from_str(s: &str) -> ModalResult<u8> {
s.parse::<u8>()
.map_err(|_| ErrMode::Cut(ctx_err("month must be a valid u8 number")))
.map_err(|_| ErrMode::Backtrack(ctx_err("month must be a valid u8 number")))
}

fn day_from_str(s: &str) -> ModalResult<u8> {
s.parse::<u8>()
.map_err(|_| ErrMode::Cut(ctx_err("day must be a valid u8 number")))
.map_err(|_| ErrMode::Backtrack(ctx_err("day must be a valid u8 number")))
}

#[cfg(test)]
Expand Down
34 changes: 34 additions & 0 deletions tests/time.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,3 +128,37 @@ fn test_time_invalid(#[case] input: &str) {
"Input string '{input}' did not produce an error when parsing"
);
}

#[rstest]
#[case::decimal_1_whole("1.123456789 seconds ago")]
#[case::decimal_2_whole("12.123456789 seconds ago")]
#[case::decimal_3_whole("123.123456789 seconds ago")]
#[case::decimal_4_whole("1234.123456789 seconds ago")]
#[case::decimal_5_whole("12345.123456789 seconds ago")]
#[case::decimal_6_whole("123456.123456789 seconds ago")]
#[case::decimal_7_whole("1234567.123456789 seconds ago")]
#[case::decimal_8_whole("12345678.123456789 seconds ago")]
#[case::decimal_9_whole("123456789.123456789 seconds ago")]
#[case::decimal_10_whole("1234567891.123456789 seconds ago")]
#[case::decimal_11_whole("12345678912.123456789 seconds ago")]
#[case::decimal_12_whole("123456789123.123456789 seconds ago")]
fn test_time_seconds_ago(#[case] input: &str) {
let result = parse_datetime::parse_datetime(input);
assert!(
result.is_ok(),
"Input string '{input}', produced {result:?}, instead of Ok(Zoned)"
);
}

#[rstest]
#[case::decimal_13_whole("1234567891234.123456789 seconds ago")]
#[case::decimal_14_whole("12345678912345.123456789 seconds ago")]
#[case::decimal_15_whole("123456789123456.123456789 seconds ago")]
Copy link

@jepler jepler Oct 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gnudate is happy with timestamps this far in the future or past

$ docker run -it --rm ubuntu:25.10  date --date="123456789123456 seconds"
date: invalid date '123456789123456 seconds'
$ docker run -it --rm ubuntu:25.10  gnudate --date="123456789123456 seconds"
Sun Sep 24 07:14:00 UTC 3914215
$ docker run -it --rm ubuntu:25.10  gnudate --date="123456789123456 seconds ago"
Mon Nov 30 18:39:36 UTC -3910165

am I misinterpreting or is this test making sure parse_datetime (and thus uutils date) will reject such a large value?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the tests were written to ensure errors were produced at the boundary line of 13 whole digits. Which is a limitation somewhere during parsing or conversion, I honestly haven't tracked it down yet. The hope is that these tests provide a little future proofing. The failure of the tests during subsequent releases would signal a change in known current behavior.

As for what parse_datetime should do, the direction seems to be to align with current implementations of date. So, my vote would be to support that and align as closely as possible to current implementations.

I do think this warrants an issue to close the gap and figure out what is causing this to not handle the same size relative time values as gnudate.

fn test_time_seconds_ago_invalid(#[case] input: &str) {
let result = parse_datetime::parse_datetime(input);
assert_eq!(
result,
Err(parse_datetime::ParseDateTimeError::InvalidInput),
"Input string '{input}' did not produce an error when parsing"
);
}