impl/typescript.ts
2,050 bytes · the TypeScript implementation · view raw
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
import { addDays } from "./dates_add_days.ts"; ← from dates.add-days ^1.0.0 · built alongside by fune
import { dayOfWeek } from "./dates_day_of_week.ts"; ← from dates.day-of-week ^1.0.0 · built alongside by fune
import { nextBillingDate } from "./subscriptions_next_billing_date.ts"; ← from subscriptions.next-billing-date ^1.0.0 · built alongside by fune
import { type Recurrence } from "./todo_item.ts"; ← from todo.item ^1.0.0 · built alongside by fune
const STEPS = new Map<string, "day" | "week" | "month" | "year">([
["daily", "day"],
["weekly", "week"],
["monthly", "month"],
["yearly", "year"],
]);
/**
* When a repeating todo comes round again after it is done.
*
* Every occurrence is the anchor plus a whole number of intervals
* (subscriptions.next-billing-date), never the previous due date plus one, so
* a todo anchored on 31 January is due 28 February and then 31 March, not the
* 28th for ever. The answer is strictly after the later of `due` and `today`:
* finishing early does not make the same occurrence due again, and finishing
* late skips the ones already missed instead of leaving them overdue.
*/
export function nextOccurrence(recurrence: Recurrence, due: string, today: string): string {
const { frequency, interval, anchor } = recurrence;
if (frequency !== "weekdays" && !STEPS.has(frequency)) {
throw new RangeError(`unknown repeat frequency "${frequency}": expected daily, weekdays, weekly, monthly or yearly`);
}
if (!Number.isInteger(interval) || interval < 1) {
throw new RangeError(`repeat interval must be 1 or more, received ${interval}`);
}
if (frequency === "weekdays" && interval !== 1) {
throw new RangeError(`weekday repeats cannot skip weeks: interval must be 1, received ${interval}`);
}
// addDays(x, 0) is the date check: it throws dates.add-days's message.
addDays(anchor, 0);
addDays(due, 0);
addDays(today, 0);
if (anchor > due) throw new RangeError(`repeat anchor ${anchor} is after the due date ${due}`);
const after = due > today ? due : today;
if (frequency === "weekdays") {
let next = addDays(after, 1);
while (dayOfWeek(next) > 5) next = addDays(next, 1);
return next;
}
return nextBillingDate(anchor, STEPS.get(frequency) as "day", interval, after);
}