Functional Weave
Code in TypeScript

todo.recurrence-label@1.0.0

impl/typescript.ts

2,112 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 { parseIsoDate } from "./dates_add_days.ts";  ← from dates.add-days ^1.0.0 · built alongside by fune
import { dayName } from "./dates_day_of_week.ts";  ← from dates.day-of-week ^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 MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
const FREQUENCIES = ["daily", "weekdays", "weekly", "monthly", "yearly"];

/** 1st, 2nd, 3rd, 4th ... 11th, 12th, 13th ... 21st, 22nd, 23rd ... 31st. */
function ordinal(n: number): string {
  if (n % 100 >= 11 && n % 100 <= 13) return `${n}th`;
  const suffix = n % 10 === 1 ? "st" : n % 10 === 2 ? "nd" : n % 10 === 3 ? "rd" : "th";
  return `${n}${suffix}`;
}

function every(interval: number, unit: string): string {
  return interval === 1 ? `Every ${unit}` : `Every ${interval} ${unit}s`;
}

/**
 * The English words for a repeat rule, for the chip next to a todo:
 * "Every weekday", "Every 2 weeks on Monday", "Every month on the 31st",
 * "Every year on 29 Feb". The weekday, day and date come from the anchor, the
 * same anchor todo.next-occurrence counts from, and invalid rules fail with
 * its messages.
 */
export function recurrenceLabel(recurrence: Recurrence): string {
  const { frequency, interval, anchor } = recurrence;
  if (!FREQUENCIES.includes(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}`);
  }
  const date = parseIsoDate(anchor);
  switch (frequency) {
    case "daily":
      return every(interval, "day");
    case "weekdays":
      return "Every weekday";
    case "weekly":
      return `${every(interval, "week")} on ${dayName(anchor)}`;
    case "monthly":
      return `${every(interval, "month")} on the ${ordinal(date.day)}`;
    default:
      return `${every(interval, "year")} on ${date.day} ${MONTHS[date.month - 1]}`;
  }
}