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
69pub 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
121pub 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
147pub 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
176pub 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
202pub 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}