···11# cron-fast
2233[](https://www.npmjs.com/package/cron-fast)
44-[](https://www.npmjs.com/package/cron-fast)
55-[](https://pkg-size.dev/cron-fast)
44+[](https://www.npmjs.com/package/cron-fast)
65[](https://jsr.io/@kbilkis/cron-fast)
76[](https://jsr.io/@kbilkis/cron-fast)
87[](https://github.com/kbilkis/cron-fast/actions/workflows/ci.yml)
98[](https://codecov.io/github/kbilkis/cron-fast)
1010-[](https://www.typescriptlang.org/)
1111-[](https://github.com/kbilkis/cron-fast/commits/master)
1212-[](https://github.com/kbilkis/cron-fast)
99+[](https://bundlejs.com/?q=cron-fast)
1310[](https://opensource.org/licenses/MIT)
14111515-**Fast JavaScript/TypeScript cron parser with timezone support.** Works everywhere: Node.js, Deno, Bun, Cloudflare Workers, and browsers. Zero dependencies.
1212+**Fast and tiny JavaScript/TypeScript cron parser with timezone support.** Works everywhere: Node.js, Deno, Bun, Cloudflare Workers, and browsers. Zero dependencies.
16131714## Features
1815
+22-1
jsr.json
···11{
22 "name": "@kbilkis/cron-fast",
33 "version": "0.1.2",
44- "description": "Fast JavaScript/TypeScript cron parser with timezone support - works in Node.js, Deno, Bun, Cloudflare Workers, and browsers. Zero dependencies.",
44+ "description": "Fast and tiny JavaScript/TypeScript cron parser with timezone support - works in Node.js, Deno, Bun, Cloudflare Workers, and browsers. Zero dependencies.",
55+ "keywords": [
66+ "javascript",
77+ "typescript",
88+ "cron",
99+ "parser",
1010+ "scheduler",
1111+ "schedule",
1212+ "nodejs",
1313+ "node",
1414+ "deno",
1515+ "bun",
1616+ "cloudflare-workers",
1717+ "browser",
1818+ "timezone",
1919+ "fast",
2020+ "lightweight",
2121+ "performance",
2222+ "zero-dependencies",
2323+ "esm",
2424+ "tree-shakeable"
2525+ ],
526 "exports": "./src/index.ts",
627 "publish": {
728 "include": ["src/**/*.ts", "README.md", "LICENSE"]
+1-1
package.json
···11{
22 "name": "cron-fast",
33 "version": "0.1.2",
44- "description": "Fast JavaScript/TypeScript cron parser with timezone support - works in Node.js, Deno, Bun, Cloudflare Workers, and browsers. Zero dependencies.",
44+ "description": "Fast and tiny JavaScript/TypeScript cron parser with timezone support - works in Node.js, Deno, Bun, Cloudflare Workers, and browsers. Zero dependencies.",
55 "keywords": [
66 "browser",
77 "bun",
+17
test/parser.test.ts
···4141 expect(result.minute).toEqual([10, 20, 30, 40]);
4242 });
43434444+ it("should parse step values with single start value", () => {
4545+ const result = parse("10/15 * * * *");
4646+ expect(result.minute).toEqual([10, 25, 40, 55]);
4747+ });
4848+4449 it("should parse comma-separated values", () => {
4550 const result = parse("0,15,30,45 9,12,15 * * *");
4651 expect(result.minute).toEqual([0, 15, 30, 45]);
···85908691 it("should throw on out of range value", () => {
8792 expect(() => parse("60 * * * *")).toThrow("out of range");
9393+ });
9494+9595+ it("should throw on out of range value in step range", () => {
9696+ expect(() => parse("70-80/5 * * * *")).toThrow("No valid values");
9797+ });
9898+9999+ it("should throw on out of range value in simple range", () => {
100100+ expect(() => parse("70-80 * * * *")).toThrow("No valid values");
101101+ });
102102+103103+ it("should throw on invalid value name", () => {
104104+ expect(() => parse("0 0 * * notaday")).toThrow("Invalid value");
88105 });
8910690107 it("should throw on impossible day/month combination (Feb 31)", () => {
+162
test/scheduler.test.ts
···952952 });
953953 });
954954 });
955955+956956+ describe("Edge case coverage", () => {
957957+ describe("minute rollover to next hour (scheduler.ts:160-161)", () => {
958958+ it("should rollover to next hour when no valid minute exists in current hour", () => {
959959+ const from = new Date("2026-03-15T10:58:00Z");
960960+ const next = nextRun("30,45 11 * * *", { from });
961961+962962+ expect(next.getUTCDate()).toBe(15);
963963+ expect(next.getUTCHours()).toBe(11);
964964+ expect(next.getUTCMinutes()).toBe(30);
965965+ });
966966+967967+ it("should hit exact branch for minute rollover with targetHour (lines 160-161)", () => {
968968+ const from = new Date("2026-03-15T14:45:00Z");
969969+ const next = nextRun("0,15 15,16 * * *", { from });
970970+971971+ expect(next.getUTCHours()).toBe(15);
972972+ expect(next.getUTCMinutes()).toBe(0);
973973+ });
974974+975975+ it("should handle prev direction minute rollover to prev hour (lines 160-161)", () => {
976976+ const from = new Date("2026-03-15T14:05:00Z");
977977+ const prev = previousRun("45,59 13,14 * * *", { from });
978978+979979+ expect(prev.getUTCHours()).toBe(13);
980980+ expect(prev.getUTCMinutes()).toBe(59);
981981+ });
982982+983983+ it("should handle minute rollover when at minute 59", () => {
984984+ const from = new Date("2026-03-15T23:58:00Z");
985985+ const next = nextRun("30 0 * * *", { from });
986986+987987+ expect(next.getUTCDate()).toBe(16);
988988+ expect(next.getUTCHours()).toBe(0);
989989+ expect(next.getUTCMinutes()).toBe(30);
990990+ });
991991+992992+ it("should handle minute rollover with restricted minutes in current hour", () => {
993993+ const from = new Date("2026-03-15T14:40:00Z");
994994+ const next = nextRun("0,15,30 15 * * *", { from });
995995+996996+ expect(next.getUTCDate()).toBe(15);
997997+ expect(next.getUTCHours()).toBe(15);
998998+ expect(next.getUTCMinutes()).toBe(0);
999999+ });
10001000+10011001+ it("should skip to next hour when no valid minutes left in current hour", () => {
10021002+ const from = new Date("2026-03-15T10:50:00Z");
10031003+ const next = nextRun("15,30 11 * * *", { from });
10041004+10051005+ expect(next.getUTCDate()).toBe(15);
10061006+ expect(next.getUTCHours()).toBe(11);
10071007+ expect(next.getUTCMinutes()).toBe(15);
10081008+ });
10091009+10101010+ it("should use nextRuns to iterate through hour boundaries", () => {
10111011+ const from = new Date("2026-03-15T10:30:00Z");
10121012+ const runs = nextRuns("30,45 11 * * *", 3, { from });
10131013+10141014+ expect(runs).toHaveLength(3);
10151015+ expect(runs[0].getUTCHours()).toBe(11);
10161016+ expect(runs[0].getUTCMinutes()).toBe(30);
10171017+ expect(runs[1].getUTCMinutes()).toBe(45);
10181018+ expect(runs[2].getUTCDate()).toBe(16);
10191019+ expect(runs[2].getUTCHours()).toBe(11);
10201020+ expect(runs[2].getUTCMinutes()).toBe(30);
10211021+ });
10221022+ });
10231023+10241024+ describe("month boundary with invalid day (scheduler.ts:239-241)", () => {
10251025+ it("should move to previous month when current month has no valid day", () => {
10261026+ const from = new Date("2026-04-01T00:00:00Z");
10271027+ const prev = previousRun("0 12 31 * *", { from });
10281028+10291029+ expect(prev.getUTCMonth()).toBe(2);
10301030+ expect(prev.getUTCDate()).toBe(31);
10311031+ expect(prev.getUTCHours()).toBe(12);
10321032+ expect(prev.getUTCMinutes()).toBe(0);
10331033+ });
10341034+10351035+ it("should trigger resetToMonthBoundary when no valid day in month for prev direction", () => {
10361036+ const from = new Date("2026-05-01T12:00:00Z");
10371037+ const prev = previousRun("0 9 31 * *", { from });
10381038+10391039+ expect(prev.getUTCMonth()).toBe(2);
10401040+ expect(prev.getUTCDate()).toBe(31);
10411041+ expect(prev.getUTCHours()).toBe(9);
10421042+ });
10431043+10441044+ it("should handle February with day 31 going to January", () => {
10451045+ const from = new Date("2026-03-01T00:00:00Z");
10461046+ const prev = previousRun("0 9 31 * *", { from });
10471047+10481048+ expect(prev.getUTCMonth()).toBe(0);
10491049+ expect(prev.getUTCDate()).toBe(31);
10501050+ expect(prev.getUTCHours()).toBe(9);
10511051+ });
10521052+10531053+ it("should handle April 31 case when going backwards", () => {
10541054+ const from = new Date("2026-05-01T00:00:00Z");
10551055+ const prev = previousRun("0 12 31 * *", { from });
10561056+10571057+ expect(prev.getUTCMonth()).toBe(2);
10581058+ expect(prev.getUTCDate()).toBe(31);
10591059+ });
10601060+10611061+ it("should handle June 31 case when going backwards", () => {
10621062+ const from = new Date("2026-07-01T00:00:00Z");
10631063+ const prev = previousRun("0 12 31 * *", { from });
10641064+10651065+ expect(prev.getUTCMonth()).toBe(4);
10661066+ expect(prev.getUTCDate()).toBe(31);
10671067+ });
10681068+10691069+ it("should handle September 31 case when going backwards", () => {
10701070+ const from = new Date("2026-10-01T00:00:00Z");
10711071+ const prev = previousRun("0 12 31 * *", { from });
10721072+10731073+ expect(prev.getUTCMonth()).toBe(7);
10741074+ expect(prev.getUTCDate()).toBe(31);
10751075+ });
10761076+10771077+ it("should handle November 31 case when going backwards", () => {
10781078+ const from = new Date("2026-12-01T00:00:00Z");
10791079+ const prev = previousRun("0 12 31 * *", { from });
10801080+10811081+ expect(prev.getUTCMonth()).toBe(9);
10821082+ expect(prev.getUTCDate()).toBe(31);
10831083+ });
10841084+10851085+ it("should trigger moveToMonth recursion for prev with no valid days", () => {
10861086+ const from = new Date("2026-06-15T12:00:00Z");
10871087+ const prev = previousRun("0 12 31 1 *", { from });
10881088+10891089+ expect(prev.getUTCMonth()).toBe(0);
10901090+ expect(prev.getUTCDate()).toBe(31);
10911091+ expect(prev.getUTCFullYear()).toBe(2026);
10921092+ });
10931093+ });
10941094+10951095+ describe("MAX_ITERATIONS safety (lines 93-94)", () => {
10961096+ it("should find matches efficiently without hitting iteration limit", () => {
10971097+ const from = new Date("2026-01-01T00:00:00Z");
10981098+ const sparseCron = "0 0 29 2 1";
10991099+11001100+ const next = nextRun(sparseCron, { from });
11011101+11021102+ expect(next.getUTCDate()).toBe(29);
11031103+ expect(next.getUTCMonth()).toBe(1);
11041104+ expect(next.getUTCFullYear()).toBe(2028);
11051105+ });
11061106+11071107+ it("should handle sparse cron expressions within iteration limit", () => {
11081108+ const from = new Date("2026-01-01T00:00:00Z");
11091109+ const next = nextRun("0 0 31 12 *", { from });
11101110+11111111+ expect(next.getUTCDate()).toBe(31);
11121112+ expect(next.getUTCMonth()).toBe(11);
11131113+ expect(next.getUTCFullYear()).toBe(2026);
11141114+ });
11151115+ });
11161116+ });
9551117});
+106
test/timezone.test.ts
···5151 expect(restored.getTime()).toBe(date.getTime());
5252 }
5353 });
5454+5555+ describe("24:00:00 format handling", () => {
5656+ it("should handle timezone conversions near midnight (24:00:00 format)", () => {
5757+ const timezone = "Pacific/Auckland";
5858+ const original = new Date("2026-03-15T00:00:00Z");
5959+6060+ const converted = convertToTimezone(original, timezone);
6161+ const restored = convertFromTimezone(converted, timezone);
6262+6363+ expect(restored.getTime()).toBe(original.getTime());
6464+ });
6565+6666+ it("should handle conversions across midnight boundary", () => {
6767+ const timezone = "Asia/Tokyo";
6868+ const dates = [
6969+ new Date("2026-03-15T14:59:59Z"),
7070+ new Date("2026-03-15T15:00:00Z"),
7171+ new Date("2026-03-15T23:59:59Z"),
7272+ new Date("2026-03-16T00:00:00Z"),
7373+ ];
7474+7575+ for (const date of dates) {
7676+ const converted = convertToTimezone(date, timezone);
7777+ const restored = convertFromTimezone(converted, timezone);
7878+7979+ expect(restored.getTime()).toBe(date.getTime());
8080+ }
8181+ });
8282+8383+ it("should normalize potential 24:00:00 in convertToTimezone (line 20)", () => {
8484+ const timezone = "Asia/Tokyo";
8585+ const utcMidnight = new Date("2026-03-15T00:00:00Z");
8686+8787+ const converted = convertToTimezone(utcMidnight, timezone);
8888+8989+ expect(converted.getUTCHours()).toBeGreaterThanOrEqual(0);
9090+ expect(converted.getUTCHours()).toBeLessThan(24);
9191+ });
9292+9393+ it("should normalize potential 24:00:00 in convertFromTimezone iteration (lines 74, 120)", () => {
9494+ const timezone = "Pacific/Auckland";
9595+ const dates = [
9696+ new Date("2026-03-15T11:00:00Z"),
9797+ new Date("2026-03-15T11:30:00Z"),
9898+ new Date("2026-03-15T11:59:59Z"),
9999+ ];
100100+101101+ for (const date of dates) {
102102+ const converted = convertToTimezone(date, timezone);
103103+ const restored = convertFromTimezone(converted, timezone);
104104+105105+ expect(restored.getUTCHours()).toBeGreaterThanOrEqual(0);
106106+ expect(restored.getUTCHours()).toBeLessThan(24);
107107+ expect(restored.getTime()).toBe(date.getTime());
108108+ }
109109+ });
110110+111111+ it("should handle edge case conversions at day boundaries in multiple timezones", () => {
112112+ const timezones = [
113113+ "Pacific/Auckland",
114114+ "Australia/Sydney",
115115+ "Asia/Tokyo",
116116+ "Asia/Shanghai",
117117+ "Europe/Moscow",
118118+ ];
119119+120120+ const utcTimes = [
121121+ new Date("2026-03-15T10:00:00Z"),
122122+ new Date("2026-03-15T11:00:00Z"),
123123+ new Date("2026-03-15T12:00:00Z"),
124124+ new Date("2026-03-15T13:00:00Z"),
125125+ new Date("2026-03-15T14:00:00Z"),
126126+ ];
127127+128128+ for (const tz of timezones) {
129129+ for (const time of utcTimes) {
130130+ const converted = convertToTimezone(time, tz);
131131+ const restored = convertFromTimezone(converted, tz);
132132+133133+ expect(restored.getUTCHours()).toBeGreaterThanOrEqual(0);
134134+ expect(restored.getUTCHours()).toBeLessThan(24);
135135+ expect(restored.getTime()).toBe(time.getTime());
136136+ }
137137+ }
138138+ });
139139+140140+ it("should handle DST gap scenarios (timezone.ts:120)", () => {
141141+ const timezone = "America/New_York";
142142+ const beforeDST = new Date("2026-03-08T06:59:59Z");
143143+144144+ const converted = convertToTimezone(beforeDST, timezone);
145145+ const restored = convertFromTimezone(converted, timezone);
146146+147147+ expect(restored.getTime()).toBe(beforeDST.getTime());
148148+ });
149149+150150+ it("should handle multiple iterations in convertFromTimezone", () => {
151151+ const timezone = "Europe/London";
152152+ const date = new Date("2026-03-28T00:30:00Z");
153153+154154+ const converted = convertToTimezone(date, timezone);
155155+ const restored = convertFromTimezone(converted, timezone);
156156+157157+ expect(restored.getTime()).toBe(date.getTime());
158158+ });
159159+ });
54160});