pub(crate) fn type_short_name<T: ?Sized>() -> &'static str {
let name = std::any::type_name::<T>();
name.rsplit("::").next().unwrap_or(name)
}
pub(crate) fn type_stem<T: ?Sized>(suffix: &str) -> &'static str {
let name = type_short_name::<T>();
name.strip_suffix(suffix)
.filter(|stem| !stem.is_empty())
.unwrap_or(name)
}
pub(crate) fn sentence_case(name: &str) -> String {
capitalize(&kebab_case(name).replace('-', " "))
}
pub(crate) fn pluralize(label: &str) -> String {
match label.rsplit_once(' ') {
Some((head, last)) => format!("{head} {}", pluralize_word(last)),
None => pluralize_word(label),
}
}
fn pluralize_word(word: &str) -> String {
if word.is_empty() {
return word.to_string();
}
let lower = word.to_lowercase();
const IRREGULAR: &[(&str, &str)] = &[
("person", "people"),
("man", "men"),
("woman", "women"),
("child", "children"),
("mouse", "mice"),
("goose", "geese"),
("foot", "feet"),
("tooth", "teeth"),
("datum", "data"),
("criterion", "criteria"),
("index", "indices"),
("matrix", "matrices"),
("vertex", "vertices"),
("axis", "axes"),
("crisis", "crises"),
("analysis", "analyses"),
];
if let Some((_, plural)) = IRREGULAR.iter().find(|(singular, _)| *singular == lower) {
return match word.chars().next() {
Some(first) if first.is_uppercase() => capitalize(plural),
_ => (*plural).to_string(),
};
}
const F_EXCEPTIONS: &[&str] = &["roof", "chief", "belief", "chef", "cliff", "cuff"];
const UNCOUNTABLE: &[&str] = &[
"fish",
"sheep",
"deer",
"moose",
"series",
"species",
"news",
"equipment",
"information",
"rice",
];
if UNCOUNTABLE.contains(&lower.as_str()) {
return word.to_string();
}
if F_EXCEPTIONS.contains(&lower.as_str()) {
format!("{word}s")
} else if lower.ends_with('f') {
format!("{}ves", &word[..word.len() - 1])
} else if lower.ends_with("fe") {
format!("{}ves", &word[..word.len() - 2])
} else if lower.ends_with('y')
&& word.len() > 1
&& !"aeiou".contains(word.chars().nth(word.len() - 2).unwrap_or(' '))
{
format!("{}ies", &word[..word.len() - 1])
} else if ["s", "ss", "sh", "ch", "x", "z"]
.iter()
.any(|suffix| lower.ends_with(suffix))
{
format!("{word}es")
} else {
format!("{word}s")
}
}
pub(crate) fn kebab_case(name: &str) -> String {
use heck::ToKebabCase;
name.to_kebab_case()
}
pub(crate) fn capitalize(s: &str) -> String {
let spaced = s.replace('_', " ");
let mut c = spaced.chars();
match c.next() {
None => String::new(),
Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
}
}
#[cfg(test)]
mod tests;