diff --git a/rust/src/parsing.rs b/rust/src/parsing.rs index 374fe3cfb..7ca634870 100644 --- a/rust/src/parsing.rs +++ b/rust/src/parsing.rs @@ -598,6 +598,7 @@ impl<'a> Parser<'a> { let mut duration: ParsedDuration = ParsedDuration::new(); let mut got_t: bool = false; let mut last_had_fraction = false; + let mut last_unit = None; loop { match self.current { @@ -609,6 +610,7 @@ impl<'a> Parser<'a> { } got_t = true; + last_unit = None; } _c => { let (value, op_fraction) = self.parse_duration_number_frac()?; @@ -620,18 +622,18 @@ impl<'a> Parser<'a> { last_had_fraction = true; } + let units = if got_t { "HMS" } else { "YMWD" }; + let unit = units.find(self.current).ok_or_else(|| { + self.parse_error("Invalid duration time unit".to_string()) + })?; + if last_unit.is_some_and(|last| unit <= last) { + return Err(self.parse_error("Duration units out of order".to_string())); + } + last_unit = Some(unit); + if got_t { match self.current { 'H' => { - if duration.minutes != 0 - || duration.seconds != 0 - || duration.microseconds != 0 - { - return Err( - self.parse_error("Duration units out of order".to_string()) - ); - } - duration.hours += value; if let Some(fraction) = op_fraction { @@ -650,12 +652,6 @@ impl<'a> Parser<'a> { } } 'M' => { - if duration.seconds != 0 || duration.microseconds != 0 { - return Err( - self.parse_error("Duration units out of order".to_string()) - ); - } - duration.minutes += value; if let Some(fraction) = op_fraction { @@ -693,12 +689,6 @@ impl<'a> Parser<'a> { )); } - if duration.months != 0 || duration.days != 0 { - return Err( - self.parse_error("Duration units out of order".to_string()) - ); - } - duration.years = value; } 'M' => { @@ -709,12 +699,6 @@ impl<'a> Parser<'a> { )); } - if duration.days != 0 { - return Err( - self.parse_error("Duration units out of order".to_string()) - ); - } - duration.months = value; } 'W' => { diff --git a/tests/parsing/test_parsing_duration.py b/tests/parsing/test_parsing_duration.py index d6a0b73d7..1f451aece 100644 --- a/tests/parsing/test_parsing_duration.py +++ b/tests/parsing/test_parsing_duration.py @@ -288,6 +288,51 @@ def test_parse_duration_invalid_order(): parse("PT1S1H") +@pytest.mark.parametrize( + "text", + [ + "P1Y2Y", + "P12M4M", + "P1W2W", + "P1D2D", + "PT1H2H", + "PT12M3M1M", + "PT1S2S", + "P0Y0Y", + "P0M0M", + "P0W0W", + "P0D0D", + "PT0H0H", + "PT0M0M", + "PT0S0S", + "P0M1Y", + "P0D1M", + "P0D1Y", + "PT0M1H", + "PT0S1M", + "PT0S1H", + ], +) +def test_parse_duration_repeated_or_reordered_components(text: str) -> None: + with pytest.raises(ParserError): + parse(text) + + +@pytest.mark.parametrize("text", ["P0Y0M0DT0H0M0S", "P0W"]) +def test_parse_duration_zero_components(text: str) -> None: + parsed = parse(text) + assert ( + parsed.years, + parsed.months, + parsed.weeks, + parsed.remaining_days, + parsed.hours, + parsed.minutes, + parsed.remaining_seconds, + parsed.microseconds, + ) == (0, 0, 0, 0, 0, 0, 0, 0) + + def test_parse_duration_invalid(): with pytest.raises(ParserError): parse("P1Dasdfasdf")