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 nameinitials( john smith )→ JS lower case and messy spacinginitials(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.
def initials(name: str) -> str
| name | string | a personal name, in any case and spacing |
| returns | string | upper-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
from fune.text.initials import initials # text.initials@^1
from typing import List
#: Skipped, not counted, in the middle of a name: "Ludwig van Beethoven" is "LB".
_PARTICLES = frozenset([
"van", "von", "de", "da", "di", "del", "della", "der", "den", "des", "du",
"la", "le", "ter", "ten", "dos", "das", "do", "bin", "ibn",
])
def _is_whitespace(ch: str) -> bool:
"""The Unicode White_Space property, spelled out so all three languages
agree: str.isspace also accepts U+001C-U+001F, which JavaScript and Rust
do not.
"""
cp = ord(ch)
return (
0x09 <= cp <= 0x0D
or cp == 0x20
or cp == 0x85
or cp == 0xA0
or cp == 0x1680
or 0x2000 <= cp <= 0x200A
or cp == 0x2028
or cp == 0x2029
or cp == 0x202F
or cp == 0x205F
or cp == 0x3000
)
def _is_skippable(ch: str) -> bool:
# ASCII punctuation and typographic quotes are skipped at the start of a part.
cp = ord(ch)
if cp < 0x80:
return not ("0" <= ch <= "9" or "A" <= ch <= "Z" or "a" <= ch <= "z")
return cp in (0x2018, 0x2019, 0x201C, 0x201D)
def _upper_one(ch: str) -> str:
# One-to-one mappings only, so "ß" stays "ß" in all three languages.
mapped = ch.upper()
return mapped if len(mapped) == 1 else ch
def _lower_one(ch: str) -> str:
mapped = ch.lower()
return mapped if len(mapped) == 1 else ch
def initials(name: str) -> str:
"""Initials from a personal name: "Jean-Paul Sartre" is "JPS"."""
if not isinstance(name, str):
raise TypeError("initials needs a string, received %r" % (name,))
words: List[str] = []
current = ""
for ch in name:
if _is_whitespace(ch):
if current:
words.append(current)
current = ""
else:
current += ch
if current:
words.append(current)
out = ""
for i, word in enumerate(words):
if 0 < i < len(words) - 1 and "".join(_lower_one(c) for c in word) in _PARTICLES:
continue
# Hyphens and full stops split a word into parts that each give a letter.
at_part_start = True
for ch in word:
if ch == "-" or ch == ".":
at_part_start = True
elif at_part_start and not _is_skippable(ch):
out += _upper_one(ch)
at_part_start = False
return outInstall
fune build
With that line in your source, in a Python project (language python in fune.project), fune build resolves it and nothing else, 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 text.initials
The manifest, vectors and README with only the Python implementation. Install it without the registry with fune add ./text.initials-1.0.0-python.fune, or fetch it from a terminal with fune pull text.initials@1.0.0:python.
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.
| Case | Arguments | Expected | |
|---|---|---|---|
| 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
| Case | Arguments | Expected | |
|---|---|---|---|
| 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
| Path | Bytes |
|---|---|
| README.md | 1,789 |
| impl/python.py | 2,366 |
| impl/rust.rs | 2,942 |
| impl/typescript.ts | 2,535 |
| vectors.json | 1,643 |