cron-fast 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -6
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -0
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +2 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -3
package/README.md
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
# cron-fast
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/cron-fast)
|
|
4
|
+
[](https://www.npmjs.com/package/cron-fast)
|
|
5
|
+
[](https://jsr.io/@kbilkis/cron-fast)
|
|
6
|
+
[](https://jsr.io/@kbilkis/cron-fast)
|
|
7
|
+
[](https://github.com/kbilkis/cron-fast/actions/workflows/ci.yml)
|
|
8
|
+
[](https://codecov.io/github/kbilkis/cron-fast)
|
|
9
|
+
[](https://bundlejs.com/?q=cron-fast)
|
|
10
|
+
[](https://opensource.org/licenses/MIT)
|
|
11
|
+
|
|
12
|
+
**Fast and tiny JavaScript/TypeScript cron parser with timezone support.** Works everywhere: Node.js, Deno, Bun, Cloudflare Workers, and browsers. Zero dependencies.
|
|
4
13
|
|
|
5
14
|
## Features
|
|
6
15
|
|
|
@@ -150,18 +159,32 @@ nextRun("0 9 * * *", { from: utc }).getTime() === nextRun("0 9 * * *", { from: e
|
|
|
150
159
|
|
|
151
160
|
**Note:** All returned Date objects are in UTC (ending with `Z` in `.toISOString()`). Use `.toLocaleString()` to display in any timezone.
|
|
152
161
|
|
|
153
|
-
##
|
|
162
|
+
## Bundle Size
|
|
163
|
+
|
|
164
|
+
cron-fast is extremely lightweight and fully tree-shakeable. Here are the actual bundle sizes for different import scenarios:
|
|
165
|
+
|
|
166
|
+
| Import | Raw | Minified | Gzipped |
|
|
167
|
+
| ---------------------------------------------------- | -------- | -------- | ----------- |
|
|
168
|
+
| `Full bundle (all exports)` | 13.56 KB | 5.88 KB | **2.26 KB** |
|
|
169
|
+
| `nextRun only` | 11.99 KB | 5.16 KB | **2.02 KB** |
|
|
170
|
+
| `previousRun only` | 12.00 KB | 5.16 KB | **2.02 KB** |
|
|
171
|
+
| `nextRuns only` | 12.38 KB | 5.31 KB | **2.08 KB** |
|
|
172
|
+
| `isValid only` | 3.92 KB | 1.75 KB | **932 B** |
|
|
173
|
+
| `parse only` | 3.80 KB | 1.70 KB | **907 B** |
|
|
174
|
+
| `isMatch only` | 5.44 KB | 2.40 KB | **1.18 KB** |
|
|
175
|
+
| `Validation only (isValid + parse)` | 3.92 KB | 1.75 KB | **934 B** |
|
|
176
|
+
| `Scheduling only (nextRun + previousRun + nextRuns)` | 12.87 KB | 5.56 KB | **2.10 KB** |
|
|
154
177
|
|
|
155
|
-
|
|
178
|
+
Import only what you need:
|
|
156
179
|
|
|
157
180
|
```typescript
|
|
158
|
-
// Small bundle - only validation
|
|
181
|
+
// Small bundle - only validation (~900 B gzipped)
|
|
159
182
|
import { isValid } from "cron-fast";
|
|
160
183
|
|
|
161
|
-
// Medium bundle - one function + dependencies
|
|
184
|
+
// Medium bundle - one function + dependencies (~2 KB gzipped)
|
|
162
185
|
import { nextRun } from "cron-fast";
|
|
163
186
|
|
|
164
|
-
// Full bundle - everything
|
|
187
|
+
// Full bundle - everything (~2.3 KB gzipped)
|
|
165
188
|
import * as cron from "cron-fast";
|
|
166
189
|
```
|
|
167
190
|
|
package/dist/index.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e={jan:1,feb:2,mar:3,apr:4,may:5,jun:6,jul:7,aug:8,sep:9,oct:10,nov:11,dec:12},t={sun:0,mon:1,tue:2,wed:3,thu:4,fri:5,sat:6};function n(n){let a=n.trim();if(!a)throw Error(`Cron expression cannot be empty`);let o=a.split(/\s+/);if(o.length!==5)throw Error(`Invalid cron expression: expected 5 fields, got ${o.length}`);let[s,c,l,u,d]=o,f=i(d,0,7,t).map(e=>e===7?0:e),p={minute:i(s,0,59),hour:i(c,0,23),day:i(l,1,31),month:i(u,1,12,e).map(e=>e-1),weekday:Array.from(new Set(f)).sort((e,t)=>e-t)};return r(p),p}function r(e){let t=e.
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e={jan:1,feb:2,mar:3,apr:4,may:5,jun:6,jul:7,aug:8,sep:9,oct:10,nov:11,dec:12},t={sun:0,mon:1,tue:2,wed:3,thu:4,fri:5,sat:6};function n(n){let a=n.trim();if(!a)throw Error(`Cron expression cannot be empty`);let o=a.split(/\s+/);if(o.length!==5)throw Error(`Invalid cron expression: expected 5 fields, got ${o.length}`);let[s,c,l,u,d]=o,f=i(d,0,7,t).map(e=>e===7?0:e),p={minute:i(s,0,59),hour:i(c,0,23),day:i(l,1,31),month:i(u,1,12,e).map(e=>e-1),weekday:Array.from(new Set(f)).sort((e,t)=>e-t),dayIsWildcard:l.trim()===`*`,weekdayIsWildcard:d.trim()===`*`};return r(p),p}function r(e){let t=e.dayIsWildcard,n=e.month.length===12;if(t||n)return;let r=[31,29,31,30,31,30,31,31,30,31,30,31],i=!1;for(let t of e.month){let n=r[t];for(let t of e.day)if(t<=n){i=!0;break}if(i)break}if(!i)throw Error(`Invalid cron expression: no valid day/month combination exists`)}function i(e,t,n,r){let i=new Set;if(e===`*`){for(let e=t;e<=n;e++)i.add(e);return Array.from(i).sort((e,t)=>e-t)}let o=e.split(`,`);for(let e of o)if(e.includes(`/`)){let[o,s]=e.split(`/`),c=parseInt(s,10);if(isNaN(c)||c<=0)throw Error(`Invalid step value: ${s}`);let l=t,u=n;if(o!==`*`)if(o.includes(`-`)){let[e,t]=o.split(`-`);l=a(e,r),u=a(t,r)}else l=a(o,r);for(let e=l;e<=u;e+=c)e>=t&&e<=n&&i.add(e)}else if(e.includes(`-`)){let[o,s]=e.split(`-`),c=a(o,r),l=a(s,r);if(c>l)throw Error(`Invalid range: ${e}`);for(let e=c;e<=l;e++)e>=t&&e<=n&&i.add(e)}else{let o=a(e,r);if(o>=t&&o<=n)i.add(o);else throw Error(`Value ${o} out of range [${t}-${n}]`)}if(i.size===0)throw Error(`No valid values in field: ${e}`);return Array.from(i).sort((e,t)=>e-t)}function a(e,t){let n=e.toLowerCase();if(t&&n in t)return t[n];let r=parseInt(e,10);if(isNaN(r))throw Error(`Invalid value: ${e}`);return r}function o(e){try{return n(e),!0}catch{return!1}}function s(e,t){let n=t.getUTCMinutes(),r=t.getUTCHours(),i=t.getUTCDate(),a=t.getUTCMonth(),o=t.getUTCDay();return e.minute.includes(n)&&e.hour.includes(r)&&e.month.includes(a)&&l(e,i,o)}function c(e){return!e.dayIsWildcard&&!e.weekdayIsWildcard}function l(e,t,n,r){let i=r===void 0?e.day.includes(t):e.day.includes(t)&&t<=r,a=e.weekday.includes(n);return c(e)?i||a:e.dayIsWildcard?e.weekdayIsWildcard?!0:a:i}function u(e,t){for(let n of e)if(n>=t)return n;return null}function d(e,t){for(let n=e.length-1;n>=0;n--)if(e[n]<=t)return e[n];return null}function f(e,t){return new Date(e,t+1,0).getDate()}function p(e,t){let[n,r]=e.toLocaleString(`en-US`,{timeZone:t,year:`numeric`,month:`2-digit`,day:`2-digit`,hour:`2-digit`,minute:`2-digit`,second:`2-digit`,hour12:!1}).split(`, `),[i,a,o]=n.split(`/`).map(Number),[s,c,l]=r.split(`:`).map(Number);return s===24&&(s=0),new Date(Date.UTC(o,i-1,a,s,c,l))}function m(e,t){let n=e.getUTCFullYear(),r=e.getUTCMonth(),i=e.getUTCDate(),a=e.getUTCHours(),o=e.getUTCMinutes(),s=e.getUTCSeconds(),c=Date.UTC(n,r,i,a,o,s),l=c,u=l,d=1/0;for(let e=0;e<3;e++){let[e,n]=new Date(l).toLocaleString(`en-US`,{timeZone:t,year:`numeric`,month:`2-digit`,day:`2-digit`,hour:`2-digit`,minute:`2-digit`,second:`2-digit`,hour12:!1}).split(`, `),[r,i,a]=e.split(`/`).map(Number),[o,s,f]=n.split(`:`).map(Number);o===24&&(o=0);let p=Date.UTC(a,r-1,i,o,s,f),m=Math.abs(c-p);if((m<d||m===d&&l>u)&&(d=m,u=l),p===c)return new Date(l);let h=c-p;l+=h}let f=c+3600*1e3,p=f;for(let e=0;e<2;e++){let[e,n]=new Date(p).toLocaleString(`en-US`,{timeZone:t,year:`numeric`,month:`2-digit`,day:`2-digit`,hour:`2-digit`,minute:`2-digit`,second:`2-digit`,hour12:!1}).split(`, `),[r,i,a]=e.split(`/`).map(Number),[o,s,c]=n.split(`:`).map(Number);o===24&&(o=0);let l=Date.UTC(a,r-1,i,o,s,c);if(l===f)return new Date(p);let u=f-l;p+=u}return new Date(u)}const h={next:{find:u,minute:e=>e.minute[0],hour:e=>e.hour[0],offset:1},prev:{find:d,minute:e=>e.minute.at(-1),hour:e=>e.hour.at(-1),offset:-1}};function g(e,t){let r=n(e),i=t?.from||new Date,a=t?.timezone,o=a?p(i,a):new Date(i);o.setUTCSeconds(0,0),o.setUTCMinutes(o.getUTCMinutes()+1);let s=b(r,o,`next`,a);if(!s)throw Error(`No matching time found within reasonable search window`);return s}function _(e,t){let r=n(e),i=t?.from||new Date,a=t?.timezone,o=a?p(i,a):new Date(i);o.setUTCSeconds(0,0),o.setUTCMinutes(o.getUTCMinutes()-1);let s=b(r,o,`prev`,a);if(!s)throw Error(`No matching time found within reasonable search window`);return s}function v(e,t,n){if(t<=0)return[];let r=[],i=n?.from||new Date;for(let a=0;a<t;a++){let t=g(e,{...n,from:i});r.push(t),i=new Date(t.getTime()+6e4)}return r}function y(e,t,r){return s(n(e),r?.timezone?p(t,r.timezone):new Date(t))}function b(e,t,n,r){let i=new Date(t);for(let t=0;t<1e3;t++){if(s(e,i))return r?m(i,r):i;x(e,i,n)}return null}function x(e,t,n){let r=h[n],i=t.getUTCMinutes(),a=t.getUTCHours(),o=t.getUTCDate(),s=t.getUTCMonth(),c=t.getUTCFullYear(),u=t.getUTCDay(),d=f(c,s);if(!e.month.includes(s)){S(e,t,n,s,c);return}if(!l(e,o,u,d)){w(e,t,n,o,s,c,d);return}if(!e.hour.includes(a)){let i=r.find(e.hour,a+r.offset);i===null?w(e,t,n,o,s,c,d):(t.setUTCHours(i),t.setUTCMinutes(r.minute(e)));return}if(!e.minute.includes(i)){let l=r.find(e.minute,i+r.offset);if(l!==null)t.setUTCMinutes(l);else{let i=r.find(e.hour,a+r.offset);i===null?w(e,t,n,o,s,c,d):(t.setUTCHours(i),t.setUTCMinutes(r.minute(e)))}return}w(e,t,n,o,s,c,d)}function S(e,t,n,r,i){let a=h[n],o=a.find(e.month,r+a.offset);if(o!==null)T(e,t,i,o,n);else{let r=n===`next`?e.month[0]:e.month.at(-1);T(e,t,i+a.offset,r,n)}}function C(e,t,n,r){let i=h[n];if(c(e)){let e=t+i.offset;return n===`next`&&e>r||n===`prev`&&e<1?null:e}return i.find(e.day,t+i.offset)}function w(e,t,n,r,i,a,o){let s=h[n],c=C(e,r,n,o);(n===`next`?c!==null&&c<=o:c!==null)?(t.setUTCDate(c),t.setUTCHours(s.hour(e)),t.setUTCMinutes(s.minute(e))):S(e,t,n,i,a)}function T(e,t,n,r,i){let a=h[i];t.setUTCFullYear(n),t.setUTCDate(1),t.setUTCMonth(r);let o=f(n,r),s=c(e);if(i===`next`){let n=s?1:u(e.day,1)??e.day[0];t.setUTCDate(Math.min(n,o))}else{let a=s?o:d(e.day,o);if(a===null){S(e,t,i,r,n);return}t.setUTCDate(a)}t.setUTCHours(a.hour(e)),t.setUTCMinutes(a.minute(e))}exports.isMatch=y,exports.isValid=o,exports.nextRun=g,exports.nextRuns=v,exports.parse=n,exports.previousRun=_;
|
|
2
2
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":[],"sources":["../src/parser.ts","../src/matcher.ts","../src/timezone.ts","../src/scheduler.ts"],"sourcesContent":["import type { ParsedCron } from \"./types.js\";\n\nconst MONTH_NAMES: Record<string, number> = {\n jan: 1,\n feb: 2,\n mar: 3,\n apr: 4,\n may: 5,\n jun: 6,\n jul: 7,\n aug: 8,\n sep: 9,\n oct: 10,\n nov: 11,\n dec: 12,\n};\n\nconst WEEKDAY_NAMES: Record<string, number> = {\n sun: 0,\n mon: 1,\n tue: 2,\n wed: 3,\n thu: 4,\n fri: 5,\n sat: 6,\n};\n\n/**\n * Parse a cron expression into structured format\n *\n * Cron format: minute hour day month weekday\n * - minute: 0-59\n * - hour: 0-23\n * - day: 1-31\n * - month: 1-12 (or JAN-DEC)\n * - weekday: 0-7 (or SUN-SAT, where 0 and 7 are Sunday)\n *\n * Note: Months are converted from cron's 1-indexed format (1-12) to\n * JavaScript's 0-indexed format (0-11) for internal consistency.\n */\nexport function parse(expression: string): ParsedCron {\n const trimmed = expression.trim();\n\n if (!trimmed) {\n throw new Error(\"Cron expression cannot be empty\");\n }\n\n const parts = trimmed.split(/\\s+/);\n\n if (parts.length !== 5) {\n throw new Error(`Invalid cron expression: expected 5 fields, got ${parts.length}`);\n }\n\n const [minuteStr, hourStr, dayStr, monthStr, weekdayStr] = parts;\n\n const weekdays = parseField(weekdayStr, 0, 7, WEEKDAY_NAMES).map((d) => (d === 7 ? 0 : d));\n\n const parsed: ParsedCron = {\n minute: parseField(minuteStr, 0, 59),\n hour: parseField(hourStr, 0, 23),\n day: parseField(dayStr, 1, 31),\n month: parseField(monthStr, 1, 12, MONTH_NAMES).map((m) => m - 1), // Convert to 0-indexed (0 = Jan, 11 = Dec)\n weekday: Array.from(new Set(weekdays)).sort((a, b) => a - b), // Dedupe and sort\n };\n\n // Validate day/month combinations\n validateDayMonthCombinations(parsed);\n\n return parsed;\n}\n\n/**\n * Validate that day/month combinations are possible\n * Rejects expressions like \"0 0 31 2 *\" (Feb 31) or \"0 0 30 2 *\" (Feb 30)\n */\nfunction validateDayMonthCombinations(parsed: ParsedCron): void {\n // If day or month is wildcard, no validation needed\n const dayIsWildcard = parsed.day.length === 31;\n const monthIsWildcard = parsed.month.length === 12;\n\n if (dayIsWildcard || monthIsWildcard) {\n return;\n }\n\n // Days in each month (0-indexed: 0=Jan, 11=Dec)\n // February can have 29 days in leap years\n const daysInMonth = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];\n\n // Check if any specified month can accommodate any specified day\n let hasValidCombination = false;\n\n for (const month of parsed.month) {\n const maxDaysInMonth = daysInMonth[month];\n\n for (const day of parsed.day) {\n if (day <= maxDaysInMonth) {\n hasValidCombination = true;\n break;\n }\n }\n\n if (hasValidCombination) {\n break;\n }\n }\n\n if (!hasValidCombination) {\n throw new Error(`Invalid cron expression: no valid day/month combination exists`);\n }\n}\n\n/**\n * Parse a single cron field (e.g., star-slash-5, 1-10, 1,3,5)\n */\nfunction parseField(\n field: string,\n min: number,\n max: number,\n names?: Record<string, number>,\n): number[] {\n const values = new Set<number>();\n\n // Handle wildcard\n if (field === \"*\") {\n for (let i = min; i <= max; i++) {\n values.add(i);\n }\n return Array.from(values).sort((a, b) => a - b);\n }\n\n // Split by comma for multiple values\n const parts = field.split(\",\");\n\n for (const part of parts) {\n // Handle step values (e.g., star-slash-5 or 10-20/2)\n if (part.includes(\"/\")) {\n const [range, stepStr] = part.split(\"/\");\n const step = parseInt(stepStr, 10);\n\n if (isNaN(step) || step <= 0) {\n throw new Error(`Invalid step value: ${stepStr}`);\n }\n\n let start = min;\n let end = max;\n\n if (range !== \"*\") {\n if (range.includes(\"-\")) {\n const [startStr, endStr] = range.split(\"-\");\n start = parseValue(startStr, names);\n end = parseValue(endStr, names);\n } else {\n start = parseValue(range, names);\n }\n }\n\n for (let i = start; i <= end; i += step) {\n if (i >= min && i <= max) {\n values.add(i);\n }\n }\n }\n // Handle ranges (e.g., 1-5)\n else if (part.includes(\"-\")) {\n const [startStr, endStr] = part.split(\"-\");\n const start = parseValue(startStr, names);\n const end = parseValue(endStr, names);\n\n if (start > end) {\n throw new Error(`Invalid range: ${part}`);\n }\n\n for (let i = start; i <= end; i++) {\n if (i >= min && i <= max) {\n values.add(i);\n }\n }\n }\n // Handle single values\n else {\n const value = parseValue(part, names);\n if (value >= min && value <= max) {\n values.add(value);\n } else {\n throw new Error(`Value ${value} out of range [${min}-${max}]`);\n }\n }\n }\n\n if (values.size === 0) {\n throw new Error(`No valid values in field: ${field}`);\n }\n\n return Array.from(values).sort((a, b) => a - b);\n}\n\n/**\n * Parse a single value (number or name)\n */\nfunction parseValue(value: string, names?: Record<string, number>): number {\n const lower = value.toLowerCase();\n\n if (names && lower in names) {\n return names[lower];\n }\n\n const num = parseInt(value, 10);\n if (isNaN(num)) {\n throw new Error(`Invalid value: ${value}`);\n }\n\n return num;\n}\n\n/**\n * Validate a cron expression\n */\nexport function isValid(expression: string): boolean {\n try {\n parse(expression);\n return true;\n } catch {\n return false;\n }\n}\n","import type { ParsedCron } from \"./types.js\";\n\n/**\n * Check if a date matches the cron expression\n */\nexport function matches(parsed: ParsedCron, date: Date): boolean {\n const minute = date.getUTCMinutes();\n const hour = date.getUTCHours();\n const day = date.getUTCDate();\n const month = date.getUTCMonth(); // 0-indexed (0 = Jan, 11 = Dec)\n const weekday = date.getUTCDay();\n\n // Check if all fields match\n return (\n parsed.minute.includes(minute) &&\n parsed.hour.includes(hour) &&\n parsed.month.includes(month) &&\n matchesDayOrWeekday(parsed, day, weekday)\n );\n}\n\n/**\n * Day-of-month and day-of-week use OR logic by default\n * If both are restricted (not *), match either one\n */\nfunction matchesDayOrWeekday(parsed: ParsedCron, day: number, weekday: number): boolean {\n const dayMatches = parsed.day.includes(day);\n const weekdayMatches = parsed.weekday.includes(weekday);\n\n // If both are wildcards (all values), both match\n const dayIsWildcard = parsed.day.length === 31;\n const weekdayIsWildcard = parsed.weekday.length === 7;\n\n // If both are restricted, use OR logic (standard cron behavior)\n if (!dayIsWildcard && !weekdayIsWildcard) {\n return dayMatches || weekdayMatches;\n }\n\n // If only one is restricted, it must match\n if (!dayIsWildcard) {\n return dayMatches;\n }\n if (!weekdayIsWildcard) {\n return weekdayMatches;\n }\n\n // Both wildcards, always matches\n return true;\n}\n\n/**\n * Find the next value in a sorted array that is >= target\n * Returns null if no such value exists\n *\n * @param values - MUST be sorted in ascending order\n * @param target - The minimum value to find\n */\nexport function findNext(values: number[], target: number): number | null {\n for (const value of values) {\n if (value >= target) {\n return value;\n }\n }\n return null;\n}\n\n/**\n * Find the previous value in a sorted array that is <= target\n * Returns null if no such value exists\n *\n * @param values - MUST be sorted in ascending order\n * @param target - The maximum value to find\n */\nexport function findPrevious(values: number[], target: number): number | null {\n for (let i = values.length - 1; i >= 0; i--) {\n if (values[i] <= target) {\n return values[i];\n }\n }\n return null;\n}\n\n/**\n * Get the number of days in a month\n *\n * @param year - The year\n * @param month - The month (0-indexed: 0 = January, 11 = December)\n * @returns The number of days in the month\n */\nexport function getDaysInMonth(year: number, month: number): number {\n // Create date for first day of next month, then go back one day\n return new Date(year, month + 1, 0).getDate();\n}\n","/** Convert a UTC date to wall-clock time in the target timezone */\nexport function convertToTimezone(date: Date, timezone: string): Date {\n // Format the date in the target timezone\n const str = date.toLocaleString(\"en-US\", {\n timeZone: timezone,\n year: \"numeric\",\n month: \"2-digit\",\n day: \"2-digit\",\n hour: \"2-digit\",\n minute: \"2-digit\",\n second: \"2-digit\",\n hour12: false,\n });\n\n // Parse formatted string: \"MM/DD/YYYY, HH:mm:ss\"\n const [datePart, timePart] = str.split(\", \");\n const [month, day, year] = datePart.split(\"/\").map(Number);\n let [hour, minute, second] = timePart.split(\":\").map(Number);\n\n if (hour === 24) hour = 0; // Normalize \"24:00:00\" to \"00:00:00\"\n\n return new Date(Date.UTC(year, month - 1, day, hour, minute, second));\n}\n\n/**\n * Convert a timezone-local date back to UTC (inverse of convertToTimezone).\n *\n * Note: During DST fall-back, multiple UTC times map to the same wall-clock time.\n * The result is implementation-defined. Avoid scheduling during DST transition hours\n * for predictable behavior.\n */\nexport function convertFromTimezone(date: Date, timezone: string): Date {\n const targetYear = date.getUTCFullYear();\n const targetMonth = date.getUTCMonth();\n const targetDay = date.getUTCDate();\n const targetHour = date.getUTCHours();\n const targetMinute = date.getUTCMinutes();\n const targetSecond = date.getUTCSeconds();\n\n // Target time as a comparable number (for checking if we found it)\n const targetTime = Date.UTC(\n targetYear,\n targetMonth,\n targetDay,\n targetHour,\n targetMinute,\n targetSecond,\n );\n\n // Start with a guess: interpret the wall-clock time as UTC\n let guess = targetTime;\n let bestGuess = guess;\n let bestDiff = Infinity;\n\n // Iteratively refine the guess (usually converges in 1-2 iterations)\n for (let i = 0; i < 3; i++) {\n const testDate = new Date(guess);\n const testStr = testDate.toLocaleString(\"en-US\", {\n timeZone: timezone,\n year: \"numeric\",\n month: \"2-digit\",\n day: \"2-digit\",\n hour: \"2-digit\",\n minute: \"2-digit\",\n second: \"2-digit\",\n hour12: false,\n });\n\n // Parse what wall-clock time this guess produces\n const [testDatePart, testTimePart] = testStr.split(\", \");\n const [testMonth, testDay, testYear] = testDatePart.split(\"/\").map(Number);\n let [testHour, testMinute, testSecond] = testTimePart.split(\":\").map(Number);\n\n if (testHour === 24) testHour = 0; // Normalize \"24:00:00\" to \"00:00:00\"\n\n const gotTime = Date.UTC(testYear, testMonth - 1, testDay, testHour, testMinute, testSecond);\n\n // Track the best guess (closest to target, but prefer later times if equal distance)\n const diff = Math.abs(targetTime - gotTime);\n if (diff < bestDiff || (diff === bestDiff && guess > bestGuess)) {\n bestDiff = diff;\n bestGuess = guess;\n }\n\n // If we got what we wanted, we're done!\n // Note: During DST fall-back, two UTC times map to the same wall-clock time.\n // This returns whichever solution the iteration converges to first (implementation-defined).\n if (gotTime === targetTime) {\n return new Date(guess);\n }\n\n // Otherwise, adjust the guess by the difference\n const adjustment = targetTime - gotTime;\n guess += adjustment;\n }\n\n // If we didn't find an exact match after 3 iterations, we're likely in a DST gap\n // (e.g., 2:30 AM during spring forward doesn't exist)\n // Try one more time: check if adding 1 hour to the target gets us closer\n const oneHourLater = targetTime + 60 * 60 * 1000;\n let guessLater = oneHourLater;\n\n for (let i = 0; i < 2; i++) {\n const testDate = new Date(guessLater);\n const testStr = testDate.toLocaleString(\"en-US\", {\n timeZone: timezone,\n year: \"numeric\",\n month: \"2-digit\",\n day: \"2-digit\",\n hour: \"2-digit\",\n minute: \"2-digit\",\n second: \"2-digit\",\n hour12: false,\n });\n\n const [testDatePart, testTimePart] = testStr.split(\", \");\n const [testMonth, testDay, testYear] = testDatePart.split(\"/\").map(Number);\n let [testHour, testMinute, testSecond] = testTimePart.split(\":\").map(Number);\n\n if (testHour === 24) testHour = 0; // Normalize \"24:00:00\" to \"00:00:00\"\n\n const gotTime = Date.UTC(testYear, testMonth - 1, testDay, testHour, testMinute, testSecond);\n\n if (gotTime === oneHourLater) {\n // Target time was in a DST gap, return the time after the gap\n return new Date(guessLater);\n }\n\n const adjustment = oneHourLater - gotTime;\n guessLater += adjustment;\n }\n\n // Return the best guess we found\n return new Date(bestGuess);\n}\n","import type { ParsedCron, CronOptions } from \"./types.js\";\nimport { parse } from \"./parser.js\";\nimport { matches, findNext, findPrevious, getDaysInMonth } from \"./matcher.js\";\nimport { convertToTimezone, convertFromTimezone } from \"./timezone.js\";\n\nconst MAX_ITERATIONS = 1000;\nconst ONE_MINUTE_MS = 60_000;\n\ntype Direction = \"next\" | \"prev\";\n\n/** Direction-specific operations for unified forward/backward traversal */\nconst DIR = {\n next: {\n find: findNext,\n minute: (p: ParsedCron) => p.minute[0],\n hour: (p: ParsedCron) => p.hour[0],\n offset: 1,\n },\n prev: {\n find: findPrevious,\n minute: (p: ParsedCron) => p.minute.at(-1)!,\n hour: (p: ParsedCron) => p.hour.at(-1)!,\n offset: -1,\n },\n} as const;\n\n/** Get the next execution time for a cron expression */\nexport function nextRun(expression: string, options?: CronOptions): Date {\n const parsed = parse(expression);\n const from = options?.from || new Date();\n const tz = options?.timezone;\n\n const start = tz ? convertToTimezone(from, tz) : new Date(from);\n start.setUTCSeconds(0, 0);\n start.setUTCMinutes(start.getUTCMinutes() + 1);\n\n const result = findMatch(parsed, start, \"next\", tz);\n if (!result) throw new Error(\"No matching time found within reasonable search window\");\n return result;\n}\n\n/** Get the previous execution time for a cron expression */\nexport function previousRun(expression: string, options?: CronOptions): Date {\n const parsed = parse(expression);\n const from = options?.from || new Date();\n const tz = options?.timezone;\n\n const start = tz ? convertToTimezone(from, tz) : new Date(from);\n start.setUTCSeconds(0, 0);\n start.setUTCMinutes(start.getUTCMinutes() - 1);\n\n const result = findMatch(parsed, start, \"prev\", tz);\n if (!result) throw new Error(\"No matching time found within reasonable search window\");\n return result;\n}\n\n/** Get next N execution times */\nexport function nextRuns(expression: string, count: number, options?: CronOptions): Date[] {\n if (count <= 0) return [];\n\n const results: Date[] = [];\n let current = options?.from || new Date();\n\n for (let i = 0; i < count; i++) {\n const next = nextRun(expression, { ...options, from: current });\n results.push(next);\n current = new Date(next.getTime() + ONE_MINUTE_MS);\n }\n return results;\n}\n\n/** Check if a date matches the cron expression */\nexport function isMatch(\n expression: string,\n date: Date,\n options?: Pick<CronOptions, \"timezone\">,\n): boolean {\n const parsed = parse(expression);\n const checkDate = options?.timezone ? convertToTimezone(date, options.timezone) : new Date(date);\n return matches(parsed, checkDate);\n}\n\n/** Find matching time using smart field-increment algorithm */\nfunction findMatch(parsed: ParsedCron, start: Date, dir: Direction, tz?: string): Date | null {\n const current = new Date(start);\n\n for (let i = 0; i < MAX_ITERATIONS; i++) {\n if (matches(parsed, current)) {\n return tz ? convertFromTimezone(current, tz) : current;\n }\n advanceDate(parsed, current, dir);\n }\n return null;\n}\n\n/**\n * Advance date to next/prev candidate time by mutating the date in place.\n *\n * Algorithm:\n * 1. Check fields from LARGEST (month) to SMALLEST (minute)\n * 2. When a field doesn't match, jump to the next valid value for that field\n * 3. Reset all smaller fields to their boundary (first value for 'next', last for 'prev')\n *\n * Example (direction='next', cron='0 9 * * *' meaning 9:00 AM daily):\n * Current: March 15, 10:30 AM\n * - Month (March)? ✓ matches\n * - Day (15)? ✓ matches\n * - Hour (10)? ✗ not in [9] → no next hour today → cascade to next day\n * - Result: March 16, 9:00 AM\n *\n * @param parsed - The parsed cron expression\n * @param date - The date to mutate (modified in place)\n * @param dir - Direction to advance ('next' or 'prev')\n */\nfunction advanceDate(parsed: ParsedCron, date: Date, dir: Direction): void {\n const d = DIR[dir];\n const minute = date.getUTCMinutes();\n const hour = date.getUTCHours();\n const day = date.getUTCDate();\n const month = date.getUTCMonth();\n const year = date.getUTCFullYear();\n const daysInMonth = getDaysInMonth(year, month);\n\n // Month mismatch\n if (!parsed.month.includes(month)) {\n moveToMonth(parsed, date, dir, month, year);\n return;\n }\n\n // Day mismatch\n if (!parsed.day.includes(day) || day > daysInMonth) {\n moveToDay(parsed, date, dir, day, month, year, daysInMonth);\n return;\n }\n\n // Hour mismatch\n if (!parsed.hour.includes(hour)) {\n const targetHour = d.find(parsed.hour, hour + d.offset);\n if (targetHour !== null) {\n // Found valid hour in same day → reset minute to boundary\n date.setUTCHours(targetHour);\n date.setUTCMinutes(d.minute(parsed));\n } else {\n // No valid hour left today → move to next/prev day\n moveToDay(parsed, date, dir, day, month, year, daysInMonth);\n }\n return;\n }\n\n // Minute mismatch\n if (!parsed.minute.includes(minute)) {\n const targetMinute = d.find(parsed.minute, minute + d.offset);\n if (targetMinute !== null) {\n // Found valid minute in same hour\n date.setUTCMinutes(targetMinute);\n } else {\n // No valid minute left → try next hour\n const targetHour = d.find(parsed.hour, hour + d.offset);\n if (targetHour !== null) {\n date.setUTCHours(targetHour);\n date.setUTCMinutes(d.minute(parsed));\n } else {\n // No valid hour left → move to next/prev day\n moveToDay(parsed, date, dir, day, month, year, daysInMonth);\n }\n }\n return;\n }\n\n // Weekday mismatch: all fields match but wrong day-of-week.\n // Skip directly to next/prev day since no hour/minute on this day can match.\n moveToDay(parsed, date, dir, day, month, year, daysInMonth);\n}\n\nfunction moveToMonth(\n parsed: ParsedCron,\n date: Date,\n dir: Direction,\n currentMonth: number,\n currentYear: number,\n): void {\n const d = DIR[dir];\n const targetMonth = d.find(parsed.month, currentMonth + d.offset);\n\n if (targetMonth !== null) {\n resetToMonthBoundary(parsed, date, currentYear, targetMonth, dir);\n } else {\n const boundaryMonth = dir === \"next\" ? parsed.month[0] : parsed.month.at(-1)!;\n resetToMonthBoundary(parsed, date, currentYear + d.offset, boundaryMonth, dir);\n }\n}\n\nfunction moveToDay(\n parsed: ParsedCron,\n date: Date,\n dir: Direction,\n currentDay: number,\n currentMonth: number,\n currentYear: number,\n daysInMonth: number,\n): void {\n const d = DIR[dir];\n const targetDay = d.find(parsed.day, currentDay + d.offset);\n const dayIsValid =\n dir === \"next\" ? targetDay !== null && targetDay <= daysInMonth : targetDay !== null;\n\n if (dayIsValid) {\n date.setUTCDate(targetDay!);\n date.setUTCHours(d.hour(parsed));\n date.setUTCMinutes(d.minute(parsed));\n } else {\n moveToMonth(parsed, date, dir, currentMonth, currentYear);\n }\n}\n\nfunction resetToMonthBoundary(\n parsed: ParsedCron,\n date: Date,\n year: number,\n month: number,\n dir: Direction,\n): void {\n const d = DIR[dir];\n date.setUTCFullYear(year);\n date.setUTCDate(1);\n date.setUTCMonth(month);\n\n const daysInMonth = getDaysInMonth(year, month);\n\n if (dir === \"next\") {\n const validDay = findNext(parsed.day, 1);\n date.setUTCDate(validDay !== null && validDay <= daysInMonth ? validDay : parsed.day[0]);\n } else {\n const prevDay = findPrevious(parsed.day, daysInMonth);\n if (prevDay !== null) {\n date.setUTCDate(prevDay);\n } else {\n // No valid day in this month, move to previous month\n moveToMonth(parsed, date, dir, month, year);\n return;\n }\n }\n\n date.setUTCHours(d.hour(parsed));\n date.setUTCMinutes(d.minute(parsed));\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":[],"sources":["../src/parser.ts","../src/matcher.ts","../src/timezone.ts","../src/scheduler.ts"],"sourcesContent":["import type { ParsedCron } from \"./types.js\";\n\nconst MONTH_NAMES: Record<string, number> = {\n jan: 1,\n feb: 2,\n mar: 3,\n apr: 4,\n may: 5,\n jun: 6,\n jul: 7,\n aug: 8,\n sep: 9,\n oct: 10,\n nov: 11,\n dec: 12,\n};\n\nconst WEEKDAY_NAMES: Record<string, number> = {\n sun: 0,\n mon: 1,\n tue: 2,\n wed: 3,\n thu: 4,\n fri: 5,\n sat: 6,\n};\n\n/**\n * Parse a cron expression into structured format\n *\n * Cron format: minute hour day month weekday\n * - minute: 0-59\n * - hour: 0-23\n * - day: 1-31\n * - month: 1-12 (or JAN-DEC)\n * - weekday: 0-7 (or SUN-SAT, where 0 and 7 are Sunday)\n *\n * Note: Months are converted from cron's 1-indexed format (1-12) to\n * JavaScript's 0-indexed format (0-11) for internal consistency.\n */\nexport function parse(expression: string): ParsedCron {\n const trimmed = expression.trim();\n\n if (!trimmed) {\n throw new Error(\"Cron expression cannot be empty\");\n }\n\n const parts = trimmed.split(/\\s+/);\n\n if (parts.length !== 5) {\n throw new Error(`Invalid cron expression: expected 5 fields, got ${parts.length}`);\n }\n\n const [minuteStr, hourStr, dayStr, monthStr, weekdayStr] = parts;\n\n const weekdays = parseField(weekdayStr, 0, 7, WEEKDAY_NAMES).map((d) => (d === 7 ? 0 : d));\n\n const parsed: ParsedCron = {\n minute: parseField(minuteStr, 0, 59),\n hour: parseField(hourStr, 0, 23),\n day: parseField(dayStr, 1, 31),\n month: parseField(monthStr, 1, 12, MONTH_NAMES).map((m) => m - 1), // Convert to 0-indexed (0 = Jan, 11 = Dec)\n weekday: Array.from(new Set(weekdays)).sort((a, b) => a - b), // Dedupe and sort\n dayIsWildcard: dayStr.trim() === \"*\",\n weekdayIsWildcard: weekdayStr.trim() === \"*\",\n };\n\n // Validate day/month combinations\n validateDayMonthCombinations(parsed);\n\n return parsed;\n}\n\n/**\n * Validate that day/month combinations are possible\n * Rejects expressions like \"0 0 31 2 *\" (Feb 31) or \"0 0 30 2 *\" (Feb 30)\n */\nfunction validateDayMonthCombinations(parsed: ParsedCron): void {\n // If day or month is wildcard, no validation needed\n const dayIsWildcard = parsed.dayIsWildcard;\n const monthIsWildcard = parsed.month.length === 12;\n\n if (dayIsWildcard || monthIsWildcard) {\n return;\n }\n\n // Days in each month (0-indexed: 0=Jan, 11=Dec)\n // February can have 29 days in leap years\n const daysInMonth = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];\n\n // Check if any specified month can accommodate any specified day\n let hasValidCombination = false;\n\n for (const month of parsed.month) {\n const maxDaysInMonth = daysInMonth[month];\n\n for (const day of parsed.day) {\n if (day <= maxDaysInMonth) {\n hasValidCombination = true;\n break;\n }\n }\n\n if (hasValidCombination) {\n break;\n }\n }\n\n if (!hasValidCombination) {\n throw new Error(`Invalid cron expression: no valid day/month combination exists`);\n }\n}\n\n/**\n * Parse a single cron field (e.g., star-slash-5, 1-10, 1,3,5)\n */\nfunction parseField(\n field: string,\n min: number,\n max: number,\n names?: Record<string, number>,\n): number[] {\n const values = new Set<number>();\n\n // Handle wildcard\n if (field === \"*\") {\n for (let i = min; i <= max; i++) {\n values.add(i);\n }\n return Array.from(values).sort((a, b) => a - b);\n }\n\n // Split by comma for multiple values\n const parts = field.split(\",\");\n\n for (const part of parts) {\n // Handle step values (e.g., star-slash-5 or 10-20/2)\n if (part.includes(\"/\")) {\n const [range, stepStr] = part.split(\"/\");\n const step = parseInt(stepStr, 10);\n\n if (isNaN(step) || step <= 0) {\n throw new Error(`Invalid step value: ${stepStr}`);\n }\n\n let start = min;\n let end = max;\n\n if (range !== \"*\") {\n if (range.includes(\"-\")) {\n const [startStr, endStr] = range.split(\"-\");\n start = parseValue(startStr, names);\n end = parseValue(endStr, names);\n } else {\n start = parseValue(range, names);\n }\n }\n\n for (let i = start; i <= end; i += step) {\n if (i >= min && i <= max) {\n values.add(i);\n }\n }\n }\n // Handle ranges (e.g., 1-5)\n else if (part.includes(\"-\")) {\n const [startStr, endStr] = part.split(\"-\");\n const start = parseValue(startStr, names);\n const end = parseValue(endStr, names);\n\n if (start > end) {\n throw new Error(`Invalid range: ${part}`);\n }\n\n for (let i = start; i <= end; i++) {\n if (i >= min && i <= max) {\n values.add(i);\n }\n }\n }\n // Handle single values\n else {\n const value = parseValue(part, names);\n if (value >= min && value <= max) {\n values.add(value);\n } else {\n throw new Error(`Value ${value} out of range [${min}-${max}]`);\n }\n }\n }\n\n if (values.size === 0) {\n throw new Error(`No valid values in field: ${field}`);\n }\n\n return Array.from(values).sort((a, b) => a - b);\n}\n\n/**\n * Parse a single value (number or name)\n */\nfunction parseValue(value: string, names?: Record<string, number>): number {\n const lower = value.toLowerCase();\n\n if (names && lower in names) {\n return names[lower];\n }\n\n const num = parseInt(value, 10);\n if (isNaN(num)) {\n throw new Error(`Invalid value: ${value}`);\n }\n\n return num;\n}\n\n/**\n * Validate a cron expression\n */\nexport function isValid(expression: string): boolean {\n try {\n parse(expression);\n return true;\n } catch {\n return false;\n }\n}\n","import type { ParsedCron } from \"./types.js\";\n\n/**\n * Check if a date matches the cron expression\n */\nexport function matches(parsed: ParsedCron, date: Date): boolean {\n const minute = date.getUTCMinutes();\n const hour = date.getUTCHours();\n const day = date.getUTCDate();\n const month = date.getUTCMonth(); // 0-indexed (0 = Jan, 11 = Dec)\n const weekday = date.getUTCDay();\n\n // Check if all fields match\n return (\n parsed.minute.includes(minute) &&\n parsed.hour.includes(hour) &&\n parsed.month.includes(month) &&\n matchesDayOrWeekday(parsed, day, weekday)\n );\n}\n\n/**\n * Check if we're in OR mode (both day and weekday are restricted, not wildcards)\n * In OR mode, we must check every day because any day might match via weekday\n */\nexport function isOrMode(parsed: ParsedCron): boolean {\n return !parsed.dayIsWildcard && !parsed.weekdayIsWildcard;\n}\n\n/**\n * Day-of-month and day-of-week use OR logic by default\n * If both are restricted (not *), match either one\n * \n * @param daysInMonth - Optional validation that day is valid for the month (used by scheduler)\n */\nexport function matchesDayOrWeekday(\n parsed: ParsedCron,\n day: number,\n weekday: number,\n daysInMonth?: number,\n): boolean {\n const dayMatches =\n daysInMonth !== undefined\n ? parsed.day.includes(day) && day <= daysInMonth\n : parsed.day.includes(day);\n const weekdayMatches = parsed.weekday.includes(weekday);\n\n // If both are restricted, use OR logic (standard cron behavior)\n if (isOrMode(parsed)) {\n return dayMatches || weekdayMatches;\n }\n\n // If only one is restricted, it must match\n if (!parsed.dayIsWildcard) {\n return dayMatches;\n }\n if (!parsed.weekdayIsWildcard) {\n return weekdayMatches;\n }\n\n // Both wildcards, always matches\n return true;\n}\n\n/**\n * Find the next value in a sorted array that is >= target\n * Returns null if no such value exists\n *\n * @param values - MUST be sorted in ascending order\n * @param target - The minimum value to find\n */\nexport function findNext(values: number[], target: number): number | null {\n for (const value of values) {\n if (value >= target) {\n return value;\n }\n }\n return null;\n}\n\n/**\n * Find the previous value in a sorted array that is <= target\n * Returns null if no such value exists\n *\n * @param values - MUST be sorted in ascending order\n * @param target - The maximum value to find\n */\nexport function findPrevious(values: number[], target: number): number | null {\n for (let i = values.length - 1; i >= 0; i--) {\n if (values[i] <= target) {\n return values[i];\n }\n }\n return null;\n}\n\n/**\n * Get the number of days in a month\n *\n * @param year - The year\n * @param month - The month (0-indexed: 0 = January, 11 = December)\n * @returns The number of days in the month\n */\nexport function getDaysInMonth(year: number, month: number): number {\n // Create date for first day of next month, then go back one day\n return new Date(year, month + 1, 0).getDate();\n}\n","/** Convert a UTC date to wall-clock time in the target timezone */\nexport function convertToTimezone(date: Date, timezone: string): Date {\n // Format the date in the target timezone\n const str = date.toLocaleString(\"en-US\", {\n timeZone: timezone,\n year: \"numeric\",\n month: \"2-digit\",\n day: \"2-digit\",\n hour: \"2-digit\",\n minute: \"2-digit\",\n second: \"2-digit\",\n hour12: false,\n });\n\n // Parse formatted string: \"MM/DD/YYYY, HH:mm:ss\"\n const [datePart, timePart] = str.split(\", \");\n const [month, day, year] = datePart.split(\"/\").map(Number);\n let [hour, minute, second] = timePart.split(\":\").map(Number);\n\n if (hour === 24) hour = 0; // Normalize \"24:00:00\" to \"00:00:00\"\n\n return new Date(Date.UTC(year, month - 1, day, hour, minute, second));\n}\n\n/**\n * Convert a timezone-local date back to UTC (inverse of convertToTimezone).\n *\n * Note: During DST fall-back, multiple UTC times map to the same wall-clock time.\n * The result is implementation-defined. Avoid scheduling during DST transition hours\n * for predictable behavior.\n */\nexport function convertFromTimezone(date: Date, timezone: string): Date {\n const targetYear = date.getUTCFullYear();\n const targetMonth = date.getUTCMonth();\n const targetDay = date.getUTCDate();\n const targetHour = date.getUTCHours();\n const targetMinute = date.getUTCMinutes();\n const targetSecond = date.getUTCSeconds();\n\n // Target time as a comparable number (for checking if we found it)\n const targetTime = Date.UTC(\n targetYear,\n targetMonth,\n targetDay,\n targetHour,\n targetMinute,\n targetSecond,\n );\n\n // Start with a guess: interpret the wall-clock time as UTC\n let guess = targetTime;\n let bestGuess = guess;\n let bestDiff = Infinity;\n\n // Iteratively refine the guess (usually converges in 1-2 iterations)\n for (let i = 0; i < 3; i++) {\n const testDate = new Date(guess);\n const testStr = testDate.toLocaleString(\"en-US\", {\n timeZone: timezone,\n year: \"numeric\",\n month: \"2-digit\",\n day: \"2-digit\",\n hour: \"2-digit\",\n minute: \"2-digit\",\n second: \"2-digit\",\n hour12: false,\n });\n\n // Parse what wall-clock time this guess produces\n const [testDatePart, testTimePart] = testStr.split(\", \");\n const [testMonth, testDay, testYear] = testDatePart.split(\"/\").map(Number);\n let [testHour, testMinute, testSecond] = testTimePart.split(\":\").map(Number);\n\n if (testHour === 24) testHour = 0; // Normalize \"24:00:00\" to \"00:00:00\"\n\n const gotTime = Date.UTC(testYear, testMonth - 1, testDay, testHour, testMinute, testSecond);\n\n // Track the best guess (closest to target, but prefer later times if equal distance)\n const diff = Math.abs(targetTime - gotTime);\n if (diff < bestDiff || (diff === bestDiff && guess > bestGuess)) {\n bestDiff = diff;\n bestGuess = guess;\n }\n\n // If we got what we wanted, we're done!\n // Note: During DST fall-back, two UTC times map to the same wall-clock time.\n // This returns whichever solution the iteration converges to first (implementation-defined).\n if (gotTime === targetTime) {\n return new Date(guess);\n }\n\n // Otherwise, adjust the guess by the difference\n const adjustment = targetTime - gotTime;\n guess += adjustment;\n }\n\n // If we didn't find an exact match after 3 iterations, we're likely in a DST gap\n // (e.g., 2:30 AM during spring forward doesn't exist)\n // Try one more time: check if adding 1 hour to the target gets us closer\n const oneHourLater = targetTime + 60 * 60 * 1000;\n let guessLater = oneHourLater;\n\n for (let i = 0; i < 2; i++) {\n const testDate = new Date(guessLater);\n const testStr = testDate.toLocaleString(\"en-US\", {\n timeZone: timezone,\n year: \"numeric\",\n month: \"2-digit\",\n day: \"2-digit\",\n hour: \"2-digit\",\n minute: \"2-digit\",\n second: \"2-digit\",\n hour12: false,\n });\n\n const [testDatePart, testTimePart] = testStr.split(\", \");\n const [testMonth, testDay, testYear] = testDatePart.split(\"/\").map(Number);\n let [testHour, testMinute, testSecond] = testTimePart.split(\":\").map(Number);\n\n if (testHour === 24) testHour = 0; // Normalize \"24:00:00\" to \"00:00:00\"\n\n const gotTime = Date.UTC(testYear, testMonth - 1, testDay, testHour, testMinute, testSecond);\n\n if (gotTime === oneHourLater) {\n // Target time was in a DST gap, return the time after the gap\n return new Date(guessLater);\n }\n\n const adjustment = oneHourLater - gotTime;\n guessLater += adjustment;\n }\n\n // Return the best guess we found\n return new Date(bestGuess);\n}\n","import type { ParsedCron, CronOptions } from \"./types.js\";\nimport { parse } from \"./parser.js\";\nimport {\n matches,\n findNext,\n findPrevious,\n getDaysInMonth,\n isOrMode,\n matchesDayOrWeekday,\n} from \"./matcher.js\";\nimport { convertToTimezone, convertFromTimezone } from \"./timezone.js\";\n\nconst MAX_ITERATIONS = 1000;\nconst ONE_MINUTE_MS = 60_000;\n\ntype Direction = \"next\" | \"prev\";\n\n/** Direction-specific operations for unified forward/backward traversal */\nconst DIR = {\n next: {\n find: findNext,\n minute: (p: ParsedCron) => p.minute[0],\n hour: (p: ParsedCron) => p.hour[0],\n offset: 1,\n },\n prev: {\n find: findPrevious,\n minute: (p: ParsedCron) => p.minute.at(-1)!,\n hour: (p: ParsedCron) => p.hour.at(-1)!,\n offset: -1,\n },\n} as const;\n\n/** Get the next execution time for a cron expression */\nexport function nextRun(expression: string, options?: CronOptions): Date {\n const parsed = parse(expression);\n const from = options?.from || new Date();\n const tz = options?.timezone;\n\n const start = tz ? convertToTimezone(from, tz) : new Date(from);\n start.setUTCSeconds(0, 0);\n start.setUTCMinutes(start.getUTCMinutes() + 1);\n\n const result = findMatch(parsed, start, \"next\", tz);\n if (!result) throw new Error(\"No matching time found within reasonable search window\");\n return result;\n}\n\n/** Get the previous execution time for a cron expression */\nexport function previousRun(expression: string, options?: CronOptions): Date {\n const parsed = parse(expression);\n const from = options?.from || new Date();\n const tz = options?.timezone;\n\n const start = tz ? convertToTimezone(from, tz) : new Date(from);\n start.setUTCSeconds(0, 0);\n start.setUTCMinutes(start.getUTCMinutes() - 1);\n\n const result = findMatch(parsed, start, \"prev\", tz);\n if (!result) throw new Error(\"No matching time found within reasonable search window\");\n return result;\n}\n\n/** Get next N execution times */\nexport function nextRuns(expression: string, count: number, options?: CronOptions): Date[] {\n if (count <= 0) return [];\n\n const results: Date[] = [];\n let current = options?.from || new Date();\n\n for (let i = 0; i < count; i++) {\n const next = nextRun(expression, { ...options, from: current });\n results.push(next);\n current = new Date(next.getTime() + ONE_MINUTE_MS);\n }\n return results;\n}\n\n/** Check if a date matches the cron expression */\nexport function isMatch(\n expression: string,\n date: Date,\n options?: Pick<CronOptions, \"timezone\">,\n): boolean {\n const parsed = parse(expression);\n const checkDate = options?.timezone ? convertToTimezone(date, options.timezone) : new Date(date);\n return matches(parsed, checkDate);\n}\n\n/** Find matching time using smart field-increment algorithm */\nfunction findMatch(parsed: ParsedCron, start: Date, dir: Direction, tz?: string): Date | null {\n const current = new Date(start);\n\n for (let i = 0; i < MAX_ITERATIONS; i++) {\n if (matches(parsed, current)) {\n return tz ? convertFromTimezone(current, tz) : current;\n }\n advanceDate(parsed, current, dir);\n }\n return null;\n}\n\n/**\n * Advance date to next/prev candidate time by mutating the date in place.\n *\n * Algorithm:\n * 1. Check fields from LARGEST (month) to SMALLEST (minute)\n * 2. When a field doesn't match, jump to the next valid value for that field\n * 3. Reset all smaller fields to their boundary (first value for 'next', last for 'prev')\n *\n * Example (direction='next', cron='0 9 * * *' meaning 9:00 AM daily):\n * Current: March 15, 10:30 AM\n * - Month (March)? ✓ matches\n * - Day (15)? ✓ matches\n * - Hour (10)? ✗ not in [9] → no next hour today → cascade to next day\n * - Result: March 16, 9:00 AM\n *\n * @param parsed - The parsed cron expression\n * @param date - The date to mutate (modified in place)\n * @param dir - Direction to advance ('next' or 'prev')\n */\nfunction advanceDate(parsed: ParsedCron, date: Date, dir: Direction): void {\n const d = DIR[dir];\n const minute = date.getUTCMinutes();\n const hour = date.getUTCHours();\n const day = date.getUTCDate();\n const month = date.getUTCMonth();\n const year = date.getUTCFullYear();\n const weekday = date.getUTCDay();\n const daysInMonth = getDaysInMonth(year, month);\n\n // Month mismatch\n if (!parsed.month.includes(month)) {\n moveToMonth(parsed, date, dir, month, year);\n return;\n }\n\n // Day/Weekday mismatch - use OR logic like matches()\n if (!matchesDayOrWeekday(parsed, day, weekday, daysInMonth)) {\n moveToDay(parsed, date, dir, day, month, year, daysInMonth);\n return;\n }\n\n // Hour mismatch\n if (!parsed.hour.includes(hour)) {\n const targetHour = d.find(parsed.hour, hour + d.offset);\n if (targetHour !== null) {\n // Found valid hour in same day → reset minute to boundary\n date.setUTCHours(targetHour);\n date.setUTCMinutes(d.minute(parsed));\n } else {\n // No valid hour left today → move to next/prev day\n moveToDay(parsed, date, dir, day, month, year, daysInMonth);\n }\n return;\n }\n\n // Minute mismatch\n if (!parsed.minute.includes(minute)) {\n const targetMinute = d.find(parsed.minute, minute + d.offset);\n if (targetMinute !== null) {\n // Found valid minute in same hour\n date.setUTCMinutes(targetMinute);\n } else {\n // No valid minute left → try next hour\n const targetHour = d.find(parsed.hour, hour + d.offset);\n if (targetHour !== null) {\n date.setUTCHours(targetHour);\n date.setUTCMinutes(d.minute(parsed));\n } else {\n // No valid hour left → move to next/prev day\n moveToDay(parsed, date, dir, day, month, year, daysInMonth);\n }\n }\n return;\n }\n\n // All fields match but we still need to advance (called from findMatch loop)\n // This happens when matches() returns false due to day/weekday mismatch\n // Move to next/prev day\n moveToDay(parsed, date, dir, day, month, year, daysInMonth);\n}\n\nfunction moveToMonth(\n parsed: ParsedCron,\n date: Date,\n dir: Direction,\n currentMonth: number,\n currentYear: number,\n): void {\n const d = DIR[dir];\n const targetMonth = d.find(parsed.month, currentMonth + d.offset);\n\n if (targetMonth !== null) {\n resetToMonthBoundary(parsed, date, currentYear, targetMonth, dir);\n } else {\n const boundaryMonth = dir === \"next\" ? parsed.month[0] : parsed.month.at(-1)!;\n resetToMonthBoundary(parsed, date, currentYear + d.offset, boundaryMonth, dir);\n }\n}\n\n/**\n * Find the next candidate day to check\n * In OR mode: advance by 1 day (must check every day)\n * Normal mode: jump to next valid day-of-month\n */\nfunction findCandidateDay(\n parsed: ParsedCron,\n currentDay: number,\n dir: Direction,\n daysInMonth: number,\n): number | null {\n const d = DIR[dir];\n const inOrMode = isOrMode(parsed);\n\n if (inOrMode) {\n // In OR mode, we must check every day (can't skip ahead)\n // because any day might match via weekday even if day-of-month doesn't match\n const targetDay = currentDay + d.offset;\n if (dir === \"next\" && targetDay > daysInMonth) {\n return null;\n }\n if (dir === \"prev\" && targetDay < 1) {\n return null;\n }\n return targetDay;\n }\n\n // Normal mode: jump to next valid day-of-month\n return d.find(parsed.day, currentDay + d.offset);\n}\n\nfunction moveToDay(\n parsed: ParsedCron,\n date: Date,\n dir: Direction,\n currentDay: number,\n currentMonth: number,\n currentYear: number,\n daysInMonth: number,\n): void {\n const d = DIR[dir];\n const targetDay = findCandidateDay(parsed, currentDay, dir, daysInMonth);\n const dayIsValid =\n dir === \"next\" ? targetDay !== null && targetDay <= daysInMonth : targetDay !== null;\n\n if (dayIsValid) {\n date.setUTCDate(targetDay!);\n date.setUTCHours(d.hour(parsed));\n date.setUTCMinutes(d.minute(parsed));\n } else {\n moveToMonth(parsed, date, dir, currentMonth, currentYear);\n }\n}\n\nfunction resetToMonthBoundary(\n parsed: ParsedCron,\n date: Date,\n year: number,\n month: number,\n dir: Direction,\n): void {\n const d = DIR[dir];\n date.setUTCFullYear(year);\n date.setUTCDate(1);\n date.setUTCMonth(month);\n\n const daysInMonth = getDaysInMonth(year, month);\n\n // Check if we're in OR mode (both day and weekday restricted)\n const inOrMode = isOrMode(parsed);\n\n if (dir === \"next\") {\n // In OR mode: start from day 1. Normal mode: jump to first valid day\n const startDay = inOrMode ? 1 : (findNext(parsed.day, 1) ?? parsed.day[0]);\n date.setUTCDate(Math.min(startDay, daysInMonth));\n } else {\n // In OR mode: start from last day. Normal mode: jump to last valid day\n const startDay = inOrMode ? daysInMonth : findPrevious(parsed.day, daysInMonth);\n if (startDay === null) {\n // No valid day in this month, move to previous month\n moveToMonth(parsed, date, dir, month, year);\n return;\n }\n date.setUTCDate(startDay);\n }\n\n date.setUTCHours(d.hour(parsed));\n date.setUTCMinutes(d.minute(parsed));\n}\n"],"mappings":"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"}
|
package/dist/index.d.cts
CHANGED
package/dist/index.d.cts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.cts","names":[],"sources":["../src/types.ts","../src/scheduler.ts","../src/parser.ts"],"mappings":";;AAGA;;UAAiB,WAAA;EAIJ;EAFX,QAAA;EAEA;EAAA,IAAA,GAAO,IAAA;AAAA;;AAUT;;;;;;UAAiB,UAAA;EACf,MAAA;EACA,IAAA;EACA,GAAA;EACA,KAAA;EACA,OAAA;AAAA;;;
|
|
1
|
+
{"version":3,"file":"index.d.cts","names":[],"sources":["../src/types.ts","../src/scheduler.ts","../src/parser.ts"],"mappings":";;AAGA;;UAAiB,WAAA;EAIJ;EAFX,QAAA;EAEA;EAAA,IAAA,GAAO,IAAA;AAAA;;AAUT;;;;;;UAAiB,UAAA;EACf,MAAA;EACA,IAAA;EACA,GAAA;EACA,KAAA;EACA,OAAA;EACA,aAAA;EACA,iBAAA;AAAA;;;AArBF;AAAA,iBC+BgB,OAAA,CAAQ,UAAA,UAAoB,OAAA,GAAU,WAAA,GAAc,IAAA;;iBAepD,WAAA,CAAY,UAAA,UAAoB,OAAA,GAAU,WAAA,GAAc,IAAA;;iBAexD,QAAA,CAAS,UAAA,UAAoB,KAAA,UAAe,OAAA,GAAU,WAAA,GAAc,IAAA;;iBAepE,OAAA,CACd,UAAA,UACA,IAAA,EAAM,IAAA,EACN,OAAA,GAAU,IAAA,CAAK,WAAA;;;AD/EjB;;;;;;;;;AAcA;;;;AAdA,iBEqCgB,KAAA,CAAM,UAAA,WAAqB,UAAA;;;;iBAmL3B,OAAA,CAAQ,UAAA"}
|
package/dist/index.d.mts
CHANGED
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/scheduler.ts","../src/parser.ts"],"mappings":";;AAGA;;UAAiB,WAAA;EAIJ;EAFX,QAAA;EAEA;EAAA,IAAA,GAAO,IAAA;AAAA;;AAUT;;;;;;UAAiB,UAAA;EACf,MAAA;EACA,IAAA;EACA,GAAA;EACA,KAAA;EACA,OAAA;AAAA;;;
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/scheduler.ts","../src/parser.ts"],"mappings":";;AAGA;;UAAiB,WAAA;EAIJ;EAFX,QAAA;EAEA;EAAA,IAAA,GAAO,IAAA;AAAA;;AAUT;;;;;;UAAiB,UAAA;EACf,MAAA;EACA,IAAA;EACA,GAAA;EACA,KAAA;EACA,OAAA;EACA,aAAA;EACA,iBAAA;AAAA;;;AArBF;AAAA,iBC+BgB,OAAA,CAAQ,UAAA,UAAoB,OAAA,GAAU,WAAA,GAAc,IAAA;;iBAepD,WAAA,CAAY,UAAA,UAAoB,OAAA,GAAU,WAAA,GAAc,IAAA;;iBAexD,QAAA,CAAS,UAAA,UAAoB,KAAA,UAAe,OAAA,GAAU,WAAA,GAAc,IAAA;;iBAepE,OAAA,CACd,UAAA,UACA,IAAA,EAAM,IAAA,EACN,OAAA,GAAU,IAAA,CAAK,WAAA;;;AD/EjB;;;;;;;;;AAcA;;;;AAdA,iBEqCgB,KAAA,CAAM,UAAA,WAAqB,UAAA;;;;iBAmL3B,OAAA,CAAQ,UAAA"}
|
package/dist/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e={jan:1,feb:2,mar:3,apr:4,may:5,jun:6,jul:7,aug:8,sep:9,oct:10,nov:11,dec:12},t={sun:0,mon:1,tue:2,wed:3,thu:4,fri:5,sat:6};function n(n){let a=n.trim();if(!a)throw Error(`Cron expression cannot be empty`);let o=a.split(/\s+/);if(o.length!==5)throw Error(`Invalid cron expression: expected 5 fields, got ${o.length}`);let[s,c,l,u,d]=o,f=i(d,0,7,t).map(e=>e===7?0:e),p={minute:i(s,0,59),hour:i(c,0,23),day:i(l,1,31),month:i(u,1,12,e).map(e=>e-1),weekday:Array.from(new Set(f)).sort((e,t)=>e-t)};return r(p),p}function r(e){let t=e.
|
|
1
|
+
const e={jan:1,feb:2,mar:3,apr:4,may:5,jun:6,jul:7,aug:8,sep:9,oct:10,nov:11,dec:12},t={sun:0,mon:1,tue:2,wed:3,thu:4,fri:5,sat:6};function n(n){let a=n.trim();if(!a)throw Error(`Cron expression cannot be empty`);let o=a.split(/\s+/);if(o.length!==5)throw Error(`Invalid cron expression: expected 5 fields, got ${o.length}`);let[s,c,l,u,d]=o,f=i(d,0,7,t).map(e=>e===7?0:e),p={minute:i(s,0,59),hour:i(c,0,23),day:i(l,1,31),month:i(u,1,12,e).map(e=>e-1),weekday:Array.from(new Set(f)).sort((e,t)=>e-t),dayIsWildcard:l.trim()===`*`,weekdayIsWildcard:d.trim()===`*`};return r(p),p}function r(e){let t=e.dayIsWildcard,n=e.month.length===12;if(t||n)return;let r=[31,29,31,30,31,30,31,31,30,31,30,31],i=!1;for(let t of e.month){let n=r[t];for(let t of e.day)if(t<=n){i=!0;break}if(i)break}if(!i)throw Error(`Invalid cron expression: no valid day/month combination exists`)}function i(e,t,n,r){let i=new Set;if(e===`*`){for(let e=t;e<=n;e++)i.add(e);return Array.from(i).sort((e,t)=>e-t)}let o=e.split(`,`);for(let e of o)if(e.includes(`/`)){let[o,s]=e.split(`/`),c=parseInt(s,10);if(isNaN(c)||c<=0)throw Error(`Invalid step value: ${s}`);let l=t,u=n;if(o!==`*`)if(o.includes(`-`)){let[e,t]=o.split(`-`);l=a(e,r),u=a(t,r)}else l=a(o,r);for(let e=l;e<=u;e+=c)e>=t&&e<=n&&i.add(e)}else if(e.includes(`-`)){let[o,s]=e.split(`-`),c=a(o,r),l=a(s,r);if(c>l)throw Error(`Invalid range: ${e}`);for(let e=c;e<=l;e++)e>=t&&e<=n&&i.add(e)}else{let o=a(e,r);if(o>=t&&o<=n)i.add(o);else throw Error(`Value ${o} out of range [${t}-${n}]`)}if(i.size===0)throw Error(`No valid values in field: ${e}`);return Array.from(i).sort((e,t)=>e-t)}function a(e,t){let n=e.toLowerCase();if(t&&n in t)return t[n];let r=parseInt(e,10);if(isNaN(r))throw Error(`Invalid value: ${e}`);return r}function o(e){try{return n(e),!0}catch{return!1}}function s(e,t){let n=t.getUTCMinutes(),r=t.getUTCHours(),i=t.getUTCDate(),a=t.getUTCMonth(),o=t.getUTCDay();return e.minute.includes(n)&&e.hour.includes(r)&&e.month.includes(a)&&l(e,i,o)}function c(e){return!e.dayIsWildcard&&!e.weekdayIsWildcard}function l(e,t,n,r){let i=r===void 0?e.day.includes(t):e.day.includes(t)&&t<=r,a=e.weekday.includes(n);return c(e)?i||a:e.dayIsWildcard?e.weekdayIsWildcard?!0:a:i}function u(e,t){for(let n of e)if(n>=t)return n;return null}function d(e,t){for(let n=e.length-1;n>=0;n--)if(e[n]<=t)return e[n];return null}function f(e,t){return new Date(e,t+1,0).getDate()}function p(e,t){let[n,r]=e.toLocaleString(`en-US`,{timeZone:t,year:`numeric`,month:`2-digit`,day:`2-digit`,hour:`2-digit`,minute:`2-digit`,second:`2-digit`,hour12:!1}).split(`, `),[i,a,o]=n.split(`/`).map(Number),[s,c,l]=r.split(`:`).map(Number);return s===24&&(s=0),new Date(Date.UTC(o,i-1,a,s,c,l))}function m(e,t){let n=e.getUTCFullYear(),r=e.getUTCMonth(),i=e.getUTCDate(),a=e.getUTCHours(),o=e.getUTCMinutes(),s=e.getUTCSeconds(),c=Date.UTC(n,r,i,a,o,s),l=c,u=l,d=1/0;for(let e=0;e<3;e++){let[e,n]=new Date(l).toLocaleString(`en-US`,{timeZone:t,year:`numeric`,month:`2-digit`,day:`2-digit`,hour:`2-digit`,minute:`2-digit`,second:`2-digit`,hour12:!1}).split(`, `),[r,i,a]=e.split(`/`).map(Number),[o,s,f]=n.split(`:`).map(Number);o===24&&(o=0);let p=Date.UTC(a,r-1,i,o,s,f),m=Math.abs(c-p);if((m<d||m===d&&l>u)&&(d=m,u=l),p===c)return new Date(l);let h=c-p;l+=h}let f=c+3600*1e3,p=f;for(let e=0;e<2;e++){let[e,n]=new Date(p).toLocaleString(`en-US`,{timeZone:t,year:`numeric`,month:`2-digit`,day:`2-digit`,hour:`2-digit`,minute:`2-digit`,second:`2-digit`,hour12:!1}).split(`, `),[r,i,a]=e.split(`/`).map(Number),[o,s,c]=n.split(`:`).map(Number);o===24&&(o=0);let l=Date.UTC(a,r-1,i,o,s,c);if(l===f)return new Date(p);let u=f-l;p+=u}return new Date(u)}const h={next:{find:u,minute:e=>e.minute[0],hour:e=>e.hour[0],offset:1},prev:{find:d,minute:e=>e.minute.at(-1),hour:e=>e.hour.at(-1),offset:-1}};function g(e,t){let r=n(e),i=t?.from||new Date,a=t?.timezone,o=a?p(i,a):new Date(i);o.setUTCSeconds(0,0),o.setUTCMinutes(o.getUTCMinutes()+1);let s=b(r,o,`next`,a);if(!s)throw Error(`No matching time found within reasonable search window`);return s}function _(e,t){let r=n(e),i=t?.from||new Date,a=t?.timezone,o=a?p(i,a):new Date(i);o.setUTCSeconds(0,0),o.setUTCMinutes(o.getUTCMinutes()-1);let s=b(r,o,`prev`,a);if(!s)throw Error(`No matching time found within reasonable search window`);return s}function v(e,t,n){if(t<=0)return[];let r=[],i=n?.from||new Date;for(let a=0;a<t;a++){let t=g(e,{...n,from:i});r.push(t),i=new Date(t.getTime()+6e4)}return r}function y(e,t,r){return s(n(e),r?.timezone?p(t,r.timezone):new Date(t))}function b(e,t,n,r){let i=new Date(t);for(let t=0;t<1e3;t++){if(s(e,i))return r?m(i,r):i;x(e,i,n)}return null}function x(e,t,n){let r=h[n],i=t.getUTCMinutes(),a=t.getUTCHours(),o=t.getUTCDate(),s=t.getUTCMonth(),c=t.getUTCFullYear(),u=t.getUTCDay(),d=f(c,s);if(!e.month.includes(s)){S(e,t,n,s,c);return}if(!l(e,o,u,d)){w(e,t,n,o,s,c,d);return}if(!e.hour.includes(a)){let i=r.find(e.hour,a+r.offset);i===null?w(e,t,n,o,s,c,d):(t.setUTCHours(i),t.setUTCMinutes(r.minute(e)));return}if(!e.minute.includes(i)){let l=r.find(e.minute,i+r.offset);if(l!==null)t.setUTCMinutes(l);else{let i=r.find(e.hour,a+r.offset);i===null?w(e,t,n,o,s,c,d):(t.setUTCHours(i),t.setUTCMinutes(r.minute(e)))}return}w(e,t,n,o,s,c,d)}function S(e,t,n,r,i){let a=h[n],o=a.find(e.month,r+a.offset);if(o!==null)T(e,t,i,o,n);else{let r=n===`next`?e.month[0]:e.month.at(-1);T(e,t,i+a.offset,r,n)}}function C(e,t,n,r){let i=h[n];if(c(e)){let e=t+i.offset;return n===`next`&&e>r||n===`prev`&&e<1?null:e}return i.find(e.day,t+i.offset)}function w(e,t,n,r,i,a,o){let s=h[n],c=C(e,r,n,o);(n===`next`?c!==null&&c<=o:c!==null)?(t.setUTCDate(c),t.setUTCHours(s.hour(e)),t.setUTCMinutes(s.minute(e))):S(e,t,n,i,a)}function T(e,t,n,r,i){let a=h[i];t.setUTCFullYear(n),t.setUTCDate(1),t.setUTCMonth(r);let o=f(n,r),s=c(e);if(i===`next`){let n=s?1:u(e.day,1)??e.day[0];t.setUTCDate(Math.min(n,o))}else{let a=s?o:d(e.day,o);if(a===null){S(e,t,i,r,n);return}t.setUTCDate(a)}t.setUTCHours(a.hour(e)),t.setUTCMinutes(a.minute(e))}export{y as isMatch,o as isValid,g as nextRun,v as nextRuns,n as parse,_ as previousRun};
|
|
2
2
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/parser.ts","../src/matcher.ts","../src/timezone.ts","../src/scheduler.ts"],"sourcesContent":["import type { ParsedCron } from \"./types.js\";\n\nconst MONTH_NAMES: Record<string, number> = {\n jan: 1,\n feb: 2,\n mar: 3,\n apr: 4,\n may: 5,\n jun: 6,\n jul: 7,\n aug: 8,\n sep: 9,\n oct: 10,\n nov: 11,\n dec: 12,\n};\n\nconst WEEKDAY_NAMES: Record<string, number> = {\n sun: 0,\n mon: 1,\n tue: 2,\n wed: 3,\n thu: 4,\n fri: 5,\n sat: 6,\n};\n\n/**\n * Parse a cron expression into structured format\n *\n * Cron format: minute hour day month weekday\n * - minute: 0-59\n * - hour: 0-23\n * - day: 1-31\n * - month: 1-12 (or JAN-DEC)\n * - weekday: 0-7 (or SUN-SAT, where 0 and 7 are Sunday)\n *\n * Note: Months are converted from cron's 1-indexed format (1-12) to\n * JavaScript's 0-indexed format (0-11) for internal consistency.\n */\nexport function parse(expression: string): ParsedCron {\n const trimmed = expression.trim();\n\n if (!trimmed) {\n throw new Error(\"Cron expression cannot be empty\");\n }\n\n const parts = trimmed.split(/\\s+/);\n\n if (parts.length !== 5) {\n throw new Error(`Invalid cron expression: expected 5 fields, got ${parts.length}`);\n }\n\n const [minuteStr, hourStr, dayStr, monthStr, weekdayStr] = parts;\n\n const weekdays = parseField(weekdayStr, 0, 7, WEEKDAY_NAMES).map((d) => (d === 7 ? 0 : d));\n\n const parsed: ParsedCron = {\n minute: parseField(minuteStr, 0, 59),\n hour: parseField(hourStr, 0, 23),\n day: parseField(dayStr, 1, 31),\n month: parseField(monthStr, 1, 12, MONTH_NAMES).map((m) => m - 1), // Convert to 0-indexed (0 = Jan, 11 = Dec)\n weekday: Array.from(new Set(weekdays)).sort((a, b) => a - b), // Dedupe and sort\n };\n\n // Validate day/month combinations\n validateDayMonthCombinations(parsed);\n\n return parsed;\n}\n\n/**\n * Validate that day/month combinations are possible\n * Rejects expressions like \"0 0 31 2 *\" (Feb 31) or \"0 0 30 2 *\" (Feb 30)\n */\nfunction validateDayMonthCombinations(parsed: ParsedCron): void {\n // If day or month is wildcard, no validation needed\n const dayIsWildcard = parsed.day.length === 31;\n const monthIsWildcard = parsed.month.length === 12;\n\n if (dayIsWildcard || monthIsWildcard) {\n return;\n }\n\n // Days in each month (0-indexed: 0=Jan, 11=Dec)\n // February can have 29 days in leap years\n const daysInMonth = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];\n\n // Check if any specified month can accommodate any specified day\n let hasValidCombination = false;\n\n for (const month of parsed.month) {\n const maxDaysInMonth = daysInMonth[month];\n\n for (const day of parsed.day) {\n if (day <= maxDaysInMonth) {\n hasValidCombination = true;\n break;\n }\n }\n\n if (hasValidCombination) {\n break;\n }\n }\n\n if (!hasValidCombination) {\n throw new Error(`Invalid cron expression: no valid day/month combination exists`);\n }\n}\n\n/**\n * Parse a single cron field (e.g., star-slash-5, 1-10, 1,3,5)\n */\nfunction parseField(\n field: string,\n min: number,\n max: number,\n names?: Record<string, number>,\n): number[] {\n const values = new Set<number>();\n\n // Handle wildcard\n if (field === \"*\") {\n for (let i = min; i <= max; i++) {\n values.add(i);\n }\n return Array.from(values).sort((a, b) => a - b);\n }\n\n // Split by comma for multiple values\n const parts = field.split(\",\");\n\n for (const part of parts) {\n // Handle step values (e.g., star-slash-5 or 10-20/2)\n if (part.includes(\"/\")) {\n const [range, stepStr] = part.split(\"/\");\n const step = parseInt(stepStr, 10);\n\n if (isNaN(step) || step <= 0) {\n throw new Error(`Invalid step value: ${stepStr}`);\n }\n\n let start = min;\n let end = max;\n\n if (range !== \"*\") {\n if (range.includes(\"-\")) {\n const [startStr, endStr] = range.split(\"-\");\n start = parseValue(startStr, names);\n end = parseValue(endStr, names);\n } else {\n start = parseValue(range, names);\n }\n }\n\n for (let i = start; i <= end; i += step) {\n if (i >= min && i <= max) {\n values.add(i);\n }\n }\n }\n // Handle ranges (e.g., 1-5)\n else if (part.includes(\"-\")) {\n const [startStr, endStr] = part.split(\"-\");\n const start = parseValue(startStr, names);\n const end = parseValue(endStr, names);\n\n if (start > end) {\n throw new Error(`Invalid range: ${part}`);\n }\n\n for (let i = start; i <= end; i++) {\n if (i >= min && i <= max) {\n values.add(i);\n }\n }\n }\n // Handle single values\n else {\n const value = parseValue(part, names);\n if (value >= min && value <= max) {\n values.add(value);\n } else {\n throw new Error(`Value ${value} out of range [${min}-${max}]`);\n }\n }\n }\n\n if (values.size === 0) {\n throw new Error(`No valid values in field: ${field}`);\n }\n\n return Array.from(values).sort((a, b) => a - b);\n}\n\n/**\n * Parse a single value (number or name)\n */\nfunction parseValue(value: string, names?: Record<string, number>): number {\n const lower = value.toLowerCase();\n\n if (names && lower in names) {\n return names[lower];\n }\n\n const num = parseInt(value, 10);\n if (isNaN(num)) {\n throw new Error(`Invalid value: ${value}`);\n }\n\n return num;\n}\n\n/**\n * Validate a cron expression\n */\nexport function isValid(expression: string): boolean {\n try {\n parse(expression);\n return true;\n } catch {\n return false;\n }\n}\n","import type { ParsedCron } from \"./types.js\";\n\n/**\n * Check if a date matches the cron expression\n */\nexport function matches(parsed: ParsedCron, date: Date): boolean {\n const minute = date.getUTCMinutes();\n const hour = date.getUTCHours();\n const day = date.getUTCDate();\n const month = date.getUTCMonth(); // 0-indexed (0 = Jan, 11 = Dec)\n const weekday = date.getUTCDay();\n\n // Check if all fields match\n return (\n parsed.minute.includes(minute) &&\n parsed.hour.includes(hour) &&\n parsed.month.includes(month) &&\n matchesDayOrWeekday(parsed, day, weekday)\n );\n}\n\n/**\n * Day-of-month and day-of-week use OR logic by default\n * If both are restricted (not *), match either one\n */\nfunction matchesDayOrWeekday(parsed: ParsedCron, day: number, weekday: number): boolean {\n const dayMatches = parsed.day.includes(day);\n const weekdayMatches = parsed.weekday.includes(weekday);\n\n // If both are wildcards (all values), both match\n const dayIsWildcard = parsed.day.length === 31;\n const weekdayIsWildcard = parsed.weekday.length === 7;\n\n // If both are restricted, use OR logic (standard cron behavior)\n if (!dayIsWildcard && !weekdayIsWildcard) {\n return dayMatches || weekdayMatches;\n }\n\n // If only one is restricted, it must match\n if (!dayIsWildcard) {\n return dayMatches;\n }\n if (!weekdayIsWildcard) {\n return weekdayMatches;\n }\n\n // Both wildcards, always matches\n return true;\n}\n\n/**\n * Find the next value in a sorted array that is >= target\n * Returns null if no such value exists\n *\n * @param values - MUST be sorted in ascending order\n * @param target - The minimum value to find\n */\nexport function findNext(values: number[], target: number): number | null {\n for (const value of values) {\n if (value >= target) {\n return value;\n }\n }\n return null;\n}\n\n/**\n * Find the previous value in a sorted array that is <= target\n * Returns null if no such value exists\n *\n * @param values - MUST be sorted in ascending order\n * @param target - The maximum value to find\n */\nexport function findPrevious(values: number[], target: number): number | null {\n for (let i = values.length - 1; i >= 0; i--) {\n if (values[i] <= target) {\n return values[i];\n }\n }\n return null;\n}\n\n/**\n * Get the number of days in a month\n *\n * @param year - The year\n * @param month - The month (0-indexed: 0 = January, 11 = December)\n * @returns The number of days in the month\n */\nexport function getDaysInMonth(year: number, month: number): number {\n // Create date for first day of next month, then go back one day\n return new Date(year, month + 1, 0).getDate();\n}\n","/** Convert a UTC date to wall-clock time in the target timezone */\nexport function convertToTimezone(date: Date, timezone: string): Date {\n // Format the date in the target timezone\n const str = date.toLocaleString(\"en-US\", {\n timeZone: timezone,\n year: \"numeric\",\n month: \"2-digit\",\n day: \"2-digit\",\n hour: \"2-digit\",\n minute: \"2-digit\",\n second: \"2-digit\",\n hour12: false,\n });\n\n // Parse formatted string: \"MM/DD/YYYY, HH:mm:ss\"\n const [datePart, timePart] = str.split(\", \");\n const [month, day, year] = datePart.split(\"/\").map(Number);\n let [hour, minute, second] = timePart.split(\":\").map(Number);\n\n if (hour === 24) hour = 0; // Normalize \"24:00:00\" to \"00:00:00\"\n\n return new Date(Date.UTC(year, month - 1, day, hour, minute, second));\n}\n\n/**\n * Convert a timezone-local date back to UTC (inverse of convertToTimezone).\n *\n * Note: During DST fall-back, multiple UTC times map to the same wall-clock time.\n * The result is implementation-defined. Avoid scheduling during DST transition hours\n * for predictable behavior.\n */\nexport function convertFromTimezone(date: Date, timezone: string): Date {\n const targetYear = date.getUTCFullYear();\n const targetMonth = date.getUTCMonth();\n const targetDay = date.getUTCDate();\n const targetHour = date.getUTCHours();\n const targetMinute = date.getUTCMinutes();\n const targetSecond = date.getUTCSeconds();\n\n // Target time as a comparable number (for checking if we found it)\n const targetTime = Date.UTC(\n targetYear,\n targetMonth,\n targetDay,\n targetHour,\n targetMinute,\n targetSecond,\n );\n\n // Start with a guess: interpret the wall-clock time as UTC\n let guess = targetTime;\n let bestGuess = guess;\n let bestDiff = Infinity;\n\n // Iteratively refine the guess (usually converges in 1-2 iterations)\n for (let i = 0; i < 3; i++) {\n const testDate = new Date(guess);\n const testStr = testDate.toLocaleString(\"en-US\", {\n timeZone: timezone,\n year: \"numeric\",\n month: \"2-digit\",\n day: \"2-digit\",\n hour: \"2-digit\",\n minute: \"2-digit\",\n second: \"2-digit\",\n hour12: false,\n });\n\n // Parse what wall-clock time this guess produces\n const [testDatePart, testTimePart] = testStr.split(\", \");\n const [testMonth, testDay, testYear] = testDatePart.split(\"/\").map(Number);\n let [testHour, testMinute, testSecond] = testTimePart.split(\":\").map(Number);\n\n if (testHour === 24) testHour = 0; // Normalize \"24:00:00\" to \"00:00:00\"\n\n const gotTime = Date.UTC(testYear, testMonth - 1, testDay, testHour, testMinute, testSecond);\n\n // Track the best guess (closest to target, but prefer later times if equal distance)\n const diff = Math.abs(targetTime - gotTime);\n if (diff < bestDiff || (diff === bestDiff && guess > bestGuess)) {\n bestDiff = diff;\n bestGuess = guess;\n }\n\n // If we got what we wanted, we're done!\n // Note: During DST fall-back, two UTC times map to the same wall-clock time.\n // This returns whichever solution the iteration converges to first (implementation-defined).\n if (gotTime === targetTime) {\n return new Date(guess);\n }\n\n // Otherwise, adjust the guess by the difference\n const adjustment = targetTime - gotTime;\n guess += adjustment;\n }\n\n // If we didn't find an exact match after 3 iterations, we're likely in a DST gap\n // (e.g., 2:30 AM during spring forward doesn't exist)\n // Try one more time: check if adding 1 hour to the target gets us closer\n const oneHourLater = targetTime + 60 * 60 * 1000;\n let guessLater = oneHourLater;\n\n for (let i = 0; i < 2; i++) {\n const testDate = new Date(guessLater);\n const testStr = testDate.toLocaleString(\"en-US\", {\n timeZone: timezone,\n year: \"numeric\",\n month: \"2-digit\",\n day: \"2-digit\",\n hour: \"2-digit\",\n minute: \"2-digit\",\n second: \"2-digit\",\n hour12: false,\n });\n\n const [testDatePart, testTimePart] = testStr.split(\", \");\n const [testMonth, testDay, testYear] = testDatePart.split(\"/\").map(Number);\n let [testHour, testMinute, testSecond] = testTimePart.split(\":\").map(Number);\n\n if (testHour === 24) testHour = 0; // Normalize \"24:00:00\" to \"00:00:00\"\n\n const gotTime = Date.UTC(testYear, testMonth - 1, testDay, testHour, testMinute, testSecond);\n\n if (gotTime === oneHourLater) {\n // Target time was in a DST gap, return the time after the gap\n return new Date(guessLater);\n }\n\n const adjustment = oneHourLater - gotTime;\n guessLater += adjustment;\n }\n\n // Return the best guess we found\n return new Date(bestGuess);\n}\n","import type { ParsedCron, CronOptions } from \"./types.js\";\nimport { parse } from \"./parser.js\";\nimport { matches, findNext, findPrevious, getDaysInMonth } from \"./matcher.js\";\nimport { convertToTimezone, convertFromTimezone } from \"./timezone.js\";\n\nconst MAX_ITERATIONS = 1000;\nconst ONE_MINUTE_MS = 60_000;\n\ntype Direction = \"next\" | \"prev\";\n\n/** Direction-specific operations for unified forward/backward traversal */\nconst DIR = {\n next: {\n find: findNext,\n minute: (p: ParsedCron) => p.minute[0],\n hour: (p: ParsedCron) => p.hour[0],\n offset: 1,\n },\n prev: {\n find: findPrevious,\n minute: (p: ParsedCron) => p.minute.at(-1)!,\n hour: (p: ParsedCron) => p.hour.at(-1)!,\n offset: -1,\n },\n} as const;\n\n/** Get the next execution time for a cron expression */\nexport function nextRun(expression: string, options?: CronOptions): Date {\n const parsed = parse(expression);\n const from = options?.from || new Date();\n const tz = options?.timezone;\n\n const start = tz ? convertToTimezone(from, tz) : new Date(from);\n start.setUTCSeconds(0, 0);\n start.setUTCMinutes(start.getUTCMinutes() + 1);\n\n const result = findMatch(parsed, start, \"next\", tz);\n if (!result) throw new Error(\"No matching time found within reasonable search window\");\n return result;\n}\n\n/** Get the previous execution time for a cron expression */\nexport function previousRun(expression: string, options?: CronOptions): Date {\n const parsed = parse(expression);\n const from = options?.from || new Date();\n const tz = options?.timezone;\n\n const start = tz ? convertToTimezone(from, tz) : new Date(from);\n start.setUTCSeconds(0, 0);\n start.setUTCMinutes(start.getUTCMinutes() - 1);\n\n const result = findMatch(parsed, start, \"prev\", tz);\n if (!result) throw new Error(\"No matching time found within reasonable search window\");\n return result;\n}\n\n/** Get next N execution times */\nexport function nextRuns(expression: string, count: number, options?: CronOptions): Date[] {\n if (count <= 0) return [];\n\n const results: Date[] = [];\n let current = options?.from || new Date();\n\n for (let i = 0; i < count; i++) {\n const next = nextRun(expression, { ...options, from: current });\n results.push(next);\n current = new Date(next.getTime() + ONE_MINUTE_MS);\n }\n return results;\n}\n\n/** Check if a date matches the cron expression */\nexport function isMatch(\n expression: string,\n date: Date,\n options?: Pick<CronOptions, \"timezone\">,\n): boolean {\n const parsed = parse(expression);\n const checkDate = options?.timezone ? convertToTimezone(date, options.timezone) : new Date(date);\n return matches(parsed, checkDate);\n}\n\n/** Find matching time using smart field-increment algorithm */\nfunction findMatch(parsed: ParsedCron, start: Date, dir: Direction, tz?: string): Date | null {\n const current = new Date(start);\n\n for (let i = 0; i < MAX_ITERATIONS; i++) {\n if (matches(parsed, current)) {\n return tz ? convertFromTimezone(current, tz) : current;\n }\n advanceDate(parsed, current, dir);\n }\n return null;\n}\n\n/**\n * Advance date to next/prev candidate time by mutating the date in place.\n *\n * Algorithm:\n * 1. Check fields from LARGEST (month) to SMALLEST (minute)\n * 2. When a field doesn't match, jump to the next valid value for that field\n * 3. Reset all smaller fields to their boundary (first value for 'next', last for 'prev')\n *\n * Example (direction='next', cron='0 9 * * *' meaning 9:00 AM daily):\n * Current: March 15, 10:30 AM\n * - Month (March)? ✓ matches\n * - Day (15)? ✓ matches\n * - Hour (10)? ✗ not in [9] → no next hour today → cascade to next day\n * - Result: March 16, 9:00 AM\n *\n * @param parsed - The parsed cron expression\n * @param date - The date to mutate (modified in place)\n * @param dir - Direction to advance ('next' or 'prev')\n */\nfunction advanceDate(parsed: ParsedCron, date: Date, dir: Direction): void {\n const d = DIR[dir];\n const minute = date.getUTCMinutes();\n const hour = date.getUTCHours();\n const day = date.getUTCDate();\n const month = date.getUTCMonth();\n const year = date.getUTCFullYear();\n const daysInMonth = getDaysInMonth(year, month);\n\n // Month mismatch\n if (!parsed.month.includes(month)) {\n moveToMonth(parsed, date, dir, month, year);\n return;\n }\n\n // Day mismatch\n if (!parsed.day.includes(day) || day > daysInMonth) {\n moveToDay(parsed, date, dir, day, month, year, daysInMonth);\n return;\n }\n\n // Hour mismatch\n if (!parsed.hour.includes(hour)) {\n const targetHour = d.find(parsed.hour, hour + d.offset);\n if (targetHour !== null) {\n // Found valid hour in same day → reset minute to boundary\n date.setUTCHours(targetHour);\n date.setUTCMinutes(d.minute(parsed));\n } else {\n // No valid hour left today → move to next/prev day\n moveToDay(parsed, date, dir, day, month, year, daysInMonth);\n }\n return;\n }\n\n // Minute mismatch\n if (!parsed.minute.includes(minute)) {\n const targetMinute = d.find(parsed.minute, minute + d.offset);\n if (targetMinute !== null) {\n // Found valid minute in same hour\n date.setUTCMinutes(targetMinute);\n } else {\n // No valid minute left → try next hour\n const targetHour = d.find(parsed.hour, hour + d.offset);\n if (targetHour !== null) {\n date.setUTCHours(targetHour);\n date.setUTCMinutes(d.minute(parsed));\n } else {\n // No valid hour left → move to next/prev day\n moveToDay(parsed, date, dir, day, month, year, daysInMonth);\n }\n }\n return;\n }\n\n // Weekday mismatch: all fields match but wrong day-of-week.\n // Skip directly to next/prev day since no hour/minute on this day can match.\n moveToDay(parsed, date, dir, day, month, year, daysInMonth);\n}\n\nfunction moveToMonth(\n parsed: ParsedCron,\n date: Date,\n dir: Direction,\n currentMonth: number,\n currentYear: number,\n): void {\n const d = DIR[dir];\n const targetMonth = d.find(parsed.month, currentMonth + d.offset);\n\n if (targetMonth !== null) {\n resetToMonthBoundary(parsed, date, currentYear, targetMonth, dir);\n } else {\n const boundaryMonth = dir === \"next\" ? parsed.month[0] : parsed.month.at(-1)!;\n resetToMonthBoundary(parsed, date, currentYear + d.offset, boundaryMonth, dir);\n }\n}\n\nfunction moveToDay(\n parsed: ParsedCron,\n date: Date,\n dir: Direction,\n currentDay: number,\n currentMonth: number,\n currentYear: number,\n daysInMonth: number,\n): void {\n const d = DIR[dir];\n const targetDay = d.find(parsed.day, currentDay + d.offset);\n const dayIsValid =\n dir === \"next\" ? targetDay !== null && targetDay <= daysInMonth : targetDay !== null;\n\n if (dayIsValid) {\n date.setUTCDate(targetDay!);\n date.setUTCHours(d.hour(parsed));\n date.setUTCMinutes(d.minute(parsed));\n } else {\n moveToMonth(parsed, date, dir, currentMonth, currentYear);\n }\n}\n\nfunction resetToMonthBoundary(\n parsed: ParsedCron,\n date: Date,\n year: number,\n month: number,\n dir: Direction,\n): void {\n const d = DIR[dir];\n date.setUTCFullYear(year);\n date.setUTCDate(1);\n date.setUTCMonth(month);\n\n const daysInMonth = getDaysInMonth(year, month);\n\n if (dir === \"next\") {\n const validDay = findNext(parsed.day, 1);\n date.setUTCDate(validDay !== null && validDay <= daysInMonth ? validDay : parsed.day[0]);\n } else {\n const prevDay = findPrevious(parsed.day, daysInMonth);\n if (prevDay !== null) {\n date.setUTCDate(prevDay);\n } else {\n // No valid day in this month, move to previous month\n moveToMonth(parsed, date, dir, month, year);\n return;\n }\n }\n\n date.setUTCHours(d.hour(parsed));\n date.setUTCMinutes(d.minute(parsed));\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/parser.ts","../src/matcher.ts","../src/timezone.ts","../src/scheduler.ts"],"sourcesContent":["import type { ParsedCron } from \"./types.js\";\n\nconst MONTH_NAMES: Record<string, number> = {\n jan: 1,\n feb: 2,\n mar: 3,\n apr: 4,\n may: 5,\n jun: 6,\n jul: 7,\n aug: 8,\n sep: 9,\n oct: 10,\n nov: 11,\n dec: 12,\n};\n\nconst WEEKDAY_NAMES: Record<string, number> = {\n sun: 0,\n mon: 1,\n tue: 2,\n wed: 3,\n thu: 4,\n fri: 5,\n sat: 6,\n};\n\n/**\n * Parse a cron expression into structured format\n *\n * Cron format: minute hour day month weekday\n * - minute: 0-59\n * - hour: 0-23\n * - day: 1-31\n * - month: 1-12 (or JAN-DEC)\n * - weekday: 0-7 (or SUN-SAT, where 0 and 7 are Sunday)\n *\n * Note: Months are converted from cron's 1-indexed format (1-12) to\n * JavaScript's 0-indexed format (0-11) for internal consistency.\n */\nexport function parse(expression: string): ParsedCron {\n const trimmed = expression.trim();\n\n if (!trimmed) {\n throw new Error(\"Cron expression cannot be empty\");\n }\n\n const parts = trimmed.split(/\\s+/);\n\n if (parts.length !== 5) {\n throw new Error(`Invalid cron expression: expected 5 fields, got ${parts.length}`);\n }\n\n const [minuteStr, hourStr, dayStr, monthStr, weekdayStr] = parts;\n\n const weekdays = parseField(weekdayStr, 0, 7, WEEKDAY_NAMES).map((d) => (d === 7 ? 0 : d));\n\n const parsed: ParsedCron = {\n minute: parseField(minuteStr, 0, 59),\n hour: parseField(hourStr, 0, 23),\n day: parseField(dayStr, 1, 31),\n month: parseField(monthStr, 1, 12, MONTH_NAMES).map((m) => m - 1), // Convert to 0-indexed (0 = Jan, 11 = Dec)\n weekday: Array.from(new Set(weekdays)).sort((a, b) => a - b), // Dedupe and sort\n dayIsWildcard: dayStr.trim() === \"*\",\n weekdayIsWildcard: weekdayStr.trim() === \"*\",\n };\n\n // Validate day/month combinations\n validateDayMonthCombinations(parsed);\n\n return parsed;\n}\n\n/**\n * Validate that day/month combinations are possible\n * Rejects expressions like \"0 0 31 2 *\" (Feb 31) or \"0 0 30 2 *\" (Feb 30)\n */\nfunction validateDayMonthCombinations(parsed: ParsedCron): void {\n // If day or month is wildcard, no validation needed\n const dayIsWildcard = parsed.dayIsWildcard;\n const monthIsWildcard = parsed.month.length === 12;\n\n if (dayIsWildcard || monthIsWildcard) {\n return;\n }\n\n // Days in each month (0-indexed: 0=Jan, 11=Dec)\n // February can have 29 days in leap years\n const daysInMonth = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];\n\n // Check if any specified month can accommodate any specified day\n let hasValidCombination = false;\n\n for (const month of parsed.month) {\n const maxDaysInMonth = daysInMonth[month];\n\n for (const day of parsed.day) {\n if (day <= maxDaysInMonth) {\n hasValidCombination = true;\n break;\n }\n }\n\n if (hasValidCombination) {\n break;\n }\n }\n\n if (!hasValidCombination) {\n throw new Error(`Invalid cron expression: no valid day/month combination exists`);\n }\n}\n\n/**\n * Parse a single cron field (e.g., star-slash-5, 1-10, 1,3,5)\n */\nfunction parseField(\n field: string,\n min: number,\n max: number,\n names?: Record<string, number>,\n): number[] {\n const values = new Set<number>();\n\n // Handle wildcard\n if (field === \"*\") {\n for (let i = min; i <= max; i++) {\n values.add(i);\n }\n return Array.from(values).sort((a, b) => a - b);\n }\n\n // Split by comma for multiple values\n const parts = field.split(\",\");\n\n for (const part of parts) {\n // Handle step values (e.g., star-slash-5 or 10-20/2)\n if (part.includes(\"/\")) {\n const [range, stepStr] = part.split(\"/\");\n const step = parseInt(stepStr, 10);\n\n if (isNaN(step) || step <= 0) {\n throw new Error(`Invalid step value: ${stepStr}`);\n }\n\n let start = min;\n let end = max;\n\n if (range !== \"*\") {\n if (range.includes(\"-\")) {\n const [startStr, endStr] = range.split(\"-\");\n start = parseValue(startStr, names);\n end = parseValue(endStr, names);\n } else {\n start = parseValue(range, names);\n }\n }\n\n for (let i = start; i <= end; i += step) {\n if (i >= min && i <= max) {\n values.add(i);\n }\n }\n }\n // Handle ranges (e.g., 1-5)\n else if (part.includes(\"-\")) {\n const [startStr, endStr] = part.split(\"-\");\n const start = parseValue(startStr, names);\n const end = parseValue(endStr, names);\n\n if (start > end) {\n throw new Error(`Invalid range: ${part}`);\n }\n\n for (let i = start; i <= end; i++) {\n if (i >= min && i <= max) {\n values.add(i);\n }\n }\n }\n // Handle single values\n else {\n const value = parseValue(part, names);\n if (value >= min && value <= max) {\n values.add(value);\n } else {\n throw new Error(`Value ${value} out of range [${min}-${max}]`);\n }\n }\n }\n\n if (values.size === 0) {\n throw new Error(`No valid values in field: ${field}`);\n }\n\n return Array.from(values).sort((a, b) => a - b);\n}\n\n/**\n * Parse a single value (number or name)\n */\nfunction parseValue(value: string, names?: Record<string, number>): number {\n const lower = value.toLowerCase();\n\n if (names && lower in names) {\n return names[lower];\n }\n\n const num = parseInt(value, 10);\n if (isNaN(num)) {\n throw new Error(`Invalid value: ${value}`);\n }\n\n return num;\n}\n\n/**\n * Validate a cron expression\n */\nexport function isValid(expression: string): boolean {\n try {\n parse(expression);\n return true;\n } catch {\n return false;\n }\n}\n","import type { ParsedCron } from \"./types.js\";\n\n/**\n * Check if a date matches the cron expression\n */\nexport function matches(parsed: ParsedCron, date: Date): boolean {\n const minute = date.getUTCMinutes();\n const hour = date.getUTCHours();\n const day = date.getUTCDate();\n const month = date.getUTCMonth(); // 0-indexed (0 = Jan, 11 = Dec)\n const weekday = date.getUTCDay();\n\n // Check if all fields match\n return (\n parsed.minute.includes(minute) &&\n parsed.hour.includes(hour) &&\n parsed.month.includes(month) &&\n matchesDayOrWeekday(parsed, day, weekday)\n );\n}\n\n/**\n * Check if we're in OR mode (both day and weekday are restricted, not wildcards)\n * In OR mode, we must check every day because any day might match via weekday\n */\nexport function isOrMode(parsed: ParsedCron): boolean {\n return !parsed.dayIsWildcard && !parsed.weekdayIsWildcard;\n}\n\n/**\n * Day-of-month and day-of-week use OR logic by default\n * If both are restricted (not *), match either one\n * \n * @param daysInMonth - Optional validation that day is valid for the month (used by scheduler)\n */\nexport function matchesDayOrWeekday(\n parsed: ParsedCron,\n day: number,\n weekday: number,\n daysInMonth?: number,\n): boolean {\n const dayMatches =\n daysInMonth !== undefined\n ? parsed.day.includes(day) && day <= daysInMonth\n : parsed.day.includes(day);\n const weekdayMatches = parsed.weekday.includes(weekday);\n\n // If both are restricted, use OR logic (standard cron behavior)\n if (isOrMode(parsed)) {\n return dayMatches || weekdayMatches;\n }\n\n // If only one is restricted, it must match\n if (!parsed.dayIsWildcard) {\n return dayMatches;\n }\n if (!parsed.weekdayIsWildcard) {\n return weekdayMatches;\n }\n\n // Both wildcards, always matches\n return true;\n}\n\n/**\n * Find the next value in a sorted array that is >= target\n * Returns null if no such value exists\n *\n * @param values - MUST be sorted in ascending order\n * @param target - The minimum value to find\n */\nexport function findNext(values: number[], target: number): number | null {\n for (const value of values) {\n if (value >= target) {\n return value;\n }\n }\n return null;\n}\n\n/**\n * Find the previous value in a sorted array that is <= target\n * Returns null if no such value exists\n *\n * @param values - MUST be sorted in ascending order\n * @param target - The maximum value to find\n */\nexport function findPrevious(values: number[], target: number): number | null {\n for (let i = values.length - 1; i >= 0; i--) {\n if (values[i] <= target) {\n return values[i];\n }\n }\n return null;\n}\n\n/**\n * Get the number of days in a month\n *\n * @param year - The year\n * @param month - The month (0-indexed: 0 = January, 11 = December)\n * @returns The number of days in the month\n */\nexport function getDaysInMonth(year: number, month: number): number {\n // Create date for first day of next month, then go back one day\n return new Date(year, month + 1, 0).getDate();\n}\n","/** Convert a UTC date to wall-clock time in the target timezone */\nexport function convertToTimezone(date: Date, timezone: string): Date {\n // Format the date in the target timezone\n const str = date.toLocaleString(\"en-US\", {\n timeZone: timezone,\n year: \"numeric\",\n month: \"2-digit\",\n day: \"2-digit\",\n hour: \"2-digit\",\n minute: \"2-digit\",\n second: \"2-digit\",\n hour12: false,\n });\n\n // Parse formatted string: \"MM/DD/YYYY, HH:mm:ss\"\n const [datePart, timePart] = str.split(\", \");\n const [month, day, year] = datePart.split(\"/\").map(Number);\n let [hour, minute, second] = timePart.split(\":\").map(Number);\n\n if (hour === 24) hour = 0; // Normalize \"24:00:00\" to \"00:00:00\"\n\n return new Date(Date.UTC(year, month - 1, day, hour, minute, second));\n}\n\n/**\n * Convert a timezone-local date back to UTC (inverse of convertToTimezone).\n *\n * Note: During DST fall-back, multiple UTC times map to the same wall-clock time.\n * The result is implementation-defined. Avoid scheduling during DST transition hours\n * for predictable behavior.\n */\nexport function convertFromTimezone(date: Date, timezone: string): Date {\n const targetYear = date.getUTCFullYear();\n const targetMonth = date.getUTCMonth();\n const targetDay = date.getUTCDate();\n const targetHour = date.getUTCHours();\n const targetMinute = date.getUTCMinutes();\n const targetSecond = date.getUTCSeconds();\n\n // Target time as a comparable number (for checking if we found it)\n const targetTime = Date.UTC(\n targetYear,\n targetMonth,\n targetDay,\n targetHour,\n targetMinute,\n targetSecond,\n );\n\n // Start with a guess: interpret the wall-clock time as UTC\n let guess = targetTime;\n let bestGuess = guess;\n let bestDiff = Infinity;\n\n // Iteratively refine the guess (usually converges in 1-2 iterations)\n for (let i = 0; i < 3; i++) {\n const testDate = new Date(guess);\n const testStr = testDate.toLocaleString(\"en-US\", {\n timeZone: timezone,\n year: \"numeric\",\n month: \"2-digit\",\n day: \"2-digit\",\n hour: \"2-digit\",\n minute: \"2-digit\",\n second: \"2-digit\",\n hour12: false,\n });\n\n // Parse what wall-clock time this guess produces\n const [testDatePart, testTimePart] = testStr.split(\", \");\n const [testMonth, testDay, testYear] = testDatePart.split(\"/\").map(Number);\n let [testHour, testMinute, testSecond] = testTimePart.split(\":\").map(Number);\n\n if (testHour === 24) testHour = 0; // Normalize \"24:00:00\" to \"00:00:00\"\n\n const gotTime = Date.UTC(testYear, testMonth - 1, testDay, testHour, testMinute, testSecond);\n\n // Track the best guess (closest to target, but prefer later times if equal distance)\n const diff = Math.abs(targetTime - gotTime);\n if (diff < bestDiff || (diff === bestDiff && guess > bestGuess)) {\n bestDiff = diff;\n bestGuess = guess;\n }\n\n // If we got what we wanted, we're done!\n // Note: During DST fall-back, two UTC times map to the same wall-clock time.\n // This returns whichever solution the iteration converges to first (implementation-defined).\n if (gotTime === targetTime) {\n return new Date(guess);\n }\n\n // Otherwise, adjust the guess by the difference\n const adjustment = targetTime - gotTime;\n guess += adjustment;\n }\n\n // If we didn't find an exact match after 3 iterations, we're likely in a DST gap\n // (e.g., 2:30 AM during spring forward doesn't exist)\n // Try one more time: check if adding 1 hour to the target gets us closer\n const oneHourLater = targetTime + 60 * 60 * 1000;\n let guessLater = oneHourLater;\n\n for (let i = 0; i < 2; i++) {\n const testDate = new Date(guessLater);\n const testStr = testDate.toLocaleString(\"en-US\", {\n timeZone: timezone,\n year: \"numeric\",\n month: \"2-digit\",\n day: \"2-digit\",\n hour: \"2-digit\",\n minute: \"2-digit\",\n second: \"2-digit\",\n hour12: false,\n });\n\n const [testDatePart, testTimePart] = testStr.split(\", \");\n const [testMonth, testDay, testYear] = testDatePart.split(\"/\").map(Number);\n let [testHour, testMinute, testSecond] = testTimePart.split(\":\").map(Number);\n\n if (testHour === 24) testHour = 0; // Normalize \"24:00:00\" to \"00:00:00\"\n\n const gotTime = Date.UTC(testYear, testMonth - 1, testDay, testHour, testMinute, testSecond);\n\n if (gotTime === oneHourLater) {\n // Target time was in a DST gap, return the time after the gap\n return new Date(guessLater);\n }\n\n const adjustment = oneHourLater - gotTime;\n guessLater += adjustment;\n }\n\n // Return the best guess we found\n return new Date(bestGuess);\n}\n","import type { ParsedCron, CronOptions } from \"./types.js\";\nimport { parse } from \"./parser.js\";\nimport {\n matches,\n findNext,\n findPrevious,\n getDaysInMonth,\n isOrMode,\n matchesDayOrWeekday,\n} from \"./matcher.js\";\nimport { convertToTimezone, convertFromTimezone } from \"./timezone.js\";\n\nconst MAX_ITERATIONS = 1000;\nconst ONE_MINUTE_MS = 60_000;\n\ntype Direction = \"next\" | \"prev\";\n\n/** Direction-specific operations for unified forward/backward traversal */\nconst DIR = {\n next: {\n find: findNext,\n minute: (p: ParsedCron) => p.minute[0],\n hour: (p: ParsedCron) => p.hour[0],\n offset: 1,\n },\n prev: {\n find: findPrevious,\n minute: (p: ParsedCron) => p.minute.at(-1)!,\n hour: (p: ParsedCron) => p.hour.at(-1)!,\n offset: -1,\n },\n} as const;\n\n/** Get the next execution time for a cron expression */\nexport function nextRun(expression: string, options?: CronOptions): Date {\n const parsed = parse(expression);\n const from = options?.from || new Date();\n const tz = options?.timezone;\n\n const start = tz ? convertToTimezone(from, tz) : new Date(from);\n start.setUTCSeconds(0, 0);\n start.setUTCMinutes(start.getUTCMinutes() + 1);\n\n const result = findMatch(parsed, start, \"next\", tz);\n if (!result) throw new Error(\"No matching time found within reasonable search window\");\n return result;\n}\n\n/** Get the previous execution time for a cron expression */\nexport function previousRun(expression: string, options?: CronOptions): Date {\n const parsed = parse(expression);\n const from = options?.from || new Date();\n const tz = options?.timezone;\n\n const start = tz ? convertToTimezone(from, tz) : new Date(from);\n start.setUTCSeconds(0, 0);\n start.setUTCMinutes(start.getUTCMinutes() - 1);\n\n const result = findMatch(parsed, start, \"prev\", tz);\n if (!result) throw new Error(\"No matching time found within reasonable search window\");\n return result;\n}\n\n/** Get next N execution times */\nexport function nextRuns(expression: string, count: number, options?: CronOptions): Date[] {\n if (count <= 0) return [];\n\n const results: Date[] = [];\n let current = options?.from || new Date();\n\n for (let i = 0; i < count; i++) {\n const next = nextRun(expression, { ...options, from: current });\n results.push(next);\n current = new Date(next.getTime() + ONE_MINUTE_MS);\n }\n return results;\n}\n\n/** Check if a date matches the cron expression */\nexport function isMatch(\n expression: string,\n date: Date,\n options?: Pick<CronOptions, \"timezone\">,\n): boolean {\n const parsed = parse(expression);\n const checkDate = options?.timezone ? convertToTimezone(date, options.timezone) : new Date(date);\n return matches(parsed, checkDate);\n}\n\n/** Find matching time using smart field-increment algorithm */\nfunction findMatch(parsed: ParsedCron, start: Date, dir: Direction, tz?: string): Date | null {\n const current = new Date(start);\n\n for (let i = 0; i < MAX_ITERATIONS; i++) {\n if (matches(parsed, current)) {\n return tz ? convertFromTimezone(current, tz) : current;\n }\n advanceDate(parsed, current, dir);\n }\n return null;\n}\n\n/**\n * Advance date to next/prev candidate time by mutating the date in place.\n *\n * Algorithm:\n * 1. Check fields from LARGEST (month) to SMALLEST (minute)\n * 2. When a field doesn't match, jump to the next valid value for that field\n * 3. Reset all smaller fields to their boundary (first value for 'next', last for 'prev')\n *\n * Example (direction='next', cron='0 9 * * *' meaning 9:00 AM daily):\n * Current: March 15, 10:30 AM\n * - Month (March)? ✓ matches\n * - Day (15)? ✓ matches\n * - Hour (10)? ✗ not in [9] → no next hour today → cascade to next day\n * - Result: March 16, 9:00 AM\n *\n * @param parsed - The parsed cron expression\n * @param date - The date to mutate (modified in place)\n * @param dir - Direction to advance ('next' or 'prev')\n */\nfunction advanceDate(parsed: ParsedCron, date: Date, dir: Direction): void {\n const d = DIR[dir];\n const minute = date.getUTCMinutes();\n const hour = date.getUTCHours();\n const day = date.getUTCDate();\n const month = date.getUTCMonth();\n const year = date.getUTCFullYear();\n const weekday = date.getUTCDay();\n const daysInMonth = getDaysInMonth(year, month);\n\n // Month mismatch\n if (!parsed.month.includes(month)) {\n moveToMonth(parsed, date, dir, month, year);\n return;\n }\n\n // Day/Weekday mismatch - use OR logic like matches()\n if (!matchesDayOrWeekday(parsed, day, weekday, daysInMonth)) {\n moveToDay(parsed, date, dir, day, month, year, daysInMonth);\n return;\n }\n\n // Hour mismatch\n if (!parsed.hour.includes(hour)) {\n const targetHour = d.find(parsed.hour, hour + d.offset);\n if (targetHour !== null) {\n // Found valid hour in same day → reset minute to boundary\n date.setUTCHours(targetHour);\n date.setUTCMinutes(d.minute(parsed));\n } else {\n // No valid hour left today → move to next/prev day\n moveToDay(parsed, date, dir, day, month, year, daysInMonth);\n }\n return;\n }\n\n // Minute mismatch\n if (!parsed.minute.includes(minute)) {\n const targetMinute = d.find(parsed.minute, minute + d.offset);\n if (targetMinute !== null) {\n // Found valid minute in same hour\n date.setUTCMinutes(targetMinute);\n } else {\n // No valid minute left → try next hour\n const targetHour = d.find(parsed.hour, hour + d.offset);\n if (targetHour !== null) {\n date.setUTCHours(targetHour);\n date.setUTCMinutes(d.minute(parsed));\n } else {\n // No valid hour left → move to next/prev day\n moveToDay(parsed, date, dir, day, month, year, daysInMonth);\n }\n }\n return;\n }\n\n // All fields match but we still need to advance (called from findMatch loop)\n // This happens when matches() returns false due to day/weekday mismatch\n // Move to next/prev day\n moveToDay(parsed, date, dir, day, month, year, daysInMonth);\n}\n\nfunction moveToMonth(\n parsed: ParsedCron,\n date: Date,\n dir: Direction,\n currentMonth: number,\n currentYear: number,\n): void {\n const d = DIR[dir];\n const targetMonth = d.find(parsed.month, currentMonth + d.offset);\n\n if (targetMonth !== null) {\n resetToMonthBoundary(parsed, date, currentYear, targetMonth, dir);\n } else {\n const boundaryMonth = dir === \"next\" ? parsed.month[0] : parsed.month.at(-1)!;\n resetToMonthBoundary(parsed, date, currentYear + d.offset, boundaryMonth, dir);\n }\n}\n\n/**\n * Find the next candidate day to check\n * In OR mode: advance by 1 day (must check every day)\n * Normal mode: jump to next valid day-of-month\n */\nfunction findCandidateDay(\n parsed: ParsedCron,\n currentDay: number,\n dir: Direction,\n daysInMonth: number,\n): number | null {\n const d = DIR[dir];\n const inOrMode = isOrMode(parsed);\n\n if (inOrMode) {\n // In OR mode, we must check every day (can't skip ahead)\n // because any day might match via weekday even if day-of-month doesn't match\n const targetDay = currentDay + d.offset;\n if (dir === \"next\" && targetDay > daysInMonth) {\n return null;\n }\n if (dir === \"prev\" && targetDay < 1) {\n return null;\n }\n return targetDay;\n }\n\n // Normal mode: jump to next valid day-of-month\n return d.find(parsed.day, currentDay + d.offset);\n}\n\nfunction moveToDay(\n parsed: ParsedCron,\n date: Date,\n dir: Direction,\n currentDay: number,\n currentMonth: number,\n currentYear: number,\n daysInMonth: number,\n): void {\n const d = DIR[dir];\n const targetDay = findCandidateDay(parsed, currentDay, dir, daysInMonth);\n const dayIsValid =\n dir === \"next\" ? targetDay !== null && targetDay <= daysInMonth : targetDay !== null;\n\n if (dayIsValid) {\n date.setUTCDate(targetDay!);\n date.setUTCHours(d.hour(parsed));\n date.setUTCMinutes(d.minute(parsed));\n } else {\n moveToMonth(parsed, date, dir, currentMonth, currentYear);\n }\n}\n\nfunction resetToMonthBoundary(\n parsed: ParsedCron,\n date: Date,\n year: number,\n month: number,\n dir: Direction,\n): void {\n const d = DIR[dir];\n date.setUTCFullYear(year);\n date.setUTCDate(1);\n date.setUTCMonth(month);\n\n const daysInMonth = getDaysInMonth(year, month);\n\n // Check if we're in OR mode (both day and weekday restricted)\n const inOrMode = isOrMode(parsed);\n\n if (dir === \"next\") {\n // In OR mode: start from day 1. Normal mode: jump to first valid day\n const startDay = inOrMode ? 1 : (findNext(parsed.day, 1) ?? parsed.day[0]);\n date.setUTCDate(Math.min(startDay, daysInMonth));\n } else {\n // In OR mode: start from last day. Normal mode: jump to last valid day\n const startDay = inOrMode ? daysInMonth : findPrevious(parsed.day, daysInMonth);\n if (startDay === null) {\n // No valid day in this month, move to previous month\n moveToMonth(parsed, date, dir, month, year);\n return;\n }\n date.setUTCDate(startDay);\n }\n\n date.setUTCHours(d.hour(parsed));\n date.setUTCMinutes(d.minute(parsed));\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cron-fast",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Fast JavaScript/TypeScript cron parser with timezone support - works in Node.js, Deno, Bun, Cloudflare Workers, and browsers. Zero dependencies.",
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"description": "Fast and tiny JavaScript/TypeScript cron parser with timezone support - works in Node.js, Deno, Bun, Cloudflare Workers, and browsers. Zero dependencies.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"browser",
|
|
7
7
|
"bun",
|
|
@@ -58,9 +58,11 @@
|
|
|
58
58
|
"@types/node": "^25.2.2",
|
|
59
59
|
"@vitest/coverage-v8": "^3.2.4",
|
|
60
60
|
"@vitest/ui": "^3.2.4",
|
|
61
|
+
"esbuild": "^0.27.3",
|
|
61
62
|
"oxfmt": "^0.28.0",
|
|
62
63
|
"oxlint": "^1.43.0",
|
|
63
64
|
"tsdown": "^0.20.3",
|
|
65
|
+
"tsx": "^4.21.0",
|
|
64
66
|
"typescript": "^5.9.3",
|
|
65
67
|
"vite": "^7.3.1",
|
|
66
68
|
"vitest": "3.2.4"
|
|
@@ -76,6 +78,7 @@
|
|
|
76
78
|
"lint:fix": "oxlint --fix",
|
|
77
79
|
"fmt": "oxfmt",
|
|
78
80
|
"fmt:check": "oxfmt --check",
|
|
79
|
-
"typecheck": "tsc --noEmit"
|
|
81
|
+
"typecheck": "tsc --noEmit",
|
|
82
|
+
"bundle-size": "tsx scripts/bundle-size.ts"
|
|
80
83
|
}
|
|
81
84
|
}
|