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
recurrence_label(frequency daily, interval 1, anchor 2026-09-28)→ Every day dailyrecurrence_label(frequency daily, interval 3, anchor 2026-09-28)→ Every 3 days every 3 daysrecurrence_label(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.
def recurrence_label(recurrence: Recurrence) -> str
| recurrence | Recurrence | the rule; the weekday, day of month or date comes from its anchor |
| returns | string | e.g. "Every 3 days", "Every year on 29 Feb" |
Your code names it in one line, in the file that uses it
from fune.todo.recurrence_label import recurrence_label # todo.recurrence-label@^1
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
from .dates_add_days import parse_iso_date ← from dates.add-days ^1.0.0 · built alongside by fune
from .dates_day_of_week import day_name ← from dates.day-of-week ^1.0.0 · built alongside by fune
from .todo_item import Recurrence ← from todo.item ^1.0.0 · built alongside by fune
_MONTHS = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec")
_FREQUENCIES = ("daily", "weekdays", "weekly", "monthly", "yearly")
def _ordinal(n: int) -> str:
"""1st, 2nd, 3rd, 4th ... 11th, 12th, 13th ... 21st, 22nd, 23rd ... 31st."""
if 11 <= n % 100 <= 13:
return f"{n}th"
suffix = {1: "st", 2: "nd", 3: "rd"}.get(n % 10, "th")
return f"{n}{suffix}"
def _every(interval: int, unit: str) -> str:
return f"Every {unit}" if interval == 1 else f"Every {interval} {unit}s"
def recurrence_label(recurrence: Recurrence) -> str:
"""English words for a repeat rule; the weekday, day and date come from the anchor."""
frequency, interval, anchor = recurrence.frequency, recurrence.interval, recurrence.anchor
if frequency not in _FREQUENCIES:
raise ValueError(f'unknown repeat frequency "{frequency}": expected daily, weekdays, weekly, monthly or yearly')
if isinstance(interval, bool) or not isinstance(interval, int) or interval < 1:
raise ValueError(f"repeat interval must be 1 or more, received {interval}")
if frequency == "weekdays" and interval != 1:
raise ValueError(f"weekday repeats cannot skip weeks: interval must be 1, received {interval}")
date = parse_iso_date(anchor)
if frequency == "daily":
return _every(interval, "day")
if frequency == "weekdays":
return "Every weekday"
if frequency == "weekly":
return f"{_every(interval, 'week')} on {day_name(anchor)}"
if frequency == "monthly":
return f"{_every(interval, 'month')} on the {_ordinal(date.day)}"
return f"{_every(interval, 'year')} on {date.day} {_MONTHS[date.month - 1]}"Install
fune build
With that line in your source, in a Python project (language python in fune.project), fune build resolves it and its 3 dependencies, pins them in fune.lock, downloads only the Python 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
The manifest, vectors and README with only the Python implementation. Install it without the registry with fune add ./todo.recurrence-label-1.0.0-python.fune, or fetch it from a terminal with fune pull todo.recurrence-label@1.0.0:python.
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.
| Case | Arguments | Expected | |
|---|---|---|---|
| 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
| Case | Arguments | Expected | |
|---|---|---|---|
| 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
| Path | Bytes |
|---|---|
| README.md | 1,276 |
| impl/python.py | 1,824 |
| impl/rust.rs | 2,438 |
| impl/typescript.ts | 2,112 |
| vectors.json | 4,536 |