#[must_use]
pub fn pascal_to_snake(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 4);
for (i, ch) in s.chars().enumerate() {
if ch.is_uppercase() && i > 0 {
out.push('_');
}
out.extend(ch.to_lowercase());
}
out
}
#[must_use]
pub fn snake_to_pascal(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut upper_next = true;
for ch in s.chars() {
if ch == '_' {
upper_next = true;
} else if upper_next {
out.extend(ch.to_uppercase());
upper_next = false;
} else {
out.push(ch);
}
}
out
}
#[must_use]
pub fn pluralize(s: &str) -> String {
if s.is_empty() {
return String::new();
}
let (prefix, last) = s
.rfind('_')
.map_or(("", s), |idx| (&s[..=idx], &s[idx + 1..]));
format!("{prefix}{}", pluralize_word(last))
}
fn pluralize_word(s: &str) -> String {
if s.is_empty() {
return String::new();
}
match s {
"person" => return "people".into(),
"child" => return "children".into(),
"man" => return "men".into(),
"woman" => return "women".into(),
"mouse" => return "mice".into(),
"goose" => return "geese".into(),
_ => {}
}
let lower = s.to_ascii_lowercase();
if lower.ends_with("ss")
|| lower.ends_with('x')
|| lower.ends_with('z')
|| lower.ends_with("ch")
|| lower.ends_with("sh")
{
return format!("{s}es");
}
if lower.ends_with('y') {
let prev = s.chars().rev().nth(1);
if prev.is_some_and(|c| !"aeiou".contains(c)) {
let mut out: String = s.chars().take(s.chars().count() - 1).collect();
out.push_str("ies");
return out;
}
}
format!("{s}s")
}
#[must_use]
pub fn snake(s: &str) -> String {
if s.contains('_') || !s.chars().any(char::is_uppercase) {
s.to_ascii_lowercase()
} else {
pascal_to_snake(s)
}
}
#[must_use]
pub fn pascal(s: &str) -> String {
if s.contains('_') || !s.chars().any(char::is_uppercase) {
snake_to_pascal(s)
} else {
s.to_owned()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pascal_to_snake_simple() {
assert_eq!(pascal_to_snake("Post"), "post");
assert_eq!(pascal_to_snake("BlogPost"), "blog_post");
assert_eq!(pascal_to_snake("UserProfile"), "user_profile");
}
#[test]
fn snake_to_pascal_simple() {
assert_eq!(snake_to_pascal("post"), "Post");
assert_eq!(snake_to_pascal("blog_post"), "BlogPost");
assert_eq!(snake_to_pascal("user_profile"), "UserProfile");
}
#[test]
fn pluralize_simple() {
assert_eq!(pluralize("post"), "posts");
assert_eq!(pluralize("user"), "users");
assert_eq!(pluralize("comment"), "comments");
}
#[test]
fn pluralize_consonant_y() {
assert_eq!(pluralize("category"), "categories");
assert_eq!(pluralize("country"), "countries");
}
#[test]
fn pluralize_vowel_y_keeps_y() {
assert_eq!(pluralize("day"), "days");
assert_eq!(pluralize("key"), "keys");
}
#[test]
fn pluralize_sibilant_endings() {
assert_eq!(pluralize("box"), "boxes");
assert_eq!(pluralize("buzz"), "buzzes");
assert_eq!(pluralize("class"), "classes");
assert_eq!(pluralize("watch"), "watches");
assert_eq!(pluralize("dish"), "dishes");
}
#[test]
fn pluralize_pluralises_only_last_segment() {
assert_eq!(pluralize("blog_post"), "blog_posts");
assert_eq!(pluralize("user_category"), "user_categories");
}
#[test]
fn pluralize_empty_string() {
assert_eq!(pluralize(""), "");
}
#[test]
fn pluralize_irregulars() {
assert_eq!(pluralize("person"), "people");
assert_eq!(pluralize("child"), "children");
}
#[test]
fn snake_idempotent_on_snake() {
assert_eq!(snake("blog_post"), "blog_post");
}
#[test]
fn snake_normalises_pascal() {
assert_eq!(snake("BlogPost"), "blog_post");
}
#[test]
fn pascal_idempotent_on_pascal() {
assert_eq!(pascal("BlogPost"), "BlogPost");
}
#[test]
fn pascal_normalises_snake() {
assert_eq!(pascal("blog_post"), "BlogPost");
}
}