recurrence.ts3.1 KBView on GitHub /**
* RRULE ↔ human, in one place.
*
* Recurrence is stated three times in this app — the event details panel, the composer that
* creates an event, and the custom-rule panel behind "Custom…" — and all three have to agree
* on what `RRULE:FREQ=WEEKLY;BYDAY=TH` reads as. These helpers are that agreement; the control
* itself is components/RecurrenceSelect.tsx.
*/
export const DAYS_OF_WEEK = ['SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA'];
export const DAY_NAMES = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
export const DAY_ABBRS = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];
export function buildWeeklyRRule(date: Date): string {
const dayCode = DAYS_OF_WEEK[date.getDay()];
return `RRULE:FREQ=WEEKLY;BYDAY=${dayCode}`;
}
export function rruleLabel(rrule: string): string {
const rule = rrule.toUpperCase();
const intervalMatch = rule.match(/INTERVAL=(\d+)/);
const interval = intervalMatch ? parseInt(intervalMatch[1]) : 1;
const countMatch = rule.match(/COUNT=(\d+)/);
const untilMatch = rule.match(/UNTIL=(\d{8})/);
let label = '';
if (rule.includes('FREQ=DAILY')) {
label = interval === 1 ? 'Daily' : `Every ${interval} days`;
} else if (rule.includes('FREQ=WEEKLY')) {
const byDayMatch = rule.match(/BYDAY=([A-Z,]+)/);
const days = byDayMatch ? byDayMatch[1].split(',') : [];
const weekdayOnly = days.length === 5 && days.every((d) => ['MO', 'TU', 'WE', 'TH', 'FR'].includes(d));
if (weekdayOnly) {
label = interval === 1 ? 'Every weekday (Mon–Fri)' : `Every ${interval} weeks, Mon–Fri`;
} else {
const dayLabels = days.map((d) => {
const i = DAYS_OF_WEEK.indexOf(d);
return i >= 0 ? DAY_ABBRS[i] : d;
});
const daysStr = dayLabels.join(', ');
label = interval === 1
? `Weekly on ${daysStr}`
: `Every ${interval} weeks on ${daysStr}`;
}
} else if (rule.includes('FREQ=MONTHLY')) {
label = interval === 1 ? 'Monthly' : `Every ${interval} months`;
} else if (rule.includes('FREQ=YEARLY')) {
label = interval === 1 ? 'Annually' : `Every ${interval} years`;
} else {
label = 'Custom';
}
if (countMatch) label += `, ${countMatch[1]} times`;
else if (untilMatch) {
const y = untilMatch[1].slice(0, 4);
const m = untilMatch[1].slice(4, 6);
const d = untilMatch[1].slice(6, 8);
label += `, until ${m}/${d}/${y}`;
}
return label;
}
export function recurrenceOptions(startDate?: Date) {
const dayName = startDate ? DAY_NAMES[startDate.getDay()] : 'the selected day';
return [
{ value: 'none', label: 'Does not repeat' },
{ value: 'RRULE:FREQ=DAILY', label: 'Daily' },
{
value: startDate ? buildWeeklyRRule(startDate) : 'RRULE:FREQ=WEEKLY',
label: `Weekly on ${dayName}`,
},
{ value: 'RRULE:FREQ=MONTHLY', label: 'Monthly' },
{ value: 'RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR', label: 'Every weekday (Mon–Fri)' },
{ value: 'custom', label: 'Custom…' },
];
}
export function parseRecurrenceLabel(recurrence?: string[] | null): string | null {
if (!recurrence || recurrence.length === 0) return null;
return rruleLabel(recurrence[0]);
}