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; } { let value: RegExpMatchArray | null; if (cron === null) { return { type: CronType.MANUAL, value: 0, }; } else if (cron === ONE_TIME_VALUE) { return { type: CronType.ONE_TIME, value: 0, }; } else if (cron.startsWith("RRULE:")) { return parseRrule(cron); } else if ((value = /^0 (\d+) \* \* \*$/i.exec(cron)) !== null) { return { type: CronType.EVERY_DAY, value: parseInt(value[1] ?? ""), }; } else if ((value = /^0 0 \* \* (\d+)$/i.exec(cron)) !== null) { return { type: CronType.EVERY_WEEK, value: parseInt(value[1] ?? ""), }; } else if ((value = /^0 0 (\d+) \* \*$/i.exec(cron)) !== null) { return { type: CronType.EVERY_MONTH, value: parseInt(value[1] ?? ""), }; } else if ((value = /^0 0 1 (\d+) \*$/i.exec(cron)) !== null) { return { type: CronType.EVERY_YEAR, value: parseInt(value[1] ?? ""), }; } else { return { type: CronType.CUSTOM, value: 0, }; } } export function parseRrule(rule: string): { type: CronType; value: number; } { try { const rrule = RRule.parseString(rule); switch (rrule.freq) { case RRule.YEARLY: return { type: CronType.EVERY_N_MONTHS, value: (rrule.interval ?? 1) * 12, }; case RRule.MONTHLY: return { type: CronType.EVERY_N_MONTHS, value: rrule.interval ?? 1, }; case RRule.WEEKLY: return { type: CronType.EVERY_N_WEEKS, value: rrule.interval ?? 1, }; case RRule.DAILY: return { type: CronType.EVERY_N_DAYS, value: rrule.interval ?? 1, }; } } catch { // ignore } return { type: CronType.MANUAL, value: 0, }; } export function getCron( type: CronType, value: number, 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: 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, }).toString(); case CronType.EVERY_N_WEEKS: return new RRule({ freq: RRule.WEEKLY, interval: value, }).toString(); case CronType.EVERY_N_DAYS: return new RRule({ freq: RRule.DAILY, interval: value, }).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 | null, cron: string | null, hash: string, ): Date | null { if (date === null || cron === null || isOneTime(cron)) { return null; } try { if (cron.startsWith("RRULE:")) { try { const rule = new RRule({ ...RRule.parseString(cron), dtstart: date, count: 2, }); return rule.all()[1] ?? null; } catch { // ignore } } const interval = CronExpressionParser.parse(cron, { currentDate: date, hashSeed: hash, }); return interval.next().toDate(); } catch { return null; } }