Skip to content

Commit f533006

Browse files
committed
Fix start_of("day") landing on the previous day when a zone springs forward at midnight
_start_of_day resets to midnight via self.at(0, 0, 0, 0), which propagates the receiver's fold. On a timezone that springs forward exactly at midnight (e.g. Chile/Continental, where 2025-09-07 00:00:00 does not exist), reaching that day through .add(days=1) yields fold=0. For a nonexistent (skipped) local time with fold=0, the timezone resolver shifts the instant backward, so midnight lands at 23:00 on the previous day and start_of("day") is wrong and non-idempotent. After resetting to midnight, if the calendar day changed then midnight was skipped, so re-create the day's first valid instant resolving forward (fold=1). The guard fires only when midnight actually moved to the previous day, so all other zones/dates are byte-identical (verified against UTC, America/New_York, Asia/Tokyo, Europe/London, Europe/Vienna's 02:00 gap, and America/Sao_Paulo's midnight fall-back). start_of("week") is covered via delegation. Fixes #915.
1 parent 5ad098b commit f533006

2 files changed

Lines changed: 28 additions & 1 deletion

File tree

src/pendulum/datetime.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -826,7 +826,19 @@ def _start_of_day(self) -> Self:
826826
"""
827827
Reset the time to 00:00:00.
828828
"""
829-
return self.at(0, 0, 0, 0)
829+
dt = self.at(0, 0, 0, 0)
830+
831+
# In zones that spring forward exactly at midnight, 00:00:00 does not
832+
# exist. When ``fold`` is 0, resolving that nonexistent time shifts it
833+
# backward into the previous day, which is not the start of this day.
834+
# Detect that the day changed and rebuild the first valid instant of
835+
# the day by resolving forward (fold=1) instead.
836+
if dt.day != self.day:
837+
dt = self.__class__.create(
838+
self.year, self.month, self.day, 0, 0, 0, 0, tz=self.tz, fold=1
839+
)
840+
841+
return dt
830842

831843
def _end_of_day(self) -> Self:
832844
"""

tests/datetime/test_start_end_of.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,3 +323,18 @@ def test_end_of_on_date_after_transition():
323323
assert d.end_of("day").offset == 3600
324324
assert d.end_of("month").offset == 3600
325325
assert d.end_of("year").offset == 3600
326+
327+
328+
def test_start_of_day_when_midnight_does_not_exist():
329+
# Chile/Continental springs forward exactly at midnight on 2025-09-07,
330+
# so 00:00:00 does not exist that day. start_of("day") must resolve to the
331+
# first valid instant of the day (01:00:00-03:00), not fall back into the
332+
# previous day. See issue #915.
333+
d = pendulum.datetime(2025, 9, 6, 0, 0, tz="Chile/Continental").add(days=1)
334+
new = d.start_of("day")
335+
336+
assert new.day == 7
337+
assert_datetime(new, 2025, 9, 7, 1, 0, 0, 0)
338+
assert new.offset == -3 * 3600
339+
# start_of("day") must be idempotent even across the midnight gap.
340+
assert new == new.start_of("day")

0 commit comments

Comments
 (0)