Files
Toolbox/apps/app_calendar_reminder/utils/recurrence.ts
T

204 lines
6.7 KiB
TypeScript

export interface Recurrence {
type: 'NONE' | 'DAILY' | 'WEEKLY';
interval: number;
by: number[];
}
export interface EventForReminder {
id: string;
title: string;
start: string;
end?: string | null;
remindAt?: string | null;
recurrence?: string | null;
completed?: boolean;
deleted?: boolean;
}
export function parseRecurrenceStr(str: string | null): Recurrence {
const r = (str || '').trim();
if (!r) return { type: 'NONE', interval: 0, by: [] };
const type = r.startsWith('DAILY') ? 'DAILY' : (r.startsWith('WEEKLY') ? 'WEEKLY' : 'NONE');
const mInt = /interval=(\d+)/.exec(r);
const interval = mInt ? Math.max(1, parseInt(mInt[1], 10)) : 1;
const mBy = /by=([0-9,]+)/.exec(r);
const by = mBy ? mBy[1].split(',').map(s => parseInt(s, 10)).filter(n => n >= 1 && n <= 7) : [];
return { type, interval, by };
}
export function setTimeFromIso(dateObj: Date, timeSourceIso: string): string {
const d = new Date(dateObj.getTime());
const t = timeSourceIso ? safeParseDate(timeSourceIso) : null;
const hh = t ? t.getHours() : 0;
const mm = t ? t.getMinutes() : 0;
d.setHours(hh, mm, 0, 0);
return d.toISOString();
}
export function extractDateStr(dateStr: string): string {
// 支持两种格式:2026-06-28T21:30:00 和 2026-06-28 21:30
if (dateStr.includes('T')) {
return dateStr.split('T')[0];
}
if (dateStr.includes(' ')) {
return dateStr.split(' ')[0];
}
return dateStr;
}
export function safeParseDate(dateStr: string): Date {
// 尝试安全地解析日期,支持多种格式
try {
// 先尝试直接解析
let date = new Date(dateStr);
if (!isNaN(date.getTime())) {
return date;
}
} catch (e) {
// 忽略
}
// 如果是 2026-06-28 21:30 格式,尝试替换空格为 T
if (dateStr.includes(' ')) {
try {
let date = new Date(dateStr.replace(' ', 'T'));
if (!isNaN(date.getTime())) {
return date;
}
} catch (e) {
// 忽略
}
}
// 最后尝试用 Date.UTC 解析
const match = dateStr.match(/(\d{4})-(\d{2})-(\d{2})/);
if (match) {
const year = parseInt(match[1], 10);
const month = parseInt(match[2], 10) - 1;
const day = parseInt(match[3], 10);
return new Date(year, month, day);
}
return new Date();
}
export function shouldShowEventOnDate(
event: { start: string; recurrence: string | null; end?: string | null },
targetDate: Date
): boolean {
const eventDate = extractDateStr(event.start);
const targetDateStr = targetDate.toISOString().split('T')[0];
// 1. 普通事件 - 开始日期就是当天
if (eventDate === targetDateStr) return true;
const evStart = safeParseDate(event.start);
const targetDayStart = new Date(targetDate.getFullYear(), targetDate.getMonth(), targetDate.getDate());
const eventStartDay = new Date(evStart.getFullYear(), evStart.getMonth(), evStart.getDate());
// 2. 持续多天的事件 - 目标日期在 [start, end] 之间
if (event.end) {
const evEnd = safeParseDate(event.end);
const eventEndDay = new Date(evEnd.getFullYear(), evEnd.getMonth(), evEnd.getDate());
// 如果目标日期在事件日期范围内,就显示
if (targetDayStart >= eventStartDay && targetDayStart <= eventEndDay) {
return true;
}
}
// 3. 循环事件
const rec = parseRecurrenceStr(event.recurrence);
if (rec.type === 'NONE') return false;
// 如果目标日期在开始日期之前,不显示
if (targetDayStart < eventStartDay) return false;
// 如果有结束日期,并且目标日期在结束日期之后,不显示
if (event.end) {
const evEnd = safeParseDate(event.end);
const eventEndDay = new Date(evEnd.getFullYear(), evEnd.getMonth(), evEnd.getDate());
if (targetDayStart > eventEndDay) return false;
}
// 没有结束日期的话,就永远显示
if (rec.type === 'DAILY') {
const diffDays = Math.floor((targetDayStart.getTime() - eventStartDay.getTime()) / (24 * 3600 * 1000));
return diffDays % rec.interval === 0;
}
if (rec.type === 'WEEKLY') {
const weeksSinceStart = Math.floor((targetDayStart.getTime() - eventStartDay.getTime()) / (7 * 24 * 3600 * 1000));
const weekday = targetDayStart.getDay() === 0 ? 7 : targetDayStart.getDay();
const by = rec.by.length ? rec.by : [(safeParseDate(event.start).getDay() || 7)];
return by.includes(weekday) && (weeksSinceStart % rec.interval === 0);
}
return false;
}
export function computeUpcomingReminders(
e: EventForReminder,
sinceMs: number,
untilMs: number
): string[] {
const rec = parseRecurrenceStr(e.recurrence || null);
const evStart = safeParseDate(e.start);
const reminders: string[] = [];
if (rec.type === 'NONE') {
const iso = e.remindAt || e.start;
if (!iso) return reminders;
const t = safeParseDate(iso).getTime();
if (t >= sinceMs && t <= untilMs) reminders.push(iso);
return reminders;
}
const startDayMs = new Date(evStart.getFullYear(), evStart.getMonth(), evStart.getDate()).getTime();
const windowStartDayMs = new Date(
new Date(sinceMs).getFullYear(),
new Date(sinceMs).getMonth(),
new Date(sinceMs).getDate()
).getTime();
const windowEndDayMs = new Date(
new Date(untilMs).getFullYear(),
new Date(untilMs).getMonth(),
new Date(untilMs).getDate()
).getTime();
let endDayMs: number;
if (e.end) {
const evEnd = safeParseDate(e.end);
endDayMs = new Date(evEnd.getFullYear(), evEnd.getMonth(), evEnd.getDate()).getTime();
} else {
// 无结束日期时,使用 untilMs 作为结束边界,避免提前终止
endDayMs = windowEndDayMs;
}
const fromDay = Math.max(startDayMs, windowStartDayMs);
const toDay = Math.min(endDayMs, windowEndDayMs);
const timeSourceIso = e.remindAt || e.start;
for (let d = fromDay; d <= toDay; d += 24 * 3600 * 1000) {
const cursor = new Date(d);
if (rec.type === 'DAILY') {
const diffDays = Math.floor((d - startDayMs) / (24 * 3600 * 1000));
if (diffDays % rec.interval === 0) {
const iso = setTimeFromIso(cursor, timeSourceIso);
const t = safeParseDate(iso).getTime();
if (t >= sinceMs && t <= untilMs) reminders.push(iso);
}
} else if (rec.type === 'WEEKLY') {
const weekday = cursor.getDay() === 0 ? 7 : cursor.getDay();
const weeksSinceStart = Math.floor((d - startDayMs) / (7 * 24 * 3600 * 1000));
const by = rec.by.length ? rec.by : [(safeParseDate(e.start).getDay() || 7)];
if (by.includes(weekday) && (weeksSinceStart % rec.interval === 0)) {
const iso = setTimeFromIso(cursor, timeSourceIso);
const t = safeParseDate(iso).getTime();
if (t >= sinceMs && t <= untilMs) reminders.push(iso);
}
}
}
return reminders;
}