From a6412f7c499ba5814890496e245c62d2906ac2c6 Mon Sep 17 00:00:00 2001 From: shguild Date: Sun, 11 Oct 2026 09:23:21 +0800 Subject: [PATCH 1/3] docs: record ordinal date year padding fix --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 58bb1312f..c220466f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Change Log +## [Unreleased] + +### Fixed +- Fixed `from_format()` with day-of-year tokens (`DDD`/`DDDD`) for years before 1000. + ## [3.2.0] - 2026-01-30 ### Added From 7e1df9c3af4cba162bd701f3e522fe1d5082a36f Mon Sep 17 00:00:00 2001 From: shguild Date: Sun, 11 Oct 2026 09:24:03 +0800 Subject: [PATCH 2/3] fix: preserve four-digit years in ordinal date parsing --- src/pendulum/formatting/formatter.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pendulum/formatting/formatter.py b/src/pendulum/formatting/formatter.py index b09977d0d..5413d3a80 100644 --- a/src/pendulum/formatting/formatter.py +++ b/src/pendulum/formatting/formatter.py @@ -477,9 +477,10 @@ def _check_parsed( validated["year"] = now.year if parsed["day_of_year"] is not None: + year = cast("int", validated["year"]) dt = cast( "pendulum.DateTime", - pendulum.parse(f"{validated['year']}-{parsed['day_of_year']:>03d}"), + pendulum.parse(f"{year:04d}-{parsed['day_of_year']:03d}"), ) validated["month"] = dt.month From 95ba0231ee52beb4f9bdc557217dd997b5976c69 Mon Sep 17 00:00:00 2001 From: shguild Date: Sun, 11 Oct 2026 09:24:33 +0800 Subject: [PATCH 3/3] test: cover early-year ordinal format round trips --- tests/datetime/test_from_format.py | 33 ++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/tests/datetime/test_from_format.py b/tests/datetime/test_from_format.py index ecf59632a..6161ecc38 100644 --- a/tests/datetime/test_from_format.py +++ b/tests/datetime/test_from_format.py @@ -220,3 +220,36 @@ def test_from_format_2_digit_year(): d = pendulum.from_format("99", "YY") assert d.year == 1999 + + +@pytest.mark.parametrize("fmt", ["YYYY-DDD", "YYYY-DDDD"]) +@pytest.mark.parametrize( + "year,month,day", + [ + (1, 1, 31), + (99, 12, 31), + (400, 2, 29), + (999, 1, 31), + (1000, 1, 31), + (9999, 12, 31), + ], +) +def test_from_format_day_of_year_round_trip(fmt, year, month, day): + expected = pendulum.datetime(year, month, day) + + assert pendulum.from_format(expected.format(fmt), fmt) == expected + + +def test_from_format_day_of_year_preserves_time_and_offset(): + d = pendulum.from_format( + "0075-032 12:34:56.123456 +05:30", "YYYY-DDDD HH:mm:ss.SSSSSS Z" + ) + + assert_datetime(d, 75, 2, 1, 12, 34, 56, 123456) + assert d.utcoffset().total_seconds() == 19800 + + +@pytest.mark.parametrize("text", ["0001-366", "0400-367", "0000-031"]) +def test_from_format_day_of_year_rejects_invalid_dates(text): + with pytest.raises(ValueError): + pendulum.from_format(text, "YYYY-DDDD")