154 lines
4.4 KiB
TypeScript
154 lines
4.4 KiB
TypeScript
|
|
/**
|
||
|
|
* Quartz 형식 Cron 표현식의 다음 실행 시간을 계산한다.
|
||
|
|
* 형식: 초 분 시 일 월 요일
|
||
|
|
*/
|
||
|
|
export function getNextExecutions(cron: string, count: number): Date[] {
|
||
|
|
const parts = cron.trim().split(/\s+/);
|
||
|
|
if (parts.length < 6) return [];
|
||
|
|
|
||
|
|
const [secField, minField, hourField, dayField, monthField, dowField] = parts;
|
||
|
|
|
||
|
|
if (hasUnsupportedToken(dayField) || hasUnsupportedToken(dowField)) {
|
||
|
|
return [];
|
||
|
|
}
|
||
|
|
|
||
|
|
const seconds = parseField(secField, 0, 59);
|
||
|
|
const minutes = parseField(minField, 0, 59);
|
||
|
|
const hours = parseField(hourField, 0, 23);
|
||
|
|
const daysOfMonth = parseField(dayField, 1, 31);
|
||
|
|
const months = parseField(monthField, 1, 12);
|
||
|
|
const daysOfWeek = parseDowField(dowField);
|
||
|
|
|
||
|
|
if (!seconds || !minutes || !hours || !months) return [];
|
||
|
|
|
||
|
|
const results: Date[] = [];
|
||
|
|
const now = new Date();
|
||
|
|
const cursor = new Date(now.getFullYear(), now.getMonth(), now.getDate(), now.getHours(), now.getMinutes(), now.getSeconds() + 1);
|
||
|
|
cursor.setMilliseconds(0);
|
||
|
|
|
||
|
|
const limit = new Date(now.getTime() + 365 * 24 * 60 * 60 * 1000);
|
||
|
|
|
||
|
|
while (results.length < count && cursor.getTime() <= limit.getTime()) {
|
||
|
|
const month = cursor.getMonth() + 1;
|
||
|
|
if (!months.includes(month)) {
|
||
|
|
cursor.setMonth(cursor.getMonth() + 1, 1);
|
||
|
|
cursor.setHours(0, 0, 0, 0);
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
const day = cursor.getDate();
|
||
|
|
const dayMatches = daysOfMonth ? daysOfMonth.includes(day) : true;
|
||
|
|
const dowMatches = daysOfWeek ? daysOfWeek.includes(cursor.getDay()) : true;
|
||
|
|
|
||
|
|
const needDayCheck = dayField !== '?' && dowField !== '?';
|
||
|
|
const dayOk = needDayCheck ? dayMatches && dowMatches : dayMatches && dowMatches;
|
||
|
|
|
||
|
|
if (!dayOk) {
|
||
|
|
cursor.setDate(cursor.getDate() + 1);
|
||
|
|
cursor.setHours(0, 0, 0, 0);
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
const hour = cursor.getHours();
|
||
|
|
if (!hours.includes(hour)) {
|
||
|
|
cursor.setHours(cursor.getHours() + 1, 0, 0, 0);
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
const minute = cursor.getMinutes();
|
||
|
|
if (!minutes.includes(minute)) {
|
||
|
|
cursor.setMinutes(cursor.getMinutes() + 1, 0, 0);
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
const second = cursor.getSeconds();
|
||
|
|
if (!seconds.includes(second)) {
|
||
|
|
cursor.setSeconds(cursor.getSeconds() + 1, 0);
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
results.push(new Date(cursor));
|
||
|
|
cursor.setSeconds(cursor.getSeconds() + 1);
|
||
|
|
}
|
||
|
|
|
||
|
|
return results;
|
||
|
|
}
|
||
|
|
|
||
|
|
function hasUnsupportedToken(field: string): boolean {
|
||
|
|
return /[LW#]/.test(field);
|
||
|
|
}
|
||
|
|
|
||
|
|
function parseField(field: string, min: number, max: number): number[] | null {
|
||
|
|
if (field === '?') return null;
|
||
|
|
if (field === '*') return range(min, max);
|
||
|
|
|
||
|
|
const values = new Set<number>();
|
||
|
|
|
||
|
|
for (const part of field.split(',')) {
|
||
|
|
const stepMatch = part.match(/^(.+)\/(\d+)$/);
|
||
|
|
if (stepMatch) {
|
||
|
|
const [, base, stepStr] = stepMatch;
|
||
|
|
const step = parseInt(stepStr, 10);
|
||
|
|
let start = min;
|
||
|
|
let end = max;
|
||
|
|
|
||
|
|
if (base === '*') {
|
||
|
|
start = min;
|
||
|
|
} else if (base.includes('-')) {
|
||
|
|
const [lo, hi] = base.split('-').map(Number);
|
||
|
|
start = lo;
|
||
|
|
end = hi;
|
||
|
|
} else {
|
||
|
|
start = parseInt(base, 10);
|
||
|
|
}
|
||
|
|
|
||
|
|
for (let v = start; v <= end; v += step) {
|
||
|
|
if (v >= min && v <= max) values.add(v);
|
||
|
|
}
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
const rangeMatch = part.match(/^(\d+)-(\d+)$/);
|
||
|
|
if (rangeMatch) {
|
||
|
|
const lo = parseInt(rangeMatch[1], 10);
|
||
|
|
const hi = parseInt(rangeMatch[2], 10);
|
||
|
|
for (let v = lo; v <= hi; v++) {
|
||
|
|
if (v >= min && v <= max) values.add(v);
|
||
|
|
}
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
const num = parseInt(part, 10);
|
||
|
|
if (!isNaN(num) && num >= min && num <= max) {
|
||
|
|
values.add(num);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return values.size > 0 ? Array.from(values).sort((a, b) => a - b) : range(min, max);
|
||
|
|
}
|
||
|
|
|
||
|
|
function parseDowField(field: string): number[] | null {
|
||
|
|
if (field === '?' || field === '*') return null;
|
||
|
|
|
||
|
|
const dayMap: Record<string, string> = {
|
||
|
|
SUN: '0', MON: '1', TUE: '2', WED: '3', THU: '4', FRI: '5', SAT: '6',
|
||
|
|
};
|
||
|
|
|
||
|
|
let normalized = field.toUpperCase();
|
||
|
|
for (const [name, num] of Object.entries(dayMap)) {
|
||
|
|
normalized = normalized.replace(new RegExp(name, 'g'), num);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Quartz uses 1=SUN..7=SAT, convert to JS 0=SUN..6=SAT
|
||
|
|
const parsed = parseField(normalized, 1, 7);
|
||
|
|
if (!parsed) return null;
|
||
|
|
|
||
|
|
return parsed.map((v) => v - 1);
|
||
|
|
}
|
||
|
|
|
||
|
|
function range(min: number, max: number): number[] {
|
||
|
|
const result: number[] = [];
|
||
|
|
for (let i = min; i <= max; i++) result.push(i);
|
||
|
|
return result;
|
||
|
|
}
|