Functional Weave
Code in TypeScript

todo.recurrence-label

English text for a repeating todo's chip: "Every weekday", "Every 2 weeks on Monday", "Every month on the 31st".

1.0.0 · published 2026-10-03 by charlie · Anterra

Pinned by 30 tests, run in TypeScript, Python and Rust.

What it does

The English words for a repeating todo's chip, from its rule:

| rule | label | | --- | --- | | daily, 1 / 3 | Every day / Every 3 days | | weekdays | Every weekday | | weekly, 1 / 2 | Every week on Monday / Every 2 weeks on Monday | | monthly, 1 / 3 | Every month on the 31st / Every 3 months on the 1st | | yearly, 1 / 2 | Every year on 29 Feb / Every 2 years on 1 Jan |

For example

  • recurrenceLabel(frequency daily, interval 1, anchor 2026-09-28) → Every day daily
  • recurrenceLabel(frequency daily, interval 3, anchor 2026-09-28) → Every 3 days every 3 days
  • recurrenceLabel(frequency daily, interval 999, anchor 2026-09-28) → Every 999 days the largest interval

The function

The same function in TypeScript, Python and Rust, pinned by the same tests. Pick your language; the choice follows you around the registry.

export function recurrenceLabel(recurrence: Recurrence): string
recurrenceRecurrencethe rule; the weekday, day of month or date comes from its anchor
returnsstringe.g. "Every 3 days", "Every year on 29 Feb"

Your code names it in one line, in the file that uses it

import { recurrenceLabel } from "#fune/todo.recurrence-label@^1";
impl/typescript.ts · 50 lines · open · 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]}`;
  }
}

Install

fune build

With that line in your source, in a TypeScript project (language typescript in fune.project), fune build resolves it and its 3 dependencies, pins them in fune.lock, downloads only the TypeScript package of each, and builds the code above into your project’s .fune/build, one readable file per capability with a header linking back here. Or pin a range in fune.project and build in one step:

fune add todo.recurrence-label
Download for TypeScript todo.recurrence-label-1.0.0-typescript.fune · 10,201 bytes sha256 fea6a72fb40a0c7d4daa7c4551a30bfa8482c10c5e1bd4c674a33df54e4669c4

The manifest, vectors and README with only the TypeScript implementation. Install it without the registry with fune add ./todo.recurrence-label-1.0.0-typescript.fune, or fetch it from a terminal with fune pull todo.recurrence-label@1.0.0:typescript.

The whole function, every language, is one file too: todo.recurrence-label-1.0.0.fune, 14,770 bytes, sha256 48e103549743096fc75b9fabac022928dc7ddd1bad9a610567b1c5d64667ad4f. It installs into a project of any language.

Customise it in your app

The seams this capability offers. Put a marker directly above a function of your own and fune build wires it into the built code; the package on the registry is not changed, the built file’s header lists it under CUSTOMISED, and fune hooks lists every hook in the project. How hooks work.

before — your function gets the arguments and returns them, changed or not, or throws to refuse the call.

// fune: before todo.recurrence-label

after — your function gets the result and the arguments, and returns the final result.

// fune: after todo.recurrence-label

replace — inside this capability’s code only, calls to a dependency go to your function, with the same signature. Other capabilities that use it are unaffected; write in * to replace it everywhere.

// fune: replace dates.add-days in todo.recurrence-label
// fune: replace dates.day-of-week in todo.recurrence-label
// fune: replace todo.item in todo.recurrence-label

step — your function runs at a numbered point inside the function’s body, receives the in-scope values it names as parameters, and may return replacements. List the points with fune show todo.recurrence-label --steps.

// fune: step todo.recurrence-label after <n|label>

Tests

A version published now needs at least 8 tests for every function, and one that expects the error for each function that throws; the registry refuses it otherwise. fune verify --all runs each case in TypeScript, Python and Rust, and a project runs them again with fune verify. This page lists the cases; it does not run them. The exact JSON is vectors.json.

CaseArgumentsExpected
daily frequency daily, interval 1, anchor 2026-09-28 → Every day
every 3 days frequency daily, interval 3, anchor 2026-09-28 → Every 3 days
the largest interval frequency daily, interval 999, anchor 2026-09-28 → Every 999 days
weekdays frequency weekdays, interval 1, anchor 2026-09-28 → Every weekday
weekly from a Monday frequency weekly, interval 1, anchor 2026-09-28 → Every week on Monday
every 2 weeks from a Sunday frequency weekly, interval 2, anchor 2026-10-04 → Every 2 weeks on Sunday
weekly from a Friday frequency weekly, interval 1, anchor 2026-10-02 → Every week on Friday
monthly on the 31st frequency monthly, interval 1, anchor 2026-01-31 → Every month on the 31st
quarterly on the 1st frequency monthly, interval 3, anchor 2026-10-01 → Every 3 months on the 1st
2nd frequency monthly, interval 1, anchor 2026-10-02 → Every month on the 2nd
Show the other 20 tests
CaseArgumentsExpected
3rd frequency monthly, interval 1, anchor 2026-10-03 → Every month on the 3rd
4th frequency monthly, interval 1, anchor 2026-10-04 → Every month on the 4th
11th, not 11st frequency monthly, interval 1, anchor 2026-10-11 → Every month on the 11th
12th, not 12nd frequency monthly, interval 1, anchor 2026-10-12 → Every month on the 12th
13th, not 13rd frequency monthly, interval 1, anchor 2026-10-13 → Every month on the 13th
21st frequency monthly, interval 1, anchor 2026-10-21 → Every month on the 21st
22nd frequency monthly, interval 2, anchor 2026-10-22 → Every 2 months on the 22nd
23rd frequency monthly, interval 1, anchor 2026-10-23 → Every month on the 23rd
30th frequency monthly, interval 1, anchor 2026-09-30 → Every month on the 30th
yearly on a leap day frequency yearly, interval 1, anchor 2024-02-29 → Every year on 29 Feb
every 2 years on New Year's Day frequency yearly, interval 2, anchor 2027-01-01 → Every 2 years on 1 Jan
yearly at Christmas frequency yearly, interval 1, anchor 2026-12-25 → Every year on 25 Dec
an unknown frequency frequency fortnightly, interval 1, anchor 2026-09-28 → error: unknown repeat frequency "fortnightly": expected daily, weekdays, weekly, monthly or yearly
interval 0 frequency daily, interval 0, anchor 2026-09-28 → error: repeat interval must be 1 or more, received 0
a negative interval frequency weekly, interval -2, anchor 2026-09-28 → error: repeat interval must be 1 or more, received -2
a fractional interval frequency daily, interval 1.5, anchor 2026-09-28 → error: repeat interval must be 1 or more, received 1.5
weekdays cannot skip weeks frequency weekdays, interval 2, anchor 2026-09-28 → error: weekday repeats cannot skip weeks: interval must be 1, received 2
an anchor that never existed frequency monthly, interval 1, anchor 2026-02-30 → error: "2026-02-30" is not a real calendar date
a malformed anchor, even for a daily rule frequency daily, interval 1, anchor 2026-9-28 → error: "2026-9-28" is not an ISO date (YYYY-MM-DD)
an anchor with a trailing newline frequency yearly, interval 1, anchor 2026-09-28 → error: is not an ISO date (YYYY-MM-DD)

More from the author

The weekday, day of the month and date all come from the anchor, the date todo.next-occurrence counts every occurrence from (weekday via dates.day-of-week). Ordinals are 1st 2nd 3rd, 4th to 20th (so 11th, 12th, 13th), then 21st 22nd 23rd and 31st.

A monthly rule on the 29th, 30th or 31st falls on the last day of shorter months, and a yearly rule on 29 Feb falls on 28 Feb outside leap years (todo.next-occurrence); the label names the anchor's day and does not repeat that caveat, so an app that wants to say it should add it.

Invalid rules fail with todo.next-occurrence's messages: an unknown frequency, an interval below 1 (or not a whole number), a weekday rule with an interval other than 1, and an anchor that is not a real ISO date (dates.add-days's messages). Like todo.next-occurrence it does not enforce todo.item's upper limit of 999; todo.item's validators do.

Files

PathBytes
README.md1,276
impl/python.py1,824
impl/rust.rs2,438
impl/typescript.ts2,112
vectors.json4,536