Repository navigation
Fix previousRuns() for months shorter than 31 days - #409
Merged
Hexagon merged 1 commit intoOct 17, 2026
Merged
Conversation
When the backward search moved to an earlier month, the day was reset to the pattern maximum (31), so apply() rolled e.g. April 31 over into May and the recursion continued at level -1. Cap the day at the last day of the month. apply() also reported a valid February 29 in a leap year as out of bounds, which hit the same path for leap day patterns. Fixes #408
Hexagon
approved these changes
Oct 17, 2026
dadezzz
pushed a commit
to dadezzz/events-cash-register
that referenced
this pull request
Oct 30, 2026
This PR contains the following updates: | Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) | |---|---|---|---| | [croner](https://croner.56k.guru) ([source](https://github.com/Hexagon/croner)) | [`10.0.1` → `11.0.0`](https://renovatebot.com/diffs/npm/croner/10.0.1/11.0.0) |  |  | --- ### Release Notes <details> <summary>Hexagon/croner (croner)</summary> ### [`v11.0.0`](https://github.com/Hexagon/croner/blob/HEAD/CHANGELOG.md#1100---2026-10-25) [Compare Source](Hexagon/croner@10.0.1...11.0.0) ##### Breaking changes - **DST handling aligned with croner-rust and Vixie cron, deviating from OCPS 1.4 §4.3.1**: jobs are either fixed-time jobs (one value each for seconds, minutes and hours) or interval jobs. - A fixed-time job in a DST gap (spring forward) runs at the first valid time after the gap. In 10.x it ran as much later as the gap was long (`0 30 2 * * *` ran at 3:30 AM, now 3:00 AM). - An interval job runs on both occurrences of a time repeated by a DST overlap (fall back), so `* * * * *` keeps running through the repeated hour ([#​343](Hexagon/croner#343), [#​407](Hexagon/croner#407)). Patterns with more than one hour, minute or second, like `0 30 1,2 * * *`, are interval jobs. - Fixed-time jobs still run once, at the first occurrence, in a DST overlap. - **`startAt` is inclusive**: a pattern match exactly at `startAt` now runs. Previously the first run was the following match. This does not apply with `interval` set, which works as in 10.x. A `startAt` between two whole seconds counts from the next whole second, and a run in the same second before a fractional `stopAt` (00:00:00 with `stopAt` 00:00:00.500) now runs. 10.x excluded it. - **With `dayOffset`, `nextRun()`, `nextRuns()`, `previousRuns()`, `msToNext()` and `enumerate()` return actual run times**, searched from the reference, the same way the scheduler and `isRunning()` do. Previously they shifted the next pattern match after the reference, so a negative offset could return a time before the reference, and `msToNext()` ignored the offset. `startAt` and `stopAt` also apply to the shifted run time. In 10.x they applied to the unshifted pattern match, so a job could run before `startAt` or after `stopAt`. - **`previousRun()` returns when the previous run started**, as documented. Previously it returned when the run finished. ##### Added - `enumerate(startAt?)` method returning a stateful `CronIterator` that supports iterator/iterable protocols (`for...of`, destructuring, `next()`, `peek()`, `reset()`) and honors `maxRuns` ([#​365](Hexagon/croner#365)). The `CronIterator` class is exported. - `allowPast` option for explicitly allowing date-based jobs scheduled more than 1 second in the past to fire immediately ([#​349](Hexagon/croner#349)). - `CatchCallbackFn`, `CronMode` and `ProtectCallbackFn` are exported from the source entry point (JSR). The npm type declarations already exposed them. ##### Changed - Cached `Intl.DateTimeFormat` instances per timezone, making timezone-aware scheduling noticeably faster (based on [#​389](Hexagon/croner#389)). - Date-based jobs dated up to 1 second in the past now fire immediately, so a job created for "now" is not lost to the time it took to create it. Older dates fire only with `allowPast: true`. Previously a date-based job in the past never fired ([#​348](Hexagon/croner#348), [#​349](Hexagon/croner#349)). - Clarified `dayOffset` JSDoc and completed missing option documentation, including iterator usage, `allowPast`, and DST behavior. - Updated the copyright year to 2026. ##### Fixed - Fixed timers being re-armed with negative delays throughout the repeated hour of a DST fall back ([#​399](Hexagon/croner#399)). `nextRun()` and `msToNext()` no longer return a time in the past when the reference is inside the repeated hour, and searches move in real time in both directions. - Fixed `nextRuns()` going backwards or repeating times around a DST spring forward. - Fixed DST handling in timezones with a 30-minute overlap (Australia/Lord_Howe) ([#​404](Hexagon/croner#404)). - Fixed `msToNext()` returning a wrong (often negative) value when passed an ISO 8601 string on a job with `timezone` set. The string was read in system local time ([#​401](Hexagon/croner#401), [#​402](Hexagon/croner#402)). - Fixed ISO 8601 strings being read in system local time on jobs with `utcOffset`, in `nextRun()`, `msToNext()`, `match()`, `startAt`, `stopAt` and one-off dates. Strings with `Z` or an offset, like `2026-01-01T00:00:00Z`, are read as that exact instant ([#​401](Hexagon/croner#401)). - Fixed `startAt` given as a `Date` being searched in system local time on jobs with `utcOffset`. - Fixed `previousRuns()` throwing for patterns with month-specific wildcards (e.g., `0 9 * 4 *`) when the reference date falls outside allowed months, and for leap-day patterns (`0 0 29 2 *`) in leap years ([#​408](Hexagon/croner#408), [#​409](Hexagon/croner#409)). - Fixed patterns that can never match: `previousRuns()` threw a `TypeError` for patterns like `0 0 30 2 *`, and `nextRun()`, `nextRuns()` and `previousRuns()` threw `Maximum call stack size exceeded` for `0 0 16 * 5#L` with `domAndDow: true`. They now return `null` and an empty list. - Fixed scheduling after system clock changes: occurrences silently skipped after forward clock steps ([#​388](Hexagon/croner#388), [#​393](Hexagon/croner#393)), timer and target disagreeing after backward clock steps ([#​417](Hexagon/croner#417)), and timer targets for `dayOffset` schedules and protected jobs ([#​395](Hexagon/croner#395)). - Fixed `isRunning()`, `nextRun()` and `msToNext()` disagreeing with the scheduler when `dayOffset` is set. - Fixed `TimeoutNegativeWarning` when a timer delay was negative. Negative delays are now clamped to 0 ([#​370](Hexagon/croner#370), [#​369](Hexagon/croner#369)). - Fixed `?` in day-of-month or day-of-week behaving differently from `*`: `0 0 ? * MON` ran every day instead of every Monday ([#​392](Hexagon/croner#392)). - Fixed `nextRun()`, `nextRuns()`, `msToNext()` and scheduling throwing a `TypeError` when a schedule with `startAt` and `interval` has no runs left ([#​390](Hexagon/croner#390)). - Fixed `RangeError` when using stepping with year field wildcard. `*/2` in the year field matches even years ([#​373](Hexagon/croner#373), [#​374](Hexagon/croner#374)). - Fixed the `@deprecated` JSDoc tag being on `dayOffset` instead of `legacyMode`. - Fixed ISO 8601 strings losing their milliseconds on jobs without `timezone`: a one-off date like `2030-01-01T00:00:00.700Z` ran at `.000`, and strings and `Date`s could differ in `stopAt`, one-off dates, `nextRun()` and `msToNext()`. Also fixed a one-off date given as a string, later in the same second as `startAt`, never running. Jobs with a named `timezone` still work in whole seconds, as in 10.x. ##### CI / Build - Updated workflow and packaging metadata around publish/test setup. The published package metadata now comes from the committed `package.json`; `build/package.template.json` is removed. - Switched declaration bundling from esbuild to rollup with rollup-plugin-dts, restoring dist parity with previous releases. - Development is now Node.js-first: `package.json` is committed and is the source of truth for version, scripts and dev dependencies, the build script runs on Node.js (`npm run build`), and tests run with `npm test`. Deno is still supported (`deno task test`, fmt/lint, JSR publishing), and `npm version` keeps `deno.json` in sync. </details> --- ### Configuration 📅 **Schedule**: (UTC) - Branch creation - At any time (no schedule defined) - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate CLI](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4xOTIuMiIsInVwZGF0ZWRJblZlciI6IjQ0LjE5Mi4yIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #408.
previousRuns()threwTypeError: Cannot read properties of undefined (reading '0')for patterns such as0 9 * 4 *,0 0 * 11 *and0 0 29 2 *when the reference date was outside the allowed months. Two things inrecurseBackward/applycaused it:apply()then rolled April 31 over into May 1 and returnedtrue, and the recursion continued withdoing - 1, which is-1at the month level. The day is now capped at the last day of the current month during that reset.apply()compared the day withDaysOfMonth[month], which is 28 for February, so a valid February 29 in a leap year was reported as out of bounds and took the same path. It now usesgetLastDayOfMonth(year, month).With the change the repro from the issue gives
2026-04-30T09:00,2024-02-29T00:00and2025-11-30T00:00, the expected values.Tests:
test/previous-runs.test.ts(April/November with*as the day, and leap days). They throw ondevand pass with this change.npx jsr add @cross/test @std/assert,TZ=UTC npx tsx --test test/*.test.ts): 623 passed. I did not rundeno fmt/deno lint/deno check.previousRuns(1, ref)for 4320 combinations of month, day and weekday fields with random reference dates, and verified thatnextRun(prev)is never before the reference (no skipped run). Valid patterns no longer throw.Patterns that can never match (e.g.
0 0 31 4 *,0 0 30 2 *) still throw frompreviousRuns(), as they did before. The backward search walks back year by year until the call stack runs out, whilenextRun()returnsnullfor them. I left that out of this PR, but I can look at it separately if you want.