Functional Weave
Code in Rust

text.initials

Initials from a personal name for avatars and signatures: "Jean-Paul Sartre" is "JPS".

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

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

What it does

The initials of a personal name, for an avatar, a monogram or an audit column: "John Smith" is "JS", "Jean-Paul Sartre" is "JPS", "J.R.R. Tolkien" is "JRRT".

## The rules

For example

  • initials(John Smith) → JS first and last name
  • initials( john smith ) → JS lower case and messy spacing
  • initials(Jean-Paul Sartre) → JPS a hyphenated forename gives two letters

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 initials(name: &str) -> String
namestringa personal name, in any case and spacing
returnsstringupper-case letters, no spaces or full stops; empty if there are none

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

fune!(text.initials@^1);  // then call initials(…)
impl/rust.rs · 97 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

/// Skipped, not counted, in the middle of a name: "Ludwig van Beethoven" is "LB".
const PARTICLES: [&str; 20] = [
    "van", "von", "de", "da", "di", "del", "della", "der", "den", "des", "du",
    "la", "le", "ter", "ten", "dos", "das", "do", "bin", "ibn",
];

/// The Unicode White_Space property, spelled out so all three languages agree.
fn is_whitespace(ch: char) -> bool {
    let cp = ch as u32;
    (0x09..=0x0d).contains(&cp)
        || cp == 0x20
        || cp == 0x85
        || cp == 0xa0
        || cp == 0x1680
        || (0x2000..=0x200a).contains(&cp)
        || cp == 0x2028
        || cp == 0x2029
        || cp == 0x202f
        || cp == 0x205f
        || cp == 0x3000
}

/// ASCII punctuation and typographic quotes are skipped at the start of a part.
fn is_skippable(ch: char) -> bool {
    if ch.is_ascii() {
        return !ch.is_ascii_alphanumeric();
    }
    matches!(ch as u32, 0x2018 | 0x2019 | 0x201c | 0x201d)
}

/// One-to-one case mappings only, so 'ß' stays 'ß' in all three languages.
fn upper_one(ch: char) -> char {
    let mapped: Vec<char> = ch.to_uppercase().collect();
    if mapped.len() == 1 {
        mapped[0]
    } else {
        ch
    }
}

fn lower_one(ch: char) -> char {
    let mapped: Vec<char> = ch.to_lowercase().collect();
    if mapped.len() == 1 {
        mapped[0]
    } else {
        ch
    }
}

/// Initials from a personal name: "Jean-Paul Sartre" is "JPS".
pub fn initials(name: &str) -> String {
    let mut words: Vec<Vec<char>> = Vec::new();
    let mut current: Vec<char> = Vec::new();
    for ch in name.chars() {
        if is_whitespace(ch) {
            if !current.is_empty() {
                words.push(std::mem::take(&mut current));
            }
        } else {
            current.push(ch);
        }
    }
    if !current.is_empty() {
        words.push(current);
    }

    let mut out = String::new();
    let count = words.len();
    for (i, word) in words.iter().enumerate() {
        let lower: String = word.iter().map(|&c| lower_one(c)).collect();
        if i > 0 && i + 1 < count && PARTICLES.contains(&lower.as_str()) {
            continue;
        }
        // Hyphens and full stops split a word into parts that each give a letter.
        let mut at_part_start = true;
        for &ch in word {
            if ch == '-' || ch == '.' {
                at_part_start = true;
            } else if at_part_start && !is_skippable(ch) {
                out.push(upper_one(ch));
                at_part_start = false;
            }
        }
    }
    out
}

pub fn fune_vector(args: &[Value]) -> Value {
    // Refuse what the typed signature cannot hold, with the wording TypeScript
    // and Python use, rather than let the conversion below quietly change it.
    if !matches!(args[0], Value::Str(_)) {
        panic!("initials needs a string, received {:?}", args[0]);
    }
    Value::Str(initials(args[0].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 nothing else, 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 text.initials
Download for Rust text.initials-1.0.0-rust.fune · 8,103 bytes sha256 072657c1feee4c6fcff10bdeab5665e8f4e2d9bf2bba4bd76fab353062c0ac51

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

The whole function, every language, is one file too: text.initials-1.0.0.fune, 13,352 bytes, sha256 e40807e13cc5bc3f27e7c437cea64cf86e010c14d9b57bb11ef839683ec2f8d7. 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 text.initials

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

// fune: after text.initials

replace — it requires no other capability, so there is no dependency to replace.

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 text.initials --steps.

// fune: step text.initials 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
first and last name John Smith → JS
lower case and messy spacing john smith → JS
a hyphenated forename gives two letters Jean-Paul Sartre → JPS
dotted initials each count J.R.R. Tolkien → JRRT
a particle in the middle is skipped Ludwig van Beethoven → LB
two particles in the middle are both skipped José María de la Cruz → JMC
a particle as the first word counts Van Morrison → VM
an apostrophe does not split a surname Mary O'Brien → MO
accented letters are upper-cased émile zola → ÉZ
a single name gives one letter Madonna → M
Show the other 7 tests
CaseArgumentsExpected
leading brackets are skipped (John) Smith → JS
typographic quotes around a nickname are skipped Christopher “Kit” Marlowe → CKM
a script without case keeps its first characters 李 小龙 → 李小
no-break spaces separate words Anne Marie → AM
the empty string has no initials →
punctuation alone has no initials - . →
a name that is not a string is an error — → error: initials needs a string

More from the author

- Words are separated by whitespace (the Unicode White_Space set, listed explicitly so all three languages agree), and each word is split again at hyphens and full stops, so hyphenated forenames and dotted initials each contribute a letter. - Each part contributes its first character, upper-cased. Leading ASCII punctuation and typographic quotes are skipped first, so "(John)" gives "J" and "“Kit”" gives "K". An apostrophe inside a part is not a split: "O'Brien" gives "O", not "OB". - Name particles (van, von, de, da, di, del, della, der, den, des, du, la, le, ter, ten, dos, das, do, bin, ibn) are skipped when they are neither the first nor the last word: "Ludwig van Beethoven" is "LB", "José María de la Cruz" is "JMC". As the first word they count: "Van Morrison" is "VM". - The result has no spaces or full stops. Add them yourself ("J. S.") if the design wants them. There is no length cap: take the first two characters if your avatar only has room for two.

## Letters outside ASCII

Upper-casing uses each language's own per-character mapping, and only when it maps to exactly one character, so "émile" gives "É" everywhere and "ß" stays "ß". A script without case contributes its first character as it is: "李 小龙" gives "李小". Those are the characters a Latin-script reader would expect; they are not necessarily how people who write in that script abbreviate names.

Characters are code points: a letter written as a base letter plus a combining accent contributes only the base letter.

The name must be a string; anything else is an error.

Files

PathBytes
README.md1,789
impl/python.py2,366
impl/rust.rs2,942
impl/typescript.ts2,535
vectors.json1,643