Calendar
recurrence.js 115 lines
Files · 6
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115export function parseLocalDate(str) {
const [y, m, d] = str.split("-").map(Number);
return new Date(y, m - 1, d);
}
export function fmtDate(d) {
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
}
function addDays(d, n) {
const r = new Date(d);
r.setDate(r.getDate() + n);
return r;
}
export function generateRecurrenceDates(rec, rangeStart, rangeEnd) {
if (
!validDate(rec.start_date) ||
!validDate(rangeStart) ||
!validDate(rangeEnd) ||
!rec.frequency
)
return [];
const dates = [];
const start = parseLocalDate(rec.start_date);
const effEnd = rec.end_date ? parseLocalDate(rec.end_date) : null;
const vStart = parseLocalDate(rangeStart);
const vEnd = parseLocalDate(rangeEnd);
const endBound = effEnd && effEnd < vEnd ? effEnd : vEnd;
const interval = Math.max(1, Number(rec.interval_val) || 1);
const freq = rec.frequency;
let d = new Date(Math.max(vStart.getTime(), start.getTime()));
while (d <= endBound) {
if (matchesPattern(d, start, freq, interval, rec)) {
dates.push(fmtDate(d));
}
d = addDays(d, 1);
}
return dates;
}
function matchesPattern(d, start, freq, interval, rec) {
if (freq === "daily") {
const diff = daysBetweenUTC(start, d);
return diff >= 0 && diff % interval === 0;
}
if (freq === "weekly") {
const configured = rec.days_of_week ? JSON.parse(rec.days_of_week) : [];
const dow = configured.length ? configured : [start.getDay()];
if (!dow.includes(d.getDay())) return false;
const weeks = weeksBetweenUTC(start, d);
return weeks >= 0 && weeks % interval === 0;
}
if (freq === "monthly") {
const monthsDiff =
(d.getFullYear() - start.getFullYear()) * 12 +
(d.getMonth() - start.getMonth());
if (monthsDiff < 0 || monthsDiff % interval !== 0) return false;
const targetDay = start.getDate();
const maxDay = new Date(d.getFullYear(), d.getMonth() + 1, 0).getDate();
return d.getDate() === Math.min(targetDay, maxDay);
}
if (freq === "yearly") {
const yearsDiff = d.getFullYear() - start.getFullYear();
if (yearsDiff < 0 || yearsDiff % interval !== 0) return false;
if (d.getMonth() !== start.getMonth()) return false;
const targetDay = start.getDate();
const maxDay = new Date(d.getFullYear(), d.getMonth() + 1, 0).getDate();
return d.getDate() === Math.min(targetDay, maxDay);
}
return false;
}
function daysBetweenUTC(a, b) {
const ua = Date.UTC(a.getFullYear(), a.getMonth(), a.getDate());
const ub = Date.UTC(b.getFullYear(), b.getMonth(), b.getDate());
return Math.round((ub - ua) / 86400000);
}
function weeksBetweenUTC(a, b) {
const aSun = Date.UTC(
a.getFullYear(),
a.getMonth(),
a.getDate() - a.getDay(),
);
const bSun = Date.UTC(
b.getFullYear(),
b.getMonth(),
b.getDate() - b.getDay(),
);
return Math.round((bSun - aSun) / (7 * 86400000));
}
export function validDate(value) {
if (!/^\d{4}-\d{2}-\d{2}$/.test(value || "")) return false;
const date = parseLocalDate(value);
return !Number.isNaN(date.getTime()) && fmtDate(date) === value;
}
export function validateRecurrence(rec) {
if (!["daily", "weekly", "monthly", "yearly"].includes(rec.frequency))
return "Choose a valid repeat frequency.";
if (!validDate(rec.startDate)) return "Choose a valid start date.";
if (rec.endDate && (!validDate(rec.endDate) || rec.endDate < rec.startDate))
return "End date must be on or after the start date.";
if (!Number.isInteger(rec.interval) || rec.interval < 1)
return "Repeat interval must be a positive integer.";
if (
rec.daysOfWeek?.some((day) => !Number.isInteger(day) || day < 0 || day > 6)
)
return "Weekdays must be between Sunday and Saturday.";
return "";
}