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.
def summarise_todos(todos: Sequence[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
@dataclass(frozen=True)
class TodoSummary:
"""The numbers a todo list shows about itself."""
total: int
#: not done
active: int
done: int
#: open and past due
overdue: int
#: open and due today
due_today: int
#: done * 100 / total rounded down; 0 for an empty list
percent_done: int
#: "0 items left", "1 item left", "3 items left"
items_left: str
Your code names it in one line, in the file that uses it
from fune.todo.summary import summarise_todos # todo.summary@^1
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
from typing import Sequence
from .dates_day_of_week import day_of_week ← from dates.day-of-week ^1.0.0 · built alongside by fune
from .math_round_div import round_div ← from math.round-div ^1.0.0 · built alongside by fune
from .todo_due_status import due_status ← from todo.due-status ^1.0.0 · built alongside by fune
from .todo_item import Todo ← from todo.item ^1.0.0 · built alongside by fune
from .todo_summary_types import TodoSummary
def summarise_todos(todos: Sequence[Todo], today: str) -> TodoSummary:
"""Counts for a todo list; percent_done rounds down so 100 means all done."""
day_of_week(today) # a bad today is an error even for an empty list
done = 0
overdue = 0
due_today = 0
for todo in todos:
status = due_status(todo, today)
if todo.done:
done += 1
if status == "overdue":
overdue += 1
if status == "today":
due_today += 1
total = len(todos)
active = total - done
return TodoSummary(
total=total,
active=active,
done=done,
overdue=overdue,
due_today=due_today,
percent_done=0 if total == 0 else round_div(done * 100, total, "down"),
items_left="1 item left" if active == 1 else "%d items left" % active,
)Install
fune build
With that line in your source, in a Python project (language python in fune.project), fune build resolves it and its 4 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.summary
The manifest, vectors and README with only the Python implementation. Install it without the registry with fune add ./todo.summary-1.0.0-python.fune, or fetch it from a terminal with fune pull todo.summary@1.0.0:python.
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 |