Skip to content

Fix start_of("day") landing on the previous day when a zone springs forward at midnight - #994

Open
youdie006 wants to merge 1 commit into
python-pendulum:masterfrom
youdie006:fix/915-start-of-day-midnight-gap
Open

Fix start_of("day") landing on the previous day when a zone springs forward at midnight#994
youdie006 wants to merge 1 commit into
python-pendulum:masterfrom
youdie006:fix/915-start-of-day-midnight-gap

Conversation

@youdie006

Copy link
Copy Markdown

Pull Request Check List

  • Added tests for changed code.
  • Updated documentation for changed code.

Fixes #915.

Problem

On a timezone that springs forward exactly at midnight (e.g. Chile/Continental, where 2025-09-07 00:00:00 does not exist), start_of("day") returns an instant on the previous calendar day and is not idempotent:

>>> pendulum.datetime(2025, 9, 6, 0, 0, tz="Chile/Continental").add(days=1).start_of("day")
DateTime(2025, 9, 6, 23, 0, 0, tzinfo=Timezone('Chile/Continental'))  # wrong: date is Sep 6

Expected the first valid instant of Sep 7, 2025-09-07 01:00:00-03:00. Zones whose transition is not at midnight (e.g. Europe/Vienna at 02:00) are unaffected.

Root cause

_start_of_day resets to midnight via self.at(0, 0, 0, 0), which propagates the receiver's fold. Reaching Sep 7 through .add(days=1) yields fold=0. For a nonexistent (skipped) local time with fold=0, the timezone resolver (tz/timezone.py) shifts the instant backward (offset_before - offset_after), so midnight lands at 23:00 on the previous day. start_of is therefore wrong and non-idempotent for that day.

Fix

Scoped to _start_of_day: after resetting to midnight, if the calendar day changed then midnight was skipped, so re-create the day's first 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). Because _start_of_week delegates to start_of("day"), this also covers start_of("week").

Note: the same latent midnight-gap could in principle affect _start_of_month/year/decade/century; this PR deliberately keeps the change minimal and scoped to _start_of_day (the path reported in #915).

Tests

Added test_start_of_day_when_midnight_does_not_exist. Red without the fix (assert 6 == 7, result was 2025-09-06 23:00), green with it. Full suite: 1459 passed, 3 skipped (tests/datetime tests/tz tests/date).


This change was prepared with AI assistance and reviewed by me before submission.

…orward 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 python-pendulum#915.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Incrementing by day fails for timezones like Chile/Continental where spring forward occurs at midnight. Works for Europe.

1 participant