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.

add benchmarks with other libraries

+770 -15
+29 -12
README.md
··· 161 161 162 162 ## Bundle Size 163 163 164 - cron-fast is extremely lightweight and fully tree-shakeable. Here are the actual bundle sizes for different import scenarios: 164 + cron-fast is extremely lightweight and fully tree-shakeable. Here are the actual bundle sizes for different import scenarios (tested with v0.1.3): 165 165 166 - | Import | Raw | Minified | Gzipped | 167 - | ---------------------------------------------------- | -------- | -------- | ----------- | 168 - | `Full bundle (all exports) ` | 14.22 KB | 6.16 KB | **2.35 KB** | 169 - | `nextRun only ` | 12.64 KB | 5.43 KB | **2.12 KB** | 170 - | `previousRun only ` | 12.65 KB | 5.43 KB | **2.12 KB** | 171 - | `nextRuns only ` | 13.03 KB | 5.58 KB | **2.18 KB** | 172 - | `isValid only ` | 4.00 KB | 1.81 KB | **951 B** | 173 - | `parse only ` | 3.89 KB | 1.76 KB | **926 B** | 174 - | `isMatch only ` | 5.59 KB | 2.54 KB | **1.22 KB** | 175 - | `Validation only (isValid + parse) ` | 4.01 KB | 1.81 KB | **952 B** | 176 - | `Scheduling only (nextRun + previousRun + nextRuns) ` | 13.52 KB | 5.83 KB | **2.20 KB** | 166 + | Import | Raw | Minified | Gzipped | 167 + | ------------------------------------------------------ | -------- | -------- | ----------- | 168 + | `Full bundle (all exports) ` | 14.22 KB | 6.16 KB | **2.35 KB** | 169 + | `nextRun only ` | 12.64 KB | 5.43 KB | **2.12 KB** | 170 + | `previousRun only ` | 12.65 KB | 5.43 KB | **2.12 KB** | 171 + | `nextRuns only ` | 13.03 KB | 5.58 KB | **2.18 KB** | 172 + | `isValid only ` | 4.00 KB | 1.81 KB | **951 B** | 173 + | `parse only ` | 3.89 KB | 1.76 KB | **926 B** | 174 + | `isMatch only ` | 5.59 KB | 2.54 KB | **1.22 KB** | 175 + | `Validation only (isValid + parse) ` | 4.01 KB | 1.81 KB | **952 B** | 176 + | `Scheduling only (nextRun + previousRun + nextRuns) ` | 13.52 KB | 5.83 KB | **2.20 KB** | 177 177 178 178 Import only what you need: 179 179 ··· 257 257 - **Validation**: Always check `isValid()` before parsing user input 258 258 - **Day 0 and 7**: Both represent Sunday in the day-of-week field 259 259 - **Ranges are inclusive**: `1-5` includes both 1 and 5 260 + 261 + ## Performance 262 + 263 + cron-fast is designed for speed and efficiency. Here's how it compares to popular alternatives: 264 + 265 + > Tested with cron-fast v0.1.3, croner v10.0.1, cron-parser v5.5.0, cron-schedule v6.0.0 266 + 267 + | Operation | cron-fast | croner | cron-parser | cron-schedule | 268 + | ------------ | -------------- | --------- | ----------- | ------------- | 269 + | Next run | **368k ops/s** | 29k ops/s | 30k ops/s | 385k ops/s | 270 + | Previous run | **368k ops/s** | 29k ops/s | 27k ops/s | 384k ops/s | 271 + | Validation | **486k ops/s** | 31k ops/s | 89k ops/s | 452k ops/s | 272 + | Parsing | **541k ops/s** | 32k ops/s | 96k ops/s | 475k ops/s | 273 + 274 + See [detailed benchmarks and feature comparison](docs/benchmark-comparison.md) for more information. 275 + 276 + Run benchmarks yourself: `pnpm benchmark` 260 277 261 278 ## License 262 279
+50
docs/benchmark-comparison.md
··· 1 + # Benchmark & Feature Comparison 2 + 3 + > Tested with cron-fast v0.1.3, croner v10.0.1, cron-parser v5.5.0, cron-schedule v6.0.0, cron-validate v1.5.3 4 + > Test on MacBook M1 pro 5 + 6 + ## Performance Benchmarks 7 + 8 + Tested with 1 second per test. 9 + 10 + ### Next Execution Time 11 + 12 + | Library | Avg ops/sec | vs cron-fast | 13 + | ------------- | ----------- | ------------ | 14 + | **cron-fast** | ~368k | baseline | 15 + | cron-schedule | ~385k | 1.0x slower | 16 + | croner | ~29k | 12.7x faster | 17 + | cron-parser | ~30k | 12.4x faster | 18 + 19 + ### Previous Execution Time 20 + 21 + | Library | Avg ops/sec | vs cron-fast | 22 + | ------------- | ----------- | ------------ | 23 + | **cron-fast** | ~368k | baseline | 24 + | cron-schedule | ~384k | 1.0x slower | 25 + | croner | ~29k | 12.7x faster | 26 + | cron-parser | ~27k | 13.5x faster | 27 + 28 + ### Validation 29 + 30 + | Library | Avg ops/sec | vs cron-fast | 31 + | ------------- | ----------- | ------------ | 32 + | **cron-fast** | ~486k | baseline | 33 + | cron-validate | ~675k | 1.4x slower | 34 + | cron-schedule | ~452k | 1.1x faster | 35 + | cron-parser | ~89k | 5.5x faster | 36 + | croner | ~31k | 15.9x faster | 37 + 38 + ### Parsing 39 + 40 + | Library | Avg ops/sec | vs cron-fast | 41 + | ------------- | ----------- | ------------ | 42 + | **cron-fast** | ~541k | baseline | 43 + | cron-validate | ~686k | 1.3x slower | 44 + | cron-schedule | ~475k | 1.1x faster | 45 + | cron-parser | ~96k | 5.7x faster | 46 + | croner | ~32k | 16.7x faster | 47 + 48 + **Note**: cron-validate is validation-only (no scheduling), which explains its speed advantage in parsing/validation. It only checks syntax without calculating dates or handling timezones, making it significantly faster for validation-only use cases. 49 + 50 + Run benchmarks yourself: `pnpm benchmark`
+7 -1
package.json
··· 65 65 "fmt": "oxfmt", 66 66 "fmt:check": "oxfmt --check", 67 67 "typecheck": "tsc --noEmit", 68 + "benchmark": "tsx scripts/benchmark.ts", 69 + "update-benchmark": "tsx scripts/benchmark.ts --update && oxfmt README.md docs/benchmark-comparison.md", 68 70 "bundle-size": "tsx scripts/bundle-size.ts", 69 - "update-bundle-size": "tsx scripts/bundle-size.ts --update" 71 + "update-bundle-size": "tsx scripts/bundle-size.ts --update && oxfmt README.md" 70 72 }, 71 73 "devDependencies": { 72 74 "@cloudflare/vitest-pool-workers": "^0.12.10", 73 75 "@types/node": "^25.2.2", 74 76 "@vitest/coverage-v8": "^3.2.4", 75 77 "@vitest/ui": "^3.2.4", 78 + "cron-parser": "^5.5.0", 79 + "cron-schedule": "^6.0.0", 80 + "cron-validate": "^1.5.3", 81 + "croner": "^10.0.1", 76 82 "esbuild": "^0.27.3", 77 83 "oxfmt": "^0.28.0", 78 84 "oxlint": "^1.43.0",
+76
pnpm-lock.yaml
··· 20 20 '@vitest/ui': 21 21 specifier: ^3.2.4 22 22 version: 3.2.4(vitest@3.2.4) 23 + cron-parser: 24 + specifier: ^5.5.0 25 + version: 5.5.0 26 + cron-schedule: 27 + specifier: ^6.0.0 28 + version: 6.0.0 29 + cron-validate: 30 + specifier: ^1.5.3 31 + version: 1.5.3 32 + croner: 33 + specifier: ^10.0.1 34 + version: 10.0.1 23 35 esbuild: 24 36 specifier: ^0.27.3 25 37 version: 0.27.3 ··· 1129 1141 resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} 1130 1142 engines: {node: '>=18'} 1131 1143 1144 + cron-parser@5.5.0: 1145 + resolution: {integrity: sha512-oML4lKUXxizYswqmxuOCpgFS8BNUJpIu6k/2HVHyaL8Ynnf3wdf9tkns0yRdJLSIjkJ+b0DXHMZEHGpMwjnPww==} 1146 + engines: {node: '>=18'} 1147 + 1148 + cron-schedule@6.0.0: 1149 + resolution: {integrity: sha512-BoZaseYGXOo5j5HUwTaegIog3JJbuH4BbrY9A1ArLjXpy+RWb3mV28F/9Gv1dDA7E2L8kngWva4NWisnLTyfgQ==} 1150 + engines: {node: '>=20'} 1151 + 1152 + cron-validate@1.5.3: 1153 + resolution: {integrity: sha512-jcu8g/3wZL8OBr4MkEcbeIdLpM8pp5Y6UoOlRktcJG3WjgpifijR0s26Yac7ywR0gC2ABtevOsz5mlD3l3gzwA==} 1154 + 1155 + croner@10.0.1: 1156 + resolution: {integrity: sha512-ixNtAJndqh173VQ4KodSdJEI6nuioBWI0V1ITNKhZZsO0pEMoDxz539T4FTTbSZ/xIOSuDnzxLVRqBVSvPNE2g==} 1157 + engines: {node: '>=18.0'} 1158 + 1132 1159 cross-spawn@7.0.6: 1133 1160 resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} 1134 1161 engines: {node: '>= 8'} ··· 1291 1318 lru-cache@10.4.3: 1292 1319 resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} 1293 1320 1321 + luxon@3.7.2: 1322 + resolution: {integrity: sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==} 1323 + engines: {node: '>=12'} 1324 + 1294 1325 magic-string@0.30.21: 1295 1326 resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} 1296 1327 ··· 1376 1407 resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} 1377 1408 engines: {node: ^10 || ^12 || >=14} 1378 1409 1410 + property-expr@2.0.6: 1411 + resolution: {integrity: sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==} 1412 + 1379 1413 quansync@1.0.0: 1380 1414 resolution: {integrity: sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==} 1381 1415 ··· 1480 1514 resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==} 1481 1515 engines: {node: '>=18'} 1482 1516 1517 + tiny-case@1.0.3: 1518 + resolution: {integrity: sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==} 1519 + 1483 1520 tinybench@2.9.0: 1484 1521 resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} 1485 1522 ··· 1514 1551 resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} 1515 1552 engines: {node: '>=14.0.0'} 1516 1553 1554 + toposort@2.0.2: 1555 + resolution: {integrity: sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==} 1556 + 1517 1557 totalist@3.0.1: 1518 1558 resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} 1519 1559 engines: {node: '>=6'} ··· 1554 1594 resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} 1555 1595 engines: {node: '>=18.0.0'} 1556 1596 hasBin: true 1597 + 1598 + type-fest@2.19.0: 1599 + resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} 1600 + engines: {node: '>=12.20'} 1557 1601 1558 1602 typescript@5.9.3: 1559 1603 resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} ··· 1706 1750 1707 1751 youch@4.1.0-beta.10: 1708 1752 resolution: {integrity: sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==} 1753 + 1754 + yup@1.7.1: 1755 + resolution: {integrity: sha512-GKHFX2nXul2/4Dtfxhozv701jLQHdf6J34YDh2cEkpqoo8le5Mg6/LrdseVLrFarmFygZTlfIhHx/QKfb/QWXw==} 1709 1756 1710 1757 snapshots: 1711 1758 ··· 2460 2507 2461 2508 cookie@1.1.1: {} 2462 2509 2510 + cron-parser@5.5.0: 2511 + dependencies: 2512 + luxon: 3.7.2 2513 + 2514 + cron-schedule@6.0.0: {} 2515 + 2516 + cron-validate@1.5.3: 2517 + dependencies: 2518 + yup: 1.7.1 2519 + 2520 + croner@10.0.1: {} 2521 + 2463 2522 cross-spawn@7.0.6: 2464 2523 dependencies: 2465 2524 path-key: 3.1.1 ··· 2634 2693 2635 2694 lru-cache@10.4.3: {} 2636 2695 2696 + luxon@3.7.2: {} 2697 + 2637 2698 magic-string@0.30.21: 2638 2699 dependencies: 2639 2700 '@jridgewell/sourcemap-codec': 1.5.5 ··· 2723 2784 picocolors: 1.1.1 2724 2785 source-map-js: 1.2.1 2725 2786 2787 + property-expr@2.0.6: {} 2788 + 2726 2789 quansync@1.0.0: {} 2727 2790 2728 2791 resolve-pkg-maps@1.0.0: {} ··· 2885 2948 glob: 10.5.0 2886 2949 minimatch: 9.0.5 2887 2950 2951 + tiny-case@1.0.3: {} 2952 + 2888 2953 tinybench@2.9.0: {} 2889 2954 2890 2955 tinyexec@0.3.2: {} ··· 2905 2970 tinyrainbow@3.0.3: {} 2906 2971 2907 2972 tinyspy@4.0.4: {} 2973 + 2974 + toposort@2.0.2: {} 2908 2975 2909 2976 totalist@3.0.1: {} 2910 2977 ··· 2947 3014 optionalDependencies: 2948 3015 fsevents: 2.3.3 2949 3016 3017 + type-fest@2.19.0: {} 3018 + 2950 3019 typescript@5.9.3: {} 2951 3020 2952 3021 unconfig-core@7.4.2: ··· 3101 3170 '@speed-highlight/core': 1.2.14 3102 3171 cookie: 1.1.1 3103 3172 youch-core: 0.3.3 3173 + 3174 + yup@1.7.1: 3175 + dependencies: 3176 + property-expr: 2.0.6 3177 + tiny-case: 1.0.3 3178 + toposort: 2.0.2 3179 + type-fest: 2.19.0
+597
scripts/benchmark.ts
··· 1 + import { nextRun, previousRun, isValid, parse } from "../src/index.js"; 2 + import { Cron } from "croner"; 3 + import { CronExpressionParser } from "cron-parser"; 4 + import { parseCronExpression } from "cron-schedule"; 5 + import cronValidateModule from "cron-validate"; 6 + import { readFileSync, writeFileSync } from "node:fs"; 7 + import { join } from "node:path"; 8 + 9 + const cronValidate = (cronValidateModule as any).default || cronValidateModule; 10 + 11 + // Get library versions 12 + const rootDir = process.cwd(); 13 + const getVersion = (pkg: string) => { 14 + try { 15 + const pkgJson = JSON.parse( 16 + readFileSync(join(rootDir, "node_modules", pkg, "package.json"), "utf-8"), 17 + ); 18 + return pkgJson.version; 19 + } catch { 20 + return "unknown"; 21 + } 22 + }; 23 + 24 + const versions = { 25 + "cron-fast": JSON.parse(readFileSync(join(rootDir, "package.json"), "utf-8")).version, 26 + croner: getVersion("croner"), 27 + "cron-parser": getVersion("cron-parser"), 28 + "cron-schedule": getVersion("cron-schedule"), 29 + "cron-validate": getVersion("cron-validate"), 30 + }; 31 + 32 + interface BenchmarkResult { 33 + name: string; 34 + library: string; 35 + opsPerSecond: number; 36 + avgTimeMs: number; 37 + iterations: number; 38 + } 39 + 40 + function benchmark( 41 + name: string, 42 + library: string, 43 + fn: () => void, 44 + durationMs = 1000, 45 + ): BenchmarkResult { 46 + // Warmup 47 + for (let i = 0; i < 100; i++) { 48 + fn(); 49 + } 50 + 51 + // Actual benchmark - run for fixed duration 52 + let iterations = 0; 53 + const start = performance.now(); 54 + const endTime = start + durationMs; 55 + 56 + while (performance.now() < endTime) { 57 + fn(); 58 + iterations++; 59 + } 60 + 61 + const end = performance.now(); 62 + const totalTimeMs = end - start; 63 + const avgTimeMs = totalTimeMs / iterations; 64 + const opsPerSecond = Math.round(1000 / avgTimeMs); 65 + 66 + return { 67 + name, 68 + library, 69 + opsPerSecond, 70 + avgTimeMs, 71 + iterations, 72 + }; 73 + } 74 + 75 + function formatNumber(num: number): string { 76 + return num.toLocaleString("en-US"); 77 + } 78 + 79 + console.log("🚀 Cron Scheduler Benchmark - Library Comparison\n"); 80 + console.log("Library versions:"); 81 + Object.entries(versions).forEach(([lib, ver]) => { 82 + console.log(` ${lib.padEnd(15)}: v${ver}`); 83 + }); 84 + console.log("\nComparing: cron-fast vs croner vs cron-parser vs cron-schedule vs cron-validate\n"); 85 + 86 + const testCases = [ 87 + { 88 + name: "Next minute", 89 + cron: "* * * * *", 90 + from: new Date("2026-01-15T10:30:00Z"), 91 + }, 92 + { 93 + name: "Specific time today", 94 + cron: "0 15 * * *", 95 + from: new Date("2026-01-15T10:30:00Z"), 96 + }, 97 + { 98 + name: "Next day at midnight", 99 + cron: "0 0 * * *", 100 + from: new Date("2026-01-15T23:30:00Z"), 101 + }, 102 + { 103 + name: "Next Monday", 104 + cron: "0 9 * * 1", 105 + from: new Date("2026-01-15T10:00:00Z"), 106 + }, 107 + { 108 + name: "First of next month", 109 + cron: "0 0 1 * *", 110 + from: new Date("2026-01-15T10:00:00Z"), 111 + }, 112 + { 113 + name: "31st of month (skips Feb)", 114 + cron: "0 12 31 * *", 115 + from: new Date("2026-02-15T10:00:00Z"), 116 + }, 117 + { 118 + name: "Every 15 minutes", 119 + cron: "*/15 * * * *", 120 + from: new Date("2026-01-15T10:07:00Z"), 121 + }, 122 + { 123 + name: "Christmas at noon", 124 + cron: "0 12 25 12 *", 125 + from: new Date("2026-01-15T10:00:00Z"), 126 + }, 127 + ]; 128 + 129 + const results: BenchmarkResult[] = []; 130 + 131 + console.log("Running benchmarks (1 second per test)...\n"); 132 + 133 + // Benchmark 1: nextRun/next execution 134 + console.log("📅 Benchmark 1: Next Execution Time\n"); 135 + 136 + for (const testCase of testCases) { 137 + // cron-fast 138 + results.push( 139 + benchmark(testCase.name, "cron-fast", () => { 140 + nextRun(testCase.cron, { from: testCase.from }); 141 + }), 142 + ); 143 + 144 + // croner 145 + results.push( 146 + benchmark(testCase.name, "croner", () => { 147 + const job = new Cron(testCase.cron, { startAt: testCase.from, paused: true }); 148 + job.nextRun(testCase.from); 149 + }), 150 + ); 151 + 152 + // cron-parser 153 + results.push( 154 + benchmark(testCase.name, "cron-parser", () => { 155 + const interval = CronExpressionParser.parse(testCase.cron, { 156 + currentDate: testCase.from, 157 + }); 158 + interval.next().toDate(); 159 + }), 160 + ); 161 + 162 + // cron-schedule 163 + results.push( 164 + benchmark(testCase.name, "cron-schedule", () => { 165 + const cron = parseCronExpression(testCase.cron); 166 + cron.getNextDate(testCase.from); 167 + }), 168 + ); 169 + } 170 + 171 + // Benchmark 2: previousRun/prev execution 172 + console.log("📅 Benchmark 2: Previous Execution Time\n"); 173 + 174 + const prevResults: BenchmarkResult[] = []; 175 + 176 + for (const testCase of testCases) { 177 + // cron-fast 178 + prevResults.push( 179 + benchmark(testCase.name, "cron-fast", () => { 180 + previousRun(testCase.cron, { from: testCase.from }); 181 + }), 182 + ); 183 + 184 + // croner 185 + prevResults.push( 186 + benchmark(testCase.name, "croner", () => { 187 + const job = new Cron(testCase.cron, { startAt: testCase.from, paused: true }); 188 + job.previousRuns(1, testCase.from); 189 + }), 190 + ); 191 + 192 + // cron-parser 193 + prevResults.push( 194 + benchmark(testCase.name, "cron-parser", () => { 195 + const interval = CronExpressionParser.parse(testCase.cron, { 196 + currentDate: testCase.from, 197 + }); 198 + interval.prev().toDate(); 199 + }), 200 + ); 201 + 202 + // cron-schedule 203 + prevResults.push( 204 + benchmark(testCase.name, "cron-schedule", () => { 205 + const cron = parseCronExpression(testCase.cron); 206 + cron.getPrevDate(testCase.from); 207 + }), 208 + ); 209 + } 210 + 211 + // Benchmark 3: Validation 212 + console.log("✅ Benchmark 3: Validation\n"); 213 + 214 + const validationResults: BenchmarkResult[] = []; 215 + const validationCases = testCases.map((tc) => tc.cron); 216 + 217 + for (const cronExpr of validationCases) { 218 + // cron-fast 219 + validationResults.push( 220 + benchmark(cronExpr, "cron-fast", () => { 221 + isValid(cronExpr); 222 + }), 223 + ); 224 + 225 + // croner 226 + validationResults.push( 227 + benchmark(cronExpr, "croner", () => { 228 + try { 229 + new Cron(cronExpr, { paused: true }); 230 + } catch { 231 + // Invalid 232 + } 233 + }), 234 + ); 235 + 236 + // cron-parser 237 + validationResults.push( 238 + benchmark(cronExpr, "cron-parser", () => { 239 + try { 240 + CronExpressionParser.parse(cronExpr); 241 + } catch { 242 + // Invalid 243 + } 244 + }), 245 + ); 246 + 247 + // cron-schedule 248 + validationResults.push( 249 + benchmark(cronExpr, "cron-schedule", () => { 250 + try { 251 + parseCronExpression(cronExpr); 252 + } catch { 253 + // Invalid 254 + } 255 + }), 256 + ); 257 + 258 + // cron-validate 259 + validationResults.push( 260 + benchmark(cronExpr, "cron-validate", () => { 261 + cronValidate(cronExpr); 262 + }), 263 + ); 264 + } 265 + 266 + // Benchmark 4: Parsing 267 + console.log("🔍 Benchmark 4: Parsing\n"); 268 + 269 + const parseResults: BenchmarkResult[] = []; 270 + 271 + for (const cronExpr of validationCases) { 272 + // cron-fast 273 + parseResults.push( 274 + benchmark(cronExpr, "cron-fast", () => { 275 + parse(cronExpr); 276 + }), 277 + ); 278 + 279 + // croner 280 + parseResults.push( 281 + benchmark(cronExpr, "croner", () => { 282 + new Cron(cronExpr, { paused: true }); 283 + }), 284 + ); 285 + 286 + // cron-parser 287 + parseResults.push( 288 + benchmark(cronExpr, "cron-parser", () => { 289 + CronExpressionParser.parse(cronExpr); 290 + }), 291 + ); 292 + 293 + // cron-schedule 294 + parseResults.push( 295 + benchmark(cronExpr, "cron-schedule", () => { 296 + parseCronExpression(cronExpr); 297 + }), 298 + ); 299 + 300 + // cron-validate 301 + parseResults.push( 302 + benchmark(cronExpr, "cron-validate", () => { 303 + cronValidate(cronExpr); 304 + }), 305 + ); 306 + } 307 + 308 + // Helper function to print results table 309 + function printResultsTable( 310 + title: string, 311 + results: BenchmarkResult[], 312 + libraries: string[], 313 + testNames: string[], 314 + ) { 315 + console.log(`\n${title}\n`); 316 + 317 + const colWidth = 14; 318 + const nameWidth = 33; 319 + 320 + // Header 321 + const header = `│ ${"Test Case".padEnd(nameWidth)} │ ${libraries.map((lib) => lib.padStart(colWidth)).join(" │ ")} │`; 322 + const separator = `├─${"─".repeat(nameWidth)}─┼─${libraries.map(() => "─".repeat(colWidth)).join("─┼─")}─┤`; 323 + const topBorder = `┌─${"─".repeat(nameWidth)}─┬─${libraries.map(() => "─".repeat(colWidth)).join("─┬─")}─┐`; 324 + const bottomBorder = `└─${"─".repeat(nameWidth)}─┴─${libraries.map(() => "─".repeat(colWidth)).join("─┴─")}─┘`; 325 + 326 + console.log(topBorder); 327 + console.log(header); 328 + console.log(separator); 329 + 330 + testNames.forEach((testName) => { 331 + const name = testName.padEnd(nameWidth); 332 + const values = libraries.map((lib) => { 333 + const result = results.find((r) => r.name === testName && r.library === lib); 334 + return result 335 + ? formatNumber(result.opsPerSecond).padStart(colWidth) 336 + : "N/A".padStart(colWidth); 337 + }); 338 + console.log(`│ ${name} │ ${values.join(" │ ")} │`); 339 + }); 340 + 341 + console.log(bottomBorder); 342 + 343 + // Calculate and display averages 344 + console.log("\n📊 Average Performance:"); 345 + libraries.forEach((lib) => { 346 + const libResults = results.filter((r) => r.library === lib); 347 + const avg = Math.round( 348 + libResults.reduce((sum, r) => sum + r.opsPerSecond, 0) / libResults.length, 349 + ); 350 + console.log(` ${lib.padEnd(15)}: ${formatNumber(avg)} ops/sec`); 351 + }); 352 + 353 + // Calculate speedup vs cron-fast 354 + const cronFastResults = results.filter((r) => r.library === "cron-fast"); 355 + const cronFastAvg = Math.round( 356 + cronFastResults.reduce((sum, r) => sum + r.opsPerSecond, 0) / cronFastResults.length, 357 + ); 358 + 359 + console.log("\n⚡ Speedup vs cron-fast:"); 360 + libraries.forEach((lib) => { 361 + if (lib !== "cron-fast") { 362 + const libResults = results.filter((r) => r.library === lib); 363 + const libAvg = Math.round( 364 + libResults.reduce((sum, r) => sum + r.opsPerSecond, 0) / libResults.length, 365 + ); 366 + const speedup = (cronFastAvg / libAvg).toFixed(1); 367 + console.log( 368 + ` ${lib.padEnd(15)}: ${speedup}x ${cronFastAvg > libAvg ? "faster" : "slower"}`, 369 + ); 370 + } 371 + }); 372 + } 373 + 374 + function updateReadme( 375 + nextAvgs: Record<string, number>, 376 + prevAvgs: Record<string, number>, 377 + validationAvgs: Record<string, number>, 378 + parseAvgs: Record<string, number>, 379 + ): void { 380 + const readmePath = join(rootDir, "README.md"); 381 + let readme = readFileSync(readmePath, "utf-8"); 382 + 383 + // Update version line 384 + const versionLine = `> Tested with cron-fast v${versions["cron-fast"]}, croner v${versions.croner}, cron-parser v${versions["cron-parser"]}, cron-schedule v${versions["cron-schedule"]}`; 385 + readme = readme.replace(/> Tested with cron-fast v.*\n/, versionLine + "\n"); 386 + 387 + // Find and replace the performance table 388 + const tableStart = readme.indexOf("| Operation"); 389 + const tableEnd = readme.indexOf("\nSee [detailed benchmarks and feature comparison]"); 390 + 391 + if (tableStart === -1 || tableEnd === -1) { 392 + throw new Error("Could not find performance table in README.md"); 393 + } 394 + 395 + const formatOps = (n: number) => `${Math.round(n / 1000)}k ops/s`; 396 + 397 + const newTable = `| Operation | cron-fast | croner | cron-parser | cron-schedule | 398 + | ------------ | ---------------------- | ---------------------- | ---------------------- | ---------------------- | 399 + | Next run | **${formatOps(nextAvgs["cron-fast"])}** | ${formatOps(nextAvgs.croner)} | ${formatOps(nextAvgs["cron-parser"])} | ${formatOps(nextAvgs["cron-schedule"])} | 400 + | Previous run | **${formatOps(prevAvgs["cron-fast"])}** | ${formatOps(prevAvgs.croner)} | ${formatOps(prevAvgs["cron-parser"])} | ${formatOps(prevAvgs["cron-schedule"])} | 401 + | Validation | **${formatOps(validationAvgs["cron-fast"])}** | ${formatOps(validationAvgs.croner)} | ${formatOps(validationAvgs["cron-parser"])} | ${formatOps(validationAvgs["cron-schedule"])} | 402 + | Parsing | **${formatOps(parseAvgs["cron-fast"])}** | ${formatOps(parseAvgs.croner)} | ${formatOps(parseAvgs["cron-parser"])} | ${formatOps(parseAvgs["cron-schedule"])} | 403 + `; 404 + 405 + readme = readme.slice(0, tableStart) + newTable + readme.slice(tableEnd); 406 + 407 + writeFileSync(readmePath, readme, "utf-8"); 408 + console.log(" ✓ Updated README.md"); 409 + } 410 + 411 + function updateBenchmarkDoc( 412 + results: BenchmarkResult[], 413 + prevResults: BenchmarkResult[], 414 + validationResults: BenchmarkResult[], 415 + parseResults: BenchmarkResult[], 416 + ): void { 417 + const docPath = join(rootDir, "docs/benchmark-comparison.md"); 418 + let doc = readFileSync(docPath, "utf-8"); 419 + 420 + // Update the version line 421 + const versionLine = `> Tested with cron-fast v${versions["cron-fast"]}, croner v${versions.croner}, cron-parser v${versions["cron-parser"]}, cron-schedule v${versions["cron-schedule"]}, cron-validate v${versions["cron-validate"]}`; 422 + doc = doc.replace(/> Tested with.*\n/, versionLine + "\n"); 423 + 424 + // Helper to calculate averages 425 + const calcAvg = (results: BenchmarkResult[], lib: string) => { 426 + const libResults = results.filter((r) => r.library === lib); 427 + return Math.round(libResults.reduce((sum, r) => sum + r.opsPerSecond, 0) / libResults.length); 428 + }; 429 + 430 + // Helper to format ops/sec 431 + const formatOps = (n: number) => `~${Math.round(n / 1000)}k`; 432 + 433 + // Helper to calculate speedup 434 + const calcSpeedup = (cronFastAvg: number, libAvg: number) => { 435 + const ratio = cronFastAvg / libAvg; 436 + return ratio > 1 ? `${ratio.toFixed(1)}x faster` : `${(1 / ratio).toFixed(1)}x slower`; 437 + }; 438 + 439 + // Calculate averages for each benchmark 440 + const nextAvgs = { 441 + "cron-fast": calcAvg(results, "cron-fast"), 442 + croner: calcAvg(results, "croner"), 443 + "cron-parser": calcAvg(results, "cron-parser"), 444 + "cron-schedule": calcAvg(results, "cron-schedule"), 445 + }; 446 + 447 + const prevAvgs = { 448 + "cron-fast": calcAvg(prevResults, "cron-fast"), 449 + croner: calcAvg(prevResults, "croner"), 450 + "cron-parser": calcAvg(prevResults, "cron-parser"), 451 + "cron-schedule": calcAvg(prevResults, "cron-schedule"), 452 + }; 453 + 454 + const validationAvgs = { 455 + "cron-fast": calcAvg(validationResults, "cron-fast"), 456 + croner: calcAvg(validationResults, "croner"), 457 + "cron-parser": calcAvg(validationResults, "cron-parser"), 458 + "cron-schedule": calcAvg(validationResults, "cron-schedule"), 459 + "cron-validate": calcAvg(validationResults, "cron-validate"), 460 + }; 461 + 462 + const parseAvgs = { 463 + "cron-fast": calcAvg(parseResults, "cron-fast"), 464 + croner: calcAvg(parseResults, "croner"), 465 + "cron-parser": calcAvg(parseResults, "cron-parser"), 466 + "cron-schedule": calcAvg(parseResults, "cron-schedule"), 467 + "cron-validate": calcAvg(parseResults, "cron-validate"), 468 + }; 469 + 470 + // Update Next Execution Time table 471 + const nextTable = `| Library | Avg ops/sec | vs cron-fast | 472 + | ------------- | ----------- | ------------ | 473 + | **cron-fast** | ${formatOps(nextAvgs["cron-fast"])} | baseline | 474 + | cron-schedule | ${formatOps(nextAvgs["cron-schedule"])} | ${calcSpeedup(nextAvgs["cron-fast"], nextAvgs["cron-schedule"])} | 475 + | croner | ${formatOps(nextAvgs.croner)} | ${calcSpeedup(nextAvgs["cron-fast"], nextAvgs.croner)} | 476 + | cron-parser | ${formatOps(nextAvgs["cron-parser"])} | ${calcSpeedup(nextAvgs["cron-fast"], nextAvgs["cron-parser"])} |`; 477 + 478 + doc = doc.replace( 479 + /### Next Execution Time\n\n\| Library.*?\n\| -.*?\n(?:\| .*?\n)+/s, 480 + `### Next Execution Time\n\n${nextTable}\n`, 481 + ); 482 + 483 + // Update Previous Execution Time table 484 + const prevTable = `| Library | Avg ops/sec | vs cron-fast | 485 + | ------------- | ----------- | ------------ | 486 + | **cron-fast** | ${formatOps(prevAvgs["cron-fast"])} | baseline | 487 + | cron-schedule | ${formatOps(prevAvgs["cron-schedule"])} | ${calcSpeedup(prevAvgs["cron-fast"], prevAvgs["cron-schedule"])} | 488 + | croner | ${formatOps(prevAvgs.croner)} | ${calcSpeedup(prevAvgs["cron-fast"], prevAvgs.croner)} | 489 + | cron-parser | ${formatOps(prevAvgs["cron-parser"])} | ${calcSpeedup(prevAvgs["cron-fast"], prevAvgs["cron-parser"])} |`; 490 + 491 + doc = doc.replace( 492 + /### Previous Execution Time\n\n\| Library.*?\n\| -.*?\n(?:\| .*?\n)+/s, 493 + `### Previous Execution Time\n\n${prevTable}\n`, 494 + ); 495 + 496 + // Update Validation table 497 + const validationTable = `| Library | Avg ops/sec | vs cron-fast | 498 + | ------------- | ----------- | ------------ | 499 + | **cron-fast** | ${formatOps(validationAvgs["cron-fast"])} | baseline | 500 + | cron-validate | ${formatOps(validationAvgs["cron-validate"])} | ${calcSpeedup(validationAvgs["cron-fast"], validationAvgs["cron-validate"])} | 501 + | cron-schedule | ${formatOps(validationAvgs["cron-schedule"])} | ${calcSpeedup(validationAvgs["cron-fast"], validationAvgs["cron-schedule"])} | 502 + | cron-parser | ${formatOps(validationAvgs["cron-parser"])} | ${calcSpeedup(validationAvgs["cron-fast"], validationAvgs["cron-parser"])} | 503 + | croner | ${formatOps(validationAvgs.croner)} | ${calcSpeedup(validationAvgs["cron-fast"], validationAvgs.croner)} |`; 504 + 505 + doc = doc.replace( 506 + /### Validation\n\n\| Library.*?\n\| -.*?\n(?:\| .*?\n)+/s, 507 + `### Validation\n\n${validationTable}\n`, 508 + ); 509 + 510 + // Update Parsing table 511 + const parseTable = `| Library | Avg ops/sec | vs cron-fast | 512 + | ------------- | ----------- | ------------ | 513 + | **cron-fast** | ${formatOps(parseAvgs["cron-fast"])} | baseline | 514 + | cron-validate | ${formatOps(parseAvgs["cron-validate"])} | ${calcSpeedup(parseAvgs["cron-fast"], parseAvgs["cron-validate"])} | 515 + | cron-schedule | ${formatOps(parseAvgs["cron-schedule"])} | ${calcSpeedup(parseAvgs["cron-fast"], parseAvgs["cron-schedule"])} | 516 + | cron-parser | ${formatOps(parseAvgs["cron-parser"])} | ${calcSpeedup(parseAvgs["cron-fast"], parseAvgs["cron-parser"])} | 517 + | croner | ${formatOps(parseAvgs.croner)} | ${calcSpeedup(parseAvgs["cron-fast"], parseAvgs.croner)} |`; 518 + 519 + doc = doc.replace( 520 + /### Parsing\n\n\| Library.*?\n\| -.*?\n(?:\| .*?\n)+/s, 521 + `### Parsing\n\n${parseTable}\n`, 522 + ); 523 + 524 + writeFileSync(docPath, doc, "utf-8"); 525 + console.log(" ✓ Updated docs/benchmark-comparison.md"); 526 + } 527 + 528 + // Print all results 529 + const testNames = testCases.map((tc) => tc.name); 530 + const allLibraries = ["cron-fast", "croner", "cron-parser", "cron-schedule"]; 531 + const allLibrariesWithValidate = [ 532 + "cron-fast", 533 + "croner", 534 + "cron-parser", 535 + "cron-schedule", 536 + "cron-validate", 537 + ]; 538 + 539 + const shouldUpdate = process.argv.includes("--update"); 540 + 541 + if (!shouldUpdate) { 542 + printResultsTable("📅 Next Execution Time Results", results, allLibraries, testNames); 543 + printResultsTable("📅 Previous Execution Time Results", prevResults, allLibraries, testNames); 544 + printResultsTable( 545 + "✅ Validation Results", 546 + validationResults, 547 + allLibrariesWithValidate, 548 + validationCases, 549 + ); 550 + printResultsTable("🔍 Parsing Results", parseResults, allLibrariesWithValidate, validationCases); 551 + 552 + console.log(`\n✨ All benchmarks completed (1 second per test)\n`); 553 + console.log("💡 Run with --update flag to update README.md and docs/benchmark-comparison.md\n"); 554 + } else { 555 + console.log("\n📝 Updating documentation...\n"); 556 + 557 + // Calculate averages for each benchmark type 558 + const calcAvg = (results: BenchmarkResult[], lib: string) => { 559 + const libResults = results.filter((r) => r.library === lib); 560 + return Math.round(libResults.reduce((sum, r) => sum + r.opsPerSecond, 0) / libResults.length); 561 + }; 562 + 563 + const nextAvgs = { 564 + "cron-fast": calcAvg(results, "cron-fast"), 565 + croner: calcAvg(results, "croner"), 566 + "cron-parser": calcAvg(results, "cron-parser"), 567 + "cron-schedule": calcAvg(results, "cron-schedule"), 568 + }; 569 + 570 + const prevAvgs = { 571 + "cron-fast": calcAvg(prevResults, "cron-fast"), 572 + croner: calcAvg(prevResults, "croner"), 573 + "cron-parser": calcAvg(prevResults, "cron-parser"), 574 + "cron-schedule": calcAvg(prevResults, "cron-schedule"), 575 + }; 576 + 577 + const validationAvgs = { 578 + "cron-fast": calcAvg(validationResults, "cron-fast"), 579 + croner: calcAvg(validationResults, "croner"), 580 + "cron-parser": calcAvg(validationResults, "cron-parser"), 581 + "cron-schedule": calcAvg(validationResults, "cron-schedule"), 582 + "cron-validate": calcAvg(validationResults, "cron-validate"), 583 + }; 584 + 585 + const parseAvgs = { 586 + "cron-fast": calcAvg(parseResults, "cron-fast"), 587 + croner: calcAvg(parseResults, "croner"), 588 + "cron-parser": calcAvg(parseResults, "cron-parser"), 589 + "cron-schedule": calcAvg(parseResults, "cron-schedule"), 590 + "cron-validate": calcAvg(parseResults, "cron-validate"), 591 + }; 592 + 593 + updateReadme(nextAvgs, prevAvgs, validationAvgs, parseAvgs); 594 + updateBenchmarkDoc(results, prevResults, validationResults, parseResults); 595 + 596 + console.log("✅ Documentation updated successfully!\n"); 597 + }
+11 -2
scripts/bundle-size.ts
··· 1 1 #!/usr/bin/env node 2 2 /** 3 3 * Bundle size analysis script 4 - * 4 + * 5 5 * Usage: 6 6 * pnpm bundle-size - Display bundle sizes only 7 7 * pnpm update-bundle-size - Display and update README.md ··· 13 13 14 14 const rootDir = process.cwd(); 15 15 const indexPath = join(rootDir, "src/index.ts"); 16 + 17 + // Get package version 18 + const packageJson = JSON.parse(readFileSync(join(rootDir, "package.json"), "utf-8")); 19 + const version = packageJson.version; 16 20 17 21 const scenarios = [ 18 22 { ··· 133 137 const readmePath = join(rootDir, "README.md"); 134 138 let readme = readFileSync(readmePath, "utf-8"); 135 139 140 + // Update version in the bundle size section header 141 + const versionPattern = /\(tested with v[\d.]+\)/; 142 + readme = readme.replace(versionPattern, `(tested with v${version})`); 143 + 136 144 // Find the bundle size table and replace it 137 145 const tableStart = readme.indexOf("| Import"); 138 146 const tableEnd = readme.indexOf("\n\nImport only what you need:"); ··· 150 158 async function main() { 151 159 const shouldUpdate = process.argv.includes("--update"); 152 160 153 - console.log("📦 Analyzing bundle sizes...\n"); 161 + console.log(`📦 cron-fast v${version} - Bundle Size Analysis\n`); 162 + console.log("Analyzing bundle sizes...\n"); 154 163 155 164 const results: BundleResult[] = []; 156 165