Skip to content

fix(Cron): do not skip earlier days when the upcoming day is missing from the month#6285

Merged
fubhy merged 1 commit into
Effect-TS:mainfrom
chatman-media:fix/cron-next-missing-day-overflow
Jun 23, 2026
Merged

fix(Cron): do not skip earlier days when the upcoming day is missing from the month#6285
fubhy merged 1 commit into
Effect-TS:mainfrom
chatman-media:fix/cron-next-missing-day-overflow

Conversation

@chatman-media

Copy link
Copy Markdown
Contributor

What

Cron.next could skip a scheduled run when the next matching day-of-month does not exist in the current month.

Reproduction

import { Cron, Either } from "effect"

const cron = Either.getOrThrow(Cron.parse("0 0 1,16,31 * *")) // runs on the 1st, 16th, 31st

// From Feb 18 2020, the next run should be March 1 (the 31st doesn't exist in Feb):
Cron.next(cron, new Date("2020-02-18T00:00:00Z"))
// → 2020-03-16T00:00:00Z   ❌  (March 1 was silently skipped)
// expected: 2020-03-01T00:00:00Z

The same happens for any expression whose day set contains a day (e.g. 31) that is absent from the current month, including */15 (which expands to days [1, 16, 31]).

Root cause

In the day-of-month branch of stepCron (packages/effect/src/Cron.ts), when there is still a matching day >= currentDay in the table, the step is computed as b = nextDay - currentDay and applied with setUTCDate(currentDay + b). If nextDay is larger than the number of days in the current month (e.g. 31 in February), setUTCDate overflows into the next month and lands on the wrong day. The subsequent loop iteration then finds the next matching day within that overshot month (e.g. the 16th), skipping the earlier matching days (e.g. the 1st).

Fix

When the selected nextDay does not exist in the current month, wrap to the first matching day of the next month instead — reusing the exact formula already used for the "no matching day left this month" case. This mirrors the behaviour of Cron.prev (which already has an analogous day 31 test) and matches other cron implementations (verified against cron-parser).

The change is scoped to the forward direction (!prev); for prev, nextDay <= currentDay <= daysInMonth, so the new condition is always false and that path is untouched.

Tests

  • Added Cron > next does not skip earlier days when the upcoming day is missing from the month, covering 1,16,31, */15, and a 30-day-month case. The test fails without the fix (returns 2020-03-16) and passes with it.
  • Full Cron suite: 25/25 pass. Typecheck (tsc -b) and ESLint clean.
  • Cross-checked Cron.next against cron-parser over 8000 randomly generated expressions/start times: 0 mismatches with the fix (the failures above were present before).

A changeset is included (effect, patch).

@github-project-automation github-project-automation Bot moved this to Discussion Ongoing in PR Backlog Jun 22, 2026
@changeset-bot

changeset-bot Bot commented Jun 22, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 333590e

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
effect Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@fubhy

fubhy commented Jun 22, 2026

Copy link
Copy Markdown
Member

Good find. Thanks!

@fubhy fubhy merged commit 95c7d2e into Effect-TS:main Jun 23, 2026
10 of 11 checks passed
@github-project-automation github-project-automation Bot moved this from Discussion Ongoing to Done in PR Backlog Jun 23, 2026
@github-actions github-actions Bot mentioned this pull request Jun 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants