pluralize_rs/
lib.rs

1#[macro_use]
2extern crate lazy_static;
3
4use regex::{Regex, RegexBuilder};
5use voca_rs::case;
6
7fn restore_case(origin: &str, to_restore: &str) -> String {
8    if origin == to_restore {
9        to_restore.to_string()
10    } else if origin == case::lower_case(origin) {
11        case::lower_case(to_restore)
12    } else if origin == case::upper_case(origin) {
13        case::upper_case(to_restore)
14    } else if origin == case::upper_first(origin) {
15        case::upper_first(to_restore)
16    } else if origin == case::camel_case(origin) {
17        case::camel_case(to_restore)
18    } else {
19        case::lower_case(to_restore)
20    }
21}
22
23macro_rules! load_config_map {
24    ($filename:expr) => {
25        include_str!($filename)
26            .split('\n')
27            .filter(|it| it.trim() != "")
28            .map(|it| {
29                let mut splitted = it
30                    .split('=')
31                    .map(|it| it.trim().trim_matches(|ch| ch == '\"'));
32                (splitted.next().unwrap(), splitted.next().unwrap())
33            })
34            .map(|(k, v)| {
35                (
36                    RegexBuilder::new(k).case_insensitive(true).build().unwrap(),
37                    v.to_string(),
38                )
39            })
40            .rev()
41            .collect()
42    };
43}
44
45lazy_static! {
46    static ref IRREGULAR: Vec<(&'static str, &'static str)> =
47        include_str!("../rules/irregular.txt")
48            .split('\n')
49            .filter(|it| it.trim() != "")
50            .map(|it| {
51                let mut splitted = it
52                    .split('=')
53                    .map(|it| it.trim().trim_matches(|ch| ch == '\"'));
54                (splitted.next().unwrap(), splitted.next().unwrap())
55            })
56            .collect();
57    static ref PLURAL_RULES: Vec<(Regex, String)> = load_config_map!("../rules/plural.txt");
58    static ref SINGLAR_RULES: Vec<(Regex, String)> = load_config_map!("../rules/singular.txt");
59    static ref UNCOUNTABLE: Vec<Regex> = include_str!("../rules/uncountable.txt")
60        .split('\n')
61        .filter(|it| it.trim() != "")
62        .map(|it| RegexBuilder::new(it)
63            .case_insensitive(true)
64            .build()
65            .unwrap())
66        .collect();
67}
68
69/// Returns whether a noun is uncountable
70///
71/// # Arguments
72///
73/// * `word` - The noun
74///
75/// # Examples
76///
77/// ```
78/// use pluralize_rs::is_uncountable;
79/// assert!(is_uncountable("water"));
80/// ```
81pub fn is_uncountable(word: &str) -> bool {
82    let lower_case = case::lower_case(word);
83    for (singular, plural) in IRREGULAR.iter() {
84        if lower_case == *singular || lower_case == *plural {
85            return false;
86        }
87    }
88    for r in UNCOUNTABLE.iter() {
89        if r.find(&lower_case).is_some() {
90            return true;
91        }
92    }
93    false
94}
95
96fn replace_with_rules(
97    word: &str,
98    mut rules: impl Iterator<Item=&'static (Regex, String)>,
99) -> String {
100    if let Some((m, mut r)) = rules
101        .find_map(|(re, replace_to)| re.captures(&word).map(move |it| (it, replace_to.clone())))
102    {
103        if r == "$0" {
104            return word.to_string();
105        }
106        let mut result = word[0..m.get(0).unwrap().start()].to_string();
107        for (i, content) in ["$1", "$2"].iter().enumerate() {
108            r = if let Some(replace_to) = m.get(i + 1).map(|it| &word[it.start()..it.end()]) {
109                r.replace(content, replace_to)
110            } else {
111                r.replace(content, "")
112            }
113        }
114        result.push_str(&r);
115        result
116    } else {
117        word.to_string()
118    }
119}
120
121/// Returns a noun's plural form, if it is uncountable, the origin value will be returned
122///
123/// # Arguments
124///
125/// * `word` - The noun
126///
127/// # Examples
128///
129/// ```
130/// use pluralize_rs::to_plural;
131/// assert_eq!(to_plural("word"), "words");
132/// ```
133pub fn to_plural(word: &str) -> String {
134    if is_uncountable(word) {
135        word.to_string()
136    } else {
137        let lower_case = case::lower_case(word);
138        for (singular, plural) in IRREGULAR.iter() {
139            if lower_case == *singular {
140                return restore_case(word, plural);
141            }
142        }
143        restore_case(word, &replace_with_rules(&word, PLURAL_RULES.iter()))
144    }
145}
146
147/// Returns wheter the noun is plural, if it is uncountable, will return true
148///
149/// # Arguments
150///
151/// * `word` - The noun
152///
153/// # Examples
154///
155/// ```
156/// use pluralize_rs::is_plural;
157/// assert!(is_plural("words"));
158/// assert!(!is_plural("word"));
159/// ```
160pub fn is_plural(word: &str) -> bool {
161    if is_uncountable(word) {
162        false
163    } else {
164        let lower_case = case::lower_case(word);
165        for (singular, plural) in IRREGULAR.iter() {
166            if lower_case == *singular {
167                return false;
168            } else if lower_case == *plural {
169                return true;
170            }
171        }
172        lower_case == replace_with_rules(&lower_case, PLURAL_RULES.iter())
173    }
174}
175
176/// Returns a noun's singular form, if it is uncountable, the origin value will be returned
177///
178/// # Arguments
179///
180/// * `word` - The noun
181///
182/// # Examples
183///
184/// ```
185/// use pluralize_rs::to_singular;
186/// assert_eq!(to_singular("words"), "word");
187/// ```
188pub fn to_singular(word: &str) -> String {
189    if is_uncountable(word) {
190        word.to_string()
191    } else {
192        let lower_case = case::lower_case(word);
193        for (singular, plural) in IRREGULAR.iter() {
194            if lower_case == *plural {
195                return restore_case(word, singular);
196            }
197        }
198        restore_case(word, &replace_with_rules(&word, SINGLAR_RULES.iter()))
199    }
200}
201
202/// Returns wheter the noun is singular, if it is uncountable, will return true
203///
204/// # Arguments
205///
206/// * `word` - The noun
207///
208/// # Examples
209///
210/// ```
211/// use pluralize_rs::is_singular;
212/// assert!(!is_singular("words"));
213/// assert!(is_singular("word"));
214/// ```
215pub fn is_singular(word: &str) -> bool {
216    if is_uncountable(word) {
217        false
218    } else {
219        let lower_case = case::lower_case(word);
220        for (singular, plural) in IRREGULAR.iter() {
221            if lower_case == *plural {
222                return false;
223            } else if lower_case == *singular {
224                return true;
225            }
226        }
227        lower_case == replace_with_rules(&lower_case, SINGLAR_RULES.iter())
228    }
229}
230
231#[cfg(test)]
232mod tests {
233    use super::*;
234
235    #[test]
236    fn it_works() {
237        let cases: Vec<_> = include_str!("../test-cases.txt")
238            .split('\n')
239            .filter(|it| !it.trim().is_empty())
240            .map(|it| {
241                let mut splitted = it.split(',');
242                (
243                    splitted.next().unwrap().trim(),
244                    splitted.next().unwrap().trim(),
245                )
246            })
247            .collect();
248        for (singular, plural) in cases {
249            println!("{} <=> {}", singular, plural);
250            assert_eq!(to_plural(singular), plural);
251            assert_eq!(to_singular(plural), singular);
252            if !is_uncountable(singular) {
253                assert!(is_singular(singular));
254                assert!(is_plural(plural));
255            }
256        }
257    }
258}