inflection_rs/
lib.rs

1#![forbid(unsafe_code)]
2#![allow(dead_code)]
3
4use std::collections::HashSet;
5use regex::Regex;
6use lazy_static::lazy_static;
7
8macro_rules! case_insensitive {
9    ($str:expr) => {{
10        $str
11            .chars()
12            .map(|c| format!("[{}{}]", c.to_string(), c.to_uppercase().to_string()))
13            .collect::<String>()
14    }};
15}
16
17type Upsc = (HashSet<String>, Vec<(Regex, String)>, Vec<(Regex, String)>, Vec<Regex>);
18lazy_static! {
19    static ref UPS: Upsc = {
20        let mut plurals: Vec<(String, String)> = vec![
21                (r"(?i)(?P<a>\w+)s?-in-law$".to_string(), "${a}s-in-law".to_string()),
22                (r"(?i)(?P<a>quiz)$".to_string(), "${a}zes".to_string()),
23                (r"(?i)^(?P<a>oxen)$".to_string(), "${a}".to_string()),
24                (r"(?i)^(?P<a>ox)$".to_string(), "${a}en".to_string()),
25                (r"(?i)(?P<a>m|l)ice$".to_string(), "${a}ice".to_string()),
26                (r"(?i)(?P<a>m|l)ouse$".to_string(), "${a}ice".to_string()),
27                (r"(?i)(?P<a>passer)s?by$".to_string(), "${a}sby".to_string()),
28                (
29                    r"(?i)(?P<a>matr|vert|ind)(?:ix|ex)$".to_string(),
30                    "${a}ices".to_string(),
31                ),
32                (r"(?i)(?P<a>x|ch|ss|sh)$".to_string(), "${a}es".to_string()),
33                (
34                    r"(?i)(?P<a>[^aeiouy]|qu)y$".to_string(),
35                    "${a}ies".to_string(),
36                ),
37                (r"(?i)(?P<a>hive)$".to_string(), "${a}s".to_string()),
38                (r"(?i)(?P<a>[lr])f$".to_string(), "${a}ves".to_string()),
39                (r"(?i)(?P<a>[^f])fe$".to_string(), "${a}ves".to_string()),
40                (r"(?i)sis$".to_string(), "ses".to_string()),
41                (r"(?i)(?P<a>[ti])a$".to_string(), "${a}a".to_string()),
42                (r"(?i)(?P<a>[ti])um$".to_string(), "${a}a".to_string()),
43                (
44                    r"(?i)(?P<a>buffal|potat|tomat|her)o$".to_string(),
45                    "${a}oes".to_string(),
46                ),
47                (r"(?i)(?P<a>bu)s$".to_string(), "${a}ses".to_string()),
48                (
49                    r"(?i)(?P<a>alias|status)$".to_string(),
50                    "${a}es".to_string(),
51                ),
52                (r"(?i)(?P<a>octop|vir|radi)i$".to_string(), "${a}i".to_string()),
53                (r"(?i)(?P<a>octop|vir|radi)us$".to_string(), "${a}i".to_string()),
54                (r"(?i)^(?P<a>ax|test)is$".to_string(), "${a}es".to_string()),
55                (r"(?i)s$".to_string(), "s".to_string()),
56                (r"$".to_string(), "s".to_string()),
57            ];
58
59            let mut singulars: Vec<(String, String)> = vec![
60                (r"(?i)(?P<a>\w+)s-in-law$".to_string(), "${a}-in-law".to_string()),
61                (r"(?i)(?P<a>database)s$".to_string(), "${a}".to_string()),
62                (r"(?i)(?P<a>quiz)zes$".to_string(), "${a}".to_string()),
63                (r"(?i)(?P<a>matr)ices$".to_string(), "${a}ix".to_string()),
64                (
65                    r"(?i)(?P<a>vert|ind)ices$".to_string(),
66                    "${a}ex".to_string(),
67                ),
68                (r"(?i)(?P<a>passer)sby$".to_string(), "${a}by".to_string()),
69                (r"(?i)^(?P<a>ox)en".to_string(), "${a}".to_string()),
70                (
71                    r"(?i)(?P<a>alias|status)(es)?$".to_string(),
72                    "${a}".to_string(),
73                ),
74                (
75                    r"(?i)(?P<a>octop|vir|radi)(us|i)$".to_string(),
76                    "${a}us".to_string(),
77                ),
78                (r"(?i)^(?P<a>a)x[ie]s$".to_string(), "${a}xis".to_string()),
79                (
80                    r"(?i)(?P<a>cris|test)(is|es)$".to_string(),
81                    "${a}is".to_string(),
82                ),
83                (r"(?i)(?P<a>shoe)s$".to_string(), "${a}".to_string()),
84                (r"(?i)(?P<a>o)es$".to_string(), "${a}".to_string()),
85                (r"(?i)(?P<a>bus)(es)?$".to_string(), "${a}".to_string()),
86                (r"(?i)(?P<a>m|l)ice$".to_string(), "${a}ouse".to_string()),
87                (r"(?i)(?P<a>x|ch|ss|sh)es$".to_string(), "${a}".to_string()),
88                (r"(?i)(?P<a>m)ovies$".to_string(), "${a}ovie".to_string()),
89                (r"(?i)(?P<a>s)eries$".to_string(), "${a}eries".to_string()),
90                (
91                    r"(?i)(?P<a>[^aeiouy]|qu)ies$".to_string(),
92                    "${a}y".to_string(),
93                ),
94                (r"(?i)(?P<a>[lr])ves$".to_string(), "${a}f".to_string()),
95                (r"(?i)(?P<a>tive)s$".to_string(), "${a}".to_string()),
96                (r"(?i)(?P<a>hive)s$".to_string(), "${a}".to_string()),
97                (r"(?i)(?P<a>[^f])ves$".to_string(), "${a}fe".to_string()),
98                (
99                    r"(?i)(?P<a>t)he(sis|ses)$".to_string(),
100                    "${a}hesis".to_string(),
101                ),
102                (
103                    r"(?i)(?P<a>s)ynop(sis|ses)$".to_string(),
104                    "${a}ynopsis".to_string(),
105                ),
106                (
107                    r"(?i)(?P<a>p)rogno(sis|ses)$".to_string(),
108                    "${a}rognosis".to_string(),
109                ),
110                (
111                    r"(?i)(?P<a>p)arenthe(sis|ses)$".to_string(),
112                    "${a}arenthesis".to_string(),
113                ),
114                (
115                    r"(?i)(?P<a>d)iagno(sis|ses)$".to_string(),
116                    "${a}iagnosis".to_string(),
117                ),
118                (
119                    r"(?i)(?P<a>b)a(sis|ses)$".to_string(),
120                    "${a}asis".to_string(),
121                ),
122                (
123                    r"(?i)(?P<a>a)naly(sis|ses)$".to_string(),
124                    "${a}nalysis".to_string(),
125                ),
126                (r"(?i)(?P<a>[ti])a$".to_string(), "${a}um".to_string()),
127                (r"(?i)(?P<a>n)ews$".to_string(), "${a}ews".to_string()),
128                (r"(?i)(?P<a>ss)$".to_string(), "${a}".to_string()),
129                (r"(?i)s$".to_string(), "".to_string()),
130            ];
131
132        let uncountable = HashSet::<String>::from([
133            "equipment".to_string(),
134            "fish".to_string(),
135            "information".to_string(),
136            "jeans".to_string(),
137            "money".to_string(),
138            "rice".to_string(),
139            "series".to_string(),
140            "sheep".to_string(),
141            "species".to_string(),
142        ]);
143
144        let uncountable_progs: Vec<Regex> = uncountable
145            .clone()
146            .into_iter()
147            .map(|x| {
148                Regex::new(&format!(r"(?i)\b({})\z", x)).unwrap()
149            })
150            .collect();
151
152        let add_irregular = |
153            plurals: &mut Vec<(String, String)>,
154            singulars: &mut Vec<(String, String)>,
155            singular: String,
156            plural: String
157        | {
158            let singular_first_char = &singular[..1];
159            let plural_first_char = &plural[..1];
160
161            let plural_stem = &plural[1..];
162            let singular_stem = &singular[1..];
163
164            if singular_first_char.to_uppercase() == plural_first_char.to_uppercase() {
165                // let a: &'static str = (r"(?i)(?P<a>".to_owned() + singular_first_char); // + r")" + singular_stem.to_owned() + "$";
166                plurals.insert(
167                    0,
168                    (
169                        format!(r"(?i)(?P<a>{}){}$", singular_first_char, singular_stem),
170                        format!("{}{}", r"${a}".to_owned(), plural_stem),
171                    ),
172                );
173                plurals.insert(
174                    0,
175                    (
176                        format!(r"(?i)(?P<a>{}){}$", plural_first_char, plural_stem),
177                        format!("{}{}", r"${a}".to_owned(), plural_stem),
178                    ),
179                );
180
181                singulars.insert(
182                    0,
183                    (
184                        format!(r"(?i)(?P<a>{}){}$", plural_first_char, plural_stem),
185                        format!("{}{}", r"${a}".to_owned(), singular_stem),
186                    ),
187                );
188            } else {
189                let plural_copy_upper1 =
190                    format!("{}{}", plural_first_char.to_uppercase(), plural_stem);
191
192                let plural_copy_lower1 =
193                    format!("{}{}", plural_first_char.to_lowercase(), plural_stem);
194
195                let plural_copy_upper2 =
196                    format!("{}{}", plural_first_char.to_uppercase(), plural_stem);
197
198                let plural_copy_lower2 =
199                    format!("{}{}", plural_first_char.to_lowercase(), plural_stem);
200
201                let singular_copy_upper1 =
202                    format!("{}{}", singular_first_char.to_uppercase(), singular_stem);
203
204                let singular_copy_lower1 =
205                    format!("{}{}", singular_first_char.to_lowercase(), singular_stem);
206
207                plurals.insert(
208                    0,
209                    (
210                        format!(
211                            r"{}{}$",
212                            singular_first_char.to_uppercase(),
213                            case_insensitive!(singular_stem)
214                        ),
215                        plural_copy_upper1,
216                    ),
217                );
218                plurals.insert(
219                    0,
220                    (
221                        format!(
222                            r"{}{}$",
223                            singular_first_char.to_lowercase(),
224                            case_insensitive!(singular_stem)
225                        ),
226                        plural_copy_lower1,
227                    ),
228                );
229                plurals.insert(
230                    0,
231                    (
232                        format!(
233                            r"{}{}$",
234                            plural_first_char.to_uppercase(),
235                            case_insensitive!(plural_stem)
236                        ),
237                        plural_copy_upper2,
238                    ),
239                );
240                plurals.insert(
241                    0,
242                    (
243                        format!(
244                            r"{}{}$",
245                            plural_first_char.to_lowercase(),
246                            case_insensitive!(plural_stem)
247                        ),
248                        plural_copy_lower2,
249                    ),
250                );
251                singulars.insert(
252                    0,
253                    (
254                        format!(
255                            r"{}{}$",
256                            plural_first_char.to_uppercase(),
257                            case_insensitive!(plural_stem)
258                        ),
259                        singular_copy_upper1,
260                    ),
261                );
262                singulars.insert(
263                    0,
264                    (
265                        format!(
266                            r"{}{}$",
267                            plural_first_char.to_lowercase(),
268                            case_insensitive!(plural_stem)
269                        ),
270                        singular_copy_lower1,
271                    ),
272                );
273            }
274        };
275
276        add_irregular(&mut plurals, &mut singulars, "person".to_string(), "people".to_string());
277        add_irregular(&mut plurals, &mut singulars, "man".to_string(), "men".to_string());
278        add_irregular(&mut plurals, &mut singulars, "human".to_string(), "humans".to_string());
279        add_irregular(&mut plurals, &mut singulars, "child".to_string(), "children".to_string());
280        add_irregular(&mut plurals, &mut singulars, "sex".to_string(), "sexes".to_string());
281        add_irregular(&mut plurals, &mut singulars, "move".to_string(), "moves".to_string());
282        add_irregular(&mut plurals, &mut singulars, "cow".to_string(), "kine".to_string());
283        add_irregular(&mut plurals, &mut singulars, "zombie".to_string(), "zombies".to_string());
284        add_irregular(&mut plurals, &mut singulars, "slave".to_string(), "slaves".to_string());
285        add_irregular(&mut plurals, &mut singulars, "this".to_string(), "this".to_string());
286        add_irregular(&mut plurals, &mut singulars, "flour".to_string(), "flour".to_string());
287        add_irregular(&mut plurals, &mut singulars, "milk".to_string(), "milk".to_string());
288        add_irregular(&mut plurals, &mut singulars, "water".to_string(), "water".to_string());
289        add_irregular(&mut plurals, &mut singulars, "reserve".to_string(), "reserves".to_string());
290        add_irregular(&mut plurals, &mut singulars, "gas".to_string(), "gasses".to_string());
291        add_irregular(&mut plurals, &mut singulars, "bias".to_string(), "biases".to_string());
292        add_irregular(&mut plurals, &mut singulars, "atlas".to_string(), "atlases".to_string());
293        add_irregular(&mut plurals, &mut singulars, "goose".to_string(), "geese".to_string());
294        add_irregular(&mut plurals, &mut singulars, "pasta".to_string(), "pastas".to_string());
295        add_irregular(&mut plurals, &mut singulars, "slice".to_string(), "slices".to_string());
296        add_irregular(&mut plurals, &mut singulars, "cactus".to_string(), "cacti".to_string());
297        add_irregular(&mut plurals, &mut singulars, "buzz".to_string(), "buzzes".to_string());
298
299        let plurals: Vec<(Regex, String)> = plurals
300            .into_iter()
301            .map(|(rule, repl)| {
302                (Regex::new(&rule).unwrap(), repl)
303            })
304            .collect();
305        
306        let singulars: Vec<(Regex, String)> = singulars
307            .into_iter()
308            .map(|(rule, repl)| {
309                (Regex::new(&rule).unwrap(), repl)
310            })
311            .collect();
312
313        (uncountable, plurals, singulars, uncountable_progs)
314    };
315}
316
317#[doc = include_str ! ("./../README.md")]
318pub mod inflection {
319    use std::collections::HashSet;
320    use regex::Regex;
321    use lazy_static::lazy_static;
322
323    use crate::UPS;
324
325    #[inline]
326    fn get_uncountable() -> &'static HashSet<String> {
327        &UPS.0
328    }
329
330    #[inline]
331    fn get_uncountable_compiled() -> &'static Vec<Regex> {
332        &UPS.3
333    }
334
335    #[inline]
336    fn get_plurals() -> &'static Vec<(Regex, String)> {
337        &UPS.1
338    }
339
340    #[inline]
341    fn get_singulars() -> &'static Vec<(Regex, String)> {
342        &UPS.2
343    }
344
345    macro_rules! create_ordinal_function {
346        ($func_name:ident, $abs:expr, $param_type:ty) => {
347            pub fn $func_name(number: $param_type) -> String {
348                let n = $abs(number);
349                match n % 100 {
350                    11 | 12 | 13 => "th".to_string(),
351                    _ => match n % 10 {
352                        1 => "st".to_string(),
353                        2 => "nd".to_string(),
354                        3 => "rd".to_string(),
355                        _ => "th".to_string(),
356                    },
357                }
358            }
359        };
360    }
361
362    macro_rules! create_ordinalize_function {
363        ($func_name:ident, $ordinal_function:ident, $param_type:ty) => {
364            pub fn $func_name(number: $param_type) -> String {
365                format!("{}{}", number, $ordinal_function(number))
366            }
367        };
368    }
369
370    create_ordinal_function!(ordinal_i8, |x: i8| x.abs(), i8);
371    create_ordinal_function!(ordinal_i16, |x: i16| x.abs(), i16);
372    create_ordinal_function!(ordinal_i32, |x: i32| x.abs(), i32);
373    create_ordinal_function!(ordinal_i64, |x: i64| x.abs(), i64);
374    create_ordinal_function!(ordinal_i128, |x: i128| x.abs(), i128);
375    create_ordinal_function!(ordinal_u8, |x: u8| x, u8);
376    create_ordinal_function!(ordinal_u16, |x: u16| x, u16);
377    create_ordinal_function!(ordinal_u32, |x: u32| x, u32);
378    create_ordinal_function!(ordinal_u64, |x: u64| x, u64);
379    create_ordinal_function!(ordinal_u128, |x: u128| x, u128);
380    create_ordinal_function!(ordinal_usize, |x: usize| x, usize);
381
382    create_ordinalize_function!(ordinalize_i8, ordinal_i8, i8);
383    create_ordinalize_function!(ordinalize_i16, ordinal_i16, i16);
384    create_ordinalize_function!(ordinalize_i32, ordinal_i32, i32);
385    create_ordinalize_function!(ordinalize_i64, ordinal_i64, i64);
386    create_ordinalize_function!(ordinalize_i128, ordinal_i128, i128);
387    create_ordinalize_function!(ordinalize_u8, ordinal_u8, u8);
388    create_ordinalize_function!(ordinalize_u16, ordinal_u16, u16);
389    create_ordinalize_function!(ordinalize_u32, ordinal_u32, u32);
390    create_ordinalize_function!(ordinalize_u64, ordinal_u64, u64);
391    create_ordinalize_function!(ordinalize_u128, ordinal_u128, u128);
392    create_ordinalize_function!(ordinalize_usize, ordinal_usize, usize);
393
394    pub fn camelize<S: AsRef<str>>(string: S) -> String {
395        camelize_upper(string, true)
396    }
397
398    pub fn camelize_upper<S: AsRef<str>>(string: S, uppercase_first_letter: bool) -> String {
399        let input_string = string.as_ref().to_owned();
400
401        if input_string.is_empty() {
402            return input_string;
403        }
404
405        if uppercase_first_letter {
406            lazy_static! {
407                static ref CU_RE: Regex = Regex::new(r"(?:^|_)(.)").unwrap();
408            }
409            let mut result: String = input_string.to_owned();
410
411            for cap in CU_RE.find_iter(&input_string) {
412                let replace_with = &cap
413                    .as_str()
414                    .chars()
415                    .last()
416                    .unwrap_or(' ')
417                    .to_uppercase()
418                    .to_string();
419                result.replace_range(cap.range(), replace_with);
420            }
421            return result;
422        }
423
424        let input_string = camelize_upper(input_string, true);
425        let mut result = string
426            .as_ref()
427            .to_string()
428            .chars()
429            .next()
430            .unwrap_or(' ')
431            .to_lowercase()
432            .to_string();
433        result.push_str(&input_string[1..]);
434        result
435    }
436
437    pub fn dasherize<S: AsRef<str>>(word: S) -> String {
438        word.as_ref().to_string().replace('_', "-")
439    }
440
441    pub fn humanize<S: AsRef<str>>(word: S) -> String {
442        lazy_static! {
443            static ref H_ID_PROG: Regex = Regex::new(r"_id$").unwrap();
444            static ref H_STEM_PROG: Regex = Regex::new(r"(?i)([a-z\d]*)").unwrap();
445            static ref H_WORD_PROG: Regex = Regex::new(r"^\w").unwrap();
446        }
447
448        let mut result: String = H_ID_PROG.replace_all(word.as_ref(), "").to_string();
449        result = result.replace('_', " ");
450
451        if result.is_empty() {
452            return result;
453        }
454
455        let updated_result = result.to_owned();
456        for cap in H_STEM_PROG.find_iter(&updated_result) {
457            let replace_with = cap.as_str().to_lowercase().to_string();
458            result.replace_range(cap.range(), &replace_with);
459        }
460
461        let updated_result = result.to_owned();
462        for cap in H_WORD_PROG.find_iter(&updated_result) {
463            let mut replace_with = cap
464                .as_str()
465                .chars()
466                .next()
467                .unwrap_or(' ')
468                .to_uppercase()
469                .to_string();
470            let last_part = &cap.as_str()[1..];
471            replace_with.push_str(last_part);
472            result.replace_range(cap.range(), &replace_with);
473        }
474        result
475    }
476
477    pub fn underscore<S: AsRef<str>>(string: S) -> String {
478        lazy_static! {
479            static ref U_PROG1: Regex = Regex::new(r"(?P<a>[A-Z]+)(?P<b>[A-Z][a-z])").unwrap();
480            static ref U_PROG2: Regex = Regex::new(r"(?P<a>[a-z\d])(?P<b>[A-Z])").unwrap();
481        }
482        let stand_in = "$a-$b";
483        let mut word = string.as_ref().to_string();
484        word = U_PROG1.replace_all(&word, stand_in).to_string();
485        word = U_PROG2.replace_all(&word, stand_in).to_string();
486        word = word.replace('-', "_");
487        word.to_lowercase()
488    }
489
490    pub fn transliterate<S: AsRef<str>>(string: S) -> String {
491        deunicode::deunicode(string.as_ref())
492    }
493
494    pub fn parameterize_with_sep<S: AsRef<str>>(string: S, sep: String) -> String {
495        let transliterated0 = transliterate(string);
496        let transliterated = transliterated0.as_str();
497
498        let is_sep_empty = sep.is_empty();
499        let sep_copy = sep.to_owned();
500
501        lazy_static! {
502            static ref PWS_CLEAN_PROG: Regex = Regex::new(r"(?i)[^a-z0-9\-_]+").unwrap();
503        }
504        let cleaned0 = PWS_CLEAN_PROG.replace_all(transliterated, sep);
505        let cleaned = cleaned0.as_ref();
506        if !is_sep_empty {
507            let re_sep = regex::escape(&sep_copy);
508            let sep_prog = Regex::new(&format!(r"{}{}", re_sep, re_sep)).unwrap();
509            let leading_sep_prog = Regex::new(&format!(r"(?i)^{}|{}$", re_sep, re_sep)).unwrap();
510
511            let rm_sep = sep_prog.replace_all(cleaned, sep_copy);
512            let rm_sep = leading_sep_prog.replace_all(&rm_sep, "");
513
514            return rm_sep.as_ref().to_lowercase();
515        }
516
517        cleaned.to_lowercase()
518    }
519
520    pub fn parameterize<S: AsRef<str>>(string: S) -> String {
521        parameterize_with_sep::<S>(string, "-".to_string())
522    }
523
524    pub fn pluralize<S: AsRef<str>>(string: S) -> String {
525        let word: &str = string.as_ref();
526        let word_is_empty = word.is_empty();
527        let word_is_in_uncountable: bool =
528            get_uncountable().contains(word.to_lowercase().as_str());
529
530        if word_is_empty || word_is_in_uncountable {
531            return word.to_string();
532        }
533
534        for (rule, repl) in get_plurals().iter() {
535            // let re = Regex::new(rule).unwrap();
536            if rule.is_match(word) {
537                return rule.replace_all(word, repl).to_string();
538            }
539        }
540
541        word.to_string()
542    }
543
544    pub fn singularize<S: AsRef<str>>(string: S) -> String {
545        let word = string.as_ref();
546
547        for re in get_uncountable_compiled().iter() {
548            // let pattern = &format!(r"(?i)\b({})\z", inf);
549            // let re = Regex::new(pattern).unwrap();
550            if re.is_match(word) {
551                return word.to_string();
552            }
553        }
554
555        for (rule, repl) in get_singulars().iter() {
556            // let re = Regex::new(rule).unwrap();
557            if rule.is_match(word) {
558                return rule.replace_all(word, repl).to_string();
559            }
560        }
561
562        word.to_string()
563    }
564
565    pub fn tableize<S: AsRef<str>>(string: S) -> String {
566        let underscore = underscore(string);
567        pluralize(underscore)
568    }
569
570    fn capitalize<S: AsRef<str>>(s: S) -> String {
571        let mut c = s.as_ref().chars();
572        match c.next() {
573            None => String::new(),
574            Some(f) => f.to_uppercase().chain(c).collect(),
575        }
576    }
577
578    pub fn titleize<S: AsRef<str>>(string: S) -> String {
579        let input_string = string.as_ref();
580        let mut result: String = underscore(&string);
581        result = humanize(result);
582        lazy_static! {
583            static ref H_FIRST_PROG: Regex = Regex::new(r"\b((\s+)?'?\w)").unwrap();
584        }
585        for cap in H_FIRST_PROG.find_iter(input_string) {
586            result.replace_range(cap.range(), cap.as_str());
587        }
588        result = result
589            .split(char::is_whitespace)
590            .map(|word| format!(" {}", capitalize(word)))
591            .collect::<String>()
592            .trim()
593            .to_string();
594        result
595    }
596
597    pub fn normalize_spaces<S: AsRef<str>>(string: S) -> String {
598        lazy_static! {
599            static ref NS_RE: Regex = Regex::new(r"\s+").unwrap();
600        }
601        let text = string.as_ref();
602        return NS_RE.replace_all(text, " ").trim().to_string();
603    }
604
605    fn _only_alpha<S: AsRef<str>>(
606        string: S,
607        check_fn: fn(c: &char) -> bool,
608        repl: Option<char>,
609    ) -> String {
610        let chars = string.as_ref().chars();
611
612        chars
613            .filter_map(|c| if !check_fn(&c) { repl } else { Some(c) })
614            .collect()
615    }
616
617    pub fn only_alpha<S: AsRef<str>>(string: S, repl: Option<char>) -> String {
618        let check_fn = |c: &char| c.is_alphabetic();
619        _only_alpha(string.as_ref(), check_fn, repl)
620    }
621
622    pub fn only_alphanum<S: AsRef<str>>(string: S, repl: Option<char>) -> String {
623        let check_fn = |c: &char| c.is_alphanumeric();
624        _only_alpha(string.as_ref(), check_fn, repl)
625    }
626
627    pub fn only_alpha_ascii<S: AsRef<str>>(string: S, repl: Option<char>) -> String {
628        let check_fn = |c: &char| c.is_ascii_alphabetic();
629        _only_alpha(string.as_ref(), check_fn, repl)
630    }
631
632    pub fn only_alphanum_ascii<S: AsRef<str>>(string: S, repl: Option<char>) -> String {
633        let check_fn = |c: &char| c.is_ascii_alphanumeric();
634        _only_alpha(string.as_ref(), check_fn, repl)
635    }
636
637    pub fn keyify<S: AsRef<str>>(string: S) -> String {
638        let result = only_alphanum_ascii(string, Some(' '));
639        let result = normalize_spaces(result);
640        let result = titleize(result);
641        let result = parameterize_with_sep(result, "_".to_string());
642        underscore(result.trim())
643    }
644}
645
646#[cfg(test)]
647mod tests {
648    use crate::inflection;
649
650    const SINGULAR_TO_PLURAL: [(&str, &str); 90] = [
651        ("search", "searches"),
652        ("switch", "switches"),
653        ("fix", "fixes"),
654        ("box", "boxes"),
655        ("process", "processes"),
656        ("address", "addresses"),
657        ("case", "cases"),
658        ("stack", "stacks"),
659        ("wish", "wishes"),
660        ("fish", "fish"),
661        ("jeans", "jeans"),
662        ("funky jeans", "funky jeans"),
663        ("category", "categories"),
664        ("query", "queries"),
665        ("ability", "abilities"),
666        ("agency", "agencies"),
667        ("movie", "movies"),
668        ("archive", "archives"),
669        ("index", "indices"),
670        ("wife", "wives"),
671        ("safe", "saves"),
672        ("half", "halves"),
673        ("move", "moves"),
674        ("salesperson", "salespeople"),
675        ("person", "people"),
676        ("spokesman", "spokesmen"),
677        ("man", "men"),
678        ("woman", "women"),
679        ("basis", "bases"),
680        ("diagnosis", "diagnoses"),
681        ("diagnosis_a", "diagnosis_as"),
682        ("datum", "data"),
683        ("medium", "media"),
684        ("stadium", "stadia"),
685        ("analysis", "analyses"),
686        ("node_child", "node_children"),
687        ("child", "children"),
688        ("experience", "experiences"),
689        ("day", "days"),
690        ("comment", "comments"),
691        ("foobar", "foobars"),
692        ("newsletter", "newsletters"),
693        ("old_news", "old_news"),
694        ("news", "news"),
695        ("series", "series"),
696        ("species", "species"),
697        ("quiz", "quizzes"),
698        ("perspective", "perspectives"),
699        ("ox", "oxen"),
700        ("passerby", "passersby"),
701        ("photo", "photos"),
702        ("buffalo", "buffaloes"),
703        ("tomato", "tomatoes"),
704        ("potato", "potatoes"),
705        ("dwarf", "dwarves"),
706        ("elf", "elves"),
707        ("information", "information"),
708        ("equipment", "equipment"),
709        ("bus", "buses"),
710        ("status", "statuses"),
711        ("status_code", "status_codes"),
712        ("mouse", "mice"),
713        ("louse", "lice"),
714        ("house", "houses"),
715        ("octopus", "octopi"),
716        ("virus", "viri"),
717        ("alias", "aliases"),
718        ("portfolio", "portfolios"),
719        ("vertex", "vertices"),
720        ("matrix", "matrices"),
721        ("matrix_fu", "matrix_fus"),
722        ("axis", "axes"),
723        ("testis", "testes"),
724        ("crisis", "crises"),
725        ("rice", "rice"),
726        ("shoe", "shoes"),
727        ("horse", "horses"),
728        ("prize", "prizes"),
729        ("edge", "edges"),
730        ("cow", "kine"),
731        ("database", "databases"),
732        ("human", "humans"),
733        ("flour", "flour"),
734        ("water", "water"),
735        ("slave", "slaves"),
736        ("milk", "milk"),
737        ("reserve", "reserves"),
738        ("gas", "gasses"),
739        ("bias", "biases"),
740        ("atlas", "atlases"),
741    ];
742
743    const CAMEL_TO_UNDERSCORE: [(&str, &str); 4] = [
744        ("Product", "product"),
745        ("SpecialGuest", "special_guest"),
746        ("ApplicationController", "application_controller"),
747        ("Area51Controller", "area51_controller"),
748    ];
749
750    const CAMEL_TO_UNDERSCORE_WITHOUT_REVERSE: [(&str, &str); 4] = [
751        ("HTMLTidy", "html_tidy"),
752        ("HTMLTidyGenerator", "html_tidy_generator"),
753        ("FreeBSD", "free_bsd"),
754        ("HTML", "html"),
755    ];
756
757    const STRING_TO_PARAMETERIZED: [(&str, &str); 8] = [
758        (r"Donald E. Knuth", "donald-e-knuth"),
759        (
760            r"Random text with *(bad)* characters",
761            "random-text-with-bad-characters",
762        ),
763        (r"Allow_Under_Scores", "allow_under_scores"),
764        (r"Trailing bad characters!@#", "trailing-bad-characters"),
765        (r"!@#Leading bad characters", "leading-bad-characters"),
766        (r"Squeeze   separators", "squeeze-separators"),
767        (r"Test with + sign", "test-with-sign"),
768        (
769            r"Test with malformed utf8 \251",
770            "test-with-malformed-utf8-251",
771        ),
772    ];
773
774    const STRING_TO_PARAMETERIZE_WITH_NO_SEPARATOR: [(&str, &str); 8] = [
775        (r"Donald E. Knuth", "donaldeknuth"),
776        (r"With-some-dashes", "with-some-dashes"),
777        (
778            r"Random text with *(bad)* characters",
779            "randomtextwithbadcharacters",
780        ),
781        (r"Trailing bad characters!@#", "trailingbadcharacters"),
782        (r"!@#Leading bad characters", "leadingbadcharacters"),
783        (r"Squeeze   separators", "squeezeseparators"),
784        (r"Test with + sign", "testwithsign"),
785        (r"Test with malformed utf8 \251", "testwithmalformedutf8251"),
786    ];
787
788    const STRING_TO_PARAMETERIZE_WITH_UNDERSCORE: [(&str, &str); 9] = [
789        (r"Donald E. Knuth", "donald_e_knuth"),
790        (
791            r"Random text with *(bad)* characters",
792            "random_text_with_bad_characters",
793        ),
794        (r"With-some-dashes", "with-some-dashes"),
795        (r"Retain_underscore", "retain_underscore"),
796        (r"Trailing bad characters!@#", "trailing_bad_characters"),
797        (r"!@#Leading bad characters", "leading_bad_characters"),
798        (r"Squeeze   separators", "squeeze_separators"),
799        (r"Test with + sign", "test_with_sign"),
800        (
801            r"Test with malformed utf8 \251",
802            "test_with_malformed_utf8_251",
803        ),
804    ];
805
806    const KEYIFY_BULK: [(&str, &str); 18] = [
807        (r"Donald E. Knuth", "donald_e_knuth"),
808        (
809            r"Random text with *(bad)* characters",
810            "random_text_with_bad_characters",
811        ),
812        (r"With-some-dashes", "with_some_dashes"),
813        (r"Retain_underscore", "retain_underscore"),
814        (r"Trailing bad characters!@#", "trailing_bad_characters"),
815        (r"!@#Leading bad characters", "leading_bad_characters"),
816        (r"Squeeze   separators", "squeeze_separators"),
817        (r"Test with + sign", "test_with_sign"),
818        (
819            r"Test with malformed utf8 \251",
820            "test_with_malformed_utf8_251",
821        ),
822        ("  --== some strange_key", "some_strange_key"),
823        ("  --== some otherKey_", "some_other_key"),
824        ("  --== some other-key_", "some_other_key"),
825        ("Some Other Key", "some_other_key"),
826        ("Some-Other-Key", "some_other_key"),
827        ("some_other_key", "some_other_key"),
828        ("      ", ""),
829        (" -----", ""),
830        ("========", ""),
831    ];
832
833    const STRING_TO_PARAMETERIZED_AND_NORMALIZED: [(&str, &str); 6] = [
834        (r"Malmö", "malmo"),
835        (r"Garçons", "garcons"),
836        (r"Ops\331", "ops-331"),
837        (r"Ærøskøbing", "aeroskobing"),
838        (r"Aßlar", "asslar"),
839        (r"日本語", "ri-ben-yu"),
840    ];
841
842    const UNDERSCORE_TO_HUMAN: [(&str, &str); 3] = [
843        ("employee_salary", "Employee salary"),
844        ("employee_id", "Employee"),
845        ("underground", "Underground"),
846    ];
847
848    const MIXTURE_TO_TITLEIZED: [(&str, &str); 12] = [
849        ("active_record", "Active Record"),
850        ("ActiveRecord", "Active Record"),
851        ("action web service", "Action Web Service"),
852        ("Action Web Service", "Action Web Service"),
853        ("Action web service", "Action Web Service"),
854        ("actionwebservice", "Actionwebservice"),
855        ("Actionwebservice", "Actionwebservice"),
856        ("david's code", "David's Code"),
857        ("David's code", "David's Code"),
858        ("david's Code", "David's Code"),
859        ("ana índia", "Ana Índia"),
860        ("Ana Índia", "Ana Índia"),
861    ];
862
863    const UNDERSCORES_TO_DASHES: [(&str, &str); 3] = [
864        ("street", "street"),
865        ("street_address", "street-address"),
866        ("person_street_address", "person-street-address"),
867    ];
868
869    const STRING_TO_TABLEIZE: [(&str, &str); 4] = [
870        ("person", "people"),
871        ("Country", "countries"),
872        ("ChildToy", "child_toys"),
873        ("_RecipeIngredient", "_recipe_ingredients"),
874    ];
875
876    #[test]
877    fn substring() {
878        assert_eq!(&"1Hello"[1..], "Hello");
879    }
880
881    #[test]
882    fn camelize_bulk() {
883        for (expected, input) in CAMEL_TO_UNDERSCORE {
884            assert_eq!(inflection::camelize(input), expected);
885        }
886    }
887
888    #[test]
889    fn pluralize_bulk() {
890        for (input, expected) in SINGULAR_TO_PLURAL {
891            assert_eq!(inflection::pluralize(input), expected);
892        }
893    }
894
895    #[test]
896    fn singularize_bulk() {
897        for (expected, input) in SINGULAR_TO_PLURAL {
898            assert_eq!(inflection::singularize(input), expected);
899        }
900    }
901
902    #[test]
903    fn underscore_bulk() {
904        for (expected, input) in UNDERSCORES_TO_DASHES {
905            assert_eq!(inflection::underscore(input), expected);
906        }
907
908        for (input, expected) in CAMEL_TO_UNDERSCORE_WITHOUT_REVERSE {
909            assert_eq!(inflection::underscore(input), expected);
910        }
911    }
912
913    #[test]
914    fn dasherize_bulk() {
915        for (input, expected) in UNDERSCORES_TO_DASHES {
916            assert_eq!(inflection::dasherize(input), expected);
917        }
918    }
919
920    #[test]
921    fn tableize_bulk() {
922        for (input, expected) in STRING_TO_TABLEIZE {
923            assert_eq!(inflection::tableize(input), expected);
924        }
925    }
926
927    #[test]
928    fn humanize_bulk() {
929        for (input, expected) in UNDERSCORE_TO_HUMAN {
930            assert_eq!(inflection::humanize(input), expected);
931        }
932    }
933
934    #[test]
935    fn titleize_bulk() {
936        for (input, expected) in MIXTURE_TO_TITLEIZED {
937            assert_eq!(inflection::titleize(input), expected);
938        }
939    }
940
941    #[test]
942    fn keyify_test() {
943        for (input, expected) in KEYIFY_BULK {
944            assert_eq!(inflection::keyify(input), expected);
945        }
946    }
947
948    #[test]
949    fn parameterize_bulk() {
950        for (input, expected) in STRING_TO_PARAMETERIZED {
951            assert_eq!(inflection::parameterize(input), expected);
952        }
953
954        for (input, expected) in STRING_TO_PARAMETERIZED_AND_NORMALIZED {
955            assert_eq!(inflection::parameterize(input), expected);
956        }
957
958        for (input, expected) in STRING_TO_PARAMETERIZE_WITH_UNDERSCORE {
959            assert_eq!(
960                inflection::parameterize_with_sep(input, "_".to_string()),
961                expected
962            );
963        }
964
965        for (input, expected) in STRING_TO_PARAMETERIZE_WITH_NO_SEPARATOR {
966            assert_eq!(
967                inflection::parameterize_with_sep(input, "".to_string()),
968                expected
969            );
970        }
971    }
972
973    macro_rules! test_ordinal {
974        ($ordinal:ident, $ordinalize:ident, $ordinalize_bulk:ident, $param_type:ty) => {
975            #[test]
976            fn $ordinal() {
977                assert_eq!(inflection::$ordinal(1), "st");
978                assert_eq!(inflection::$ordinal(2), "nd");
979                assert_eq!(inflection::$ordinal(3), "rd");
980                assert_eq!(inflection::$ordinal(4), "th");
981                assert_eq!(inflection::$ordinal(10), "th");
982
983                assert_eq!(inflection::$ordinal(1002), "nd");
984                assert_eq!(inflection::$ordinal(1003), "rd");
985            }
986
987            #[test]
988            fn $ordinalize() {
989                assert_eq!(inflection::$ordinalize(1), "1st");
990                assert_eq!(inflection::$ordinalize(2), "2nd");
991                assert_eq!(inflection::$ordinalize(3), "3rd");
992                assert_eq!(inflection::$ordinalize(4), "4th");
993                assert_eq!(inflection::$ordinalize(10), "10th");
994                assert_eq!(inflection::$ordinalize(1002), "1002nd");
995                assert_eq!(inflection::$ordinalize(1003), "1003rd");
996            }
997
998            #[test]
999            fn $ordinalize_bulk() {
1000                let ordinal_numbers: [($param_type, &str); 31] = [
1001                    (0, "0th"),
1002                    (1, "1st"),
1003                    (2, "2nd"),
1004                    (3, "3rd"),
1005                    (4, "4th"),
1006                    (5, "5th"),
1007                    (6, "6th"),
1008                    (7, "7th"),
1009                    (8, "8th"),
1010                    (9, "9th"),
1011                    (10, "10th"),
1012                    (11, "11th"),
1013                    (12, "12th"),
1014                    (13, "13th"),
1015                    (14, "14th"),
1016                    (20, "20th"),
1017                    (21, "21st"),
1018                    (22, "22nd"),
1019                    (23, "23rd"),
1020                    (24, "24th"),
1021                    (100, "100th"),
1022                    (101, "101st"),
1023                    (102, "102nd"),
1024                    (103, "103rd"),
1025                    (104, "104th"),
1026                    (110, "110th"),
1027                    (111, "111th"),
1028                    (112, "112th"),
1029                    (113, "113th"),
1030                    (1000, "1000th"),
1031                    (1001, "1001st"),
1032                ];
1033
1034                for (input, expected) in ordinal_numbers {
1035                    assert_eq!(inflection::$ordinalize(input), expected);
1036                }
1037            }
1038        };
1039    }
1040
1041    test_ordinal!(ordinal_u16, ordinalize_u16, oridinalize_u16_bulk, u16);
1042    test_ordinal!(ordinal_u32, ordinalize_u32, oridinalize_u32_bulk, u32);
1043    test_ordinal!(ordinal_u64, ordinalize_u64, oridinalize_u64_bulk, u64);
1044    test_ordinal!(ordinal_u128, ordinalize_u128, oridinalize_u128_bulk, u128);
1045
1046    test_ordinal!(ordinal_i16, ordinalize_i16, oridinalize_i16_bulk, i16);
1047    test_ordinal!(ordinal_i32, ordinalize_i32, oridinalize_i32_bulk, i32);
1048    test_ordinal!(ordinal_i64, ordinalize_i64, oridinalize_i64_bulk, i64);
1049    test_ordinal!(ordinal_i128, ordinalize_i128, oridinalize_i128_bulk, i128);
1050}