Skip to content

Commit 01ed192

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 191fbd7 commit 01ed192

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
@@ -1658,6 +1658,29 @@ def _add():
16581658
(datetime(2012, 11, 4, 2, 15, 45), AAA, NORMAL),
16591659
)
16601660

1661+
@call
1662+
def _add():
1663+
# Numeric day rules count from January 1, including leap days.
1664+
STD = ZoneOffset("STD", ZERO)
1665+
DST = ZoneOffset("DST", ONE_H, ONE_H)
1666+
1667+
for day in (0, 10, 59, 60, 364, 365):
1668+
tzstr = f"STD0DST,{day}/2,{day}/14"
1669+
day_cases = []
1670+
for year in (2019, 2020):
1671+
date = datetime(year, 1, 1) + timedelta(days=day)
1672+
day_cases.extend((
1673+
(date.replace(hour=1, minute=59), STD, NORMAL),
1674+
(date.replace(hour=2, fold=0), STD, GAP),
1675+
(date.replace(hour=2, fold=1), DST, GAP),
1676+
(date.replace(hour=3), DST, NORMAL),
1677+
(date.replace(hour=12, minute=59), DST, NORMAL),
1678+
(date.replace(hour=13, fold=0), DST, FOLD),
1679+
(date.replace(hour=13, fold=1), STD, FOLD),
1680+
(date.replace(hour=14), STD, NORMAL),
1681+
))
1682+
cases[tzstr] = day_cases
1683+
16611684
cls.test_cases = cases
16621685

16631686

‎Lib/zoneinfo/_zoneinfo.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -583,6 +583,9 @@ def year_to_epoch(self, year):
583583
d = self.d
584584
if self.julian and d >= 59 and calendar.isleap(year):
585585
d += 1
586+
elif not self.julian:
587+
# Numeric day rules are zero-based, unlike Julian day rules.
588+
d += 1
586589

587590
epoch = (days_before_year + d) * 86400
588591
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)