import { CronType } from "@/enums"; import { CronExpressionParser } from "cron-parser"; import { RRule } from "rrule"; const ONE_TIME_VALUE = "@one-time"; export function parseCron(cron: string | null): { type: CronType; value: number; date: Date | null; } { let value: RegExpMatchArray | null; if (cron === null) { return { type: CronType.MANUAL, value: 0, date: null, }; } else if (cron === ONE_TIME_VALUE) { return { type: CronType.ONE_TIME, value: 0, date: null, }; } else if (cron.startsWith("RRULE:") || cron.startsWith("DTSTART:")) { return parseRrule(cron); } else if ((value = /^0 (\d+) \* \* \*$/i.exec(cron)) !== null) { return { type: CronType.EVERY_DAY, value: parseInt(value[1] ?? ""), date: null, }; } else if ((value = /^0 0 \* \* (\d+)$/i.exec(cron)) !== null) { return { type: CronType.EVERY_WEEK, value: parseInt(value[1] ?? ""), date: null, }; } else if ((value = /^0 0 (\d+) \* \*$/i.exec(cron)) !== null) { return { type: CronType.EVERY_MONTH, value: parseInt(value[1] ?? ""), date: null, }; } else if ((value = /^0 0 L \* \*$/i.exec(cron)) !== null) { return { type: CronType.EVERY_MONTH, value: 32, date: null, }; } else if ((value = /^0 0 \* \* (\d+)#1$/i.exec(cron)) !== null) { return { type: CronType.EVERY_MONTH, value: -parseInt(value[1] ?? ""), date: null, }; } else if ((value = /^0 0 \* \* (\d+)L$/i.exec(cron)) !== null) { return { type: CronType.EVERY_MONTH, value: -parseInt(value[1] ?? "") - 7, date: null, }; } else if ((value = /^0 0 1 (\d+) \*$/i.exec(cron)) !== null) { return { type: CronType.EVERY_YEAR, value: parseInt(value[1] ?? ""), date: null, }; } else { return { type: CronType.CUSTOM, value: 0, date: null, }; } } export function parseRrule(rule: string): { type: CronType; value: number; date: Date | null; } { try { const rrule = RRule.parseString(rule); switch (rrule.freq) { case RRule.YEARLY: return { type: CronType.EVERY_N_MONTHS, value: (rrule.interval ?? 1) * 12, date: rrule.dtstart ?? null, }; case RRule.MONTHLY: return { type: CronType.EVERY_N_MONTHS, value: rrule.interval ?? 1, date: rrule.dtstart ?? null, }; case RRule.WEEKLY: return { type: CronType.EVERY_N_WEEKS, value: rrule.interval ?? 1, date: rrule.dtstart ?? null, }; case RRule.DAILY: return { type: CronType.EVERY_N_DAYS, value: rrule.interval ?? 1, date: rrule.dtstart ?? null, }; } } catch { // ignore } return { type: CronType.MANUAL, value: 0, date: null, }; } export function getCron( type: CronType, value: number, date: Date, fallback: string, ): string | null { switch (type) { case CronType.ONE_TIME: return ONE_TIME_VALUE; case CronType.EVERY_DAY: return `0 ${value.toFixed(0)} * * *`; case CronType.EVERY_WEEK: return `0 0 * * ${value.toFixed(0)}`; case CronType.EVERY_MONTH: if (value === 32) { return `0 0 L * *`; } if (value < 0 && value >= -7) { return `0 0 * * ${(-value).toFixed(0)}#1`; } if (value < -7 && value >= -14) { return `0 0 * * ${(-value - 7).toFixed(0)}L`; } return `0 0 ${value.toFixed(0)} * *`; case CronType.EVERY_YEAR: return `0 0 1 ${value.toFixed(0)} *`; case CronType.EVERY_N_MONTHS: return new RRule({ freq: RRule.MONTHLY, interval: value, dtstart: date, }).toString(); case CronType.EVERY_N_WEEKS: return new RRule({ freq: RRule.WEEKLY, interval: value, dtstart: date, }).toString(); case CronType.EVERY_N_DAYS: return new RRule({ freq: RRule.DAILY, interval: value, dtstart: date, }).toString(); case CronType.CUSTOM: if (validCron(fallback)) { return fallback; } } return null; } export function isOneTime(cron: string | null): boolean { return cron === ONE_TIME_VALUE; } export function validCron(cron: string): boolean { try { CronExpressionParser.parse(cron); return true; } catch { return false; } } export function nextCronReset( date: Date, cron: string | null, hash: string, ): Date | null { if (cron === null || isOneTime(cron)) { return null; } try { if (cron.startsWith("RRULE:")) { try { const rule = new RRule({ ...RRule.parseString(cron), count: 2, }); return rule.after(date) ?? null; } catch { // ignore } } const interval = CronExpressionParser.parse(cron, { currentDate: date, hashSeed: hash, }); return interval.next().toDate(); } catch { return null; } }