pub(crate) fn to_snake_case(name: &str) -> String { let mut result = String::with_capacity(name.len() + 4); let mut chars = name.chars().peekable(); while let Some(current) = chars.next() { if let Some(_next) = chars.peek() { if current.is_uppercase() { if !result.is_empty() { result.push('_'); } result.push(current.to_lowercase().next().unwrap()); } else { result.push(current); } } else { // Last character result.push(current.to_lowercase().next().unwrap()); } } pluralize(&result) } pub(crate) fn pluralize(word: &str) -> String { if word.is_empty() { return String::new(); } // Handle possessives and existing plurals if word.ends_with("'s") || word.ends_with("'") || word.ends_with("s's") || word.ends_with("s'") { return word.to_string(); } // Compound words (handle last word) if word.contains(" of ") { let parts: Vec<&str> = word.split(" of ").collect(); return format!("{} of {}", pluralize(parts[0]), parts[1]); } // Compound words with hyphens if word.contains('-') { let parts: Vec<&str> = word.split('-').collect(); return format!("{}-{}", pluralize(parts[0]), parts[1..].join("-") ); } // Invariant words (same singular and plural) match word.to_lowercase().as_str() { "sheep" | "deer" | "moose" | "swine" | "buffalo" | "fish" | "trout" | "salmon" | "pike" | "aircraft" | "series" | "species" | "means" | "crossroads" | "swiss" | "portuguese" | "vietnamese" | "japanese" | "chinese" | "chassis" | "corps" | "headquarters" | "diabetes" | "news" | "odds" | "innings" => return word.to_string(), _ => {} } // Irregular plurals match word.to_lowercase().as_str() { // People "person" => "people", "man" => "men", "woman" => "women", "child" => "children", "tooth" => "teeth", "foot" => "feet", "mouse" => "mice", "louse" => "lice", "goose" => "geese", "ox" => "oxen", // Latin/Greek endings "alumnus" => "alumni", "alga" => "algae", "larva" => "larvae", "vertex" => "vertices", "index" => "indices", "matrix" => "matrices", "criterion" => "criteria", "phenomenon" => "phenomena", "datum" => "data", "medium" => "media", "analysis" => "analyses", "thesis" => "theses", "crisis" => "crises", "appendix" => "appendices", "stimulus" => "stimuli", "radius" => "radii", "axis" => "axes", "hypothesis" => "hypotheses", "basis" => "bases", "diagnosis" => "diagnoses", "formula" => "formulae", "fungus" => "fungi", "nucleus" => "nuclei", "syllabus" => "syllabi", "focus" => "foci", "cactus" => "cacti", "bacterium" => "bacteria", "curriculum" => "curricula", "memorandum" => "memoranda", "millennium" => "millennia", _ => return apply_general_rules(word), }.to_string() } fn apply_general_rules(word: &str) -> String { // Words ending in -o if word.ends_with('o') { match word.to_lowercase().as_str() { // -o → -oes w if matches!(w, "hero" | "potato" | "tomato" | "echo" | "tornado" | "torpedo" | "veto" | "mosquito" | "volcano" | "buffalo" | "domino" | "embargo") => { return format!("{}es", word); } // -o → -os _ => return format!("{}s", word), } } // Words ending in -f or -fe if word.ends_with('f') { match word.to_lowercase().as_str() { w if matches!(w, "roof" | "belief" | "chief" | "reef") => { return format!("{}s", word); } _ => return format!("{}ves", &word[..word.len() - 1]), } } if word.ends_with("fe") { return format!("{}ves", &word[..word.len() - 2]); } // Words ending in -y if word.ends_with('y') { let chars: Vec = word.chars().collect(); if chars.len() > 1 { let before_y = chars[chars.len() - 2]; if !matches!(before_y, 'a' | 'e' | 'i' | 'o' | 'u') { return format!("{}ies", &word[..word.len() - 1]); } } return format!("{}s", word); } // Words ending in sibilants (-s, -ss, -sh, -ch, -x, -z) if word.ends_with('s') || word.ends_with("ss") || word.ends_with("sh") || word.ends_with("ch") || word.ends_with('x') || word.ends_with('z') { return format!("{}es", word); } // Default case: add 's' format!("{}s", word) }