// 極簡 5 欄位 cron 比對:minute hour day-of-month month day-of-week。 // 支援星號、步進(星號斜線n)、列表(a,b,c)、範圍(a-b)、單一數字。 // 給 worker 每分鐘輪詢時判斷規則是否「到點」。 function fieldMatches(field: string, value: number, min: number, max: number): boolean { if (field === "*") return true; for (const part of field.split(",")) { const token = part.trim(); if (token === "") continue; // step: */n 或 a-b/n 或 */n const stepMatch = token.match(/^(\*|\d+(?:-\d+)?)\/(\d+)$/); if (stepMatch) { const step = parseInt(stepMatch[2], 10); if (step <= 0) continue; let rangeStart = min; let rangeEnd = max; if (stepMatch[1] !== "*") { const rangeParts = stepMatch[1].split("-"); rangeStart = parseInt(rangeParts[0], 10); rangeEnd = rangeParts[1] !== undefined ? parseInt(rangeParts[1], 10) : max; } if (value < rangeStart || value > rangeEnd) continue; if ((value - rangeStart) % step === 0) return true; continue; } // range: a-b const rangeMatch = token.match(/^(\d+)-(\d+)$/); if (rangeMatch) { const start = parseInt(rangeMatch[1], 10); const end = parseInt(rangeMatch[2], 10); if (value >= start && value <= end) return true; continue; } // single if (/^\d+$/.test(token) && parseInt(token, 10) === value) return true; } return false; } export function isValidCron(expr: string): boolean { const fields = expr.trim().split(/\s+/); return fields.length === 5; } export function cronMatches(expr: string, date: Date = new Date()): boolean { const fields = expr.trim().split(/\s+/); if (fields.length !== 5) return false; const [minute, hour, dom, month, dow] = fields; const normalizedDow = date.getDay(); // 0-6 (Sun=0) return ( fieldMatches(minute, date.getMinutes(), 0, 59) && fieldMatches(hour, date.getHours(), 0, 23) && fieldMatches(dom, date.getDate(), 1, 31) && fieldMatches(month, date.getMonth() + 1, 1, 12) && fieldMatches(dow, normalizedDow, 0, 6) ); }