码桶
发现社区成员的开源项目
cron.js3.7 KB
/**
* lib/cron.js — 轻量 cron / 间隔 计算(无第三方依赖)
*
* 支持两种模式:
* 1) 间隔 interval:每 N 分钟 / 每 N 小时(由调度器直接计算)
* 2) 标准 5 字段 cron:分 时 日 月 周
* 例:每 5 分钟 -> 分字段写 星号/5 : * /5 * * * *
* 工作日 09:00 -> 0 9 * * 1-5
* 每月 1 号 00:00 -> 0 0 1 * *
* 1 月里每个周日 -> * * * 1 1 (月+周同时限制时按「或」匹配)
* 字段支持:* ? , - / (步长) 以及 0-7(7 等同周日 0)
* 注意:本注释内用「星号/5」表示,避免和块注释的 * / 冲突。
*/
// 解析单个字段为允许值集合,并返回是否等于通配(*/?)
function parseField(field, min, max) {
const set = new Set();
if (field === '*' || field === '?') {
for (let i = min; i <= max; i++) set.add(i);
return { set, star: true };
}
const parts = field.split(',');
for (let part of parts) {
let step = 1;
let range = part;
if (part.includes('/')) {
const sp = part.split('/');
step = parseInt(sp[1], 10);
range = sp[0] === '' ? '*' : sp[0];
}
let lo, hi;
if (range === '*' || range === '?') {
lo = min; hi = max;
} else if (range.includes('-')) {
const seg = range.split('-');
lo = parseInt(seg[0], 10);
hi = parseInt(seg[1], 10);
} else {
lo = hi = parseInt(range, 10);
}
if (!Number.isFinite(lo) || !Number.isFinite(hi) || step < 1) {
throw new Error('cron 字段格式错误: ' + field);
}
for (let i = lo; i <= hi; i += step) {
if (i >= min && i <= max) set.add(i);
}
}
if (set.size === 0) throw new Error('cron 字段无有效值: ' + field);
return { set, star: false };
}
function parseCron(expr) {
const fields = (expr || '').trim().split(/\s+/);
if (fields.length !== 5) {
throw new Error('cron 表达式需为 5 个字段(分 时 日 月 周),当前为 ' + fields.length + ' 个');
}
const minute = parseField(fields[0], 0, 59);
const hour = parseField(fields[1], 0, 23);
const dom = parseField(fields[2], 1, 31);
const month = parseField(fields[3], 1, 12);
const dow = parseField(fields[4], 0, 7); // 含 7 作为周日别名
return { minute, hour, dom, month, dow };
}
function matches(c, date) {
if (!c.minute.set.has(date.getMinutes())) return false;
if (!c.hour.set.has(date.getHours())) return false;
if (!c.month.set.has(date.getMonth() + 1)) return false;
const dom = date.getDate();
const wd = date.getDay(); // 0=周日
const dowMatches = c.dow.set.has(wd) || (wd === 0 && c.dow.set.has(7));
if (c.dom.star || c.dow.star) {
// 任一为通配:按「与」匹配
if (!c.dom.set.has(dom)) return false;
if (!dowMatches) return false;
} else {
// 日 与 周 同时指定:按 cron 惯例「或」匹配
if (!(c.dom.set.has(dom) || dowMatches)) return false;
}
return true;
}
/**
* 计算 expr 在 afterEpochSec 之后的下一次触发时间(epoch 秒),找不到返回 null。
* @param {string} expr 5 字段 cron
* @param {number} afterEpochSec 起始时间(不含),通常为 Math.floor(Date.now()/1000)
* @param {number} maxYears 搜索上限(年),默认 4 年
*/
function nextCronTime(expr, afterEpochSec, maxYears = 4) {
const c = parseCron(expr);
let t = (afterEpochSec + 1) * 1000; // 从下一秒开始
const limit = t + maxYears * 366 * 24 * 3600 * 1000;
while (t <= limit) {
const d = new Date(t);
if (matches(c, d)) return Math.floor(t / 1000);
t += 60000; // 按分钟步进
}
return null;
}
module.exports = { parseCron, nextCronTime, matches };