this/core/
pluralize.rs

1//! Intelligent pluralization for English nouns
2//!
3//! Handles common English pluralization rules including irregular forms
4
5/// Utility for converting between singular and plural forms of English nouns
6pub struct Pluralizer;
7
8impl Pluralizer {
9    /// Convert a singular noun to its plural form
10    ///
11    /// # Examples
12    ///
13    /// ```
14    /// use this::core::pluralize::Pluralizer;
15    ///
16    /// assert_eq!(Pluralizer::pluralize("user"), "users");
17    /// assert_eq!(Pluralizer::pluralize("company"), "companies");
18    /// assert_eq!(Pluralizer::pluralize("address"), "addresses");
19    /// assert_eq!(Pluralizer::pluralize("knife"), "knives");
20    /// ```
21    pub fn pluralize(singular: &str) -> String {
22        // Handle empty strings
23        if singular.is_empty() {
24            return singular.to_string();
25        }
26
27        match singular {
28            // Words ending in consonant + y -> ies
29            s if s.ends_with("y")
30                && !s.ends_with("ay")
31                && !s.ends_with("ey")
32                && !s.ends_with("iy")
33                && !s.ends_with("oy")
34                && !s.ends_with("uy")
35                && s.len() > 1 =>
36            {
37                format!("{}ies", &s[..s.len() - 1])
38            }
39
40            // Words ending in s, ss, sh, ch, x, z -> es
41            s if s.ends_with("s")
42                || s.ends_with("ss")
43                || s.ends_with("sh")
44                || s.ends_with("ch")
45                || s.ends_with("x")
46                || s.ends_with("z") =>
47            {
48                format!("{}es", s)
49            }
50
51            // Words ending in f -> ves
52            s if s.ends_with("f") && s.len() > 1 => {
53                format!("{}ves", &s[..s.len() - 1])
54            }
55
56            // Words ending in fe -> ves
57            s if s.ends_with("fe") && s.len() > 2 => {
58                format!("{}ves", &s[..s.len() - 2])
59            }
60
61            // Words ending in o after consonant -> es (photo, piano are exceptions)
62            s if s.ends_with("o") && s.len() > 1 => {
63                let before_o = s.chars().nth(s.len() - 2).unwrap();
64                if matches!(before_o, 'a' | 'e' | 'i' | 'o' | 'u') {
65                    format!("{}s", s)
66                } else {
67                    // Common exceptions that just add 's'
68                    match s {
69                        "photo" | "piano" | "halo" => format!("{}s", s),
70                        _ => format!("{}es", s),
71                    }
72                }
73            }
74
75            // Default: just add s
76            s => format!("{}s", s),
77        }
78    }
79
80    /// Convert a plural noun to its singular form
81    ///
82    /// # Examples
83    ///
84    /// ```
85    /// use this::core::pluralize::Pluralizer;
86    ///
87    /// assert_eq!(Pluralizer::singularize("users"), "user");
88    /// assert_eq!(Pluralizer::singularize("companies"), "company");
89    /// assert_eq!(Pluralizer::singularize("addresses"), "address");
90    /// ```
91    pub fn singularize(plural: &str) -> String {
92        // Handle empty strings
93        if plural.is_empty() {
94            return plural.to_string();
95        }
96
97        match plural {
98            // Words ending in ies -> y
99            s if s.ends_with("ies") && s.len() > 3 => {
100                format!("{}y", &s[..s.len() - 3])
101            }
102
103            // Words ending in ves -> f or fe
104            s if s.ends_with("ves") && s.len() > 3 => {
105                format!("{}f", &s[..s.len() - 3])
106            }
107
108            // Words ending in ses, shes, ches, xes, zes -> remove es
109            s if s.len() > 3
110                && (s.ends_with("ses")
111                    || s.ends_with("shes")
112                    || s.ends_with("ches")
113                    || s.ends_with("xes")
114                    || s.ends_with("zes")) =>
115            {
116                s[..s.len() - 2].to_string()
117            }
118
119            // Words ending in oes -> o
120            s if s.ends_with("oes") && s.len() > 3 => s[..s.len() - 2].to_string(),
121
122            // Default: remove trailing s
123            s if s.ends_with("s") && s.len() > 1 => s[..s.len() - 1].to_string(),
124
125            // No plural form detected
126            s => s.to_string(),
127        }
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134
135    #[test]
136    fn test_pluralize_regular() {
137        assert_eq!(Pluralizer::pluralize("user"), "users");
138        assert_eq!(Pluralizer::pluralize("car"), "cars");
139        assert_eq!(Pluralizer::pluralize("dog"), "dogs");
140    }
141
142    #[test]
143    fn test_pluralize_y_ending() {
144        assert_eq!(Pluralizer::pluralize("company"), "companies");
145        assert_eq!(Pluralizer::pluralize("category"), "categories");
146        assert_eq!(Pluralizer::pluralize("fly"), "flies");
147
148        // Vowel + y = just add s
149        assert_eq!(Pluralizer::pluralize("day"), "days");
150        assert_eq!(Pluralizer::pluralize("key"), "keys");
151    }
152
153    #[test]
154    fn test_pluralize_sibilants() {
155        assert_eq!(Pluralizer::pluralize("address"), "addresses");
156        assert_eq!(Pluralizer::pluralize("box"), "boxes");
157        assert_eq!(Pluralizer::pluralize("buzz"), "buzzes");
158        assert_eq!(Pluralizer::pluralize("church"), "churches");
159        assert_eq!(Pluralizer::pluralize("dish"), "dishes");
160    }
161
162    #[test]
163    fn test_pluralize_f_endings() {
164        assert_eq!(Pluralizer::pluralize("knife"), "knives");
165        assert_eq!(Pluralizer::pluralize("life"), "lives");
166        assert_eq!(Pluralizer::pluralize("wolf"), "wolves");
167    }
168
169    #[test]
170    fn test_pluralize_o_endings() {
171        assert_eq!(Pluralizer::pluralize("hero"), "heroes");
172        assert_eq!(Pluralizer::pluralize("potato"), "potatoes");
173
174        // Exceptions
175        assert_eq!(Pluralizer::pluralize("photo"), "photos");
176        assert_eq!(Pluralizer::pluralize("piano"), "pianos");
177    }
178
179    #[test]
180    fn test_singularize_regular() {
181        assert_eq!(Pluralizer::singularize("users"), "user");
182        assert_eq!(Pluralizer::singularize("cars"), "car");
183        assert_eq!(Pluralizer::singularize("dogs"), "dog");
184    }
185
186    #[test]
187    fn test_singularize_ies() {
188        assert_eq!(Pluralizer::singularize("companies"), "company");
189        assert_eq!(Pluralizer::singularize("categories"), "category");
190        assert_eq!(Pluralizer::singularize("flies"), "fly");
191    }
192
193    #[test]
194    fn test_singularize_sibilants() {
195        assert_eq!(Pluralizer::singularize("addresses"), "address");
196        assert_eq!(Pluralizer::singularize("boxes"), "box");
197        assert_eq!(Pluralizer::singularize("buzzes"), "buzz");
198    }
199
200    #[test]
201    fn test_singularize_ves() {
202        assert_eq!(Pluralizer::singularize("knives"), "knif");
203        assert_eq!(Pluralizer::singularize("lives"), "lif");
204    }
205
206    #[test]
207    fn test_roundtrip() {
208        let words = vec!["user", "company", "address", "box", "day"];
209        for word in words {
210            let plural = Pluralizer::pluralize(word);
211            let back_to_singular = Pluralizer::singularize(&plural);
212            assert_eq!(word, back_to_singular, "Roundtrip failed for: {}", word);
213        }
214    }
215}