inflector/string/singularize/
mod.rs1use regex::Regex;
2use string::constants::UNACCONTABLE_WORDS;
3
4macro_rules! special_cases{
5 ($s:ident, $($singular: expr => $plural:expr), *) => {
6 match &$s[..] {
7 $(
8 $singular => {
9 return $plural.to_owned();
10 },
11 )*
12 _ => ()
13 }
14 }
15}
16
17
18pub fn to_singular(non_singular_string: &str) -> String {
78 if UNACCONTABLE_WORDS.contains(&non_singular_string.as_ref()) {
79 non_singular_string.to_owned()
80 } else {
81 special_cases![non_singular_string,
82 "oxen" => "ox",
83 "boxes" => "box",
84 "men" => "man",
85 "women" => "woman",
86 "dice" => "die",
87 "yeses" => "yes",
88 "feet" => "foot",
89 "eaves" => "eave",
90 "geese" => "goose",
91 "teeth" => "tooth",
92 "quizzes" => "quiz"
93 ];
94 for &(ref rule, replace) in RULES.iter().rev() {
95 if let Some(captures) = rule.captures(&non_singular_string) {
96 if let Some(c) = captures.get(1) {
97 let mut buf = String::new();
98 captures.expand(&format!("{}{}", c.as_str(), replace), &mut buf);
99 return buf;
100 }
101 }
102 }
103
104 format!("{}", non_singular_string)
105 }
106}
107
108macro_rules! add_rule{
109 ($r:ident, $rule:expr => $replace:expr) => {
110 $r.push((Regex::new($rule).unwrap(), $replace));
111 }
112}
113
114macro_rules! rules{
115 ($r:ident; $($rule:expr => $replace:expr), *) => {
116 $(
117 add_rule!{$r, $rule => $replace}
118 )*
119 }
120}
121
122
123lazy_static!{
124 static ref RULES: Vec<(Regex, &'static str)> = {
125 let mut r = Vec::with_capacity(27);
126 rules![r;
127 r"(\w*)s$" => "",
128 r"(\w*)(ss)$" => "$2",
129 r"(n)ews$" => "ews",
130 r"(\w*)(o)es$" => "",
131 r"(\w*)([ti])a$" => "um",
132 r"((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)(sis|ses)$" => "sis",
133 r"(^analy)(sis|ses)$" => "sis",
134 r"(\w*)([^f])ves$" => "fe",
135 r"(\w*)(hive)s$" => "",
136 r"(\w*)(tive)s$" => "",
137 r"(\w*)([lr])ves$" => "f",
138 r"(\w*([^aeiouy]|qu))ies$" => "y",
139 r"(s)eries$" => "eries",
140 r"(m)ovies$" => "ovie",
141 r"(\w*)(x|ch|ss|sh)es$" => "$2",
142 r"(m|l)ice$" => "ouse",
143 r"(bus)(es)?$" => "",
144 r"(shoe)s$" => "",
145 r"(cris|test)(is|es)$" => "is",
146 r"^(a)x[ie]s$" => "xis",
147 r"(octop|vir)(us|i)$" => "us",
148 r"(alias|status)(es)?$" => "",
149 r"^(ox)en" => "",
150 r"(vert|ind)ices$" => "ex",
151 r"(matr)ices$" => "ix",
152 r"(quiz)zes$" => "",
153 r"(database)s$" => ""
154 ];
155 r
156 };
157}
158
159#[test]
160fn singularize_ies_suffix() {
161 assert_eq!("reply", to_singular("replies"));
162 assert_eq!("lady", to_singular("ladies"));
163 assert_eq!("soliloquy", to_singular("soliloquies"));
164}
165
166#[test]
167fn singularize_ss_suffix() {
168 assert_eq!("glass", to_singular("glass"));
169 assert_eq!("access", to_singular("access"));
170 assert_eq!("glass", to_singular("glasses"));
171 assert_eq!("witch", to_singular("witches"));
172 assert_eq!("dish", to_singular("dishes"));
173}
174
175#[test]
176fn singularize_string_if_a_regex_will_match() {
177 let expected_string: String = "ox".to_owned();
178 let asserted_string: String = to_singular("oxen");
179 assert!(expected_string == asserted_string);
180
181}
182
183#[test]
184fn singularize_string_returns_none_option_if_no_match() {
185 let expected_string: String = "bacon".to_owned();
186 let asserted_string: String = to_singular("bacon");
187
188 assert!(expected_string == asserted_string);
189}