Skip to content

Commit 95c7d2e

Browse files
fix(Cron): do not skip earlier days when the upcoming day is missing from the month (#6285)
Co-authored-by: chatman-media <chatman-media@users.noreply.github.com>
1 parent 99d5575 commit 95c7d2e

3 files changed

Lines changed: 26 additions & 0 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"effect": patch
3+
---
4+
5+
Fix `Cron.next` skipping earlier matching days when the upcoming day-of-month does not exist in the current month. For an expression like `0 0 1,16,31 * *`, advancing from a date past the 16th selected day 31; in a month without 31 days this overflowed into the following month and landed on a later matching day (e.g. the 16th), silently skipping the 1st. `Cron.next` now wraps to the first matching day of the next month in that case, matching the behaviour of `Cron.prev` and other cron implementations.

packages/effect/src/Cron.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -584,6 +584,12 @@ const stepCron = (cron: Cron, startFrom: DateTime.DateTime.Input | undefined, di
584584
} else {
585585
b = daysInMonth(current) - currentDay + boundary.day
586586
}
587+
} else if (!prev && nextDay > daysInMonth(current)) {
588+
// The next matching day does not exist in the current month (e.g. day 31
589+
// in a 30-day month). Setting it directly would overflow into the following
590+
// month and skip its earlier matching days, so wrap to the first matching
591+
// day of the next month instead.
592+
b = daysInMonth(current) - currentDay + boundary.day
587593
} else {
588594
b = nextDay - currentDay
589595
}

packages/effect/test/Cron.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,21 @@ describe("Cron", () => {
245245
deepStrictEqual(prev(cron, from), new Date("2024-01-31T00:00:00.000Z"))
246246
})
247247

248+
it("next does not skip earlier days when the upcoming day is missing from the month", () => {
249+
const tz = DateTime.zoneUnsafeMakeNamed("UTC")
250+
const cron = Cron.unsafeParse("0 0 1,16,31 * *", tz)
251+
// From Feb 18 the next matching day in February would be the 31st, which does
252+
// not exist. Rolling onto it must not overshoot past March 1 (also a match).
253+
deepStrictEqual(next(cron, new Date("2020-02-18T00:00:00.000Z")), new Date("2020-03-01T00:00:00.000Z"))
254+
// `*/15` expands to days [1, 16, 31] and exhibits the same wrap.
255+
deepStrictEqual(
256+
next(Cron.unsafeParse("0 0 */15 * *", tz), new Date("2020-02-18T00:00:00.000Z")),
257+
new Date("2020-03-01T00:00:00.000Z")
258+
)
259+
// 30-day month: from the 20th the next day is the 31st (missing) → July 1.
260+
deepStrictEqual(next(cron, new Date("2024-06-20T00:00:00.000Z")), new Date("2024-07-01T00:00:00.000Z"))
261+
})
262+
248263
it("prev clamps to the last valid day when rolling back a month with only month constraints", () => {
249264
const tz = DateTime.zoneUnsafeMakeNamed("UTC")
250265
const cron = Cron.unsafeParse("0 0 0 * FEB *", tz)

0 commit comments

Comments
 (0)