Repository navigation
gh-159714: Fix ZoneInfo Julian day 59 in leap years - #159721
Conversation
Julian day 59 is February 28 in every year, but both implementations were applying the leap-day adjustment starting at day 59. Apply it starting at day 60 instead. Add regression cases for J59 as both the start and end of DST, including gaps, folds and UTC conversion in leap and common years.
| // We subtract off an additional 1 day to account for January 1st (we want | ||
| // the number of full days *before* the date of the transition - partial | ||
| // days are accounted for in the hour, minute and second portions. | ||
| int64_t days_before_year = ymd_to_ord(year, 1, 1) - EPOCHORDINAL - 1; |
There was a problem hiding this comment.
Hmmm, the - 1 here is only right for the one-based Jn form, where J1 is January 1. For the zero-based n form, 0 is already January 1, so adding day to this base lands every n rule one day early....
>>> import datetime
... import io
... import struct
... import zoneinfo
... from zoneinfo import _zoneinfo
...
...
... def tzif(footer: bytes) -> io.BytesIO:
... # A TZif v3 file with no explicit transitions.
... header = b"TZif3" + b"\0" * 15 + struct.pack(">6l", 0, 0, 0, 0, 1, 4)
... body = struct.pack(">lbb", 0, 0, 0) + b"STD\0"
... return io.BytesIO(header + body + header + body + b"\n" + footer + b"\n")
...
...
... for implementation in (zoneinfo, _zoneinfo):
... tz = implementation.ZoneInfo.from_file(tzif(b"STD0DST-1,59/0,J300/0"))
... print(implementation.__name__)
... for year in (2023, 2024):
... local = datetime.datetime(year, 2, 28, 12, tzinfo=tz)
... print(local.date(), local.tzname())
...
zoneinfo
2023-02-28 DST
2024-02-28 DST
zoneinfo._zoneinfo
2023-02-28 DST
2024-02-28 DSTI'd expect STD in both years as day 59 is March 1 in 2023 and February 29 in 2024, so February 28 is still before the transition. Another fun bug (isn't it great when patches find bugs ;-)!
There was a problem hiding this comment.
Oh wait, I just looked through the issues, you already have it as #159712
|
Thanks @pganssle for the PR, and @StanFromIreland for merging it 🌮🎉.. I'm working now to backport this PR to: 3.14, 3.15. |
|
GH-160052 is a backport of this pull request to the 3.15 branch. |
|
GH-160053 is a backport of this pull request to the 3.14 branch. |
Fixes #159714.
J59in a TZ string always means February 28, but bothZoneInfoimplementations were adding a leap day starting at day 59. This moves the adjustment to day 60, soJ59remains February 28 andJ60remains March 1.The regression cases cover
J59as both the start and end of DST, along with neighboringJ58andJ60rules, including gaps, folds and UTC conversion in common and leap years. They include century years on both sides of the leap-year rule../python -m test test_zoneinfo -vpasses on a debug build, with 277 tests run and 30 skipped. Reverting the fix produces 18 failures in the shared tests, covering both implementations and both local-time lookup and UTC conversion.ZoneInfoevaluates TZ ruleJ59as February 29 in leap years #159714