Files
notify/backend/src/services/recurrence.ts
Michael Dong a98e12f286 first commit
2026-02-05 11:24:40 +08:00

52 lines
1.8 KiB
TypeScript

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