Fast and tiny JavaScript/TypeScript cron parser with timezone support
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

removed redundant timezone validation for perf

+193 -185
+47 -23
README.md
··· 55 55 ## Quick Start 56 56 57 57 ```typescript 58 - import { nextRun, previousRun, isValid } from "cron-fast"; 58 + import { nextRun, previousRun, isValid, describe } from "cron-fast"; 59 59 60 60 // Get next execution time (UTC) 61 61 const next = nextRun("0 9 * * *"); ··· 72 72 if (isValid("0 9 * * *")) { 73 73 console.log("Valid cron expression!"); 74 74 } 75 + 76 + // Get human-readable description 77 + console.log(describe("*/5 * * * *")); // "Every 5 minutes" 75 78 ``` 76 79 77 80 ## API 78 81 79 - ### `nextRun(expression, options?)` 82 + ### `nextRun(expression: string, options?: CronOptions): Date` 80 83 81 84 Get the next execution time for a cron expression. **Throws** if the expression or timezone is invalid. 82 85 ··· 86 89 nextRun("0 9 * * *", { from: new Date("2026-03-15") }); // Next after Mar 15, 2026 87 90 ``` 88 91 89 - ### `previousRun(expression, options?)` 92 + ### `previousRun(expression: string, options?: CronOptions): Date` 90 93 91 94 Get the previous execution time. **Throws** if the expression or timezone is invalid. 92 95 ··· 95 98 previousRun("0 9 * * *", { timezone: "Asia/Tokyo" }); 96 99 ``` 97 100 98 - ### `nextRuns(expression, count, options?)` 101 + ### `nextRuns(expression: string, count: number, options?: CronOptions): Date[]` 99 102 100 103 Get next N execution times. **Throws** if the expression or timezone is invalid. 101 104 ··· 103 106 nextRuns("0 9 * * *", 5); // Next 5 occurrences 104 107 ``` 105 108 106 - ### `isValid(expression)` 109 + ### `isValid(expression: string): boolean` 107 110 108 111 Validate a cron expression. 109 112 ··· 112 115 isValid("invalid"); // false 113 116 ``` 114 117 115 - ### `isMatch(expression, date, options?)` 118 + ### `isMatch(expression: string, date: Date, options?: CronOptions): boolean` 116 119 117 120 Check if a date matches the cron expression. **Throws** if the expression or timezone is invalid. 118 121 ··· 120 123 isMatch("0 9 * * *", new Date("2026-03-15T09:00:00Z")); // true 121 124 ``` 122 125 123 - ### `parse(expression)` 126 + ### `parse(expression: string): ParsedCron` 124 127 125 128 Parse a cron expression into its components. **Throws** if the expression is invalid. 126 129 ··· 129 132 // Returns: { minute: [0], hour: [9], day: [1, 2, ..., 31], month: [0, 1, 2, ..., 11], weekday: [1,2,3,4,5] } 130 133 ``` 131 134 132 - ### `describe(expression)` 135 + ### `describe(expression: string): string` 133 136 134 137 Get a human-readable description of a cron expression. Returns `"Invalid cron expression"` if the expression is invalid. 135 138 136 139 ```typescript 137 140 describe("*/5 * * * *"); // "Every 5 minutes" 138 - describe("0 9 * * 1-5"); // "At minute 0, between 9 AM and 5 PM, on weekdays" 139 - describe("*/15 3,4 1-4 */3 6"); 140 - // "Every 15 minutes, at 3 AM, 4 AM, on days 1-4 of the month or on Saturday, every 3 months" 141 + describe("0 9 * * 1-5"); // "At 9:00 AM, on weekdays" 142 + describe("*/15 3,4 1-4 */3 6"); // Every 15 minutes, at 3 AM or 4 AM, on the 1st through 4th or on Saturdays every 3 months 141 143 describe("invalid"); // "Invalid cron expression" 142 144 ``` 143 145 146 + ### Types 147 + 148 + ```typescript 149 + interface CronOptions { 150 + timezone?: string; // IANA timezone string (e.g., 'America/New_York') 151 + from?: Date; // Reference date (defaults to now) 152 + } 153 + 154 + interface ParsedCron { 155 + minute: number[]; // 0-59 156 + hour: number[]; // 0-23 157 + day: number[]; // 1-31 158 + month: number[]; // 0-11 (0 = January) 159 + weekday: number[]; // 0-6 (0 = Sunday) 160 + dayIsWildcard: boolean; 161 + weekdayIsWildcard: boolean; 162 + } 163 + ``` 164 + 144 165 ## Cron Expression Format 145 166 146 167 ``` ··· 180 201 181 202 ## Bundle Size 182 203 183 - cron-fast is extremely lightweight and fully tree-shakeable. Here are the actual bundle sizes for different import scenarios (tested with v2.3.0): 204 + cron-fast is extremely lightweight and fully tree-shakeable. Here are the actual bundle sizes for different import scenarios (tested with v3.0.0): 184 205 185 206 | Import | Raw | Minified | Gzipped | 186 207 | ------------------------------------------------------ | -------- | -------- | ----------- | 187 - | `Full bundle (all exports) ` | 21.86 KB | 10.11 KB | **3.61 KB** | 188 - | `nextRun only ` | 13.11 KB | 6.02 KB | **2.22 KB** | 189 - | `previousRun only ` | 13.12 KB | 6.02 KB | **2.22 KB** | 190 - | `nextRuns only ` | 13.50 KB | 6.17 KB | **2.28 KB** | 208 + | `Full bundle (all exports) ` | 21.47 KB | 9.88 KB | **3.55 KB** | 209 + | `nextRun only ` | 12.73 KB | 5.79 KB | **2.14 KB** | 210 + | `previousRun only ` | 12.73 KB | 5.79 KB | **2.14 KB** | 211 + | `nextRuns only ` | 13.11 KB | 5.94 KB | **2.21 KB** | 191 212 | `isValid only ` | 4.44 KB | 2.22 KB | **984 B** | 192 213 | `parse only ` | 4.32 KB | 2.18 KB | **961 B** | 193 214 | `describe only ` | 11.54 KB | 5.57 KB | **2.11 KB** | 194 - | `isMatch only ` | 6.34 KB | 3.14 KB | **1.33 KB** | 215 + | `isMatch only ` | 6.04 KB | 2.96 KB | **1.26 KB** | 195 216 | `Validation only (isValid + parse) ` | 4.45 KB | 2.23 KB | **986 B** | 196 - | `Scheduling only (nextRun + previousRun + nextRuns) ` | 13.90 KB | 6.35 KB | **2.30 KB** | 217 + | `Scheduling only (nextRun + previousRun + nextRuns) ` | 13.51 KB | 6.12 KB | **2.22 KB** | 197 218 198 219 Import only what you need: 199 220 ··· 281 302 ## Tips & Gotchas 282 303 283 304 - **Invalid input throws**: Most functions (`nextRun`, `previousRun`, `nextRuns`, `isMatch`, `parse`) throw an error for invalid cron expressions. `nextRun`, `previousRun`, `nextRuns`, and `isMatch` also throw for invalid timezones. Use `isValid()` to pre-validate user input, or wrap calls in try/catch. Note: `describe()` returns `"Invalid cron expression"` instead of throwing. 305 + - **5-field format only**: cron-fast does not support 6-field cron (with seconds). Use standard 5-field format. 306 + - **Month indexing**: Input uses cron convention (1-12), but `ParsedCron.month` uses JavaScript convention (0-11) 307 + - **Impossible dates are invalid**: Expressions like `0 0 31 2 *` (February 31st) are treated as invalid since they can never match. 284 308 - **Timezone handling**: The cron expression is interpreted in the timezone you specify, but the returned Date is always in UTC 285 309 - **Daylight saving time**: Use IANA timezone names (like "America/New_York") instead of abbreviations (like "EST") 286 310 - **Day 0 and 7**: Both represent Sunday in the day-of-week field ··· 290 314 291 315 cron-fast is designed for speed and efficiency. Here's how it compares to popular alternatives: 292 316 293 - > Tested with cron-fast v2.3.0, croner v10.0.1, cron-parser v5.5.0, cron-schedule v6.0.0 on Node.js v22.18.0 317 + > Tested with cron-fast v3.0.0, croner v10.0.1, cron-parser v5.5.0, cron-schedule v6.0.0 on Node.js v22.18.0 294 318 295 319 | Operation | cron-fast | croner | cron-parser | cron-schedule | 296 320 | ------------ | --------------- | --------- | ----------- | ------------- | 297 - | Next run | **913k ops/s** | 31k ops/s | 33k ops/s | 381k ops/s | 298 - | Previous run | **991k ops/s** | 30k ops/s | 38k ops/s | 391k ops/s | 299 - | Validation | **1899k ops/s** | 33k ops/s | 94k ops/s | 452k ops/s | 300 - | Parsing | **1913k ops/s** | 33k ops/s | 95k ops/s | 447k ops/s | 321 + | Next run | **911k ops/s** | 31k ops/s | 34k ops/s | 352k ops/s | 322 + | Previous run | **1003k ops/s** | 32k ops/s | 39k ops/s | 399k ops/s | 323 + | Validation | **1958k ops/s** | 34k ops/s | 97k ops/s | 462k ops/s | 324 + | Parsing | **1982k ops/s** | 35k ops/s | 98k ops/s | 469k ops/s | 301 325 302 326 See [detailed benchmarks and feature comparison](docs/benchmark-comparison.md) (including Deno and Bun runtimes) for more information. 303 327
+47 -47
docs/benchmark-comparison-bun.md
··· 1 1 # Benchmark & Feature Comparison 2 2 3 - > Tested with bun v1.3.9, cron-fast v2.3.0, croner v10.0.1, cron-parser v5.5.0, cron-schedule v6.0.0, cron-validate v1.5.3 3 + > Tested with bun v1.3.9, cron-fast v3.0.0, croner v10.0.1, cron-parser v5.5.0, cron-schedule v6.0.0, cron-validate v1.5.3 4 4 > Tested on MacBook M1 pro 5 5 6 6 ## Performance Benchmarks ··· 11 11 12 12 | Library | Avg ops/sec | vs cron-fast | 13 13 | ------------- | ----------- | ------------ | 14 - | **cron-fast** | ~656k | baseline | 15 - | cron-schedule | ~318k | 2.1x faster | 16 - | croner | ~56k | 11.6x faster | 17 - | cron-parser | ~37k | 17.6x faster | 14 + | **cron-fast** | ~655k | baseline | 15 + | cron-schedule | ~304k | 2.2x faster | 16 + | croner | ~55k | 11.8x faster | 17 + | cron-parser | ~36k | 18.2x faster | 18 18 19 19 ### Previous Execution Time 20 20 21 21 | Library | Avg ops/sec | vs cron-fast | 22 22 | ------------- | ----------- | ------------ | 23 - | **cron-fast** | ~604k | baseline | 24 - | cron-schedule | ~319k | 1.9x faster | 25 - | croner | ~57k | 10.5x faster | 26 - | cron-parser | ~40k | 15.3x faster | 23 + | **cron-fast** | ~616k | baseline | 24 + | cron-schedule | ~311k | 2.0x faster | 25 + | croner | ~53k | 11.5x faster | 26 + | cron-parser | ~38k | 16.0x faster | 27 27 28 28 ### Validation 29 29 30 30 | Library | Avg ops/sec | vs cron-fast | 31 31 | ------------- | ----------- | ------------ | 32 - | **cron-fast** | ~827k | baseline | 33 - | cron-validate | ~961k | 1.2x slower | 34 - | cron-schedule | ~354k | 2.3x faster | 35 - | cron-parser | ~121k | 6.8x faster | 36 - | croner | ~61k | 13.4x faster | 32 + | **cron-fast** | ~814k | baseline | 33 + | cron-validate | ~941k | 1.2x slower | 34 + | cron-schedule | ~345k | 2.4x faster | 35 + | cron-parser | ~117k | 6.9x faster | 36 + | croner | ~58k | 14.1x faster | 37 37 38 38 ### Parsing 39 39 40 40 | Library | Avg ops/sec | vs cron-fast | 41 41 | ------------- | ----------- | ------------ | 42 - | **cron-fast** | ~858k | baseline | 43 - | cron-validate | ~955k | 1.1x slower | 44 - | cron-schedule | ~352k | 2.4x faster | 45 - | cron-parser | ~121k | 7.1x faster | 46 - | croner | ~60k | 14.4x faster | 42 + | **cron-fast** | ~862k | baseline | 43 + | cron-validate | ~943k | 1.1x slower | 44 + | cron-schedule | ~349k | 2.5x faster | 45 + | cron-parser | ~118k | 7.3x faster | 46 + | croner | ~61k | 14.1x faster | 47 47 48 48 Run benchmarks yourself: `pnpm benchmark:bun` 49 49 ··· 53 53 54 54 | Test Case | cron-fast | cron-schedule | croner | cron-parser | 55 55 | --------------------------- | --------: | ------------: | -----: | ----------: | 56 - | Every minute | ~1180k | ~158k ✓ | ~60k ✓ | ~37k ✓ | 57 - | Sparse: First of month | ~606k | ~414k ✓ | ~59k ✓ | ~22k ✓ | 58 - | Sparse: 31st (skips months) | ~585k | ~415k ✓ | ~53k ✓ | ~9k ✓ | 59 - | Step: Every 15 minutes | ~569k | ~216k ✓ | ~59k ✓ | ~63k ✓ | 60 - | Specific: 9 AM daily | ~704k | ~289k ✓ | ~61k ✓ | ~45k ✓ | 61 - | OR-mode: 15th OR Monday | ~458k | ~467k | ~52k ✓ | ~41k ✓ | 62 - | Weekdays: Mon-Fri 9 AM | ~490k | ~269k ✓ | ~50k ✓ | ~44k ✓ | 56 + | Every minute | ~1216k | ~159k ✓ | ~60k ✓ | ~37k ✓ | 57 + | Sparse: First of month | ~610k | ~415k ✓ | ~58k ✓ | ~21k ✓ | 58 + | Sparse: 31st (skips months) | ~600k | ~420k ✓ | ~56k ✓ | ~9k ✓ | 59 + | Step: Every 15 minutes | ~564k | ~203k ✓ | ~58k ✓ | ~62k ✓ | 60 + | Specific: 9 AM daily | ~699k | ~259k ✓ | ~60k ✓ | ~44k ✓ | 61 + | OR-mode: 15th OR Monday | ~431k | ~413k | ~49k ✓ | ~38k ✓ | 62 + | Weekdays: Mon-Fri 9 AM | ~467k | ~258k ✓ | ~45k ✓ | ~41k ✓ | 63 63 64 64 ✓ = cron-fast is faster (≥10% faster) | ✗ = cron-fast is slower (≥10% slower) 65 65 ··· 67 67 68 68 | Test Case | cron-fast | cron-schedule | croner | cron-parser | 69 69 | --------------------------- | --------: | ------------: | -----: | ----------: | 70 - | Every minute | ~528k | ~158k ✓ | ~58k ✓ | ~37k ✓ | 71 - | Sparse: First of month | ~735k | ~430k ✓ | ~57k ✓ | ~11k ✓ | 72 - | Sparse: 31st (skips months) | ~547k | ~412k ✓ | ~56k ✓ | ~10k ✓ | 73 - | Step: Every 15 minutes | ~547k | ~215k ✓ | ~60k ✓ | ~60k ✓ | 74 - | Specific: 9 AM daily | ~645k | ~286k ✓ | ~59k ✓ | ~49k ✓ | 75 - | OR-mode: 15th OR Monday | ~691k | ~462k ✓ | ~57k ✓ | ~59k ✓ | 76 - | Weekdays: Mon-Fri 9 AM | ~532k | ~268k ✓ | ~54k ✓ | ~50k ✓ | 70 + | Every minute | ~697k | ~146k ✓ | ~57k ✓ | ~35k ✓ | 71 + | Sparse: First of month | ~649k | ~408k ✓ | ~55k ✓ | ~11k ✓ | 72 + | Sparse: 31st (skips months) | ~594k | ~398k ✓ | ~52k ✓ | ~10k ✓ | 73 + | Step: Every 15 minutes | ~530k | ~206k ✓ | ~51k ✓ | ~58k ✓ | 74 + | Specific: 9 AM daily | ~642k | ~279k ✓ | ~51k ✓ | ~47k ✓ | 75 + | OR-mode: 15th OR Monday | ~674k | ~478k ✓ | ~59k ✓ | ~61k ✓ | 76 + | Weekdays: Mon-Fri 9 AM | ~525k | ~259k ✓ | ~49k ✓ | ~47k ✓ | 77 77 78 78 ✓ = cron-fast is faster (≥10% faster) | ✗ = cron-fast is slower (≥10% slower) 79 79 ··· 81 81 82 82 | Test Case | cron-fast | cron-schedule | cron-parser | croner | cron-validate | 83 83 | --------------- | --------: | ------------: | ----------: | -----: | ------------: | 84 - | \* \* \* \* \* | ~464k | ~157k ✓ | ~50k ✓ | ~64k ✓ | ~952k ✗ | 85 - | 0 0 1 \* \* | ~1128k | ~463k ✓ | ~156k ✓ | ~62k ✓ | ~989k ✓ | 86 - | 0 12 31 \* \* | ~977k | ~463k ✓ | ~159k ✓ | ~60k ✓ | ~946k | 87 - | _/15 _ \* \* \* | ~642k | ~218k ✓ | ~85k ✓ | ~62k ✓ | ~990k ✗ | 88 - | 0 9 \* \* \* | ~940k | ~305k ✓ | ~109k ✓ | ~61k ✓ | ~954k | 89 - | 0 9 15 \* 1 | ~874k | ~574k ✓ | ~178k ✓ | ~61k ✓ | ~966k | 90 - | 0 9 \* \* 1-5 | ~764k | ~294k ✓ | ~108k ✓ | ~61k ✓ | ~931k ✗ | 84 + | \* \* \* \* \* | ~432k | ~165k ✓ | ~48k ✓ | ~59k ✓ | ~949k ✗ | 85 + | 0 0 1 \* \* | ~1156k | ~441k ✓ | ~148k ✓ | ~60k ✓ | ~963k ✓ | 86 + | 0 12 31 \* \* | ~972k | ~453k ✓ | ~159k ✓ | ~57k ✓ | ~905k | 87 + | _/15 _ \* \* \* | ~611k | ~208k ✓ | ~84k ✓ | ~55k ✓ | ~946k ✗ | 88 + | 0 9 \* \* \* | ~917k | ~291k ✓ | ~105k ✓ | ~58k ✓ | ~915k | 89 + | 0 9 15 \* 1 | ~858k | ~564k ✓ | ~169k ✓ | ~59k ✓ | ~931k | 90 + | 0 9 \* \* 1-5 | ~749k | ~295k ✓ | ~109k ✓ | ~57k ✓ | ~979k ✗ | 91 91 92 92 ✓ = cron-fast is faster (≥10% faster) | ✗ = cron-fast is slower (≥10% slower) 93 93 ··· 95 95 96 96 | Test Case | cron-fast | cron-schedule | cron-parser | croner | cron-validate | 97 97 | --------------- | --------: | ------------: | ----------: | -----: | ------------: | 98 - | \* \* \* \* \* | ~962k | ~163k ✓ | ~52k ✓ | ~58k ✓ | ~946k | 99 - | 0 0 1 \* \* | ~842k | ~456k ✓ | ~159k ✓ | ~58k ✓ | ~958k ✗ | 100 - | 0 12 31 \* \* | ~964k | ~450k ✓ | ~161k ✓ | ~62k ✓ | ~941k | 101 - | _/15 _ \* \* \* | ~648k | ~227k ✓ | ~86k ✓ | ~59k ✓ | ~989k ✗ | 102 - | 0 9 \* \* \* | ~949k | ~307k ✓ | ~109k ✓ | ~63k ✓ | ~937k | 103 - | 0 9 15 \* 1 | ~877k | ~570k ✓ | ~171k ✓ | ~57k ✓ | ~956k | 104 - | 0 9 \* \* 1-5 | ~766k | ~295k ✓ | ~109k ✓ | ~61k ✓ | ~956k ✗ | 98 + | \* \* \* \* \* | ~997k | ~162k ✓ | ~51k ✓ | ~59k ✓ | ~913k | 99 + | 0 0 1 \* \* | ~864k | ~447k ✓ | ~151k ✓ | ~61k ✓ | ~957k | 100 + | 0 12 31 \* \* | ~966k | ~455k ✓ | ~153k ✓ | ~63k ✓ | ~918k | 101 + | _/15 _ \* \* \* | ~618k | ~221k ✓ | ~86k ✓ | ~61k ✓ | ~997k ✗ | 102 + | 0 9 \* \* \* | ~964k | ~296k ✓ | ~107k ✓ | ~62k ✓ | ~914k | 103 + | 0 9 15 \* 1 | ~870k | ~562k ✓ | ~171k ✓ | ~59k ✓ | ~950k | 104 + | 0 9 \* \* 1-5 | ~756k | ~299k ✓ | ~107k ✓ | ~62k ✓ | ~951k ✗ | 105 105 106 106 ✓ = cron-fast is faster (≥10% faster) | ✗ = cron-fast is slower (≥10% slower)
+46 -46
docs/benchmark-comparison-deno.md
··· 1 1 # Benchmark & Feature Comparison 2 2 3 - > Tested with deno v2.6.8, cron-fast v2.3.0, croner v10.0.1, cron-parser v5.5.0, cron-schedule v6.0.0, cron-validate v1.5.3 3 + > Tested with deno v2.6.8, cron-fast v3.0.0, croner v10.0.1, cron-parser v5.5.0, cron-schedule v6.0.0, cron-validate v1.5.3 4 4 > Tested on MacBook M1 pro 5 5 6 6 ## Performance Benchmarks ··· 11 11 12 12 | Library | Avg ops/sec | vs cron-fast | 13 13 | ------------- | ----------- | ------------ | 14 - | **cron-fast** | ~894k | baseline | 15 - | cron-schedule | ~400k | 2.2x faster | 14 + | **cron-fast** | ~870k | baseline | 15 + | cron-schedule | ~398k | 2.2x faster | 16 16 | croner | ~31k | 28.5x faster | 17 - | cron-parser | ~35k | 25.2x faster | 17 + | cron-parser | ~33k | 26.0x faster | 18 18 19 19 ### Previous Execution Time 20 20 21 21 | Library | Avg ops/sec | vs cron-fast | 22 22 | ------------- | ----------- | ------------ | 23 - | **cron-fast** | ~1014k | baseline | 24 - | cron-schedule | ~427k | 2.4x faster | 25 - | croner | ~32k | 32.0x faster | 26 - | cron-parser | ~41k | 24.7x faster | 23 + | **cron-fast** | ~974k | baseline | 24 + | cron-schedule | ~414k | 2.4x faster | 25 + | croner | ~31k | 31.9x faster | 26 + | cron-parser | ~38k | 25.7x faster | 27 27 28 28 ### Validation 29 29 30 30 | Library | Avg ops/sec | vs cron-fast | 31 31 | ------------- | ----------- | ------------ | 32 - | **cron-fast** | ~1923k | baseline | 33 - | cron-validate | ~660k | 2.9x faster | 34 - | cron-schedule | ~463k | 4.2x faster | 35 - | cron-parser | ~101k | 19.1x faster | 36 - | croner | ~34k | 57.3x faster | 32 + | **cron-fast** | ~1841k | baseline | 33 + | cron-validate | ~626k | 2.9x faster | 34 + | cron-schedule | ~463k | 4.0x faster | 35 + | cron-parser | ~97k | 18.9x faster | 36 + | croner | ~33k | 55.4x faster | 37 37 38 38 ### Parsing 39 39 40 40 | Library | Avg ops/sec | vs cron-fast | 41 41 | ------------- | ----------- | ------------ | 42 - | **cron-fast** | ~1931k | baseline | 43 - | cron-validate | ~668k | 2.9x faster | 44 - | cron-schedule | ~482k | 4.0x faster | 45 - | cron-parser | ~103k | 18.7x faster | 46 - | croner | ~34k | 57.4x faster | 42 + | **cron-fast** | ~1873k | baseline | 43 + | cron-validate | ~634k | 3.0x faster | 44 + | cron-schedule | ~472k | 4.0x faster | 45 + | cron-parser | ~98k | 19.2x faster | 46 + | croner | ~34k | 55.3x faster | 47 47 48 48 Run benchmarks yourself: `pnpm benchmark:deno` 49 49 ··· 53 53 54 54 | Test Case | cron-fast | cron-schedule | croner | cron-parser | 55 55 | --------------------------- | --------: | ------------: | -----: | ----------: | 56 - | Every minute | ~1381k | ~149k ✓ | ~32k ✓ | ~33k ✓ | 57 - | Sparse: First of month | ~829k | ~543k ✓ | ~32k ✓ | ~19k ✓ | 58 - | Sparse: 31st (skips months) | ~744k | ~531k ✓ | ~30k ✓ | ~7k ✓ | 59 - | Step: Every 15 minutes | ~932k | ~267k ✓ | ~33k ✓ | ~57k ✓ | 60 - | Specific: 9 AM daily | ~1044k | ~370k ✓ | ~32k ✓ | ~45k ✓ | 61 - | OR-mode: 15th OR Monday | ~523k | ~581k | ~30k ✓ | ~40k ✓ | 62 - | Weekdays: Mon-Fri 9 AM | ~809k | ~362k ✓ | ~30k ✓ | ~46k ✓ | 56 + | Every minute | ~1357k | ~148k ✓ | ~32k ✓ | ~32k ✓ | 57 + | Sparse: First of month | ~805k | ~536k ✓ | ~30k ✓ | ~18k ✓ | 58 + | Sparse: 31st (skips months) | ~737k | ~536k ✓ | ~31k ✓ | ~7k ✓ | 59 + | Step: Every 15 minutes | ~934k | ~275k ✓ | ~33k ✓ | ~55k ✓ | 60 + | Specific: 9 AM daily | ~997k | ~364k ✓ | ~31k ✓ | ~42k ✓ | 61 + | OR-mode: 15th OR Monday | ~486k | ~581k ✗ | ~28k ✓ | ~36k ✓ | 62 + | Weekdays: Mon-Fri 9 AM | ~778k | ~342k ✓ | ~29k ✓ | ~43k ✓ | 63 63 64 64 ✓ = cron-fast is faster (≥10% faster) | ✗ = cron-fast is slower (≥10% slower) 65 65 ··· 67 67 68 68 | Test Case | cron-fast | cron-schedule | croner | cron-parser | 69 69 | --------------------------- | --------: | ------------: | -----: | ----------: | 70 - | Every minute | ~1430k | ~192k ✓ | ~33k ✓ | ~37k ✓ | 71 - | Sparse: First of month | ~1023k | ~583k ✓ | ~32k ✓ | ~9k ✓ | 72 - | Sparse: 31st (skips months) | ~757k | ~520k ✓ | ~32k ✓ | ~9k ✓ | 73 - | Step: Every 15 minutes | ~931k | ~277k ✓ | ~32k ✓ | ~59k ✓ | 74 - | Specific: 9 AM daily | ~1074k | ~386k ✓ | ~33k ✓ | ~53k ✓ | 75 - | OR-mode: 15th OR Monday | ~1016k | ~658k ✓ | ~31k ✓ | ~67k ✓ | 76 - | Weekdays: Mon-Fri 9 AM | ~867k | ~378k ✓ | ~31k ✓ | ~54k ✓ | 70 + | Every minute | ~1418k | ~181k ✓ | ~31k ✓ | ~35k ✓ | 71 + | Sparse: First of month | ~988k | ~564k ✓ | ~30k ✓ | ~9k ✓ | 72 + | Sparse: 31st (skips months) | ~725k | ~508k ✓ | ~31k ✓ | ~8k ✓ | 73 + | Step: Every 15 minutes | ~911k | ~272k ✓ | ~32k ✓ | ~54k ✓ | 74 + | Specific: 9 AM daily | ~1017k | ~363k ✓ | ~30k ✓ | ~48k ✓ | 75 + | OR-mode: 15th OR Monday | ~944k | ~648k ✓ | ~30k ✓ | ~62k ✓ | 76 + | Weekdays: Mon-Fri 9 AM | ~816k | ~358k ✓ | ~29k ✓ | ~49k ✓ | 77 77 78 78 ✓ = cron-fast is faster (≥10% faster) | ✗ = cron-fast is slower (≥10% slower) 79 79 ··· 81 81 82 82 | Test Case | cron-fast | cron-schedule | cron-parser | croner | cron-validate | 83 83 | --------------- | --------: | ------------: | ----------: | -----: | ------------: | 84 - | \* \* \* \* \* | ~2216k | ~200k ✓ | ~47k ✓ | ~34k ✓ | ~620k ✓ | 85 - | 0 0 1 \* \* | ~2222k | ~540k ✓ | ~132k ✓ | ~34k ✓ | ~669k ✓ | 86 - | 0 12 31 \* \* | ~1806k | ~625k ✓ | ~133k ✓ | ~34k ✓ | ~648k ✓ | 87 - | _/15 _ \* \* \* | ~1653k | ~292k ✓ | ~68k ✓ | ~33k ✓ | ~714k ✓ | 88 - | 0 9 \* \* \* | ~2278k | ~407k ✓ | ~93k ✓ | ~35k ✓ | ~645k ✓ | 89 - | 0 9 15 \* 1 | ~1859k | ~769k ✓ | ~137k ✓ | ~31k ✓ | ~682k ✓ | 90 - | 0 9 \* \* 1-5 | ~1423k | ~409k ✓ | ~95k ✓ | ~34k ✓ | ~645k ✓ | 84 + | \* \* \* \* \* | ~2158k | ~198k ✓ | ~45k ✓ | ~33k ✓ | ~582k ✓ | 85 + | 0 0 1 \* \* | ~2065k | ~602k ✓ | ~125k ✓ | ~34k ✓ | ~624k ✓ | 86 + | 0 12 31 \* \* | ~1737k | ~599k ✓ | ~126k ✓ | ~33k ✓ | ~636k ✓ | 87 + | _/15 _ \* \* \* | ~1660k | ~285k ✓ | ~66k ✓ | ~34k ✓ | ~664k ✓ | 88 + | 0 9 \* \* \* | ~2176k | ~399k ✓ | ~87k ✓ | ~33k ✓ | ~608k ✓ | 89 + | 0 9 15 \* 1 | ~1736k | ~759k ✓ | ~142k ✓ | ~33k ✓ | ~661k ✓ | 90 + | 0 9 \* \* 1-5 | ~1357k | ~402k ✓ | ~90k ✓ | ~32k ✓ | ~604k ✓ | 91 91 92 92 ✓ = cron-fast is faster (≥10% faster) | ✗ = cron-fast is slower (≥10% slower) 93 93 ··· 95 95 96 96 | Test Case | cron-fast | cron-schedule | cron-parser | croner | cron-validate | 97 97 | --------------- | --------: | ------------: | ----------: | -----: | ------------: | 98 - | \* \* \* \* \* | ~2214k | ~199k ✓ | ~47k ✓ | ~32k ✓ | ~622k ✓ | 99 - | 0 0 1 \* \* | ~2182k | ~637k ✓ | ~133k ✓ | ~34k ✓ | ~678k ✓ | 100 - | 0 12 31 \* \* | ~1835k | ~624k ✓ | ~132k ✓ | ~33k ✓ | ~669k ✓ | 101 - | _/15 _ \* \* \* | ~1675k | ~291k ✓ | ~72k ✓ | ~33k ✓ | ~717k ✓ | 102 - | 0 9 \* \* \* | ~2307k | ~404k ✓ | ~93k ✓ | ~35k ✓ | ~648k ✓ | 103 - | 0 9 15 \* 1 | ~1868k | ~805k ✓ | ~149k ✓ | ~35k ✓ | ~693k ✓ | 104 - | 0 9 \* \* 1-5 | ~1437k | ~413k ✓ | ~96k ✓ | ~34k ✓ | ~651k ✓ | 98 + | \* \* \* \* \* | ~2128k | ~200k ✓ | ~46k ✓ | ~33k ✓ | ~605k ✓ | 99 + | 0 0 1 \* \* | ~2130k | ~606k ✓ | ~128k ✓ | ~34k ✓ | ~643k ✓ | 100 + | 0 12 31 \* \* | ~1777k | ~616k ✓ | ~125k ✓ | ~34k ✓ | ~637k ✓ | 101 + | _/15 _ \* \* \* | ~1693k | ~285k ✓ | ~68k ✓ | ~34k ✓ | ~675k ✓ | 102 + | 0 9 \* \* \* | ~2241k | ~406k ✓ | ~89k ✓ | ~34k ✓ | ~615k ✓ | 103 + | 0 9 15 \* 1 | ~1767k | ~786k ✓ | ~137k ✓ | ~34k ✓ | ~652k ✓ | 104 + | 0 9 \* \* 1-5 | ~1377k | ~405k ✓ | ~90k ✓ | ~33k ✓ | ~614k ✓ | 105 105 106 106 ✓ = cron-fast is faster (≥10% faster) | ✗ = cron-fast is slower (≥10% slower)
+47 -47
docs/benchmark-comparison-node.md
··· 1 1 # Benchmark & Feature Comparison 2 2 3 - > Tested with node v22.18.0, cron-fast v2.3.0, croner v10.0.1, cron-parser v5.5.0, cron-schedule v6.0.0, cron-validate v1.5.3 3 + > Tested with node v22.18.0, cron-fast v3.0.0, croner v10.0.1, cron-parser v5.5.0, cron-schedule v6.0.0, cron-validate v1.5.3 4 4 > Tested on MacBook M1 pro 5 5 6 6 ## Performance Benchmarks ··· 11 11 12 12 | Library | Avg ops/sec | vs cron-fast | 13 13 | ------------- | ----------- | ------------ | 14 - | **cron-fast** | ~913k | baseline | 15 - | cron-schedule | ~381k | 2.4x faster | 16 - | croner | ~31k | 29.1x faster | 17 - | cron-parser | ~33k | 27.4x faster | 14 + | **cron-fast** | ~911k | baseline | 15 + | cron-schedule | ~352k | 2.6x faster | 16 + | croner | ~31k | 29.6x faster | 17 + | cron-parser | ~34k | 27.1x faster | 18 18 19 19 ### Previous Execution Time 20 20 21 21 | Library | Avg ops/sec | vs cron-fast | 22 22 | ------------- | ----------- | ------------ | 23 - | **cron-fast** | ~991k | baseline | 24 - | cron-schedule | ~391k | 2.5x faster | 25 - | croner | ~30k | 32.6x faster | 26 - | cron-parser | ~38k | 26.4x faster | 23 + | **cron-fast** | ~1003k | baseline | 24 + | cron-schedule | ~399k | 2.5x faster | 25 + | croner | ~32k | 31.6x faster | 26 + | cron-parser | ~39k | 25.8x faster | 27 27 28 28 ### Validation 29 29 30 30 | Library | Avg ops/sec | vs cron-fast | 31 31 | ------------- | ----------- | ------------ | 32 - | **cron-fast** | ~1899k | baseline | 33 - | cron-validate | ~656k | 2.9x faster | 34 - | cron-schedule | ~452k | 4.2x faster | 35 - | cron-parser | ~94k | 20.2x faster | 36 - | croner | ~33k | 57.9x faster | 32 + | **cron-fast** | ~1958k | baseline | 33 + | cron-validate | ~680k | 2.9x faster | 34 + | cron-schedule | ~462k | 4.2x faster | 35 + | cron-parser | ~97k | 20.2x faster | 36 + | croner | ~34k | 56.9x faster | 37 37 38 38 ### Parsing 39 39 40 40 | Library | Avg ops/sec | vs cron-fast | 41 41 | ------------- | ----------- | ------------ | 42 - | **cron-fast** | ~1913k | baseline | 43 - | cron-validate | ~665k | 2.9x faster | 44 - | cron-schedule | ~447k | 4.3x faster | 45 - | cron-parser | ~95k | 20.2x faster | 46 - | croner | ~33k | 58.1x faster | 42 + | **cron-fast** | ~1982k | baseline | 43 + | cron-validate | ~690k | 2.9x faster | 44 + | cron-schedule | ~469k | 4.2x faster | 45 + | cron-parser | ~98k | 20.2x faster | 46 + | croner | ~35k | 57.3x faster | 47 47 48 48 Run benchmarks yourself: `pnpm benchmark` 49 49 ··· 53 53 54 54 | Test Case | cron-fast | cron-schedule | croner | cron-parser | 55 55 | --------------------------- | --------: | ------------: | -----: | ----------: | 56 - | Every minute | ~1445k | ~178k ✓ | ~32k ✓ | ~32k ✓ | 57 - | Sparse: First of month | ~877k | ~509k ✓ | ~32k ✓ | ~19k ✓ | 58 - | Sparse: 31st (skips months) | ~813k | ~512k ✓ | ~30k ✓ | ~8k ✓ | 59 - | Step: Every 15 minutes | ~939k | ~270k ✓ | ~34k ✓ | ~56k ✓ | 60 - | Specific: 9 AM daily | ~1032k | ~360k ✓ | ~34k ✓ | ~43k ✓ | 61 - | OR-mode: 15th OR Monday | ~520k | ~515k | ~31k ✓ | ~34k ✓ | 62 - | Weekdays: Mon-Fri 9 AM | ~764k | ~323k ✓ | ~27k ✓ | ~41k ✓ | 56 + | Every minute | ~1422k | ~180k ✓ | ~31k ✓ | ~32k ✓ | 57 + | Sparse: First of month | ~854k | ~509k ✓ | ~31k ✓ | ~17k ✓ | 58 + | Sparse: 31st (skips months) | ~806k | ~509k ✓ | ~28k ✓ | ~7k ✓ | 59 + | Step: Every 15 minutes | ~940k | ~258k ✓ | ~32k ✓ | ~54k ✓ | 60 + | Specific: 9 AM daily | ~1013k | ~356k ✓ | ~33k ✓ | ~43k ✓ | 61 + | OR-mode: 15th OR Monday | ~519k | ~549k | ~31k ✓ | ~38k ✓ | 62 + | Weekdays: Mon-Fri 9 AM | ~823k | ~104k ✓ | ~30k ✓ | ~44k ✓ | 63 63 64 64 ✓ = cron-fast is faster (≥10% faster) | ✗ = cron-fast is slower (≥10% slower) 65 65 ··· 67 67 68 68 | Test Case | cron-fast | cron-schedule | croner | cron-parser | 69 69 | --------------------------- | --------: | ------------: | -----: | ----------: | 70 - | Every minute | ~1410k | ~181k ✓ | ~32k ✓ | ~34k ✓ | 71 - | Sparse: First of month | ~1012k | ~541k ✓ | ~29k ✓ | ~8k ✓ | 72 - | Sparse: 31st (skips months) | ~783k | ~479k ✓ | ~31k ✓ | ~8k ✓ | 73 - | Step: Every 15 minutes | ~868k | ~263k ✓ | ~31k ✓ | ~56k ✓ | 74 - | Specific: 9 AM daily | ~1043k | ~361k ✓ | ~30k ✓ | ~48k ✓ | 75 - | OR-mode: 15th OR Monday | ~979k | ~585k ✓ | ~31k ✓ | ~62k ✓ | 76 - | Weekdays: Mon-Fri 9 AM | ~844k | ~329k ✓ | ~29k ✓ | ~47k ✓ | 70 + | Every minute | ~1324k | ~184k ✓ | ~31k ✓ | ~36k ✓ | 71 + | Sparse: First of month | ~1019k | ~545k ✓ | ~31k ✓ | ~9k ✓ | 72 + | Sparse: 31st (skips months) | ~786k | ~478k ✓ | ~31k ✓ | ~8k ✓ | 73 + | Step: Every 15 minutes | ~932k | ~264k ✓ | ~33k ✓ | ~56k ✓ | 74 + | Specific: 9 AM daily | ~1066k | ~365k ✓ | ~33k ✓ | ~49k ✓ | 75 + | OR-mode: 15th OR Monday | ~1027k | ~610k ✓ | ~32k ✓ | ~64k ✓ | 76 + | Weekdays: Mon-Fri 9 AM | ~864k | ~350k ✓ | ~31k ✓ | ~50k ✓ | 77 77 78 78 ✓ = cron-fast is faster (≥10% faster) | ✗ = cron-fast is slower (≥10% slower) 79 79 ··· 81 81 82 82 | Test Case | cron-fast | cron-schedule | cron-parser | croner | cron-validate | 83 83 | --------------- | --------: | ------------: | ----------: | -----: | ------------: | 84 - | \* \* \* \* \* | ~2187k | ~179k ✓ | ~44k ✓ | ~33k ✓ | ~593k ✓ | 85 - | 0 0 1 \* \* | ~2194k | ~601k ✓ | ~120k ✓ | ~32k ✓ | ~656k ✓ | 86 - | 0 12 31 \* \* | ~1754k | ~590k ✓ | ~117k ✓ | ~31k ✓ | ~650k ✓ | 87 - | _/15 _ \* \* \* | ~1632k | ~277k ✓ | ~66k ✓ | ~33k ✓ | ~696k ✓ | 88 - | 0 9 \* \* \* | ~2251k | ~374k ✓ | ~84k ✓ | ~33k ✓ | ~657k ✓ | 89 - | 0 9 15 \* 1 | ~1826k | ~748k ✓ | ~138k ✓ | ~33k ✓ | ~692k ✓ | 90 - | 0 9 \* \* 1-5 | ~1447k | ~396k ✓ | ~88k ✓ | ~33k ✓ | ~650k ✓ | 84 + | \* \* \* \* \* | ~2218k | ~195k ✓ | ~46k ✓ | ~35k ✓ | ~653k ✓ | 85 + | 0 0 1 \* \* | ~2141k | ~612k ✓ | ~124k ✓ | ~33k ✓ | ~673k ✓ | 86 + | 0 12 31 \* \* | ~1890k | ~628k ✓ | ~126k ✓ | ~35k ✓ | ~683k ✓ | 87 + | _/15 _ \* \* \* | ~1719k | ~280k ✓ | ~67k ✓ | ~34k ✓ | ~729k ✓ | 88 + | 0 9 \* \* \* | ~2325k | ~401k ✓ | ~89k ✓ | ~35k ✓ | ~670k ✓ | 89 + | 0 9 15 \* 1 | ~1936k | ~748k ✓ | ~138k ✓ | ~35k ✓ | ~708k ✓ | 90 + | 0 9 \* \* 1-5 | ~1481k | ~373k ✓ | ~89k ✓ | ~34k ✓ | ~647k ✓ | 91 91 92 92 ✓ = cron-fast is faster (≥10% faster) | ✗ = cron-fast is slower (≥10% slower) 93 93 ··· 95 95 96 96 | Test Case | cron-fast | cron-schedule | cron-parser | croner | cron-validate | 97 97 | --------------- | --------: | ------------: | ----------: | -----: | ------------: | 98 - | \* \* \* \* \* | ~2220k | ~190k ✓ | ~45k ✓ | ~34k ✓ | ~657k ✓ | 99 - | 0 0 1 \* \* | ~2217k | ~615k ✓ | ~123k ✓ | ~34k ✓ | ~676k ✓ | 100 - | 0 12 31 \* \* | ~1841k | ~625k ✓ | ~121k ✓ | ~33k ✓ | ~664k ✓ | 101 - | _/15 _ \* \* \* | ~1629k | ~281k ✓ | ~66k ✓ | ~32k ✓ | ~717k ✓ | 102 - | 0 9 \* \* \* | ~2259k | ~333k ✓ | ~82k ✓ | ~31k ✓ | ~636k ✓ | 103 - | 0 9 15 \* 1 | ~1800k | ~713k ✓ | ~138k ✓ | ~33k ✓ | ~673k ✓ | 104 - | 0 9 \* \* 1-5 | ~1424k | ~375k ✓ | ~87k ✓ | ~33k ✓ | ~634k ✓ | 98 + | \* \* \* \* \* | ~2235k | ~193k ✓ | ~47k ✓ | ~35k ✓ | ~668k ✓ | 99 + | 0 0 1 \* \* | ~2285k | ~628k ✓ | ~126k ✓ | ~34k ✓ | ~695k ✓ | 100 + | 0 12 31 \* \* | ~1895k | ~625k ✓ | ~125k ✓ | ~35k ✓ | ~697k ✓ | 101 + | _/15 _ \* \* \* | ~1722k | ~282k ✓ | ~68k ✓ | ~35k ✓ | ~731k ✓ | 102 + | 0 9 \* \* \* | ~2323k | ~400k ✓ | ~88k ✓ | ~35k ✓ | ~674k ✓ | 103 + | 0 9 15 \* 1 | ~1915k | ~759k ✓ | ~142k ✓ | ~34k ✓ | ~708k ✓ | 104 + | 0 9 \* \* 1-5 | ~1495k | ~397k ✓ | ~90k ✓ | ~34k ✓ | ~660k ✓ | 105 105 106 106 ✓ = cron-fast is faster (≥10% faster) | ✗ = cron-fast is slower (≥10% slower)
+1 -1
jsr.json
··· 1 1 { 2 2 "name": "@kbilkis/cron-fast", 3 - "version": "2.3.0", 3 + "version": "3.0.0", 4 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 "javascript",
+1 -1
package.json
··· 1 1 { 2 2 "name": "cron-fast", 3 - "version": "2.3.0", 3 + "version": "3.0.0", 4 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",
-16
src/timezone.ts
··· 1 - function isValidTimezone(timezone: string): boolean { 2 - if (typeof timezone !== "string" || !timezone.trim()) return false; 3 - try { 4 - Intl.DateTimeFormat(undefined, { timeZone: timezone }); 5 - return true; 6 - } catch { 7 - return false; 8 - } 9 - } 10 - 11 1 /** 12 2 * Convert a UTC date to wall-clock time in the target timezone. 13 - * @throws {Error} If timezone is invalid 14 3 */ 15 4 export function convertToTimezone(date: Date, timezone: string): Date { 16 - if (!isValidTimezone(timezone)) throw new Error(`Invalid timezone: "${timezone}"`); 17 - 18 5 const str = date.toLocaleString("en-US", { 19 6 timeZone: timezone, 20 7 year: "numeric", ··· 38 25 39 26 /** 40 27 * Convert a timezone-local date back to UTC (inverse of convertToTimezone). 41 - * @throws {Error} If timezone is invalid 42 28 * 43 29 * Note: During DST fall-back, multiple UTC times map to the same wall-clock time. 44 30 * The result is implementation-defined. Avoid scheduling during DST transition hours 45 31 * for predictable behavior. 46 32 */ 47 33 export function convertFromTimezone(date: Date, timezone: string): Date { 48 - if (!isValidTimezone(timezone)) throw new Error(`Invalid timezone: "${timezone}"`); 49 - 50 34 // Target time as a comparable number (for checking if we found it) 51 35 const targetTime = Date.UTC( 52 36 date.getUTCFullYear(),
+4 -4
test/scheduler.test.ts
··· 1471 1471 it("should throw for invalid timezone in nextRun", () => { 1472 1472 const from = new Date("2026-03-15T14:00:00Z"); 1473 1473 expect(() => nextRun("0 9 * * *", { from, timezone: "Invalid/Timezone" })).toThrow( 1474 - 'Invalid timezone: "Invalid/Timezone"', 1474 + "Invalid time zone specified", 1475 1475 ); 1476 1476 }); 1477 1477 1478 1478 it("should throw for invalid timezone in previousRun", () => { 1479 1479 const from = new Date("2026-03-15T14:00:00Z"); 1480 1480 expect(() => previousRun("0 9 * * *", { from, timezone: "NotATimezone" })).toThrow( 1481 - 'Invalid timezone: "NotATimezone"', 1481 + "Invalid time zone specified", 1482 1482 ); 1483 1483 }); 1484 1484 1485 1485 it("should throw for invalid timezone in isMatch", () => { 1486 1486 const date = new Date("2026-03-15T09:00:00Z"); 1487 1487 expect(() => isMatch("0 9 * * *", date, { timezone: "Fake/Zone" })).toThrow( 1488 - 'Invalid timezone: "Fake/Zone"', 1488 + "Invalid time zone specified", 1489 1489 ); 1490 1490 }); 1491 1491 1492 1492 it("should throw for empty timezone string in nextRun", () => { 1493 1493 const from = new Date("2026-03-15T14:00:00Z"); 1494 - expect(() => nextRun("0 9 * * *", { from, timezone: "" })).toThrow('Invalid timezone: ""'); 1494 + expect(() => nextRun("0 9 * * *", { from, timezone: "" })).toThrow("Invalid time zone"); 1495 1495 }); 1496 1496 }); 1497 1497 });