Functional Weave
Code in TypeScript

todo.next-occurrence@1.0.0

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);
}