Functional Weave
Code in Rust

todo.tag-counts

How many todos carry each tag, for a tag sidebar: most used first, open todos only unless asked.

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

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

What it does

How many todos carry each tag, for a tag sidebar or filter chips: a list of `{ tag, count }`, most used first, ties in tag order (code point, via collections.sort-by, so `b-2`, `b10`, `b2`, not natural-number order).

- With `includeDone` false only open todos count, so the sidebar shows what is left to do; a tag whose todos are all done is left out rather than listed with a zero. - A todo counts once per tag, even if its tag list repeats one. - Tags are counted as stored. todo.item keeps them normalised (todo.normalise-tags), so `home` and `#Home` are already one tag.

For example

  • tag_counts(todos ×6, false) → ×3 open todos only: ties go alphabetically
  • tag_counts(todos ×6, true) → ×4 including done todos adds bills and more home and work
  • tag_counts(, true) → an empty list has no tags

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 tag_counts(todos: &[Todo], include_done: bool) -> Vec<TagCount>
todosTodo[]
include_doneboolfalse counts only open todos
returnsTagCount[]count descending, then tag ascending; no zero counts

The type it declares, generated into your project

/// One tag and how many todos carry it.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TagCount {
    pub tag: String,
    pub count: i64,
}

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

fune!(todo.tag-counts@^1);  // then call tag_counts(…)
impl/rust.rs · 43 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::collections_sort_by::sort_by;  ← from collections.sort-by ^1.0.0 · built alongside by fune
use super::todo_item::{todo_from_value, Todo};  ← from todo.item ^1.0.0 · built alongside by fune

/// How many todos carry each tag, for a tag sidebar: a todo counts once per
/// tag, done todos only when `include_done`, most used first, then tag in
/// code-point order.
pub fn tag_counts(todos: &[Todo], include_done: bool) -> Vec<TagCount> {
    let mut counts: Vec<(String, i64)> = Vec::new();
    for todo in todos {
        if todo.done && !include_done {
            continue;
        }
        let mut seen: Vec<&String> = Vec::new();
        for tag in &todo.tags {
            if seen.contains(&tag) {
                continue;
            }
            seen.push(tag);
            match counts.iter_mut().find(|(t, _)| t == tag) {
                Some(entry) => entry.1 += 1,
                None => counts.push((tag.clone(), 1)),
            }
        }
    }
    let rows: Vec<Value> = counts
        .iter()
        .map(|(tag, count)| Value::obj(vec![("tag", Value::str(tag)), ("count", Value::Int(*count))]))
        .collect();
    sort_by(&sort_by(&rows, "tag", "asc"), "count", "desc")
        .iter()
        .map(|r| TagCount { tag: r.get("tag").as_str().to_string(), count: r.get("count").as_i64() })
        .collect()
}

pub fn tag_count_to_value(row: &TagCount) -> Value {
    Value::obj(vec![("tag", Value::str(&row.tag)), ("count", Value::Int(row.count))])
}

pub fn fune_vector(args: &[Value]) -> Value {
    let todos: Vec<Todo> = args[0].as_arr().iter().map(todo_from_value).collect();
    Value::Arr(tag_counts(&todos, args[1].as_bool()).iter().map(tag_count_to_value).collect())
}

Install

fune build

With that line in your source, in a Rust project (language rust in fune.project), fune build resolves it and its 2 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.tag-counts
Download for Rust todo.tag-counts-1.0.0-rust.fune · 11,281 bytes sha256 167df7b61e693c2625506060d9e0b034d10c557cecc2d21f9a1e830c69d2f451

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

The whole function, every language, is one file too: todo.tag-counts-1.0.0.fune, 13,164 bytes, sha256 3666265c41bbbb558c4084744eea9703df9e92cb87e8831c6fd0e5f1d8e56185. 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.tag-counts

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

// fune: after todo.tag-counts

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 collections.sort-by in todo.tag-counts
// fune: replace todo.item in todo.tag-counts

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.tag-counts --steps.

// fune: step todo.tag-counts 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
open todos only: ties go alphabetically todos ×6, false → ×3
including done todos adds bills and more home and work todos ×6, true → ×4
an empty list has no tags , true →
tags only on done todos are left out, not counted as zero todos ×2, false →
todos without tags todos ×2, true →
a tag repeated on one todo counts once todos ×1, true → ×1
ties are code-point order, not natural order: b-2, b10, b2 todos ×1, false → ×4
count beats alphabet todos ×3, false → ×2
one todo's tags come out alphabetically, not in its own order todos ×1, false → ×2
a done todo counts when asked todos ×1, true → ×1

Files

PathBytes
README.md601
impl/python.py775
impl/rust.rs1,632
impl/typescript.ts1,000
vectors.json6,278