fix: stop hour-step loop early on spring-forward DST days to prevent day skip#400
Conversation
…overshooting On spring-forward DST transition days, one applyDateOperation(Add, Hour) call jumps 2 wall-clock hours (e.g. 01:00 → 03:00). The hour-stepping loop in #matchHour computed steps as `nextHour - currentHour`, which caused it to overshoot the target by 1 hour on any day that includes a spring-forward transition. For example, with '0 8 * * 0' (Sundays at 8am, America/Chicago) and currentDate on Saturday night before the US spring-forward day (Mar 8 2026), next() returned the following Sunday (Mar 15) instead of Mar 8, skipping the DST day entirely. Fix: break out of the stepping loop as soon as the current hour reaches or passes the target, so the DST jump cannot cause an overshoot. Adds a regression test covering this exact scenario.
harrisiirak
left a comment
There was a problem hiding this comment.
Hi @malenkov!
I've left some comments, but the PR looks good to me in general.
| currentDate.applyDateOperation(dateMathVerb, TimeUnit.Hour, hours.length); | ||
| // On spring-forward days, one step jumps 2 wall-clock hours (e.g. 01:00 → 03:00), | ||
| // which can overshoot the target hour. Stop as soon as we reach or pass it. | ||
| if (!reverse && currentDate.getHours() >= nextHour) break; |
There was a problem hiding this comment.
nitpic: Adding comment overshoot protection is probably fine enough
There was a problem hiding this comment.
Done, shortened to a one-liner.
| expect(prev.getMinutes()).toEqual(0); | ||
| }); | ||
|
|
||
| test('does not skip spring-forward DST day when targeted by day-of-week', () => { |
There was a problem hiding this comment.
suggestion: Let's also add reverse direction test case to ensure that it works in both ways.
suggestion: Rename it to "does not overshoot target hour on spring-forward DST day" or something similar
There was a problem hiding this comment.
Done, renamed both tests and added a reverse direction case.
| }); | ||
|
|
||
| test('does not skip spring-forward DST day when targeted by day-of-week', () => { | ||
| // March 8, 2026 is the US spring-forward day (America/Chicago clocks jump 01:00→03:00). |
There was a problem hiding this comment.
nitpick: Probably don't need that long comment about the behavior, it's rather self explaining already.
Bug
On spring-forward DST transition days,
next()skips the entire day when the cron expression targets that day via day-of-week, returning the following week instead.Minimal repro (US spring-forward, March 8 2026):
Root Cause
In
#matchHour(CronExpression.ts), when the current date falls on a DST transition day, the code steps hour-by-hour instead of directly setting the target hour. The number of steps is computed asnextHour - currentHourin local wall-clock time.On a spring-forward day, one
applyDateOperation(Add, Hour)call jumps 2 wall-clock hours (e.g.01:00 → 03:00), so the loop overshoots the target by 1 hour:8 - 0 = 800:00 → 01:0001:00 → 03:00← DST jump (2 hours)03→04→05→06→07→0808:00 → 09:00← overshoot!Now at
09:00,findNearestValue(9)on[8]returnsnull→ the code jumps a full day → repeats until the next Sunday.Fix
Break out of the stepping loop as soon as the current hour reaches or passes the target:
Tests
Added a regression test in
CronExpressionParser.test.tscovering this exact scenario. All 264 existing tests continue to pass.