Skip to content

Commit bd5ef4d

Browse files
committed
gh-159715: Explain year-boundary cases and lookup margins
1 parent 191fbd7 commit bd5ef4d

2 files changed

Lines changed: 34 additions & 8 deletions

File tree

‎Lib/test/test_zoneinfo/test_zoneinfo.py‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1112,20 +1112,35 @@ def test_transitions_in_adjacent_year(self):
11121112
# rule evaluation. Cover starts and ends, either sign of DST, and
11131113
# rules displaced by both UTC offsets and extended transition hours.
11141114
cases = (
1115+
# Positive DST starts on January 1 locally, but December 31 UTC.
11151116
("STD-2DST-3,J1/0,J200/0", datetime(2100, 12, 31, 22), 2, 3),
1117+
# Positive DST starts in January from the previous year's J365.
11161118
("STD2DST1,J365/26,J200/0", datetime(2101, 1, 1, 4), -2, -1),
1119+
# Negative DST starts on January 1 locally, but December 31 UTC.
11171120
("STD-2DST-1,J1/0,J200/0", datetime(2100, 12, 31, 22), 2, 1),
1121+
# Negative DST starts in January from the previous year's J365.
11181122
("STD2DST3,J365/26,J200/0", datetime(2101, 1, 1, 4), -2, -3),
1123+
# Positive DST ends on January 1 locally, but December 31 UTC.
11191124
("STD-2DST-3,J200/0,J1/0", datetime(2100, 12, 31, 21), 3, 2),
1125+
# Positive DST ends in January from the previous year's J365.
11201126
("STD2DST1,J200/0,J365/26", datetime(2101, 1, 1, 3), -1, -2),
1127+
# Negative DST ends on January 1 locally, but December 31 UTC.
11211128
("STD-2DST-1,J200/0,J1/0", datetime(2100, 12, 31, 23), 1, 2),
1129+
# Negative DST ends in January from the previous year's J365.
11221130
("STD2DST3,J200/0,J365/26", datetime(2101, 1, 1, 5), -3, -2),
1131+
# The most negative transition hour moves J1 into December.
11231132
("STD0DST-1,J1/-167,J200/0", datetime(2099, 12, 25, 1), 0, 1),
1133+
# The largest transition hour moves J365 into January.
11241134
("STD0DST-1,J200/0,J365/167", datetime(2101, 1, 6, 22), 1, 0),
1135+
# A month/week rule from year 0 supplies a transition in year 1.
11251136
("STD0DST-1,M12.5.0/167,M6.1.0/0", datetime(1, 1, 6, 23), 0, 1),
1137+
# A year-10000 rule supplies a transition within datetime's range.
11261138
("STD0DST-1,J1/-167,J200/0", datetime(9999, 12, 25, 1), 0, 1),
1139+
# A season starting in rule year 2023 ends in January 2025.
11271140
("STD0DST-1,J365/167,J365/166", datetime(2025, 1, 6, 21), 1, 0),
1141+
# The next season starts two hours after the preceding one ends.
11281142
("STD0DST-1,J365/167,J365/166", datetime(2025, 1, 6, 23), 0, 1),
1143+
# The same two-rule-year lookback must work through year -1.
11291144
("STD0DST-1,J365/167,J365/166", datetime(1, 1, 6, 21), 1, 0),
11301145
)
11311146
for tzstr, transition, old_hours, new_hours in cases:

‎Lib/zoneinfo/_zoneinfo.py‎

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,17 @@
1111
EPOCH = datetime(1970, 1, 1)
1212
EPOCHORDINAL = datetime(1970, 1, 1).toordinal()
1313

14+
_SECONDS_PER_DAY = 24 * 60 * 60
15+
# Parsed transition times are less than 168 hours and explicit UTC offsets
16+
# less than 25 hours (26 with the default one-hour DST adjustment). Offset
17+
# differences are less than 50 hours. Their sum is less than 244 hours, so
18+
# an 11-day margin also covers looking back across a fold.
19+
_TZSTR_YEAR_BOUNDARY_MARGIN = 11 * _SECONDS_PER_DAY
20+
# Use the common-year length; in leap years this leaves an extra day of margin.
21+
_TZSTR_FAST_PATH_END = 365 * _SECONDS_PER_DAY - _TZSTR_YEAR_BOUNDARY_MARGIN
22+
# Each rule's day-of-year can vary by up to seven days between years.
23+
_TZSTR_RULE_ORDER_MARGIN = 2 * 7 * _SECONDS_PER_DAY
24+
1425
# It is relatively expensive to construct new timedelta objects, and in most
1526
# cases we're looking at the same deltas, like integer numbers of hours, etc.
1627
# To improve speed and memory use, we'll keep a dictionary with references
@@ -474,15 +485,14 @@ def _dst_period(self, year):
474485
def _get_trans_info(self, ts, year, fold):
475486
"""Get the information about the current transition - tti"""
476487

477-
# Even the largest parsed transition time (<168h), UTC offset
478-
# (<25h), and fold (<50h) cannot reach 11 days from a year boundary.
479-
year_start = (_post_epoch_days_before_year(year) + 1) * 86400
480-
if year_start + 11 * 86400 <= ts < year_start + 354 * 86400:
488+
year_start = (_post_epoch_days_before_year(year) + 1) * _SECONDS_PER_DAY
489+
if (year_start + _TZSTR_YEAR_BOUNDARY_MARGIN <= ts
490+
< year_start + _TZSTR_FAST_PATH_END):
481491
start, end = self.transitions(year)
482492
# Each rule's day-of-year can move by up to seven days between
483493
# years. Nearby rules can reverse order, allowing a previous
484494
# year's southern season to overlap this year's northern one.
485-
if abs(end - start - self.dst_diff) > 14 * 86400:
495+
if abs(end - start - self.dst_diff) > _TZSTR_RULE_ORDER_MARGIN:
486496
if fold == (self.dst_diff >= 0):
487497
end -= self.dst_diff
488498
else:
@@ -518,12 +528,13 @@ def _get_trans_info(self, ts, year, fold):
518528
return self.std
519529

520530
def _get_trans_info_fromutc(self, ts, year):
521-
year_start = (_post_epoch_days_before_year(year) + 1) * 86400
522-
if year_start + 11 * 86400 <= ts < year_start + 354 * 86400:
531+
year_start = (_post_epoch_days_before_year(year) + 1) * _SECONDS_PER_DAY
532+
if (year_start + _TZSTR_YEAR_BOUNDARY_MARGIN <= ts
533+
< year_start + _TZSTR_FAST_PATH_END):
523534
start, end = self.transitions(year)
524535
start -= self.std.utcoff.total_seconds()
525536
end -= self.dst.utcoff.total_seconds()
526-
if abs(end - start) > 14 * 86400:
537+
if abs(end - start) > _TZSTR_RULE_ORDER_MARGIN:
527538
before = ts - abs(self.dst_diff)
528539
if start < end:
529540
isdst = start <= ts < end

0 commit comments

Comments
 (0)