Repository navigation
fix(offset): treat a numeric offset after a time of day as a zone correction #326
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -816,4 +816,47 @@ mod tests { | |
| // duplicate check. GNU rejects it too. | ||
| assert!(parse(&mut "j +05:00").is_err()); | ||
| } | ||
| /// A numeric offset after a time of day is a time zone correction, even | ||
| /// when a relative item follows it. GNU `date` reads | ||
| /// `2026-08-27 12:00 +3 hours` as the zone `+03:00` plus a bare `hours` | ||
| /// (which means one hour), i.e. 10:00 UTC. | ||
| #[test] | ||
| fn numeric_offset_before_relative_item() { | ||
| for (expected, input) in [ | ||
| ("2026-08-27 10:00:00", "2026-08-27 12:00 +3 hours"), | ||
| ("2026-08-27 10:00:00", "2026-08-27 12:00 +3 hour"), | ||
| ("2026-08-27 10:00:00", "2026-08-27 12:00 +3hours"), | ||
| ("2026-08-27 10:00:00", "2026-08-27 12:00 + 3 hours"), | ||
| ("2026-08-27 10:00:00", "2026-08-27 12:00 +03 hours"), | ||
| ("2026-08-27 10:00:00", "2026-08-27 12:00 +0300 hours"), | ||
| ("2026-08-27 10:00:00", "2026-08-27 12:00:00 +3 hours"), | ||
| ("2026-08-27 10:05:00", "2026-08-27 12:00 +3 hours 5 minutes"), | ||
| ("2026-08-27 08:00:00", "2026-08-27 12:00 +3 hours ago"), | ||
| ("2026-08-28 09:00:00", "2026-08-27 12:00 +3 days"), | ||
| ("2026-08-27 09:01:00", "2026-08-27 12:00 +3 minutes"), | ||
| ("2026-08-27 16:00:00", "2026-08-27 12:00 -3 hours"), | ||
| ("2026-08-27 00:00:00", "2026-08-27 12:00 +13 hours"), | ||
| // A named zone already fills the zone slot, so the numeric value | ||
| // stays a relative item. | ||
| ("2026-08-27 15:00:00", "2026-08-27 12:00 utc +3 hours"), | ||
| // Without a time of day there is no zone slot to fill either. | ||
| ("2026-08-27 03:00:00", "2026-08-27 +3 hours"), | ||
| // A fractional number is never a zone correction. | ||
| ("2026-08-27 12:00:01", "2026-08-27 12:00 +1.5 seconds"), | ||
| ] { | ||
| let parsed = parse(&mut &*input) | ||
| .map(at_utc) | ||
| .expect("parsing failed during tests") | ||
| .with_time_zone(TimeZone::UTC); | ||
| assert_eq!( | ||
| expected, | ||
| parsed.strftime("%Y-%m-%d %H:%M:%S").to_string(), | ||
| "{input}" | ||
| ); | ||
| } | ||
|
|
||
| // `+30` is not a valid zone correction, and GNU rejects the whole | ||
| // input rather than falling back to a relative item. | ||
| assert!(parse(&mut "2026-08-27 12:00 +30 hours").is_err()); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. out of range is a hard error now, so please check what |
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -31,7 +31,7 @@ use winnow::{ | |
| combinator::{alt, peek}, | ||
| error::{ContextError, ErrMode}, | ||
| stream::{AsChar, Stream}, | ||
| token::take_while, | ||
| token::{one_of, take_while}, | ||
| ModalResult, Parser, | ||
| }; | ||
|
|
||
|
|
@@ -193,12 +193,17 @@ pub(super) fn parse_local(input: &mut &str) -> ModalResult<()> { | |
|
|
||
| /// Parse a timezone starting with `+` or `-`. | ||
| pub(super) fn timezone_offset(input: &mut &str) -> ModalResult<Offset> { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this changes a lot of parses, did you run the gnu date tests from coreutils with it? |
||
| // Strings like "+8 years" are ambiguous, they can either be parsed as a | ||
| // timezone offset "+8" and a relative time "years", or just a relative time | ||
| // "+8 years". GNU date parses them the second way, so we do the same here. | ||
| // | ||
| // Return early if the input can be parsed as a relative time. | ||
| if peek(relative::parse).parse_next(input).is_ok() { | ||
| // A fractional number is never a zone correction: GNU reads | ||
| // `12:00 +1.5 seconds` as a relative item, so leave it to that parser. | ||
| let has_fraction = peek(( | ||
| plus_or_minus::<ErrMode<ContextError>>, | ||
| s(dec_uint_str), | ||
| '.', | ||
| one_of(AsChar::is_dec_digit), | ||
| )) | ||
| .parse_next(input) | ||
| .is_ok(); | ||
| if has_fraction { | ||
| return Err(ErrMode::Backtrack(ContextError::new())); | ||
| } | ||
|
|
||
|
|
@@ -433,7 +438,7 @@ mod tests { | |
| "+2500", // invalid: hours > 24 | ||
| "-2361", // invalid: minutes > 60 | ||
| "+2401", // invalid: minutes > 0 when hours == 24 | ||
| "+23 days", // invalid: ambiguous with relative time parsing | ||
| "+25 days", // invalid: hours > 24, even when followed by a relative item | ||
| ] { | ||
| let mut s = input; | ||
| assert!(timezone_offset(&mut s).is_err(), "{input}"); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please add a few of these cases in tests/time.rs too, that's where we keep the gnu-compared expectations