https://github.com/python/cpython/commit/4daa1cc2d57f0686a82a993713e40835756ce4b2 commit: 4daa1cc2d57f0686a82a993713e40835756ce4b2 branch: 3.14 author: Miss Islington (bot) <[email protected]> committer: StanFromIreland <[email protected]> date: 2026-10-21T17:16:50Z summary:
[3.14] gh-159714: Fix `ZoneInfo` Julian day 59 in leap years (GH-159721) (#160053) (cherry picked from commit 16c0adc745977270378db1c1d7105d969bd76680) Co-authored-by: Paul Ganssle <[email protected]> files: A Misc/NEWS.d/next/Library/2026-10-19-12-00-00.gh-issue-159714.j59fix.rst M Lib/test/test_zoneinfo/test_zoneinfo.py M Lib/zoneinfo/_zoneinfo.py M Modules/_zoneinfo.c diff --git a/Lib/test/test_zoneinfo/test_zoneinfo.py b/Lib/test/test_zoneinfo/test_zoneinfo.py index 5acb1ab25e2c2d8..47a1450d35d74da 100644 --- a/Lib/test/test_zoneinfo/test_zoneinfo.py +++ b/Lib/test/test_zoneinfo/test_zoneinfo.py @@ -1492,6 +1492,33 @@ def _add(): (datetime(2020, 12, 31, 23, 59, 59, 999999), AAA, NORMAL), ) + @call + def _add(): + # Julian days 58, 59 and 60 always mean February 27, February 28 + # and March 1, including in leap years. + AAA = ZoneOffset("AAA", ZERO) + BBB = ZoneOffset("BBB", ONE_H, ONE_H) + + for start, end, start_date, end_date in ( + (58, 59, (2, 27), (2, 28)), + (59, 60, (2, 28), (3, 1)), + ): + tzstr = f"AAA0BBB,J{start}/12,J{end}/12" + cases[tzstr] = [] + for year in (2019, 2020, 2000, 2100): + start_dt = datetime(year, *start_date, 12) + end_dt = datetime(year, *end_date, 12) + cases[tzstr].extend(( + (start_dt - timedelta(seconds=1), AAA, NORMAL), + (start_dt, AAA, GAP), + (start_dt.replace(fold=1), BBB, GAP), + (start_dt + ONE_H, BBB, NORMAL), + (end_dt - ONE_H - timedelta(seconds=1), BBB, NORMAL), + (end_dt - ONE_H, BBB, FOLD), + ((end_dt - ONE_H).replace(fold=1), AAA, FOLD), + (end_dt, AAA, NORMAL), + )) + @call def _add(): # Taken from America/Godthab, this rule has a transition on the diff --git a/Lib/zoneinfo/_zoneinfo.py b/Lib/zoneinfo/_zoneinfo.py index 48154775568b6dc..c7ec9b1bf36e223 100644 --- a/Lib/zoneinfo/_zoneinfo.py +++ b/Lib/zoneinfo/_zoneinfo.py @@ -535,7 +535,7 @@ def year_to_epoch(self, year): days_before_year = _post_epoch_days_before_year(year) d = self.d - if self.julian and d >= 59 and calendar.isleap(year): + if self.julian and d >= 60 and calendar.isleap(year): d += 1 epoch = (days_before_year + d) * 86400 diff --git a/Misc/NEWS.d/next/Library/2026-10-19-12-00-00.gh-issue-159714.j59fix.rst b/Misc/NEWS.d/next/Library/2026-10-19-12-00-00.gh-issue-159714.j59fix.rst new file mode 100644 index 000000000000000..7b61ca79cbd2b57 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-19-12-00-00.gh-issue-159714.j59fix.rst @@ -0,0 +1,2 @@ +Fix :class:`zoneinfo.ZoneInfo` interpreting the Julian day rule ``J59`` as +February 29 in leap years instead of February 28. diff --git a/Modules/_zoneinfo.c b/Modules/_zoneinfo.c index 40b82ec029b1854..b488d6dacef1b0b 100644 --- a/Modules/_zoneinfo.c +++ b/Modules/_zoneinfo.c @@ -1433,7 +1433,7 @@ dayrule_year_to_timestamp(TransitionRuleType *base_self, int year) // you would use a Julian day, e.g. J91 always refers to April 1st and J365 // always refers to December 31st. uint16_t day = self->day; - if (self->julian && day >= 59 && is_leap_year(year)) { + if (self->julian && day >= 60 && is_leap_year(year)) { day += 1; } _______________________________________________ Python-checkins mailing list -- [email protected] To unsubscribe send an email to [email protected] https://mail.python.org/mailman3//lists/python-checkins.python.org Member address: [email protected]
