use once_cell::sync::Lazy;
use parking_lot::RwLock;
use regex::Regex;
#[derive(Clone, Debug)]
struct Rule {
pattern: Regex,
replacement: &'static str,
}
impl Rule {
fn new(pattern: &str, replacement: &'static str) -> Self {
Self {
pattern: Regex::new(pattern)
.unwrap_or_else(|error| panic!("invalid inflector regex `{pattern}`: {error}")),
replacement,
}
}
}
#[derive(Clone, Debug, Default)]
struct Inflections {
plurals: Vec<Rule>,
singulars: Vec<Rule>,
irregular_singular_to_plural: Vec<(String, String)>,
irregular_plural_to_singular: Vec<(String, String)>,
uncountables: Vec<String>,
}
impl Inflections {
fn english() -> Self {
let mut inflections = Self::default();
inflections.add_plural(r"$", "s");
inflections.add_plural(r"(?i)s$", "s");
inflections.add_plural(r"(?i)^(ax|test)is$", "${1}es");
inflections.add_plural(r"(?i)(octop|vir)us$", "${1}i");
inflections.add_plural(r"(?i)(octop|vir)i$", "${1}i");
inflections.add_plural(r"(?i)(alias|status)$", "${1}es");
inflections.add_plural(r"(?i)(bu)s$", "${1}ses");
inflections.add_plural(r"(?i)(buffal|tomat)o$", "${1}oes");
inflections.add_plural(r"(?i)([ti])um$", "${1}a");
inflections.add_plural(r"(?i)([ti])a$", "${1}a");
inflections.add_plural(r"(?i)sis$", "ses");
inflections.add_plural(r"(?i)(?:([^f])fe|([lr])f)$", "${1}${2}ves");
inflections.add_plural(r"(?i)(hive)$", "${1}s");
inflections.add_plural(r"(?i)([^aeiouy]|qu)y$", "${1}ies");
inflections.add_plural(r"(?i)(x|ch|ss|sh)$", "${1}es");
inflections.add_plural(r"(?i)(matr|vert|ind)(?:ix|ex)$", "${1}ices");
inflections.add_plural(r"(?i)^(m|l)ouse$", "${1}ice");
inflections.add_plural(r"(?i)^(m|l)ice$", "${1}ice");
inflections.add_plural(r"(?i)^(ox)$", "${1}en");
inflections.add_plural(r"(?i)^(oxen)$", "${1}");
inflections.add_plural(r"(?i)(quiz)$", "${1}zes");
inflections.add_singular(r"(?i)s$", "");
inflections.add_singular(r"(?i)(ss)$", "${1}");
inflections.add_singular(r"(?i)(n)ews$", "${1}ews");
inflections.add_singular(r"(?i)([ti])a$", "${1}um");
inflections.add_singular(
r"(?i)((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)(sis|ses)$",
"${1}sis",
);
inflections.add_singular(r"(?i)(^analy)(sis|ses)$", "${1}sis");
inflections.add_singular(r"(?i)([^f])ves$", "${1}fe");
inflections.add_singular(r"(?i)(hive)s$", "${1}");
inflections.add_singular(r"(?i)(tive)s$", "${1}");
inflections.add_singular(r"(?i)([lr])ves$", "${1}f");
inflections.add_singular(r"(?i)([^aeiouy]|qu)ies$", "${1}y");
inflections.add_singular(r"(?i)(s)eries$", "${1}eries");
inflections.add_singular(r"(?i)(m)ovies$", "${1}ovie");
inflections.add_singular(r"(?i)(x|ch|ss|sh)es$", "${1}");
inflections.add_singular(r"(?i)^(m|l)ice$", "${1}ouse");
inflections.add_singular(r"(?i)(bus)(es)?$", "${1}");
inflections.add_singular(r"(?i)(o)es$", "${1}");
inflections.add_singular(r"(?i)(shoe)s$", "${1}");
inflections.add_singular(r"(?i)(cris|test)(is|es)$", "${1}is");
inflections.add_singular(r"(?i)^(a)x[ie]s$", "${1}xis");
inflections.add_singular(r"(?i)(octop|vir)(us|i)$", "${1}us");
inflections.add_singular(r"(?i)(alias|status)(es)?$", "${1}");
inflections.add_singular(r"(?i)^(ox)en$", "${1}");
inflections.add_singular(r"(?i)(vert|ind)ices$", "${1}ex");
inflections.add_singular(r"(?i)(matr)ices$", "${1}ix");
inflections.add_singular(r"(?i)(quiz)zes$", "${1}");
inflections.add_singular(r"(?i)(database)s$", "${1}");
for (singular, plural) in [
("person", "people"),
("man", "men"),
("child", "children"),
("sex", "sexes"),
("move", "moves"),
("zombie", "zombies"),
("cow", "kine"),
("genus", "genera"),
] {
inflections.add_irregular(singular, plural);
}
inflections.add_uncountables(&[
"equipment",
"information",
"rice",
"money",
"species",
"series",
"fish",
"sheep",
"jeans",
"police",
]);
inflections
.irregular_singular_to_plural
.sort_by(|left, right| right.0.len().cmp(&left.0.len()));
inflections
.irregular_plural_to_singular
.sort_by(|left, right| right.0.len().cmp(&left.0.len()));
inflections
}
fn add_plural(&mut self, pattern: &str, replacement: &'static str) {
self.plurals.push(Rule::new(pattern, replacement));
}
fn add_singular(&mut self, pattern: &str, replacement: &'static str) {
self.singulars.push(Rule::new(pattern, replacement));
}
fn add_irregular(&mut self, singular: &str, plural: &str) {
self.irregular_singular_to_plural
.push((singular.to_ascii_lowercase(), plural.to_ascii_lowercase()));
self.irregular_plural_to_singular
.push((plural.to_ascii_lowercase(), singular.to_ascii_lowercase()));
}
fn add_uncountables(&mut self, words: &[&str]) {
self.uncountables
.extend(words.iter().map(|word| word.to_ascii_lowercase()));
}
fn pluralize(&self, word: &str) -> String {
if word.is_empty() || self.is_uncountable(word) {
return word.to_string();
}
if self.has_irregular_suffix(word, &self.irregular_plural_to_singular) {
return word.to_string();
}
if let Some(result) =
self.replace_irregular_suffix(word, &self.irregular_singular_to_plural)
{
return result;
}
self.apply_rules(word, &self.plurals)
}
fn singularize(&self, word: &str) -> String {
if word.is_empty() || self.is_uncountable(word) {
return word.to_string();
}
if self.has_irregular_suffix(word, &self.irregular_singular_to_plural) {
return word.to_string();
}
if let Some(result) =
self.replace_irregular_suffix(word, &self.irregular_plural_to_singular)
{
return result;
}
self.apply_rules(word, &self.singulars)
}
fn apply_rules(&self, word: &str, rules: &[Rule]) -> String {
for rule in rules.iter().rev() {
if rule.pattern.is_match(word) {
return rule.pattern.replace(word, rule.replacement).into_owned();
}
}
word.to_string()
}
fn is_uncountable(&self, word: &str) -> bool {
self.uncountables
.iter()
.any(|entry| ends_with_whole_word_case_insensitive(word, entry))
}
fn has_irregular_suffix(&self, word: &str, mappings: &[(String, String)]) -> bool {
mappings
.iter()
.any(|(from, _)| has_ascii_suffix(word, from))
}
fn replace_irregular_suffix(
&self,
word: &str,
mappings: &[(String, String)],
) -> Option<String> {
mappings.iter().find_map(|(from, to)| {
if !has_ascii_suffix(word, from) {
return None;
}
let split_at = word.len().saturating_sub(from.len());
let prefix = &word[..split_at];
let matched = &word[split_at..];
let replacement = preserve_case(to, matched);
Some(format!("{prefix}{replacement}"))
})
}
}
static INFLECTIONS: Lazy<RwLock<Inflections>> = Lazy::new(|| RwLock::new(Inflections::english()));
static NON_PARAMETER_RE: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"[^A-Za-z0-9\-_]+")
.unwrap_or_else(|error| panic!("invalid parameterize regex: {error}"))
});
pub fn pluralize(word: &str) -> String {
INFLECTIONS.read().pluralize(word)
}
pub fn singularize(word: &str) -> String {
INFLECTIONS.read().singularize(word)
}
pub fn camelize(word: &str) -> String {
camelize_with_case(word, true)
}
pub fn camelize_lower(word: &str) -> String {
camelize_with_case(word, false)
}
pub fn underscore(word: &str) -> String {
if word.is_empty() {
return String::new();
}
let normalized = word.replace("::", "/");
let chars: Vec<char> = normalized.chars().collect();
let mut result = String::with_capacity(normalized.len() + 8);
for (index, current) in chars.iter().copied().enumerate() {
let previous = index
.checked_sub(1)
.and_then(|position| chars.get(position).copied());
let next = chars.get(index + 1).copied();
let current = if current == '-' { '_' } else { current };
if current.is_uppercase() {
if let Some(previous) = previous {
let previous_is_separator = matches!(previous, '/' | '_' | '-');
let should_insert = !previous_is_separator
&& (previous.is_lowercase()
|| previous.is_ascii_digit()
|| (previous.is_uppercase()
&& next.is_some_and(|value| value.is_lowercase())));
if should_insert {
result.push('_');
}
}
result.extend(current.to_lowercase());
} else {
result.extend(current.to_lowercase());
}
}
result
}
pub fn humanize(word: &str) -> String {
humanize_with_options(word, true, false)
}
pub fn titleize(word: &str) -> String {
titlecase_words(&humanize_with_options(&underscore(word), true, false))
}
pub fn tableize(word: &str) -> String {
pluralize(&underscore(word))
}
pub fn classify(word: &str) -> String {
let table = word.rsplit('.').next().unwrap_or(word);
camelize(&singularize(table))
}
pub fn dasherize(word: &str) -> String {
word.replace('_', "-")
}
pub fn demodulize(word: &str) -> String {
word.rsplit("::").next().unwrap_or(word).to_string()
}
pub fn foreign_key(word: &str) -> String {
foreign_key_with_separator(word, true)
}
pub fn ordinal(number: i64) -> &'static str {
let absolute = number.unsigned_abs();
if (11..=13).contains(&(absolute % 100)) {
return "th";
}
match absolute % 10 {
1 => "st",
2 => "nd",
3 => "rd",
_ => "th",
}
}
pub fn ordinalize(number: i64) -> String {
format!("{number}{}", ordinal(number))
}
pub fn parameterize(word: &str) -> String {
parameterize_internal(word, "-")
}
pub fn parameterize_with_sep(word: &str, sep: &str) -> String {
parameterize_internal(word, sep)
}
fn camelize_with_case(word: &str, uppercase_first: bool) -> String {
if word.is_empty() {
return String::new();
}
let mut result = String::new();
let mut first_piece = true;
for path_segment in word.split('/') {
if !result.is_empty() {
result.push_str("::");
}
let mut segment_result = String::new();
for piece in path_segment.split('_') {
if piece.is_empty() {
continue;
}
if first_piece && !uppercase_first {
segment_result.push_str(&lowercase_first(piece));
} else {
segment_result.push_str(&capitalize_first(piece));
}
first_piece = false;
}
result.push_str(&segment_result);
}
result
}
fn humanize_with_options(word: &str, capitalize: bool, keep_id_suffix: bool) -> String {
if word.is_empty() {
return String::new();
}
let mut result = word.trim_start_matches('_').replace('_', " ");
if !keep_id_suffix && word.ends_with("_id") && result.ends_with(" id") {
let trimmed = result.trim_end_matches(" id");
result = trimmed.to_string();
}
let result = lowercase_words(&result);
if capitalize {
uppercase_first_alpha(&result)
} else {
result
}
}
fn foreign_key_with_separator(word: &str, separate: bool) -> String {
let suffix = if separate { "_id" } else { "id" };
format!("{}{}", underscore(&demodulize(word)), suffix)
}
fn parameterize_internal(word: &str, separator: &str) -> String {
let transliterated = transliterate(word);
let mut result = NON_PARAMETER_RE
.replace_all(&transliterated, separator)
.into_owned();
if !separator.is_empty() {
if let [separator_char] = separator.chars().collect::<Vec<_>>().as_slice() {
result = squeeze_separator_char(&result, *separator_char);
} else {
let repeated_separator =
Regex::new(&format!(r"(?:{}){{2,}}", regex::escape(separator))).unwrap_or_else(
|error| panic!("invalid separator regex `{separator}`: {error}"),
);
result = repeated_separator
.replace_all(&result, separator)
.into_owned();
}
while result.starts_with(separator) {
result.drain(..separator.len());
}
while result.ends_with(separator) {
let new_length = result.len().saturating_sub(separator.len());
result.truncate(new_length);
}
}
result.make_ascii_lowercase();
result
}
fn lowercase_first(input: &str) -> String {
let mut chars = input.chars();
match chars.next() {
Some(first) => first.to_lowercase().collect::<String>() + chars.as_str(),
None => String::new(),
}
}
fn capitalize_first(input: &str) -> String {
let mut chars = input.chars();
match chars.next() {
Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
None => String::new(),
}
}
fn uppercase_first_alpha(input: &str) -> String {
let mut result = String::with_capacity(input.len());
let mut changed = false;
for character in input.chars() {
if !changed && character.is_alphabetic() {
result.extend(character.to_uppercase());
changed = true;
} else {
result.push(character);
}
}
result
}
fn lowercase_words(input: &str) -> String {
let mut result = String::with_capacity(input.len());
let mut current = String::new();
for character in input.chars() {
if character.is_alphanumeric() {
current.push(character);
} else {
if !current.is_empty() {
result.push_str(¤t.to_lowercase());
current.clear();
}
result.push(character);
}
}
if !current.is_empty() {
result.push_str(¤t.to_lowercase());
}
result
}
fn titlecase_words(input: &str) -> String {
let characters: Vec<char> = input.chars().collect();
let mut result = String::with_capacity(input.len());
for (index, character) in characters.iter().copied().enumerate() {
if character.is_lowercase() && is_titlecase_boundary(&characters, index) {
result.extend(character.to_uppercase());
} else {
result.push(character);
}
}
result
}
fn is_titlecase_boundary(characters: &[char], index: usize) -> bool {
if index == 0 {
return true;
}
let previous = characters[index - 1];
if previous.is_alphanumeric() {
return false;
}
!(index >= 2
&& characters[index - 2].is_alphanumeric()
&& matches!(previous, '\'' | '’' | '`' | '(' | ')'))
}
fn preserve_case(replacement: &str, matched: &str) -> String {
if matched.chars().all(|character| !character.is_lowercase()) {
replacement.to_uppercase()
} else if matched
.chars()
.next()
.is_some_and(|character| character.is_uppercase())
{
capitalize_first(replacement)
} else {
replacement.to_string()
}
}
fn has_ascii_suffix(word: &str, suffix: &str) -> bool {
word.len() >= suffix.len() && word[word.len() - suffix.len()..].eq_ignore_ascii_case(suffix)
}
fn ends_with_whole_word_case_insensitive(word: &str, suffix: &str) -> bool {
if !has_ascii_suffix(word, suffix) {
return false;
}
let split_at = word.len().saturating_sub(suffix.len());
match word[..split_at].chars().last() {
Some(character) => !is_word_character(character),
None => true,
}
}
fn is_word_character(character: char) -> bool {
character.is_alphanumeric() || character == '_'
}
fn squeeze_separator_char(input: &str, separator: char) -> String {
let mut result = String::with_capacity(input.len());
let mut previous_was_separator = false;
for character in input.chars() {
if character == separator {
if !previous_was_separator {
result.push(character);
}
previous_was_separator = true;
} else {
result.push(character);
previous_was_separator = false;
}
}
result
}
fn transliterate(input: &str) -> String {
let mut result = String::with_capacity(input.len());
for character in input.chars() {
match character {
character if character.is_ascii() => result.push(character),
'À' | 'Á' | 'Â' | 'Ã' | 'Ä' | 'Å' | 'Ā' | 'Ă' | 'Ą' | 'Ǎ' | 'Ǻ' => {
result.push('A')
}
'à' | 'á' | 'â' | 'ã' | 'ä' | 'å' | 'ā' | 'ă' | 'ą' | 'ǎ' | 'ǻ' => {
result.push('a')
}
'Æ' => result.push_str("AE"),
'æ' => result.push_str("ae"),
'Ç' | 'Ć' | 'Ĉ' | 'Ċ' | 'Č' => result.push('C'),
'ç' | 'ć' | 'ĉ' | 'ċ' | 'č' => result.push('c'),
'Ð' | 'Ď' | 'Đ' => result.push('D'),
'ð' | 'ď' | 'đ' => result.push('d'),
'È' | 'É' | 'Ê' | 'Ë' | 'Ē' | 'Ĕ' | 'Ė' | 'Ę' | 'Ě' => result.push('E'),
'è' | 'é' | 'ê' | 'ë' | 'ē' | 'ĕ' | 'ė' | 'ę' | 'ě' => result.push('e'),
'Ĝ' | 'Ğ' | 'Ġ' | 'Ģ' => result.push('G'),
'ĝ' | 'ğ' | 'ġ' | 'ģ' => result.push('g'),
'Ĥ' | 'Ħ' => result.push('H'),
'ĥ' | 'ħ' => result.push('h'),
'Ì' | 'Í' | 'Î' | 'Ï' | 'Ĩ' | 'Ī' | 'Ĭ' | 'Į' | 'İ' => result.push('I'),
'ì' | 'í' | 'î' | 'ï' | 'ĩ' | 'ī' | 'ĭ' | 'į' | 'ı' => result.push('i'),
'Ĵ' => result.push('J'),
'ĵ' => result.push('j'),
'Ķ' => result.push('K'),
'ķ' => result.push('k'),
'Ĺ' | 'Ļ' | 'Ľ' | 'Ŀ' | 'Ł' => result.push('L'),
'ĺ' | 'ļ' | 'ľ' | 'ŀ' | 'ł' => result.push('l'),
'Ñ' | 'Ń' | 'Ņ' | 'Ň' => result.push('N'),
'ñ' | 'ń' | 'ņ' | 'ň' => result.push('n'),
'Ò' | 'Ó' | 'Ô' | 'Õ' | 'Ö' | 'Ø' | 'Ō' | 'Ŏ' | 'Ő' | 'Ǿ' => result.push('O'),
'ò' | 'ó' | 'ô' | 'õ' | 'ö' | 'ø' | 'ō' | 'ŏ' | 'ő' | 'ǿ' => result.push('o'),
'Œ' => result.push_str("OE"),
'œ' => result.push_str("oe"),
'Ŕ' | 'Ŗ' | 'Ř' => result.push('R'),
'ŕ' | 'ŗ' | 'ř' => result.push('r'),
'Ś' | 'Ŝ' | 'Ş' | 'Š' | 'Ș' => result.push('S'),
'ś' | 'ŝ' | 'ş' | 'š' | 'ș' => result.push('s'),
'ẞ' => result.push_str("SS"),
'ß' => result.push_str("ss"),
'Ţ' | 'Ť' | 'Ŧ' | 'Ț' => result.push('T'),
'ţ' | 'ť' | 'ŧ' | 'ț' => result.push('t'),
'Ù' | 'Ú' | 'Û' | 'Ü' | 'Ũ' | 'Ū' | 'Ŭ' | 'Ů' | 'Ű' | 'Ų' => result.push('U'),
'ù' | 'ú' | 'û' | 'ü' | 'ũ' | 'ū' | 'ŭ' | 'ů' | 'ű' | 'ų' => result.push('u'),
'Ŵ' => result.push('W'),
'ŵ' => result.push('w'),
'Ý' | 'Ÿ' | 'Ŷ' => result.push('Y'),
'ý' | 'ÿ' | 'ŷ' => result.push('y'),
'Ź' | 'Ż' | 'Ž' => result.push('Z'),
'ź' | 'ż' | 'ž' => result.push('z'),
'Þ' => result.push_str("TH"),
'þ' => result.push_str("th"),
_ => {}
}
}
result
}
#[cfg(test)]
mod inflector_tests {
use super::*;
fn assert_plural_pair(singular: &str, plural: &str) {
assert_eq!(pluralize(singular), plural);
assert_eq!(
pluralize(&capitalize_first(singular)),
capitalize_first(plural)
);
assert_eq!(singularize(plural), singular);
assert_eq!(
singularize(&capitalize_first(plural)),
capitalize_first(singular)
);
assert_eq!(pluralize(plural), plural);
assert_eq!(singularize(singular), singular);
}
macro_rules! plural_pair_tests {
($($name:ident => ($singular:expr, $plural:expr)),+ $(,)?) => {
$(
#[test]
fn $name() {
assert_plural_pair($singular, $plural);
}
)+
};
}
plural_pair_tests! {
inflector_plural_pair_001 => ("search", "searches"),
inflector_plural_pair_002 => ("switch", "switches"),
inflector_plural_pair_003 => ("fix", "fixes"),
inflector_plural_pair_004 => ("box", "boxes"),
inflector_plural_pair_005 => ("process", "processes"),
inflector_plural_pair_006 => ("address", "addresses"),
inflector_plural_pair_007 => ("case", "cases"),
inflector_plural_pair_008 => ("stack", "stacks"),
inflector_plural_pair_009 => ("wish", "wishes"),
inflector_plural_pair_010 => ("fish", "fish"),
inflector_plural_pair_011 => ("jeans", "jeans"),
inflector_plural_pair_012 => ("funky jeans", "funky jeans"),
inflector_plural_pair_013 => ("my money", "my money"),
inflector_plural_pair_014 => ("category", "categories"),
inflector_plural_pair_015 => ("query", "queries"),
inflector_plural_pair_016 => ("ability", "abilities"),
inflector_plural_pair_017 => ("agency", "agencies"),
inflector_plural_pair_018 => ("movie", "movies"),
inflector_plural_pair_019 => ("archive", "archives"),
inflector_plural_pair_020 => ("index", "indices"),
inflector_plural_pair_021 => ("wife", "wives"),
inflector_plural_pair_022 => ("safe", "saves"),
inflector_plural_pair_023 => ("half", "halves"),
inflector_plural_pair_024 => ("move", "moves"),
inflector_plural_pair_025 => ("salesperson", "salespeople"),
inflector_plural_pair_026 => ("person", "people"),
inflector_plural_pair_027 => ("spokesman", "spokesmen"),
inflector_plural_pair_028 => ("man", "men"),
inflector_plural_pair_029 => ("woman", "women"),
inflector_plural_pair_030 => ("basis", "bases"),
inflector_plural_pair_031 => ("diagnosis", "diagnoses"),
inflector_plural_pair_032 => ("diagnosis_a", "diagnosis_as"),
inflector_plural_pair_033 => ("datum", "data"),
inflector_plural_pair_034 => ("medium", "media"),
inflector_plural_pair_035 => ("stadium", "stadia"),
inflector_plural_pair_036 => ("analysis", "analyses"),
inflector_plural_pair_037 => ("my_analysis", "my_analyses"),
inflector_plural_pair_038 => ("node_child", "node_children"),
inflector_plural_pair_039 => ("child", "children"),
inflector_plural_pair_040 => ("experience", "experiences"),
inflector_plural_pair_041 => ("day", "days"),
inflector_plural_pair_042 => ("comment", "comments"),
inflector_plural_pair_043 => ("foobar", "foobars"),
inflector_plural_pair_044 => ("newsletter", "newsletters"),
inflector_plural_pair_045 => ("old_news", "old_news"),
inflector_plural_pair_046 => ("news", "news"),
inflector_plural_pair_047 => ("series", "series"),
inflector_plural_pair_048 => ("miniseries", "miniseries"),
inflector_plural_pair_049 => ("species", "species"),
inflector_plural_pair_050 => ("quiz", "quizzes"),
inflector_plural_pair_051 => ("perspective", "perspectives"),
inflector_plural_pair_052 => ("ox", "oxen"),
inflector_plural_pair_053 => ("photo", "photos"),
inflector_plural_pair_054 => ("buffalo", "buffaloes"),
inflector_plural_pair_055 => ("tomato", "tomatoes"),
inflector_plural_pair_056 => ("dwarf", "dwarves"),
inflector_plural_pair_057 => ("elf", "elves"),
inflector_plural_pair_058 => ("information", "information"),
inflector_plural_pair_059 => ("equipment", "equipment"),
inflector_plural_pair_060 => ("bus", "buses"),
inflector_plural_pair_061 => ("status", "statuses"),
inflector_plural_pair_062 => ("status_code", "status_codes"),
inflector_plural_pair_063 => ("mouse", "mice"),
inflector_plural_pair_064 => ("louse", "lice"),
inflector_plural_pair_065 => ("house", "houses"),
inflector_plural_pair_066 => ("octopus", "octopi"),
inflector_plural_pair_067 => ("virus", "viri"),
inflector_plural_pair_068 => ("alias", "aliases"),
inflector_plural_pair_069 => ("portfolio", "portfolios"),
inflector_plural_pair_070 => ("vertex", "vertices"),
inflector_plural_pair_071 => ("matrix", "matrices"),
inflector_plural_pair_072 => ("matrix_fu", "matrix_fus"),
inflector_plural_pair_073 => ("axis", "axes"),
inflector_plural_pair_074 => ("taxi", "taxis"),
inflector_plural_pair_075 => ("testis", "testes"),
inflector_plural_pair_076 => ("crisis", "crises"),
inflector_plural_pair_077 => ("rice", "rice"),
inflector_plural_pair_078 => ("shoe", "shoes"),
inflector_plural_pair_079 => ("horse", "horses"),
inflector_plural_pair_080 => ("prize", "prizes"),
inflector_plural_pair_081 => ("edge", "edges"),
inflector_plural_pair_082 => ("database", "databases"),
inflector_plural_pair_083 => ("|ice", "|ices"),
inflector_plural_pair_084 => ("|ouse", "|ouses"),
inflector_plural_pair_085 => ("slice", "slices"),
inflector_plural_pair_086 => ("police", "police"),
}
#[test]
fn inflector_camelize_and_underscore_tables() {
let camel_to_underscore = [
("Product", "product"),
("SpecialGuest", "special_guest"),
("ApplicationController", "application_controller"),
("Area51Controller", "area51_controller"),
("AppCDir", "app_c_dir"),
("Accountsv2N2Test", "accountsv2_n2_test"),
];
for (camel, underscored) in camel_to_underscore {
assert_eq!(underscore(camel), underscored);
assert_eq!(camelize(underscored), camel);
}
let underscore_to_lower_camel = [
("product", "product"),
("special_guest", "specialGuest"),
("application_controller", "applicationController"),
("area51_controller", "area51Controller"),
];
for (underscored, lower_camel) in underscore_to_lower_camel {
assert_eq!(camelize_lower(underscored), lower_camel);
}
let camel_without_reverse = [
("HTMLTidy", "html_tidy"),
("HTMLTidyGenerator", "html_tidy_generator"),
("FreeBSD", "free_bsd"),
("HTML", "html"),
("ForceXMLController", "force_xml_controller"),
("product", "product"),
];
for (camel, underscored) in camel_without_reverse {
assert_eq!(underscore(camel), underscored);
}
let modular = [
("Admin::Product", "admin/product"),
(
"Users::Commission::Department",
"users/commission/department",
),
(
"UsersSection::CommissionDepartment",
"users_section/commission_department",
),
];
for (camel, underscored) in modular {
assert_eq!(underscore(camel), underscored);
assert_eq!(camelize(underscored), camel);
}
}
#[test]
fn inflector_humanize_titleize_demodulize_and_classify_tables() {
let humanized = [
("employee_salary", "Employee salary"),
("employee_id", "Employee"),
("employee id", "Employee id"),
("employee id etc", "Employee id etc"),
("underground", "Underground"),
("_id", "Id"),
("_external_id", "External"),
];
for (input, expected) in humanized {
assert_eq!(humanize(input), expected);
}
let titleized = [
("active_record", "Active Record"),
("ActiveRecord", "Active Record"),
("action web service", "Action Web Service"),
("Action Web Service", "Action Web Service"),
("Action web service", "Action Web Service"),
("actionwebservice", "Actionwebservice"),
("Actionwebservice", "Actionwebservice"),
("david's code", "David's Code"),
("David's code", "David's Code"),
("david's Code", "David's Code"),
("sgt. pepper's", "Sgt. Pepper's"),
("i've just seen a face", "I've Just Seen A Face"),
("maybe you'll be there", "Maybe You'll Be There"),
("¿por qué?", "¿Por Qué?"),
("Fred’s", "Fred’s"),
("Fred`s", "Fred`s"),
("this was 'fake news'", "This Was 'Fake News'"),
("new name(s)", "New Name(s)"),
("new (names)", "New (Names)"),
("their (mis)deeds", "Their (Mis)deeds"),
("confirmation num", "Confirmation Num"),
];
for (input, expected) in titleized {
assert_eq!(titleize(input), expected);
}
assert_eq!(demodulize("MyApplication::Billing::Account"), "Account");
assert_eq!(demodulize("Account"), "Account");
assert_eq!(demodulize("::Account"), "Account");
assert_eq!(demodulize(""), "");
assert_eq!(classify("ham_and_eggs"), "HamAndEgg");
assert_eq!(classify("posts"), "Post");
assert_eq!(classify("calculus"), "Calculu");
assert_eq!(classify("public.posts"), "Post");
}
#[test]
fn inflector_foreign_key_and_tableize_tables() {
let foreign_keys = [
("Person", "person_id"),
("MyApplication::Billing::Account", "account_id"),
];
for (class_name, expected) in foreign_keys {
assert_eq!(foreign_key(class_name), expected);
}
let foreign_keys_without_separator = [
("Person", "personid"),
("MyApplication::Billing::Account", "accountid"),
];
for (class_name, expected) in foreign_keys_without_separator {
assert_eq!(foreign_key_with_separator(class_name, false), expected);
}
let tableized = [
("PrimarySpokesman", "primary_spokesmen"),
("NodeChild", "node_children"),
("Calculu", "calculus"),
];
for (class_name, expected) in tableized {
assert_eq!(tableize(class_name), expected);
}
}
#[test]
fn inflector_parameterize_tables() {
let parameterized = [
("Donald E. Knuth", "donald-e-knuth"),
(
"Random text with *(bad)* characters",
"random-text-with-bad-characters",
),
("Allow_Under_Scores", "allow_under_scores"),
("Trailing bad characters!@#", "trailing-bad-characters"),
("!@#Leading bad characters", "leading-bad-characters"),
("Squeeze separators", "squeeze-separators"),
("Test with + sign", "test-with-sign"),
(
"Test with malformed utf8 \u{00A9}",
"test-with-malformed-utf8",
),
];
for (input, expected) in parameterized {
assert_eq!(parameterize(input), expected);
assert_eq!(
parameterize_with_sep(input, "__sep__"),
expected.replace('-', "__sep__")
);
}
let underscored = [
("Donald E. Knuth", "donald_e_knuth"),
(
"Random text with *(bad)* characters",
"random_text_with_bad_characters",
),
("With-some-dashes", "with-some-dashes"),
("Retain_underscore", "retain_underscore"),
("Trailing bad characters!@#", "trailing_bad_characters"),
("!@#Leading bad characters", "leading_bad_characters"),
("Squeeze separators", "squeeze_separators"),
("Test with + sign", "test_with_sign"),
(
"Test with malformed utf8 \u{00A9}",
"test_with_malformed_utf8",
),
];
for (input, expected) in underscored {
assert_eq!(parameterize_with_sep(input, "_"), expected);
}
let normalized = [
("Malmö", "malmo"),
("Garçons", "garcons"),
("Ops\u{00D9}", "opsu"),
("Ærøskøbing", "aeroskobing"),
("Aßlar", "asslar"),
("Japanese: 日本語", "japanese"),
];
for (input, expected) in normalized {
assert_eq!(parameterize(input), expected);
}
assert_eq!(parameterize_with_sep("Donald E. Knuth", ""), "donaldeknuth");
}
#[test]
fn inflector_ordinal_tables() {
let ordinalized = [
(-1, "-1st"),
(-2, "-2nd"),
(-3, "-3rd"),
(-4, "-4th"),
(-5, "-5th"),
(-6, "-6th"),
(-7, "-7th"),
(-8, "-8th"),
(-9, "-9th"),
(-10, "-10th"),
(-11, "-11th"),
(-12, "-12th"),
(-13, "-13th"),
(-14, "-14th"),
(-20, "-20th"),
(-21, "-21st"),
(-22, "-22nd"),
(-23, "-23rd"),
(-24, "-24th"),
(-100, "-100th"),
(-101, "-101st"),
(-102, "-102nd"),
(-103, "-103rd"),
(-104, "-104th"),
(-110, "-110th"),
(-111, "-111th"),
(-112, "-112th"),
(-113, "-113th"),
(-1000, "-1000th"),
(-1001, "-1001st"),
(0, "0th"),
(1, "1st"),
(2, "2nd"),
(3, "3rd"),
(4, "4th"),
(5, "5th"),
(6, "6th"),
(7, "7th"),
(8, "8th"),
(9, "9th"),
(10, "10th"),
(11, "11th"),
(12, "12th"),
(13, "13th"),
(14, "14th"),
(20, "20th"),
(21, "21st"),
(22, "22nd"),
(23, "23rd"),
(24, "24th"),
(100, "100th"),
(101, "101st"),
(102, "102nd"),
(103, "103rd"),
(104, "104th"),
(110, "110th"),
(111, "111th"),
(112, "112th"),
(113, "113th"),
(1000, "1000th"),
(1001, "1001st"),
];
for (number, expected) in ordinalized {
assert_eq!(ordinalize(number), expected);
let suffix = expected
.trim_start_matches('-')
.trim_start_matches(|character: char| character.is_ascii_digit());
assert_eq!(ordinal(number), suffix);
}
}
#[test]
fn inflector_irregularity_table() {
let irregularities = [
("person", "people"),
("man", "men"),
("child", "children"),
("sex", "sexes"),
("move", "moves"),
("zombie", "zombies"),
("cow", "kine"),
("genus", "genera"),
];
for (singular, plural) in irregularities {
assert_eq!(pluralize(singular), plural);
assert_eq!(pluralize(plural), plural);
assert_eq!(singularize(plural), singular);
assert_eq!(singularize(singular), singular);
}
}
#[test]
fn inflector_pluralize_preserves_existing_plural_words() {
assert_eq!(pluralize("plurals"), "plurals");
assert_eq!(pluralize("Plurals"), "Plurals");
}
#[test]
fn inflector_uncountable_suffix_is_not_greedy() {
let mut inflections = Inflections::english();
inflections.add_uncountables(&["ors"]);
assert_eq!(inflections.singularize("ors"), "ors");
assert_eq!(inflections.pluralize("ors"), "ors");
assert_eq!(inflections.singularize("sponsor"), "sponsor");
assert_eq!(inflections.pluralize("sponsor"), "sponsors");
}
#[test]
fn inflector_camelize_collapses_embedded_underscores() {
assert_eq!(camelize("Camel_Case"), "CamelCase");
assert_eq!(camelize_lower("Camel_Case"), "camelCase");
}
#[test]
fn inflector_camelize_lower_downcases_existing_pascal_case() {
assert_eq!(camelize_lower("Capital"), "capital");
assert_eq!(camelize_lower("capital"), "capital");
}
#[test]
fn inflector_underscore_handles_additional_rails_misdirections() {
let cases = [
("CapiController", "capi_controller"),
("HttpsApis", "https_apis"),
("Html5", "html5"),
("RoRails", "ro_rails"),
];
for (camel, underscored) in cases {
assert_eq!(underscore(camel), underscored, "camel {camel}");
}
}
#[test]
fn inflector_camelize_handles_additional_rails_misdirections() {
let cases = [
("capi_controller", "CapiController"),
("html5", "Html5"),
("ro_rails", "RoRails"),
];
for (underscored, camel) in cases {
assert_eq!(camelize(underscored), camel, "underscored {underscored}");
}
}
#[test]
fn inflector_dasherize_and_empty_string_edges() {
let dashed = [
("street", "street"),
("street_address", "street-address"),
("person_street_address", "person-street-address"),
];
for (input, expected) in dashed {
assert_eq!(dasherize(input), expected);
}
assert_eq!(pluralize(""), "");
assert_eq!(singularize(""), "");
assert_eq!(camelize(""), "");
assert_eq!(camelize_lower(""), "");
assert_eq!(underscore(""), "");
assert_eq!(humanize(""), "");
assert_eq!(titleize(""), "");
assert_eq!(tableize(""), "");
assert_eq!(classify(""), "");
assert_eq!(dasherize(""), "");
assert_eq!(demodulize(""), "");
assert_eq!(foreign_key(""), "_id");
assert_eq!(parameterize(""), "");
assert_eq!(parameterize_with_sep("", "_"), "");
}
macro_rules! humanize_keep_id_suffix_tests {
($($name:ident => ($input:expr, $expected:expr)),+ $(,)?) => {
$(
#[test]
fn $name() {
assert_eq!(humanize_with_options($input, true, true), $expected);
}
)+
};
}
humanize_keep_id_suffix_tests! {
inflector_humanize_keep_id_suffix_001 => ("this_is_a_string_ending_with_id", "This is a string ending with id"),
inflector_humanize_keep_id_suffix_002 => ("employee_id", "Employee id"),
inflector_humanize_keep_id_suffix_003 => ("employee_id_something_else", "Employee id something else"),
inflector_humanize_keep_id_suffix_004 => ("underground", "Underground"),
inflector_humanize_keep_id_suffix_005 => ("employee id", "Employee id"),
inflector_humanize_keep_id_suffix_006 => ("employee id etc", "Employee id etc"),
inflector_humanize_keep_id_suffix_007 => ("_id", "Id"),
inflector_humanize_keep_id_suffix_008 => ("_external_id", "External id"),
}
macro_rules! humanize_without_capitalize_tests {
($($name:ident => ($input:expr, $expected:expr)),+ $(,)?) => {
$(
#[test]
fn $name() {
assert_eq!(humanize_with_options($input, false, false), $expected);
}
)+
};
}
humanize_without_capitalize_tests! {
inflector_humanize_without_capitalize_001 => ("employee_salary", "employee salary"),
inflector_humanize_without_capitalize_002 => ("employee_id", "employee"),
inflector_humanize_without_capitalize_003 => ("underground", "underground"),
}
macro_rules! titleize_keep_id_suffix_tests {
($($name:ident => ($input:expr, $expected:expr)),+ $(,)?) => {
$(
#[test]
fn $name() {
assert_eq!(
titlecase_words(&humanize_with_options(&underscore($input), true, true)),
$expected
);
}
)+
};
}
titleize_keep_id_suffix_tests! {
inflector_titleize_keep_id_suffix_001 => ("this_is_a_string_ending_with_id", "This Is A String Ending With Id"),
inflector_titleize_keep_id_suffix_002 => ("EmployeeId", "Employee Id"),
inflector_titleize_keep_id_suffix_003 => ("Author Id", "Author Id"),
}
macro_rules! camelize_round_trip_tests {
($($name:ident => ($camel:expr, $underscored:expr)),+ $(,)?) => {
$(
#[test]
fn $name() {
assert_eq!(underscore($camel), $underscored);
assert_eq!(camelize($underscored), $camel);
}
)+
};
}
camelize_round_trip_tests! {
inflector_camelize_round_trip_001 => ("Product", "product"),
inflector_camelize_round_trip_002 => ("SpecialGuest", "special_guest"),
inflector_camelize_round_trip_003 => ("ApplicationController", "application_controller"),
inflector_camelize_round_trip_004 => ("Area51Controller", "area51_controller"),
inflector_camelize_round_trip_005 => ("AppCDir", "app_c_dir"),
inflector_camelize_round_trip_006 => ("Accountsv2N2Test", "accountsv2_n2_test"),
}
camelize_round_trip_tests! {
inflector_module_round_trip_001 => ("Admin::Product", "admin/product"),
inflector_module_round_trip_002 => ("Users::Commission::Department", "users/commission/department"),
inflector_module_round_trip_003 => ("UsersSection::CommissionDepartment", "users_section/commission_department"),
}
macro_rules! demodulize_tests {
($($name:ident => ($input:expr, $expected:expr)),+ $(,)?) => {
$(
#[test]
fn $name() {
assert_eq!(demodulize($input), $expected);
}
)+
};
}
demodulize_tests! {
inflector_demodulize_001 => ("MyApplication::Billing::Account", "Account"),
inflector_demodulize_002 => ("Account", "Account"),
inflector_demodulize_003 => ("::Account", "Account"),
inflector_demodulize_004 => ("MyApplication::Billing", "Billing"),
}
macro_rules! tableize_round_trip_tests {
($($name:ident => ($class_name:expr, $table_name:expr)),+ $(,)?) => {
$(
#[test]
fn $name() {
assert_eq!(tableize($class_name), $table_name);
assert_eq!(classify($table_name), $class_name);
}
)+
};
}
tableize_round_trip_tests! {
inflector_tableize_round_trip_001 => ("PrimarySpokesman", "primary_spokesmen"),
inflector_tableize_round_trip_002 => ("NodeChild", "node_children"),
inflector_tableize_round_trip_003 => ("Calculu", "calculus"),
}
macro_rules! classify_with_schema_tests {
($($name:ident => ($table_name:expr, $class_name:expr)),+ $(,)?) => {
$(
#[test]
fn $name() {
assert_eq!(classify(&format!("schema.{}", $table_name)), $class_name);
}
)+
};
}
classify_with_schema_tests! {
inflector_classify_with_schema_001 => ("primary_spokesmen", "PrimarySpokesman"),
inflector_classify_with_schema_002 => ("node_children", "NodeChild"),
inflector_classify_with_schema_003 => ("calculus", "Calculu"),
}
macro_rules! parameterize_custom_separator_tests {
($($name:ident => ($input:expr, $expected:expr)),+ $(,)?) => {
$(
#[test]
fn $name() {
assert_eq!(parameterize_with_sep($input, "__sep__"), $expected);
}
)+
};
}
parameterize_custom_separator_tests! {
inflector_parameterize_custom_separator_001 => ("Donald E. Knuth", "donald__sep__e__sep__knuth"),
inflector_parameterize_custom_separator_002 => ("Random text with *(bad)* characters", "random__sep__text__sep__with__sep__bad__sep__characters"),
inflector_parameterize_custom_separator_003 => ("Allow_Under_Scores", "allow_under_scores"),
inflector_parameterize_custom_separator_004 => ("Trailing bad characters!@#", "trailing__sep__bad__sep__characters"),
inflector_parameterize_custom_separator_005 => ("!@#Leading bad characters", "leading__sep__bad__sep__characters"),
inflector_parameterize_custom_separator_006 => ("Squeeze separators", "squeeze__sep__separators"),
inflector_parameterize_custom_separator_007 => ("Test with + sign", "test__sep__with__sep__sign"),
inflector_parameterize_custom_separator_008 => ("Test with malformed utf8 \u{00A9}", "test__sep__with__sep__malformed__sep__utf8"),
}
macro_rules! custom_irregular_rule_tests {
($($name:ident => ($singular:expr, $plural:expr)),+ $(,)?) => {
$(
#[test]
fn $name() {
let mut inflections = Inflections::english();
inflections.add_irregular($singular, $plural);
assert_eq!(inflections.pluralize($singular), $plural);
assert_eq!(inflections.singularize($plural), $singular);
assert_eq!(
inflections.pluralize(&capitalize_first($singular)),
capitalize_first($plural)
);
}
)+
};
}
custom_irregular_rule_tests! {
inflector_custom_irregular_rule_001 => ("tooth", "teeth"),
inflector_custom_irregular_rule_002 => ("goose", "geese"),
inflector_custom_irregular_rule_003 => ("foot", "feet"),
inflector_custom_irregular_rule_004 => ("formula", "formulae"),
inflector_custom_irregular_rule_005 => ("radius", "radii"),
inflector_custom_irregular_rule_006 => ("stimulus", "stimuli"),
inflector_custom_irregular_rule_007 => ("brother", "brethren"),
inflector_custom_irregular_rule_008 => ("die", "dice"),
}
macro_rules! custom_uncountable_rule_tests {
($($name:ident => $word:expr),+ $(,)?) => {
$(
#[test]
fn $name() {
let mut inflections = Inflections::english();
inflections.add_uncountables(&[$word]);
let phrase = format!("my {}", $word);
assert_eq!(inflections.pluralize($word), $word);
assert_eq!(inflections.singularize($word), $word);
assert_eq!(inflections.pluralize(&phrase), phrase);
}
)+
};
}
custom_uncountable_rule_tests! {
inflector_custom_uncountable_rule_001 => "metadata",
inflector_custom_uncountable_rule_002 => "moose",
inflector_custom_uncountable_rule_003 => "craft",
inflector_custom_uncountable_rule_004 => "offspring",
}
#[test]
fn inflector_humanize_preserves_accented_case_transforms() {
assert_eq!(humanize("áÉÍÓÚ"), "Áéíóú");
}
#[test]
fn inflector_humanize_preserves_cyrillic_case_transforms() {
assert_eq!(humanize("аБВГДЕ"), "Абвгде");
}
macro_rules! transliteration_tests {
($($name:ident => ($input:expr, $expected:expr)),+ $(,)?) => {
$(
#[test]
fn $name() {
assert_eq!(parameterize($input), $expected);
}
)+
};
}
transliteration_tests! {
inflector_transliteration_001 => ("À", "a"),
inflector_transliteration_002 => ("à", "a"),
inflector_transliteration_003 => ("Æ", "ae"),
inflector_transliteration_004 => ("æ", "ae"),
inflector_transliteration_005 => ("Ç", "c"),
inflector_transliteration_006 => ("ç", "c"),
inflector_transliteration_007 => ("Ð", "d"),
inflector_transliteration_008 => ("ð", "d"),
inflector_transliteration_009 => ("È", "e"),
inflector_transliteration_010 => ("è", "e"),
inflector_transliteration_011 => ("Ĝ", "g"),
inflector_transliteration_012 => ("ĝ", "g"),
inflector_transliteration_013 => ("Ĥ", "h"),
inflector_transliteration_014 => ("ĥ", "h"),
inflector_transliteration_015 => ("Ì", "i"),
inflector_transliteration_016 => ("ì", "i"),
inflector_transliteration_017 => ("Ł", "l"),
inflector_transliteration_018 => ("ł", "l"),
inflector_transliteration_019 => ("Ñ", "n"),
inflector_transliteration_020 => ("ñ", "n"),
inflector_transliteration_021 => ("Œ", "oe"),
inflector_transliteration_022 => ("œ", "oe"),
inflector_transliteration_023 => ("ẞ", "ss"),
inflector_transliteration_024 => ("ß", "ss"),
inflector_transliteration_025 => ("Þ", "th"),
inflector_transliteration_026 => ("þ", "th"),
inflector_transliteration_027 => ("Ý", "y"),
inflector_transliteration_028 => ("ý", "y"),
inflector_transliteration_029 => ("Ŵ", "w"),
inflector_transliteration_030 => ("ŵ", "w"),
inflector_transliteration_031 => ("Ź", "z"),
inflector_transliteration_032 => ("ź", "z"),
}
macro_rules! preserve_case_tests {
($($name:ident => ($replacement:expr, $matched:expr, $expected:expr)),+ $(,)?) => {
$(
#[test]
fn $name() {
assert_eq!(preserve_case($replacement, $matched), $expected);
}
)+
};
}
preserve_case_tests! {
inflector_preserve_case_001 => ("person", "PERSON", "PERSON"),
inflector_preserve_case_002 => ("person", "Person", "Person"),
inflector_preserve_case_003 => ("person", "person", "person"),
}
macro_rules! whole_word_suffix_tests {
($($name:ident => ($word:expr, $suffix:expr, $expected:expr)),+ $(,)?) => {
$(
#[test]
fn $name() {
assert_eq!(ends_with_whole_word_case_insensitive($word, $suffix), $expected);
}
)+
};
}
whole_word_suffix_tests! {
inflector_whole_word_suffix_001 => ("money", "money", true),
inflector_whole_word_suffix_002 => ("my money", "money", true),
inflector_whole_word_suffix_003 => ("old_money", "money", false),
inflector_whole_word_suffix_004 => ("moneymaker", "money", false),
}
macro_rules! parameterize_trim_and_squeeze_tests {
($($name:ident => ($input:expr, $separator:expr, $expected:expr)),+ $(,)?) => {
$(
#[test]
fn $name() {
assert_eq!(parameterize_with_sep($input, $separator), $expected);
}
)+
};
}
parameterize_trim_and_squeeze_tests! {
inflector_parameterize_trim_and_squeeze_001 => (" spaced ", "__", "spaced"),
inflector_parameterize_trim_and_squeeze_002 => ("++spaced++", "__", "spaced"),
inflector_parameterize_trim_and_squeeze_003 => ("a---b", "-", "a-b"),
inflector_parameterize_trim_and_squeeze_004 => ("a + + b", "__", "a__b"),
}
macro_rules! foreign_key_without_separator_tests {
($($name:ident => ($input:expr, $expected:expr)),+ $(,)?) => {
$(
#[test]
fn $name() {
assert_eq!(foreign_key_with_separator($input, false), $expected);
}
)+
};
}
foreign_key_without_separator_tests! {
inflector_foreign_key_without_separator_001 => ("Person", "personid"),
inflector_foreign_key_without_separator_002 => ("MyApplication::Billing::Account", "accountid"),
}
macro_rules! underscore_dasherize_round_trip_tests {
($($name:ident => ($input:expr, $expected:expr)),+ $(,)?) => {
$(
#[test]
fn $name() {
assert_eq!(dasherize($input), $expected);
assert_eq!(underscore($expected), $input);
}
)+
};
}
underscore_dasherize_round_trip_tests! {
inflector_underscore_dasherize_round_trip_001 => ("street", "street"),
inflector_underscore_dasherize_round_trip_002 => ("street_address", "street-address"),
inflector_underscore_dasherize_round_trip_003 => ("person_street_address", "person-street-address"),
}
macro_rules! lower_camel_tests {
($($name:ident => ($input:expr, $expected:expr)),+ $(,)?) => {
$(
#[test]
fn $name() {
assert_eq!(camelize_lower($input), $expected);
}
)+
};
}
lower_camel_tests! {
inflector_lower_camel_001 => ("product", "product"),
inflector_lower_camel_002 => ("special_guest", "specialGuest"),
inflector_lower_camel_003 => ("application_controller", "applicationController"),
inflector_lower_camel_004 => ("area51_controller", "area51Controller"),
}
#[test]
fn inflector_lowercase_first_only_changes_first_character() {
assert_eq!(lowercase_first("HTML"), "hTML");
}
#[test]
fn inflector_capitalize_first_only_changes_first_character() {
assert_eq!(capitalize_first("report"), "Report");
}
#[test]
fn inflector_uppercase_first_alpha_skips_leading_punctuation() {
assert_eq!(uppercase_first_alpha("¿por qué?"), "¿Por qué?");
}
#[test]
fn inflector_uppercase_first_alpha_leaves_non_alpha_strings_unchanged() {
assert_eq!(uppercase_first_alpha("123"), "123");
}
macro_rules! ordinal_boundary_tests {
($($name:ident => ($number:expr, $suffix:expr)),+ $(,)?) => {
$(
#[test]
fn $name() {
assert_eq!(ordinal($number), $suffix);
assert_eq!(ordinalize($number), format!("{}{}", $number, $suffix));
}
)+
};
}
ordinal_boundary_tests! {
inflector_ordinal_boundary_001 => (-1, "st"),
inflector_ordinal_boundary_002 => (-2, "nd"),
inflector_ordinal_boundary_003 => (-3, "rd"),
inflector_ordinal_boundary_004 => (-11, "th"),
inflector_ordinal_boundary_005 => (0, "th"),
inflector_ordinal_boundary_006 => (1, "st"),
inflector_ordinal_boundary_007 => (2, "nd"),
inflector_ordinal_boundary_008 => (3, "rd"),
inflector_ordinal_boundary_009 => (11, "th"),
inflector_ordinal_boundary_010 => (12, "th"),
inflector_ordinal_boundary_011 => (13, "th"),
inflector_ordinal_boundary_012 => (21, "st"),
inflector_ordinal_boundary_013 => (22, "nd"),
inflector_ordinal_boundary_014 => (23, "rd"),
inflector_ordinal_boundary_015 => (111, "th"),
}
}