Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,251 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import { parseCron } from "../src/index.js";
interface Vector {
expr: string;
after: number;
tz: string;
expected: number;
label: string;
}
const utc = (y: number, mo: number, d: number, h = 0, mi = 0, s = 0): number =>
Date.UTC(y, mo, d, h, mi, s);
const vectors: Vector[] = [
// Hourly variants
{
expr: "0 * * * *",
after: utc(2026, 4, 11, 10, 30),
tz: "UTC",
expected: utc(2026, 4, 11, 11, 0),
label: "hourly @00",
},
{
expr: "*/15 * * * *",
after: utc(2026, 4, 11, 10, 7),
tz: "UTC",
expected: utc(2026, 4, 11, 10, 15),
label: "every 15 minutes",
},
{
expr: "*/15 * * * *",
after: utc(2026, 4, 11, 10, 45),
tz: "UTC",
expected: utc(2026, 4, 11, 11, 0),
label: "every 15 wrap",
},
{
expr: "*/30 * * * *",
after: utc(2026, 4, 11, 10, 29),
tz: "UTC",
expected: utc(2026, 4, 11, 10, 30),
label: "every 30 minutes",
},
// Daily
{
expr: "0 9 * * *",
after: utc(2026, 4, 11, 8, 0),
tz: "UTC",
expected: utc(2026, 4, 11, 9, 0),
label: "daily 9am",
},
{
expr: "0 0 * * *",
after: utc(2026, 4, 11, 12, 0),
tz: "UTC",
expected: utc(2026, 4, 12, 0, 0),
label: "daily midnight",
},
{
expr: "@daily",
after: utc(2026, 4, 11, 12, 0),
tz: "UTC",
expected: utc(2026, 4, 12, 0, 0),
label: "@daily macro",
},
{
expr: "@hourly",
after: utc(2026, 4, 11, 10, 30),
tz: "UTC",
expected: utc(2026, 4, 11, 11, 0),
label: "@hourly macro",
},
// Weekdays / weekends
// 2026-05-09 is Saturday. Next 9am weekday = Mon 2026-05-11.
{
expr: "0 9 * * MON-FRI",
after: utc(2026, 4, 9),
tz: "UTC",
expected: utc(2026, 4, 11, 9, 0),
label: "9am weekdays",
},
{
expr: "0 12 * * 0",
after: utc(2026, 4, 11),
tz: "UTC",
expected: utc(2026, 4, 17, 12, 0),
label: "noon Sundays",
},
{
expr: "0 9 * * 7",
after: utc(2026, 4, 11),
tz: "UTC",
expected: utc(2026, 4, 17, 9, 0),
label: "9am Sundays via 7",
},
// Monthly / yearly
{
expr: "0 0 1 * *",
after: utc(2026, 4, 11),
tz: "UTC",
expected: utc(2026, 5, 1, 0, 0),
label: "first of month",
},
{
expr: "@monthly",
after: utc(2026, 4, 11),
tz: "UTC",
expected: utc(2026, 5, 1, 0, 0),
label: "@monthly macro",
},
{
expr: "0 0 1 1 *",
after: utc(2026, 0, 2),
tz: "UTC",
expected: utc(2027, 0, 1, 0, 0),
label: "annual Jan 1",
},
{
expr: "@yearly",
after: utc(2026, 0, 2),
tz: "UTC",
expected: utc(2027, 0, 1, 0, 0),
label: "@yearly macro",
},
// Month aliases
{
expr: "0 0 1 JUL *",
after: utc(2026, 4, 11),
tz: "UTC",
expected: utc(2026, 6, 1, 0, 0),
label: "month alias JUL",
},
// 4 specific minutes
{
expr: "0,15,30,45 9-17 * * MON-FRI",
after: utc(2026, 4, 11, 9, 16),
tz: "UTC",
expected: utc(2026, 4, 11, 9, 30),
label: "quarter-hour business hours",
},
{
expr: "0,15,30,45 9-17 * * MON-FRI",
after: utc(2026, 4, 11, 17, 46),
tz: "UTC",
expected: utc(2026, 4, 12, 9, 0),
label: "wrap to next business day",
},
// Range/step variations
{
expr: "0 10-12/2 * * *",
after: utc(2026, 4, 11, 9, 0),
tz: "UTC",
expected: utc(2026, 4, 11, 10, 0),
label: "step in range first",
},
{
expr: "0 10-12/2 * * *",
after: utc(2026, 4, 11, 11, 0),
tz: "UTC",
expected: utc(2026, 4, 11, 12, 0),
label: "step in range second",
},
// Leap year
{
expr: "0 0 29 2 *",
after: utc(2026, 0, 1),
tz: "UTC",
expected: utc(2028, 1, 29, 0, 0),
label: "leap day next",
},
// DST — America/New_York 2026-03-08 spring-forward
{
expr: "0 3 * * *",
after: utc(2026, 2, 8, 6, 0),
tz: "America/New_York",
// 03:00 EDT on 2026-03-08 = 07:00 UTC
expected: utc(2026, 2, 8, 7, 0),
label: "NY 3am day-of-spring-forward",
},
// Europe/London non-DST anchor
// 2026-01-01 09:00 London = 09:00 UTC (GMT in January)
{
expr: "0 9 * * *",
after: utc(2025, 11, 31, 23, 0),
tz: "Europe/London",
expected: utc(2026, 0, 1, 9, 0),
label: "London winter 9am",
},
// Australia/Sydney winter 12:00 local = 02:00 UTC (UTC+10).
// after = 2026-05-31T00:00Z = 2026-05-31T10:00 Sydney, next noon
// Sydney = 2026-05-31T12:00 local = 2026-05-31T02:00 UTC.
{
expr: "0 12 * * *",
after: utc(2026, 4, 31, 0, 0),
tz: "Australia/Sydney",
expected: utc(2026, 4, 31, 2, 0),
label: "Sydney winter noon",
},
// Seconds + Quartz variants (need seconds: true)
// Tested separately because parseCron options differ.
];
describe("vectors — 5-field UTC + DST", () => {
for (const v of vectors) {
it(v.label, { timeout: 5000 }, () => {
const expr = parseCron(v.expr);
const next = expr.nextRun(v.after, v.tz);
expect(next).toBe(v.expected);
});
}
});
describe("vectors — 6-field seconds", () => {
it("every 10 seconds", { timeout: 5000 }, () => {
const expr = parseCron("*/10 * * * * *", { seconds: true });
expect(expr.nextRun(utc(2026, 4, 11, 10, 0, 5), "UTC")).toBe(
utc(2026, 4, 11, 10, 0, 10)
);
});
it("seconds with hour wrap", { timeout: 5000 }, () => {
const expr = parseCron("30 0 * * * *", { seconds: true });
expect(expr.nextRun(utc(2026, 4, 11, 10, 0, 30), "UTC")).toBe(
utc(2026, 4, 11, 11, 0, 30)
);
});
});
describe("vectors — 7-field Quartz year", () => {
it("year-constrained next run", { timeout: 5000 }, () => {
const expr = parseCron("0 0 12 1 1 * 2030", {
seconds: true,
years: true,
});
expect(expr.nextRun(utc(2026, 0, 1), "UTC")).toBe(
utc(2030, 0, 1, 12, 0, 0)
);
});
});