/**
* Task recurrence helpers
*
* Pure, platform-independent logic for repeating tasks so the scheduling math
* can be unit-tested without storage or UI.
*/
export type RecurrenceKind = 'weekly' | 'monthly' | 'everyNDays' | 'weekdays';
export interface Recurrence {
kind: RecurrenceKind;
/** For 'everyNDays': repeat every N days (>= 1). */
intervalDays?: number;
/** For 'weekdays': days of week to repeat on, 0 = Sunday … 6 = Saturday. */
weekdays?: number[];
}
/** Short weekday labels, indexed by Date.getDay() (0 = Sunday). */
export const WEEKDAY_LABELS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
/** Validate that a recurrence is internally consistent. */
export function isValidRecurrence(recurrence: Recurrence): boolean {
switch (recurrence.kind) {
case 'weekly':
case 'monthly':
return true;
case 'everyNDays':
return Number.isInteger(recurrence.intervalDays) && (recurrence.intervalDays as number) >= 1;
case 'weekdays':
return (
Array.isArray(recurrence.weekdays) &&
recurrence.weekdays.length > 0 &&
recurrence.weekdays.every((d) => Number.isInteger(d) && d >= 0 && d <= 6)
);
default:
return false;
}
}
/**
* Compute the next due date (timestamp) strictly after `from`, preserving the
* time of day.
*/
export function computeNextDueDate(from: number, recurrence: Recurrence): number {
const date = new Date(from);
switch (recurrence.kind) {
case 'weekly':
date.setDate(date.getDate() + 7);
return date.getTime();
case 'everyNDays':
date.setDate(date.getDate() + (recurrence.intervalDays ?? 1));
return date.getTime();
case 'monthly':
date.setMonth(date.getMonth() + 1);
return date.getTime();
case 'weekdays': {
const days = new Set(recurrence.weekdays ?? []);
for (let offset = 1; offset <= 7; offset++) {
const candidate = new Date(from);
candidate.setDate(candidate.getDate() + offset);
if (days.has(candidate.getDay())) {
return candidate.getTime();
}
}
// Fallback (only reachable with an invalid empty weekday set).
date.setDate(date.getDate() + 7);
return date.getTime();
}
}
}
/** Human-readable description of a recurrence, e.g. "Weekly", "Every 3 days". */
export function describeRecurrence(recurrence: Recurrence): string {
switch (recurrence.kind) {
case 'weekly':
return 'Weekly';
case 'monthly':
return 'Monthly';
case 'everyNDays': {
const n = recurrence.intervalDays ?? 1;
return n === 1 ? 'Every day' : `Every ${n} days`;
}
case 'weekdays': {
const labels = (recurrence.weekdays ?? [])
.slice()
.sort((a, b) => a - b)
.map((d) => WEEKDAY_LABELS[d]);
return labels.join(', ');
}
default:
return '';
}
}