/**
* 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;
}