import type { RecurrenceRule } from "@prisma/client"; const clampDay = (year: number, monthIndex: number, day: number) => { const lastDay = new Date(Date.UTC(year, monthIndex + 1, 0)).getUTCDate(); return Math.min(day, lastDay); }; export const addMonthsWithClamp = (date: Date, months: number) => { const year = date.getUTCFullYear(); const month = date.getUTCMonth(); const day = date.getUTCDate(); const hour = date.getUTCHours(); const minute = date.getUTCMinutes(); const second = date.getUTCSeconds(); const targetMonth = month + months; const targetYear = year + Math.floor(targetMonth / 12); const normalizedMonth = ((targetMonth % 12) + 12) % 12; const clampedDay = clampDay(targetYear, normalizedMonth, day); return new Date(Date.UTC(targetYear, normalizedMonth, clampedDay, hour, minute, second)); }; export const addYearsWithClamp = (date: Date, years: number) => { const targetYear = date.getUTCFullYear() + years; const month = date.getUTCMonth(); const day = date.getUTCDate(); const hour = date.getUTCHours(); const minute = date.getUTCMinutes(); const second = date.getUTCSeconds(); const clampedDay = clampDay(targetYear, month, day); return new Date(Date.UTC(targetYear, month, clampedDay, hour, minute, second)); }; export const nextDueAt = (dueAt: Date, rule: RecurrenceRule) => { const interval = rule.interval ?? 1; switch (rule.type) { case "hourly": return new Date(dueAt.getTime() + interval * 60 * 60 * 1000); case "daily": return new Date(dueAt.getTime() + interval * 24 * 60 * 60 * 1000); case "weekly": return new Date(dueAt.getTime() + interval * 7 * 24 * 60 * 60 * 1000); case "monthly": return addMonthsWithClamp(dueAt, interval); case "yearly": return addYearsWithClamp(dueAt, interval); default: return dueAt; } };