Spaces:
Runtime error
Runtime error
| export interface CronOccurrence { | |
| atMs: number | |
| dayKey: string | |
| } | |
| interface ParsedField { | |
| any: boolean | |
| matches: (value: number) => boolean | |
| } | |
| interface ParsedCron { | |
| minute: ParsedField | |
| hour: ParsedField | |
| dayOfMonth: ParsedField | |
| month: ParsedField | |
| dayOfWeek: ParsedField | |
| } | |
| function normalizeCronExpression(raw: string): string { | |
| const trimmed = raw.trim() | |
| const tzSuffixMatch = trimmed.match(/^(.*)\s+\([^)]+\)$/) | |
| return (tzSuffixMatch?.[1] || trimmed).trim() | |
| } | |
| function parseToken(token: string, min: number, max: number): { any: boolean; values: Set<number> } { | |
| const valueSet = new Set<number>() | |
| const trimmed = token.trim() | |
| if (trimmed === '*') { | |
| for (let i = min; i <= max; i += 1) valueSet.add(i) | |
| return { any: true, values: valueSet } | |
| } | |
| for (const part of trimmed.split(',')) { | |
| const section = part.trim() | |
| if (!section) continue | |
| const [rangePart, stepPart] = section.split('/') | |
| const step = stepPart ? Number(stepPart) : 1 | |
| if (!Number.isFinite(step) || step <= 0) continue | |
| if (rangePart === '*') { | |
| for (let i = min; i <= max; i += step) valueSet.add(i) | |
| continue | |
| } | |
| if (rangePart.includes('-')) { | |
| const [fromRaw, toRaw] = rangePart.split('-') | |
| const from = Number(fromRaw) | |
| const to = Number(toRaw) | |
| if (!Number.isFinite(from) || !Number.isFinite(to)) continue | |
| const start = Math.max(min, Math.min(max, from)) | |
| const end = Math.max(min, Math.min(max, to)) | |
| for (let i = start; i <= end; i += step) valueSet.add(i) | |
| continue | |
| } | |
| const single = Number(rangePart) | |
| if (!Number.isFinite(single)) continue | |
| if (single >= min && single <= max) valueSet.add(single) | |
| } | |
| return { any: false, values: valueSet } | |
| } | |
| function parseField(token: string, min: number, max: number): ParsedField { | |
| const parsed = parseToken(token, min, max) | |
| return { | |
| any: parsed.any, | |
| matches: (value: number) => parsed.values.has(value), | |
| } | |
| } | |
| function parseCron(raw: string): ParsedCron | null { | |
| const normalized = normalizeCronExpression(raw) | |
| const parts = normalized.split(/\s+/).filter(Boolean) | |
| if (parts.length !== 5) return null | |
| return { | |
| minute: parseField(parts[0], 0, 59), | |
| hour: parseField(parts[1], 0, 23), | |
| dayOfMonth: parseField(parts[2], 1, 31), | |
| month: parseField(parts[3], 1, 12), | |
| dayOfWeek: parseField(parts[4], 0, 6), | |
| } | |
| } | |
| function matchesDay(parsed: ParsedCron, date: Date): boolean { | |
| const dayOfMonthMatches = parsed.dayOfMonth.matches(date.getDate()) | |
| const dayOfWeekMatches = parsed.dayOfWeek.matches(date.getDay()) | |
| if (parsed.dayOfMonth.any && parsed.dayOfWeek.any) return true | |
| if (parsed.dayOfMonth.any) return dayOfWeekMatches | |
| if (parsed.dayOfWeek.any) return dayOfMonthMatches | |
| return dayOfMonthMatches || dayOfWeekMatches | |
| } | |
| export function buildDayKey(date: Date): string { | |
| const year = date.getFullYear() | |
| const month = String(date.getMonth() + 1).padStart(2, '0') | |
| const day = String(date.getDate()).padStart(2, '0') | |
| return `${year}-${month}-${day}` | |
| } | |
| export function getCronOccurrences( | |
| schedule: string, | |
| rangeStartMs: number, | |
| rangeEndMs: number, | |
| max = 1000 | |
| ): CronOccurrence[] { | |
| if (!schedule || !Number.isFinite(rangeStartMs) || !Number.isFinite(rangeEndMs)) return [] | |
| if (rangeEndMs <= rangeStartMs || max <= 0) return [] | |
| const parsed = parseCron(schedule) | |
| if (!parsed) return [] | |
| const occurrences: CronOccurrence[] = [] | |
| const cursor = new Date(rangeStartMs) | |
| cursor.setSeconds(0, 0) | |
| if (cursor.getTime() < rangeStartMs) { | |
| cursor.setMinutes(cursor.getMinutes() + 1, 0, 0) | |
| } | |
| while (cursor.getTime() < rangeEndMs && occurrences.length < max) { | |
| if ( | |
| parsed.month.matches(cursor.getMonth() + 1) && | |
| matchesDay(parsed, cursor) && | |
| parsed.hour.matches(cursor.getHours()) && | |
| parsed.minute.matches(cursor.getMinutes()) | |
| ) { | |
| occurrences.push({ | |
| atMs: cursor.getTime(), | |
| dayKey: buildDayKey(cursor), | |
| }) | |
| } | |
| cursor.setMinutes(cursor.getMinutes() + 1, 0, 0) | |
| } | |
| return occurrences | |
| } | |