/**
* Daily reminder helpers
*
* Pure, platform-independent logic for the daily task reminder so it can be
* unit-tested without the native notifications module.
*/
export interface ReminderTime {
/** Hour of day in 24h format (0-23). */
hour: number;
/** Minute of hour (0-59). */
minute: number;
}
/** The subset of a task the reminder needs to count it. */
export interface ReminderTask {
dueDate?: number;
completed?: boolean;
}
/** One scheduled reminder: when it fires and what it will say. */
export interface ReminderOccurrence {
triggerAt: Date;
taskCount: number;
body: string;
}
/**
* A reminder time for every weekday, indexed like `Date.getDay()`:
* 0 = Sunday ... 6 = Saturday.
*/
export type ReminderSchedule = ReminderTime[];
/** Default reminder time: 12:00 (noon). */
export const DEFAULT_REMINDER_TIME: ReminderTime = { hour: 12, minute: 0 };
/** Weekday names, indexed like `Date.getDay()`. */
export const WEEKDAY_LABELS = [
'Sunday',
'Monday',
'Tuesday',
'Wednesday',
'Thursday',
'Friday',
'Saturday',
];
/** Weekday indices in the order they are shown, Monday first. */
export const WEEKDAY_DISPLAY_ORDER = [1, 2, 3, 4, 5, 6, 0];
/** Build a schedule that reminds at the same time on every weekday. */
export function createUniformSchedule(time: ReminderTime): ReminderSchedule {
return WEEKDAY_LABELS.map(() => ({ ...time }));
}
/** Default schedule: the default time on every weekday. */
export const DEFAULT_REMINDER_SCHEDULE: ReminderSchedule =
createUniformSchedule(DEFAULT_REMINDER_TIME);
/** Copy of `schedule` with `weekday` set to `time`. */
export function withReminderTimeForWeekday(
schedule: ReminderSchedule,
weekday: number,
time: ReminderTime
): ReminderSchedule {
return schedule.map((existing, day) => (day === weekday ? { ...time } : { ...existing }));
}
/** The reminder time configured for the weekday `date` falls on. */
export function getReminderTimeForDate(
schedule: ReminderSchedule,
date: Date
): ReminderTime {
return schedule[date.getDay()];
}
/** How many days of reminders are kept scheduled ahead. */
export const REMINDER_WINDOW_DAYS = 7;
/**
* Build the reminder notification body, reflecting how many tasks are
* scheduled for today.
*/
export function buildReminderBody(taskCount: number): string {
if (!Number.isFinite(taskCount) || taskCount <= 0) {
return 'No tasks scheduled for today';
}
const noun = taskCount === 1 ? 'task' : 'tasks';
return `You have ${taskCount} ${noun} scheduled for today`;
}
/** Count the open tasks due on the calendar day of `date`. */
export function countTasksDueOn(tasks: ReminderTask[], date: Date): number {
const dayStart = new Date(date);
dayStart.setHours(0, 0, 0, 0);
const dayEnd = new Date(dayStart);
dayEnd.setDate(dayEnd.getDate() + 1);
return tasks.filter(task => {
if (task.completed) return false;
if (!task.dueDate) return false;
return task.dueDate >= dayStart.getTime() && task.dueDate < dayEnd.getTime();
}).length;
}
/**
* Build the upcoming reminders for the next `days` days, each at the time its
* own weekday is configured for and carrying the task count of that day.
* Occurrences whose time has already passed are left out, so the first entry
* is always in the future.
*/
export function buildReminderOccurrences(
tasks: ReminderTask[],
schedule: ReminderSchedule,
{ now, days }: { now: Date; days: number }
): ReminderOccurrence[] {
const occurrences: ReminderOccurrence[] = [];
for (let offset = 0; offset < days; offset++) {
const triggerAt = new Date(now);
triggerAt.setDate(triggerAt.getDate() + offset);
const { hour, minute } = getReminderTimeForDate(schedule, triggerAt);
triggerAt.setHours(hour, minute, 0, 0);
if (triggerAt.getTime() <= now.getTime()) {
continue;
}
const taskCount = countTasksDueOn(tasks, triggerAt);
occurrences.push({ triggerAt, taskCount, body: buildReminderBody(taskCount) });
}
return occurrences;
}
/** Format a reminder time as a zero-padded 24h string, e.g. "08:05". */
export function formatReminderTime({ hour, minute }: ReminderTime): string {
const hh = String(hour).padStart(2, '0');
const mm = String(minute).padStart(2, '0');
return `${hh}:${mm}`;
}
/**
* Describe a schedule in one phrase, naming the time when every weekday
* shares it.
*/
export function describeReminderSchedule(schedule: ReminderSchedule): string {
const first = formatReminderTime(schedule[0]);
const uniform = schedule.every(time => formatReminderTime(time) === first);
return uniform ? `${first} every day` : 'at a time set per weekday';
}
/** Validate that a reminder time has in-range, integer hour and minute. */
export function isValidReminderTime(time: ReminderTime): boolean {
const { hour, minute } = time;
return (
Number.isInteger(hour) &&
Number.isInteger(minute) &&
hour >= 0 &&
hour <= 23 &&
minute >= 0 &&
minute <= 59
);
}
/** Validate that a schedule covers every weekday with a valid time. */
export function isValidReminderSchedule(schedule: ReminderSchedule): boolean {
return (
Array.isArray(schedule) &&
schedule.length === WEEKDAY_LABELS.length &&
schedule.every(isValidReminderTime)
);
}