42 lines
1.5 KiB
TypeScript
42 lines
1.5 KiB
TypeScript
/** 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())}`
|
|
}
|