diff --git a/packages/schedule/tool-schedule/src/domain.ts b/packages/schedule/tool-schedule/src/domain.ts index 197864c309..3ef23ae816 100644 --- a/packages/schedule/tool-schedule/src/domain.ts +++ b/packages/schedule/tool-schedule/src/domain.ts @@ -815,12 +815,15 @@ function ownedLowYearCronInstant( function nextCronInstant(rule: ParsedCronRule, timeZone: string, after: number): number | undefined { if (!rule.hasMatchingDate) return undefined let cursor = after + const formatter = cronLocalFormatter(timeZone) if (new Date(after).getUTCFullYear() <= CRONER_LOW_YEAR_CUTOFF) { const lower = ownedLowYearCronInstant(rule, timeZone, after, 1) if (lower !== undefined) return lower + if (localProjection(formatter, after).offset % 60_000 !== 0) { + cursor = cursorBeforeNextOffsetTransition(formatter, after) + } } const evaluator = cronEvaluator(rule, timeZone) - const formatter = cronLocalFormatter(timeZone) let gapCorrections = 0 while (cursor < MAX_FOUR_DIGIT_YEAR_MS) { const candidate = evaluator.nextRun(new Date(cursor)) @@ -840,6 +843,7 @@ function nextCronInstant(rule: ParsedCronRule, timeZone: string, after: number): } gapCorrections = 0 if (epoch > MAX_FOUR_DIGIT_YEAR_MS) return undefined + /* v8 ignore next 3 -- current IANA data leaves sub-minute LMT at its first transition. */ if (epoch % 60_000 !== 0) { cursor = cursorBeforeNextOffsetTransition(formatter, epoch) continue diff --git a/packages/schedule/tool-schedule/tests/cron.spec.ts b/packages/schedule/tool-schedule/tests/cron.spec.ts index 079e398142..ec5df8b367 100644 --- a/packages/schedule/tool-schedule/tests/cron.spec.ts +++ b/packages/schedule/tool-schedule/tests/cron.spec.ts @@ -246,15 +246,23 @@ describe('Croner calendar adapter', () => { }) it('skips a sub-minute local-mean-time era before iterating dense safe-year matches', () => { - const record = createCronScheduleRecord( + const yearOne = createCronScheduleRecord( + ScheduleId('schedule-sub-minute-offset-year-one'), + 'standard-time handoff', + '*/5 * * * *', + 'Europe/Amsterdam', + Date.parse('0001-01-01T00:00:00.000Z'), + ) + const yearOneHundred = createCronScheduleRecord( ScheduleId('schedule-sub-minute-offset'), 'standard-time handoff', '*/5 * * * *', 'Europe/Amsterdam', Date.parse('0100-01-01T00:00:00.000Z'), ) - expect(new Date(record.scheduledAt).getUTCFullYear()).toBeGreaterThan(109) - expect(Math.abs(Date.parse(record.scheduledAt) % 60_000)).toBe(0) + expect(yearOne.scheduledAt).toBe(yearOneHundred.scheduledAt) + expect(new Date(yearOne.scheduledAt).getUTCFullYear()).toBeGreaterThan(109) + expect(Math.abs(Date.parse(yearOne.scheduledAt) % 60_000)).toBe(0) }, 1_000) it('selects the latest current match after a persisted baseline', () => {