Skip to content

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

Description

@ChangkeunJ

Description

CronTime.getNextDateFrom(start, timeZone) returns an instant earlier than start when start is in the first half of the repeated hour of a fall-back DST transition and the call is made before that transition. CronJob.nextDates(n) and CronTime.sendAt(n) chain through it, so they hand back a list that runs backwards over that hour. The scheduling loop itself isn't affected, see Context.

Bisected: 4.1.0 is fine, 4.1.1 and every published version since returns the earlier instant. The only src change between those tags is 8cf0712 (#966), luxon is 3.5.0 on both sides. #1049 describes a similar symptom, but its repro imports node-cron/node-cron; this one is against this package.

Expected Behavior

For 30 1 * * * in America/New_York, asked from 2026-11-01T06:00:00Z (01:00 EST, the second 01:00 of the night), the next date is 2026-11-01T06:30:00Z (01:30 EST). That's what 3.5.0, 4.0.0 and 4.1.0 return, and getNextDateFrom should never return something at or before its start.

Actual Behavior

2026-11-01T06:00:00Z -> 2026-11-01T05:30:00.000Z 30 min before start
2026-11-01T06:15:00Z -> 2026-11-01T05:30:00.000Z 45 min before start
2026-11-01T06:29:00Z -> 2026-11-01T05:30:00.000Z 59 min before start

05:30Z is 01:30 EDT, the first occurrence, already in the past. Through nextDates(8) on * * * * * with the clock at 05:58Z it comes out as 05:59Z 06:00Z 05:01Z 05:02Z 05:03Z ....

Possible Fix

The search runs on a UTC clone and the result is rebuilt with DateTime.fromFormat(..., { zone: timeZone }). For a wall time that exists twice, luxon picks the occurrence by seeding with the zone offset at Settings.now() (fromObject, offsetProvis = zone.offset(tsNow), datetime.js line 800 in 3.7.2). Before the transition that's EDT, so 01:30 becomes 05:30Z. The ambiguity handling that follows (hourTestDate > start and the half hour variant) only ever moves the result earlier, so nothing pushes it past start and 05:30Z is returned as is. Once the real clock is past the transition luxon seeds with EST and the same call returns 06:30Z, which is why this hides most of the year.

I have a fix that returns the later occurrence when the resolved date is <= start, with a regression test that pins the clock before the transition so it fails on current main whatever day it's run. Full suite 167/167 under TZ=Europe/Paris, lint clean. Happy to open the PR.

Steps to Reproduce

import { CronTime } from 'cron';
// America/New_York falls back on 2026-11-01: 01:00-02:00 EDT (05:00-06:00Z) is followed by 01:00-02:00 EST (06:00-07:00Z)
const t = new CronTime('30 1 * * *', 'America/New_York');
for (const iso of ['2026-11-01T06:00:00Z', '2026-11-01T06:15:00Z', '2026-11-01T06:29:00Z']) {
  const start = new Date(iso);
  const next = t.getNextDateFrom(start, 'America/New_York').toJSDate();
  console.log(iso, '->', next.toISOString(), next < start ? `${(start - next) / 60000} min before start` : '');
}

Same output under TZ=UTC, TZ=America/New_York and TZ=Asia/Seoul. Run it before 2026-11-01 (or with the clock faked to any EDT date) to see it; after the transition the same call is correct.

Context

I was comparing how schedulers handle the repeated hour and this one came back with a date before its input. What isn't affected: a running job calls sendAt() from the current clock, and by the time it's inside the repeated hour luxon already seeds with the new offset. Faking the clock at 1 minute steps across the fall-back night in America/New_York, Australia/Sydney and Australia/Lord_Howe for 7 expressions (4,851 calls) never returned a time at or before now, and jobs run through the transition on sinon fake timers fired 240/240 minutes with no warnings and no duplicates. So this is about the "next N dates" answers, not about jobs firing wrong in production.

Your Environment

  • cron version: 4.4.0 (luxon 3.7.2)
  • NodeJS version: v20.20.2
  • Operating System and version: Ubuntu, Linux 5.15
  • TypeScript version (if applicable): n/a, plain ESM
  • Link to your project (if applicable): n/a

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions