https://github.com/python/cpython/commit/16c0adc745977270378db1c1d7105d969bd76680
commit: 16c0adc745977270378db1c1d7105d969bd76680
branch: main
author: Paul Ganssle <[email protected]>
committer: StanFromIreland <[email protected]>
date: 2026-10-21T17:48:08+01:00
summary:
gh-159714: Fix `ZoneInfo` Julian day 59 in leap years (#159721)
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 41ba92342ff2cbd..c6393743e93c3df 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 46ab3499e003542..45f65b9d30b6374 100644
--- a/Modules/_zoneinfo.c
+++ b/Modules/_zoneinfo.c
@@ -1436,7 +1436,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]