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]

Reply via email to