todo.summary
Counts for a todo list's header or footer: total, active, done, overdue, due today, percent done and "3 items left".
1.0.0 · published 2026-10-03 by charlie · Anterra
Pinned by 13 tests, run in TypeScript, Python and Rust.
What it does
The numbers a todo list shows about itself, in one call: how many there are, how many are open and done, how many are overdue or due today, the percentage done and the "N items left" text.
- `overdue` and `dueToday` come from todo.due-status, so they count exactly the todos in the Overdue and Today sections of todo.group-by-due. Done todos are never counted as overdue. - **`percentDone` rounds down** (math.round-div, mode `down`): 199 of 200 done is 99, not 100, so the bar only reads 100% when everything is done, and only reads 0% when nothing is. An empty list is 0. - `itemsLeft` counts active todos, with English plurals: `"0 items left"`, `"1 item left"`, `"2 items left"`. A localised app builds its own string from `active`. - `today` is checked even for an empty list, and a malformed or impossible date anywhere is an error (dates.add-days's messages).
For example
summarise_todos(, 2026-09-28)→ total 0, active 0, done 0, overdue 0, due today 0, percent done 0, items left 0 items left an empty list is all zerossummarise_todos(todos ×3, 2026-09-28)→ total 3, active 0, done 3, overdue 0, due today 0, percent done 100, items left 0 items left everything done is 100%, 0 items left, and a done late todo is not overduesummarise_todos(todos ×1, 2026-09-28)→ total 1, active 1, done 0, overdue 0, due today 1, percent done 0, items left 1 item left one open todo due today is 1 item left
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 summarise_todos(todos: &[Todo], today: &str) -> TodoSummary
| todos | Todo[] | |
| today | date | the user's local date, from the app; never the clock |
| returns | TodoSummary |
The type it declares, generated into your project
/// The numbers a todo list shows about itself.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TodoSummary {
pub total: i64,
/// not done
pub active: i64,
pub done: i64,
/// open and past due
pub overdue: i64,
/// open and due today
pub due_today: i64,
/// done * 100 / total rounded down; 0 for an empty list
pub percent_done: i64,
/// "0 items left", "1 item left", "3 items left"
pub items_left: String,
}
Your code names it in one line, in the file that uses it
fune!(todo.summary@^1); // then call summarise_todos(…)
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_day_of_week::day_of_week; ← from dates.day-of-week ^1.0.0 · built alongside by fune
use super::math_round_div::round_div; ← from math.round-div ^1.0.0 · built alongside by fune
use super::todo_due_status::due_status; ← from todo.due-status ^1.0.0 · built alongside by fune
use super::todo_item::Todo; ← from todo.item ^1.0.0 · built alongside by fune
use super::todo_item_validate_todo::todos_from_value;
/// Counts for a todo list. percent_done rounds down so 100 means everything
/// is done; overdue and due_today agree with due_status.
///
/// # Panics
/// Panics on an impossible or malformed date.
pub fn summarise_todos(todos: &[Todo], today: &str) -> TodoSummary {
day_of_week(today); // a bad today is an error even for an empty list
let mut done = 0;
let mut overdue = 0;
let mut due_today = 0;
for todo in todos {
let status = due_status(todo, today);
if todo.done {
done += 1;
}
if status == "overdue" {
overdue += 1;
}
if status == "today" {
due_today += 1;
}
}
let total = todos.len() as i64;
let active = total - done;
TodoSummary {
total,
active,
done,
overdue,
due_today,
percent_done: if total == 0 { 0 } else { round_div(done * 100, total, "down") },
items_left: if active == 1 { "1 item left".to_string() } else { format!("{} items left", active) },
}
}
pub fn todo_summary_to_value(s: &TodoSummary) -> Value {
Value::obj(vec![
("total", Value::Int(s.total)),
("active", Value::Int(s.active)),
("done", Value::Int(s.done)),
("overdue", Value::Int(s.overdue)),
("dueToday", Value::Int(s.due_today)),
("percentDone", Value::Int(s.percent_done)),
("itemsLeft", Value::str(&s.items_left)),
])
}
pub fn fune_vector(args: &[Value]) -> Value {
todo_summary_to_value(&summarise_todos(&todos_from_value(&args[0]), args[1].as_str()))
}Install
fune build
With that line in your source, in a Rust project (language rust in fune.project), fune build resolves it and its 4 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.summary
The manifest, vectors and README with only the Rust implementation. Install it without the registry with fune add ./todo.summary-1.0.0-rust.fune, or fetch it from a terminal with fune pull todo.summary@1.0.0:rust.
The whole function, every language, is one file too: todo.summary-1.0.0.fune, 66,009 bytes, sha256 c18e7aa1361361be82208402f37176789c3697b153d357e69072e6d82a83db45. 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.summary
after — your function gets the result and the arguments, and returns the final result.
// fune: after todo.summary
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.day-of-week in todo.summary
// fune: replace math.round-div in todo.summary
// fune: replace todo.due-status in todo.summary
// fune: replace todo.item in todo.summary
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.summary --steps.
// fune: step todo.summary 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 | |
|---|---|---|---|
| an empty list is all zeros | , 2026-09-28 | → | total 0, active 0, done 0, overdue 0, due today 0, percent done 0, items left 0 items left |
| everything done is 100%, 0 items left, and a done late todo is not overdue | todos ×3, 2026-09-28 | → | total 3, active 0, done 3, overdue 0, due today 0, percent done 100, items left 0 items left |
| one open todo due today is 1 item left | todos ×1, 2026-09-28 | → | total 1, active 1, done 0, overdue 0, due today 1, percent done 0, items left 1 item left |
| a mixed list | todos ×4, 2026-09-28 | → | total 4, active 3, done 1, overdue 1, due today 1, percent done 25, items left 3 items left |
| two of three done rounds down to 66 | todos ×3, 2026-09-28 | → | total 3, active 1, done 2, overdue 0, due today 0, percent done 66, items left 1 item left |
| one of three done rounds down to 33 | todos ×3, 2026-09-28 | → | total 3, active 2, done 1, overdue 0, due today 0, percent done 33, items left 2 items left |
| 199 of 200 done is 99, not 100 | todos ×200, 2026-09-28 | → | total 200, active 1, done 199, overdue 0, due today 0, percent done 99, items left 1 item left |
| a done todo due today does not count as due today | todos ×2, 2026-09-28 | → | total 2, active 1, done 1, overdue 0, due today 1, percent done 50, items left 1 item left |
| two overdue, from last year and yesterday | todos ×3, 2026-09-28 | → | total 3, active 3, done 0, overdue 2, due today 0, percent done 0, items left 3 items left |
| on Sunday, yesterday's todo is overdue | todos ×2, 2026-10-04 | → | total 2, active 2, done 0, overdue 1, due today 1, percent done 0, items left 2 items left |
Show the other 3 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| a malformed today is an error even for an empty list | , 20260928 | → | error: is not an ISO date |
| a today that never existed | todos ×1, 2026-02-29 | → | error: is not a real calendar date |
| a due date that never existed | todos ×1, 2026-09-28 | → | error: is not a real calendar date |
Files
| Path | Bytes |
|---|---|
| README.md | 898 |
| impl/python.py | 1,074 |
| impl/rust.rs | 1,831 |
| impl/typescript.ts | 1,100 |
| vectors.json | 50,954 |