Skip to content

fix: do not return a date before start during a backward DST jump - #1088

Open
ChangkeunJ wants to merge 1 commit into
kelektiv:mainfrom
ChangkeunJ:fix/dst-repeated-hour
Open

fix: do not return a date before start during a backward DST jump#1088
ChangkeunJ wants to merge 1 commit into
kelektiv:mainfrom
ChangkeunJ:fix/dst-repeated-hour

Conversation

@ChangkeunJ

Copy link
Copy Markdown

Description

getNextDateFrom rebuilds the matched wall time with DateTime.fromFormat(..., { zone }). In the repeated hour of a fall-back transition that wall time is ambiguous and luxon seeds it with the offset of the current clock (fromObject, zone.offset(Settings.now())), so before the transition it picks the first occurrence. When start already sits in the second occurrence the result comes back 30 to 60 minutes before start, and the two follow-up checks in that function only ever move the result earlier, so nothing catches it.

This adds one guard after those checks: if the resolved date is not after start, try +30, +60 and +120 minutes and return the one that keeps the same hour and minute and is after start. Those three steps cover every backward offset change in tzdata for 2026 and 2027.

Related Issue

fixes #1087

Motivation and Context

getNextDateFrom(date, tz), CronJob.nextDates(n) and CronTime.sendAt(n) return a date in the past for any start between 01:00 and 01:29 EST on 2026-11-01 in America/New_York with 30 1 * * *, and the chained calls walk backwards (05:59Z, 06:00Z, 05:01Z, ...). Regression since 4.1.1 (#966). The scheduling loop itself is not affected, real jobs run through the transition on fake timers fire once and on time.

How Has This Been Tested?

The new test in tests/crontime.test.ts pins the clock before the transition with sinon.useFakeTimers, so it fails on main whatever the calendar date says (Expected: true, Received: false) and passes with the fix. npm test (TZ=Europe/Paris) 167 passed, npm run lint clean, Node 20.20.2, luxon 3.7.2.

I also compared the patched and unpatched builds against a brute force oracle built from wall clock tables for America/New_York, America/Santiago, Australia/Sydney, Australia/Lord_Howe, Antarctica/Troll and Asia/Seoul across the 2026 and 2027 transitions, about 170k calls with the luxon clock seeded before and after each transition. The guard fires only where the unpatched build went backwards, and never on spring-forward or in a zone without DST.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist:

  • My code follows the code style of this project.
  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.
  • I have added tests to cover my changes.
  • All new and existing tests passed.
  • If my change introduces a breaking change, I have added a ! after the type/scope in the title (see the Conventional Commits standard).

When the clock is still before a fall-back transition, luxon resolves an
ambiguous wall time to its first occurrence, so getNextDateFrom could hand
back the earlier of the two identical hours even when start already sat in
the later one. Take the later occurrence in that case.

fixes kelektiv#1087
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.

getNextDateFrom(date, timeZone) returns a date before date inside the repeated hour of a fall-back transition (regression in 4.1.1)

1 participant