5 段式 cron 只有 5 个字段,写错基本错在三处:*/n从哪个值起步、日和周是并集还是交集、2 月 29 日不是每年都有。
我把这三处做成了一张 30 天日历,然后一个个场景点过去。
同样是「工作日跑一次」,0 3 * * 1-5未来 30 天跑 20 次,0 9-18 * * 1-5就要跑 200 次。
0 0 1 * *30 天只跑 1 次;0 0 29 2 *从 2027 年 2 月 27 日往后数,下一次要等 366 天,之后每次间隔 1461 天。
这些数字不是背下来的,是我在页面上换基准时间一格一格点出来的。
在线案例与源码:https://atomgit.com/deli007/demo_park/tree/main/codearts-cron-expression-lab
成品只有一个 index.html,双击就能用,不装任何依赖,页面自己会跑 12 条自检。
想自己看一遍:git clone仓库后双击 index.html,或者用python -m http.server 8000起个静态服务。
一、为什么值得做:这题人人会写,但很少有人把 30 天跑一遍
搜「cron 表达式」,出来的大多是一张字段范围表加一个在线计算器。
范围表告诉你分钟是 0–59,但不会告诉你*/15展开成 0,15,30,45 还是 15,30,45,60。
也不会告诉你0 0 1 * 1到底是「每月 1 号并且是周一」还是「每月 1 号或者周一」。
我把它做成了一个能点、能换基准时间、还会自己验算的页面。
二、准备环境:打开码道 Web,把需求写成一段话
浏览器打开码道 Web 版:https://devcloud.cn-north-4.huaweicloud.com/chat?source=dmzntgwsf&sourcead=dmzntgwwbwz ,登录后在对话框里写需求,就能拿到一个能跑的单文件应用。
码道有三种使用方式:WebUI(浏览器对话)、TUI(终端命令行)和桌面 IDE(IDE 插件)。本文用 WebUI 版演示。
需求我写了 7 条,事后看最值钱的是第 4 条和第 6 条。
第 4 条要求画 30 天日历并统计执行次数,第 6 条要求页面自带自检脚本并把结果显示出来。
有了这两条,页面就不再是一个「下次执行时间」查询框,而是一台能自己验算的日历。
三、需求原文(可以照着复制)
做一个单文件网页应用 index.html(原生 HTML+CSS+JS,无外链、无 CDN、无构建、可离线打开),主题是 cron 表达式可视化实验室。 1. 顶部一个输入框输入 5 段式 cron 表达式(分 时 日 月 周),旁边给出 8 个常用预设按钮:`0 3 * * *`、`0 3 * * 1-5`、`*/15 * * * *`、`0 9-18 * * 1-5`、`0 0 1 * *`、`30 6 * * 0`、`0 */2 * * *`、`0 0 29 2 *`。 2. 逐字段解析:把输入按 5 段切开,每段单独一列,显示字段名、取值范围的合法区间、这段展开后的具体取值集合(例如 `*/15` 展开成 0,15,30,45;`1-5` 展开成 1~5;`0` 就是 0),非法的一段标红并说明错在哪。 3. 下一次执行:给出从基准时间起接下来 10 次的执行时间(精确到分钟,显示年月日 星期 时分),第一行还要显示「距现在多久」精确到分钟。 4. 未来 30 天日历视图:按天铺一张月历,命中的日期高亮,每格标出当天会跑几次;旁边统计面板显示「未来 30 天共执行 N 次」「平均每天 M 次」「最密集的一天是哪天、跑几次」「平均间隔」。 5. 基准时间可调:提供一个 datetime-local 输入,改它之后所有结果按新基准重算(这样能把 `0 0 29 2 *` 这种闰年表达式验出来)。再加一个「星期字段 0 和 7 都表示周日」的开关说明。 6. 页面上必须有自检脚本并把每条断言结果显示出来(例如 `0 3 * * 1-5` 未来 30 天执行次数、`*/15 * * * *` 平均间隔=15 分钟、`0 0 29 2 *` 在 2028-02 有 1 次而 2027-02 有 0 次、星期 0 与 7 等价),改动参数后重跑自检。 7. 输入非法参数(段数不是 5、写了 99 或者 25 这种越界数字、`*/0`、`1-` 这种残缺范围)要给明确中文提示,保留上一次有效值,界面不清空。 完成后打开预览,让我直接看到运行效果。四、页面长什么样:一张月历,加十条下次执行
最上面是表达式输入框、8 个常用预设、基准时间输入,还有一个「星期字段 0 和 7 都表示周日」的开关。
中间一行是五个字段的逐段解析,每个字段给出合法区间和展开后的取值集合。
最下面是月历和统计面板,命中的日期高亮,角标 ×N 是当天跑几次。
五、实测:换个表达式,数字全变
基准时间统一取 2026-10-17 23:16,把 7 个表达式挨个跑了一遍。
| 表达式 | 未来 30 天执行 | 平均每天 | 平均间隔 | 第一条下次执行 |
|---|---|---|---|---|
0 3 * * 1-5 | 20 次 | 0.7 次 | ≈1895 分钟,1 天 7 小时 35 分 | 2026-10-19 周一 03:00 |
0 9-18 * * 1-5 | 200 次 | 6.7 次 | ≈184 分钟,3 小时 4 分 | 2026-10-19 周一 09:00 |
*/15 * * * * | 2880 次 | 96 次 | ≈15 分钟 | 2026-10-17 周六 23:30 |
*/5 * * * * | 8640 次 | 288 次 | ≈5 分钟 | 2026-10-17 周六 23:20 |
0 */2 * * * | 360 次 | 12 次 | ≈120 分钟 | 2026-10-18 周日 00:00 |
0 0 1 * * | 1 次 | 0 次 | 仅 1 次,无法计算 | 2026-11-01 周日 00:00 |
0 0 * * 6,0 | 10 次 | 0.3 次 | ≈4640 分钟,3 天 5 小时 20 分 | 2026-10-18 周日 00:00 |
*/15 * * * *那行最能说明问题:30 天 2880 次,平均每天 96 次,正好是 24 乘 4。
0 0 * * 6,0是「周六或周日零点」,30 天命中 10 次。
它的字段解析显示取值是 0 和 6,页面把周六统一排到了周日后面。
六、日和周同时受限,是并集不是交集
这一条我用0 0 1 * 1单独验了一遍,语义是「每月 1 号或者每周一」。
30 天里它命中 5 次,分别是 10-19 周一、10-26 周一、11-01 周日、11-02 周一、11-09 周一。
11-01 是 1 号但不是周一,11-02 是周一但不是 1 号,两天都执行了,确实是并集。
如果你要的是「每月 1 号并且是周一」,5 段式 cron 写不出来,只能在脚本里再判断一次日期。
七、闰年:换个基准日,间隔从 1 天变成 1461 天
基准拉到 2028-02-28 10:00,0 0 29 2 *的下一条是 2028 年 2 月 29 日,距基准 14 小时。
基准换回 2027-02-27,同一个表达式的下一条变成 2028-02-29,距基准 366 天,未来 30 天执行 0 次。
再往外是0 0 31 2 *,2 月没有 31 号,页面直接给了一句「在可预见的未来(约 2 年窗口)没有命中任何时间点,请调整表达式或基准时间。」
八、核心那段代码:字段展开只用一个正则
每个字段都走同一个正则,星号、单值、范围、步进、逗号列表都在里面。
constm=String(p).match(/^(\*|\d+)(?:-(\d+))?(?:\/(\d+))?$/);if(!m)returnfail(`片段 "${p}" 无法解析(支持:*、a、a-b、*/n、a-b/n、逗号组合)`);if(step<1)returnfail(`片段 "${p}" 中的步进值${step}必须 ≥ 1`);日和周的关系写在dayHit里,两个字段只要有一个受限,就按并集判断。
functiondayHit(d,E){if(!E.months.has(d.getMonth()+1))returnfalse;constdom=d.getDate(),dow=d.getDay();if(E.domFull&&E.dowFull)returntrue;if(E.domFull)returnE.dows.has(dow);if(E.dowFull)returnE.doms.has(dom);returnE.doms.has(dom)||E.dows.has(dow);}collectNext按天往后扫,命中就展开当天的时分组合,凑够 10 条为止,外面还挂了一条 40 万天的保险丝。
九、验证与踩坑
页面加载后跑 12 条断言,内置参数下 12/12 通过。
前 7 条盯字段展开、平均间隔、闰年和周日等价,后 5 条专门验非法输入。
这里有一条我自己踩到的坑。
自检里「拒绝非法输入 · 1-」那条是绿的,但它绿得不对。
1-只有一段,被段数校验先拦下了,报的是「表达式只有 1 段,需要恰好 5 段」。
真正的残缺范围提示要到字段内部才看得到,我补测了一次0 1- * * *,页面才给出「片段 “1-” 无法解析(支持:、a、a-b、/n、a-b/n、逗号组合)」。
断言绿了不等于断言测到了你想测的东西,这一条我记下来了。
失败路径我也点了一遍,把分钟字段写成*/0。
页面给的原话是「片段 “*/0” 中的步进值 0 必须 ≥ 1」。
结果区不白屏,橙色提示写着「当前表达式无效,以下保留最近一次有效结果;修正表达式后会自动更新。」
另外三种非法输入的原文我也抄下来了。
99 * * * *给出的是「片段 “99” 起始值 99 超出合法区间 0–59」0 3 * *给出的是「表达式只有 4 段,需要恰好 5 段(分 时 日 月 周)。例:0 3 * * 1-5」* * * * * *给出的是「表达式有 6 段,需要恰好 5 段(分 时 日 月 周),请删除多余段」
十、实测数据汇总
| 项目 | 实测值 |
|---|---|
| 页面自检 | 12 / 12 通过 |
| 单文件体积 | 26858 个字符,621 行 |
| 外部依赖 | 1 个 script 标签,0 个 link 标签 |
| 资源请求 | 0 条,性能接口里资源列表是空的 |
*/15 * * * *平均间隔 | ≈15 分钟 |
0 0 29 2 *相邻两次间隔 | 1461 天 |
0 0 */5 * *日字段展开 | 1,6,11,16,21,26,31 |
「资源请求 0 条」是用浏览器性能接口复核的,打开页面到把十来个表达式点完,资源列表一直是空数组。
十一、本地复现
双击 index.html 就能用,不需要装任何依赖。
想走静态服务:python -m http.server 8000,然后访问 http://localhost:8000 。
想看它有没有偷偷联网,按 F12 打开网络面板再切几个表达式,列表里不会多出请求。
十二、说点不足
只支持 5 段式,没有秒字段和年份字段,Quartz 的L、W、#这些扩展一个都没做。
所以「每月最后一个工作日」这类写法,这个页面写不出来。
时区全按浏览器本地时间算,没有夏令时处理,也没有单独的时区选择。
页面在北京打开就算北京时间,放到服务器上看差 8 小时。
*/n一律从字段最小值起步,日字段写*/5展开成 1,6,11,16,21,26,31,不是 5,10,15。
窗口是写死的 30 天,最近 10 次也是写死的,没有做成可调。
页面只做预测,不管执行。
任务跑失败会不会重试、机器关机期间错过的任务要不要补,这些真正运维时会遇到的问题它一概不回答。
直接下载试玩
这个 Demo 已经完整开源,源代码、本文档和全部截图都在公开仓库里,可以直接下载来玩:
- Demo Park 仓库:https://atomgit.com/deli007/demo_park
- 本案例目录:https://atomgit.com/deli007/demo_park/tree/main/codearts-cron-expression-lab
打开上面的链接,进入 codearts-cron-expression-lab 文件夹,把 index.html 下载到本地,用浏览器打开即可运行。
十三、用码道做这个页面的体会
第一条是需求里把期望值写死。
我把「*/15展开 = 0,15,30,45」「周日 0 和 7 等价」直接写进需求,交回来的页面就自带这些断言。
第二条是断言尽量写关系式。
0 3 * * 1-5那次我给的是 18~24 次的区间,因为结果随基准日是周几浮动,写死一个常量以后一改参数就会假红。
第三条是让页面把统计也做出来。
只给一个「下次执行时间」查询框,文章里就没有可对比的数字;加上日历和次数统计之后,才有上面那张表。
第四条是失败路径单独交代。
我在需求里写了非法输入要给中文提示并保留上一次有效值,交回来的就是标红加橙色提示,结果区不清空。
第五条是边界要自己验。
2 月 29 日那段我拿了 2028 和 2027 两个基准日各跑一遍,才敢把 366 天和 1461 天写进来。
第六条是自检全绿不等于页面全对。
前面那条「1-」就是例子,函数的断言绿了,真正拦下它的却是段数校验。
接下来我打算把时区和秒级字段补上,再看看能不能把「每月最后一个工作日」这类 Quartz 写法也加进去。