/** 5 段 cron 单字段匹配:支持 *、n、*\/k、a-b、逗号组合;超出子集返回 false */ function fieldMatch(field: string, v: number): boolean { return field.split(',').some((seg) => { const step = /^\*\/(\d+)$/.exec(seg) if (step) return Number(step[1]) > 0 && v % Number(step[1]) === 0 const range = /^(\d+)-(\d+)$/.exec(seg) if (range) return v >= Number(range[1]) && v <= Number(range[2]) return seg === '*' || seg === String(v) }) } /** 计算 5 段 cron 的下次执行时间(本地时区): * 从下一分钟起逐分钟扫描 8 天,表达式非法或窗口内无匹配返回 null。 * 日与周同时受限时按 AND 近似(面板生成的表达式不会同时限制)。 */ export function nextCronRun(expr: string): Date | null { const p = expr.trim().split(/\s+/) if (p.length !== 5) return null const t = new Date() t.setSeconds(0, 0) for (let i = 0; i < 8 * 24 * 60; i++) { t.setMinutes(t.getMinutes() + 1) if ( fieldMatch(p[0], t.getMinutes()) && fieldMatch(p[1], t.getHours()) && fieldMatch(p[2], t.getDate()) && fieldMatch(p[3], t.getMonth() + 1) && fieldMatch(p[4], t.getDay()) ) return new Date(t) } return null } const pad2 = (n: number) => String(n).padStart(2, '0') /** 下次执行的短文案(MM-DD HH:mm);无法计算返回空串 */ export function nextCronRunLabel(expr: string): string { const d = nextCronRun(expr) if (!d) return '' return `${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2(d.getHours())}:${pad2(d.getMinutes())}` }