Functional Weave
Code in Rust

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 daily
  • recurrence_label(frequency daily, interval 3, anchor 2026-09-28) → Every 3 days every 3 days
  • recurrence_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.

pub fn recurrence_label(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

fune!(todo.recurrence-label@^1);  // then call recurrence_label(…)
impl/rust.rs · 66 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.

use super::funejson::Value;  ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use super::dates_add_days::parse_iso_date;  ← from dates.add-days ^1.0.0 · built alongside by fune
use super::dates_day_of_week::day_name;  ← from dates.day-of-week ^1.0.0 · built alongside by fune
use super::todo_item::{recurrence_from_value, Recurrence};  ← from todo.item ^1.0.0 · built alongside by fune

const MONTHS: [&str; 12] = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];

/// 1st, 2nd, 3rd, 4th ... 11th, 12th, 13th ... 21st, 22nd, 23rd ... 31st.
fn ordinal(n: i64) -> String {
    if (11..=13).contains(&(n % 100)) {
        return format!("{}th", n);
    }
    let suffix = match n % 10 {
        1 => "st",
        2 => "nd",
        3 => "rd",
        _ => "th",
    };
    format!("{}{}", n, suffix)
}

fn every(interval: i64, unit: &str) -> String {
    if interval == 1 {
        format!("Every {}", unit)
    } else {
        format!("Every {} {}s", interval, unit)
    }
}

/// English words for a repeat rule; the weekday, day and date come from the anchor.
///
/// # Panics
/// Panics on an unknown frequency, an interval below 1 (or other than 1 for
/// weekdays), or an anchor that is not a real ISO date.
pub fn recurrence_label(recurrence: &Recurrence) -> String {
    let frequency = recurrence.frequency.as_str();
    let interval = recurrence.interval;
    if !["daily", "weekdays", "weekly", "monthly", "yearly"].contains(&frequency) {
        panic!(
            "unknown repeat frequency \"{}\": expected daily, weekdays, weekly, monthly or yearly",
            frequency
        );
    }
    if interval < 1 {
        panic!("repeat interval must be 1 or more, received {}", interval);
    }
    if frequency == "weekdays" && interval != 1 {
        panic!("weekday repeats cannot skip weeks: interval must be 1, received {}", interval);
    }
    let date = parse_iso_date(&recurrence.anchor);
    match frequency {
        "daily" => every(interval, "day"),
        "weekdays" => "Every weekday".to_string(),
        "weekly" => format!("{} on {}", every(interval, "week"), day_name(&recurrence.anchor)),
        "monthly" => format!("{} on the {}", every(interval, "month"), ordinal(date.day)),
        _ => format!("{} on {} {}", every(interval, "year"), date.day, MONTHS[(date.month - 1) as usize]),
    }
}

pub fn fune_vector(args: &[Value]) -> Value {
    if let Value::Float(f) = args[0].get("interval") {
        panic!("repeat interval must be 1 or more, received {}", f);
    }
    let rule = recurrence_from_value(&args[0]).expect("a recurrence");
    Value::Str(recurrence_label(&rule))
}

Install

fune build

With that line in your source, in a Rust project (language rust in fune.project), fune build resolves it and its 3 dependencies, pins them in fune.lock, downloads only the Rust 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. A crate’s build.rs runs it before every compile. Or pin a range in fune.project and build in one step:

fune add todo.recurrence-label
Download for Rust todo.recurrence-label-1.0.0-rust.fune · 10,545 bytes sha256 27fa877deef4753b603665785cc4f51f34e045d196f8160002350fa8d4f75f43

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

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