Functional Weave
Code in TypeScript

text.initials@1.0.0

impl/rust.rs

2,942 bytes · the Rust implementation · view 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()))
}