Skip to content
Open
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
6 changes: 6 additions & 0 deletions rust/src/parsing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -597,6 +597,7 @@ impl<'a> Parser<'a> {

let mut duration: ParsedDuration = ParsedDuration::new();
let mut got_t: bool = false;
let mut has_time_component = false;
let mut last_had_fraction = false;

loop {
Expand Down Expand Up @@ -683,6 +684,7 @@ impl<'a> Parser<'a> {
)
}
}
has_time_component = true;
} else {
match self.current {
'Y' => {
Expand Down Expand Up @@ -791,6 +793,10 @@ impl<'a> Parser<'a> {
}
}

if got_t && !has_time_component {
return Err(self.parse_error("Missing time component in duration".to_string()));
}

parsed.duration = Some(duration);

Ok(())
Expand Down
5 changes: 5 additions & 0 deletions src/pendulum/parsing/iso8601.py
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,11 @@ def _parse_iso8601_duration(text: str, **options: str) -> Duration | None:
if not m or (not m.group("w") and not m.group("ymd") and not m.group("hms")):
return None

if m.group("timesep") and not any(
m.group(unit) for unit in ("hours", "minutes", "seconds")
):
return None

years = 0
months = 0
weeks = 0
Expand Down
15 changes: 12 additions & 3 deletions tests/parsing/test_parse_iso8601.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,14 @@ def test_parse_iso8601_invalid():
("PT1.5H", (0, 0, 0, 0, 1, 30, 0, 0)),
("PT1,5H", (0, 0, 0, 0, 1, 30, 0, 0)),
("P2Y30M4DT5H6M7S", (2, 30, 0, 4, 5, 6, 7, 0)),
("PT0H", (0, 0, 0, 0, 0, 0, 0, 0)),
("PT0M", (0, 0, 0, 0, 0, 0, 0, 0)),
("PT0S", (0, 0, 0, 0, 0, 0, 0, 0)),
("PT0.0S", (0, 0, 0, 0, 0, 0, 0, 0)),
("P0D", (0, 0, 0, 0, 0, 0, 0, 0)),
("P1DT0S", (0, 0, 0, 1, 0, 0, 0, 0)),
("P1Y2MT0H", (1, 2, 0, 0, 0, 0, 0, 0)),
("P1DT2S", (0, 0, 0, 1, 0, 0, 2, 0)),
],
)
def test_parse_iso8601_duration(
Expand All @@ -210,7 +218,8 @@ def test_parse_iso8601_duration(
) == expected


def test_parse_iso8601_duration_invalid():
# Must include at least one element
@pytest.mark.parametrize("text", ["P", "PT", "P1DT", "P0DT", "P1Y2MT", "P1.5DT"])
def test_parse_iso8601_duration_invalid(text: str) -> None:
# Durations need a component, and T must be followed by a time component.
with pytest.raises(ValueError):
parse_iso8601("P")
parse_iso8601(text)
28 changes: 28 additions & 0 deletions tests/test_parsing.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
from __future__ import annotations

import pytest

import pendulum

from pendulum.parsing import ParserError
from tests.conftest import assert_date
from tests.conftest import assert_datetime
from tests.conftest import assert_duration
Expand Down Expand Up @@ -95,6 +98,31 @@ def test_parse_duration() -> None:
assert_duration(duration, 0, 0, 2, 0, 0, 0, 0)


@pytest.mark.parametrize("text", ["PT", "P1DT", "P0DT", "P1Y2MT", "P1.5DT"])
def test_parse_duration_without_time_components(text: str) -> None:
with pytest.raises(ParserError):
pendulum.parse(text, strict=True)


@pytest.mark.parametrize(
["text", "days", "seconds"],
[
("PT0H", 0, 0),
("PT0M", 0, 0),
("PT0S", 0, 0),
("PT0.0S", 0, 0),
("P0D", 0, 0),
("P1DT0S", 1, 0),
("P1DT2S", 1, 2),
],
)
def test_parse_duration_time_components(text: str, days: int, seconds: int) -> None:
duration = pendulum.parse(text, strict=True)

assert isinstance(duration, pendulum.Duration)
assert_duration(duration, 0, 0, 0, days, 0, 0, seconds)


def test_parse_interval() -> None:
text = "2008-05-11T15:30:00Z/P1Y2M10DT2H30M"

Expand Down