Skip to content

Commit 5b2ce22

Browse files
committed
gh-159712: Fix zero-based day rules in zoneinfo
Numeric TZ day rules count January 1 as day zero, but both implementations were using the same year baseline as one-based Julian rules. Account for the difference so numeric rules no longer transition one day early. Add local-time and UTC conversion cases for numeric rules, including gaps, folds, leap days and the first and last days of the year.
1 parent b9374b4 commit 5b2ce22

4 files changed

Lines changed: 32 additions & 0 deletions

File tree

‎Lib/test/test_zoneinfo/test_zoneinfo.py‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1673,6 +1673,29 @@ def _add():
16731673
(datetime(2012, 11, 4, 2, 15, 45), AAA, NORMAL),
16741674
)
16751675

1676+
@call
1677+
def _add():
1678+
# Numeric day rules count from January 1, including leap days.
1679+
STD = ZoneOffset("STD", ZERO)
1680+
DST = ZoneOffset("DST", ONE_H, ONE_H)
1681+
1682+
for day in (0, 10, 59, 60, 364, 365):
1683+
tzstr = f"STD0DST,{day}/2,{day}/14"
1684+
day_cases = []
1685+
for year in (2019, 2020):
1686+
date = datetime(year, 1, 1) + timedelta(days=day)
1687+
day_cases.extend((
1688+
(date.replace(hour=1, minute=59), STD, NORMAL),
1689+
(date.replace(hour=2, fold=0), STD, GAP),
1690+
(date.replace(hour=2, fold=1), DST, GAP),
1691+
(date.replace(hour=3), DST, NORMAL),
1692+
(date.replace(hour=12, minute=59), DST, NORMAL),
1693+
(date.replace(hour=13, fold=0), DST, FOLD),
1694+
(date.replace(hour=13, fold=1), STD, FOLD),
1695+
(date.replace(hour=14), STD, NORMAL),
1696+
))
1697+
cases[tzstr] = day_cases
1698+
16761699
cls.test_cases = cases
16771700

16781701

‎Lib/zoneinfo/_zoneinfo.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -598,6 +598,9 @@ def year_to_epoch(self, year):
598598
d = self.d
599599
if self.julian and d >= 59 and calendar.isleap(year):
600600
d += 1
601+
elif not self.julian:
602+
# Numeric day rules are zero-based, unlike Julian day rules.
603+
d += 1
601604

602605
epoch = (days_before_year + d) * 86400
603606
epoch += self.hour * 3600 + self.minute * 60 + self.second
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix :class:`zoneinfo.ZoneInfo` evaluating numeric day-of-year rules in TZ
2+
strings one day early.

‎Modules/_zoneinfo.c‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1439,6 +1439,10 @@ dayrule_year_to_timestamp(TransitionRuleType *base_self, int year)
14391439
if (self->julian && day >= 59 && is_leap_year(year)) {
14401440
day += 1;
14411441
}
1442+
else if (!self->julian) {
1443+
// Numeric day rules are zero-based, unlike Julian day rules.
1444+
day += 1;
1445+
}
14421446

14431447
return (days_before_year + day) * 86400 + (int64_t)self->hour * 3600 +
14441448
(int64_t)self->minute * 60 + self->second;

0 commit comments

Comments
 (0)