Functional Weave
Code in TypeScript

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.

export function initials(name: string): 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

import { initials } from "#fune/text.initials@^1";
impl/typescript.ts · 82 lines · open · raw
/**
 * The Unicode White_Space property, spelled out so all three languages agree:
 * JavaScript's \s, Python's str.isspace and Rust's char::is_whitespace each
 * draw the line slightly differently.
 */
function isWhitespace(cp: number): boolean {
  return (
    (cp >= 0x09 && cp <= 0x0d) ||
    cp === 0x20 ||
    cp === 0x85 ||
    cp === 0xa0 ||
    cp === 0x1680 ||
    (cp >= 0x2000 && cp <= 0x200a) ||
    cp === 0x2028 ||
    cp === 0x2029 ||
    cp === 0x202f ||
    cp === 0x205f ||
    cp === 0x3000
  );
}

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

/** ASCII punctuation and typographic quotes are skipped at the start of a part. */
function isSkippable(ch: string): boolean {
  const cp = ch.codePointAt(0) as number;
  if (cp < 0x80) {
    const alnum = (cp >= 0x30 && cp <= 0x39) || (cp >= 0x41 && cp <= 0x5a) || (cp >= 0x61 && cp <= 0x7a);
    return !alnum;
  }
  return cp === 0x2018 || cp === 0x2019 || cp === 0x201c || cp === 0x201d;
}

/** One-to-one case mappings only, so "ß" stays "ß" in all three languages. */
function upperOne(ch: string): string {
  const mapped = ch.toUpperCase();
  return Array.from(mapped).length === 1 ? mapped : ch;
}

function lowerOne(ch: string): string {
  const mapped = ch.toLowerCase();
  return Array.from(mapped).length === 1 ? mapped : ch;
}

/** Initials from a personal name: "Jean-Paul Sartre" is "JPS". */
export function initials(name: string): string {
  if (typeof name !== "string") {
    throw new TypeError(`initials needs a string, received ${name}`);
  }

  const words: string[][] = [];
  let current: string[] = [];
  for (const ch of name) {
    if (isWhitespace(ch.codePointAt(0) as number)) {
      if (current.length) words.push(current);
      current = [];
    } else {
      current.push(ch);
    }
  }
  if (current.length) words.push(current);

  let out = "";
  words.forEach((word, i) => {
    if (i > 0 && i < words.length - 1 && PARTICLES.has(word.map(lowerOne).join(""))) return;
    // Hyphens and full stops split a word into parts that each give a letter.
    let atPartStart = true;
    for (const ch of word) {
      if (ch === "-" || ch === ".") {
        atPartStart = true;
      } else if (atPartStart && !isSkippable(ch)) {
        out += upperOne(ch);
        atPartStart = false;
      }
    }
  });
  return out;
}

Install

fune build

With that line in your source, in a TypeScript project (language typescript in fune.project), fune build resolves it and nothing else, pins them in fune.lock, downloads only the TypeScript 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 text.initials
Download for TypeScript text.initials-1.0.0-typescript.fune · 7,706 bytes sha256 608d912d8b4696409591c01be7dcfe1ae8533b5c9adccb6dedbf496111f1e89b

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

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