snp-batch-validation/frontend/src/utils/cronPreview.ts

154 lines
4.4 KiB
TypeScript
Raw Normal View 히스토리

/**
* 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;
}