use super::funejson::Value; /// 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 = ch.to_uppercase().collect(); if mapped.len() == 1 { mapped[0] } else { ch } } fn lower_one(ch: char) -> char { let mapped: Vec = 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::new(); let mut current: Vec = 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())) }