1pub fn stem(word: &str) -> String {
17 let chars: Vec<char> = word.chars().collect();
18 if chars.len() <= 2 {
19 return word.to_owned();
20 }
21 let stemmed = stem_chars(chars);
22 stemmed.into_iter().collect()
23}
24
25fn stem_chars(mut w: Vec<char>) -> Vec<char> {
26 step_1a(&mut w);
27 step_1b(&mut w);
28 step_1c(&mut w);
29 step_2(&mut w);
30 step_3(&mut w);
31 step_4(&mut w);
32 step_5a(&mut w);
33 step_5b(&mut w);
34 w
35}
36
37fn is_consonant(w: &[char], i: usize) -> bool {
38 let c = w[i];
39 if matches!(c, 'a' | 'e' | 'i' | 'o' | 'u') {
40 return false;
41 }
42 if c == 'y' {
43 return i == 0 || !is_consonant(w, i - 1);
44 }
45 true
46}
47
48fn measure(w: &[char], j: isize) -> usize {
50 if j < 0 {
51 return 0;
52 }
53 let j = j as usize;
54 let mut n = 0usize;
55 let mut i = 0usize;
56 loop {
57 if i > j {
58 return n;
59 }
60 if !is_consonant(w, i) {
61 break;
62 }
63 i += 1;
64 }
65 i += 1;
66 loop {
67 loop {
68 if i > j {
69 return n;
70 }
71 if is_consonant(w, i) {
72 break;
73 }
74 i += 1;
75 }
76 i += 1;
77 n += 1;
78 loop {
79 if i > j {
80 return n;
81 }
82 if !is_consonant(w, i) {
83 break;
84 }
85 i += 1;
86 }
87 i += 1;
88 }
89}
90
91fn vowel_in_stem(w: &[char], j: isize) -> bool {
92 if j < 0 {
93 return false;
94 }
95 let j = j as usize;
96 (0..=j).any(|i| !is_consonant(w, i))
97}
98
99fn double_consonant(w: &[char], j: usize) -> bool {
100 j >= 1 && w[j] == w[j - 1] && is_consonant(w, j)
101}
102
103fn cvc(w: &[char], i: usize) -> bool {
104 if i < 2 || !is_consonant(w, i) || is_consonant(w, i - 1) || !is_consonant(w, i - 2) {
105 return false;
106 }
107 !matches!(w[i], 'w' | 'x' | 'y')
108}
109
110fn ends_with(w: &[char], suffix: &[char]) -> bool {
111 if suffix.len() > w.len() {
112 return false;
113 }
114 let start = w.len() - suffix.len();
115 &w[start..] == suffix
116}
117
118fn ends_with_str(w: &[char], suffix: &str) -> bool {
119 let s: Vec<char> = suffix.chars().collect();
120 ends_with(w, &s)
121}
122
123fn truncate(w: &mut Vec<char>, n: usize) {
124 let new_len = w.len() - n;
125 w.truncate(new_len);
126}
127
128fn replace_suffix(w: &mut Vec<char>, suffix_len: usize, replacement: &str) {
129 truncate(w, suffix_len);
130 w.extend(replacement.chars());
131}
132
133fn step_1a(w: &mut Vec<char>) {
134 if ends_with_str(w, "sses") || ends_with_str(w, "ies") {
135 truncate(w, 2);
136 } else if !ends_with_str(w, "ss") && ends_with_str(w, "s") {
137 truncate(w, 1);
138 }
139}
140
141fn step_1b(w: &mut Vec<char>) {
142 if ends_with_str(w, "eed") {
143 let stem_len = w.len() - 3;
144 if measure(w, stem_len as isize - 1) > 0 {
145 truncate(w, 1);
146 }
147 return;
148 }
149 let mut matched = false;
150 for suffix in ["ed", "ing"] {
151 if ends_with_str(w, suffix)
152 && vowel_in_stem(w, w.len() as isize - suffix.len() as isize - 1)
153 {
154 truncate(w, suffix.len());
155 matched = true;
156 break;
157 }
158 }
159 if !matched {
160 return;
161 }
162 if ends_with_str(w, "at") || ends_with_str(w, "bl") || ends_with_str(w, "iz") {
163 w.push('e');
164 } else if double_consonant(w, w.len() - 1) && !matches!(w[w.len() - 1], 'l' | 's' | 'z') {
165 truncate(w, 1);
166 } else if measure(w, w.len() as isize - 1) == 1 && cvc(w, w.len() - 1) {
167 w.push('e');
168 }
169}
170
171fn step_1c(w: &mut [char]) {
172 if ends_with_str(w, "y") && vowel_in_stem(w, w.len() as isize - 2) {
173 let last = w.len() - 1;
174 w[last] = 'i';
175 }
176}
177
178fn apply_replacement_table(w: &mut Vec<char>, table: &[(&str, &str)]) {
179 for (suffix, replacement) in table {
180 if ends_with_str(w, suffix) {
181 let stem_len = w.len() - suffix.chars().count();
182 if measure(w, stem_len as isize - 1) > 0 {
183 replace_suffix(w, suffix.chars().count(), replacement);
184 }
185 return;
186 }
187 }
188}
189
190fn step_2(w: &mut Vec<char>) {
191 apply_replacement_table(
192 w,
193 &[
194 ("ational", "ate"),
195 ("tional", "tion"),
196 ("enci", "ence"),
197 ("anci", "ance"),
198 ("izer", "ize"),
199 ("abli", "able"),
200 ("alli", "al"),
201 ("entli", "ent"),
202 ("eli", "e"),
203 ("ousli", "ous"),
204 ("ization", "ize"),
205 ("ation", "ate"),
206 ("ator", "ate"),
207 ("alism", "al"),
208 ("iveness", "ive"),
209 ("fulness", "ful"),
210 ("ousness", "ous"),
211 ("aliti", "al"),
212 ("iviti", "ive"),
213 ("biliti", "ble"),
214 ],
215 );
216}
217
218fn step_3(w: &mut Vec<char>) {
219 apply_replacement_table(
220 w,
221 &[
222 ("icate", "ic"),
223 ("ative", ""),
224 ("alize", "al"),
225 ("iciti", "ic"),
226 ("ical", "ic"),
227 ("ful", ""),
228 ("ness", ""),
229 ],
230 );
231}
232
233fn step_4(w: &mut Vec<char>) {
234 const SUFFIXES: &[&str] = &[
235 "al", "ance", "ence", "er", "ic", "able", "ible", "ant", "ement", "ment", "ent", "ion",
236 "ou", "ism", "ate", "iti", "ous", "ive", "ize",
237 ];
238 for suffix in SUFFIXES {
239 if ends_with_str(w, suffix) {
240 let stem_len = w.len() - suffix.chars().count();
241 if measure(w, stem_len as isize - 1) > 1 {
242 if *suffix == "ion" {
243 if stem_len > 0 && matches!(w[stem_len - 1], 's' | 't') {
244 truncate(w, suffix.chars().count());
245 }
246 } else {
247 truncate(w, suffix.chars().count());
248 }
249 }
250 return;
251 }
252 }
253}
254
255fn step_5a(w: &mut Vec<char>) {
256 if ends_with_str(w, "e") {
257 let stem_len = w.len() - 1;
258 let m = measure(w, stem_len as isize - 1);
259 if m > 1 || (m == 1 && !cvc(w, stem_len - 1)) {
260 truncate(w, 1);
261 }
262 }
263}
264
265fn step_5b(w: &mut Vec<char>) {
266 let last = w.len().saturating_sub(1);
267 if measure(w, last as isize) > 1 && double_consonant(w, last) && w[last] == 'l' {
268 truncate(w, 1);
269 }
270}
271
272#[cfg(test)]
273mod tests {
274 use super::*;
275
276 #[test]
277 fn short_words_pass_through() {
278 assert_eq!(stem("a"), "a");
279 assert_eq!(stem("by"), "by");
280 }
281
282 #[test]
283 fn known_examples() {
284 assert_eq!(stem("caresses"), "caress");
285 assert_eq!(stem("ponies"), "poni");
286 assert_eq!(stem("ties"), "ti");
287 assert_eq!(stem("caress"), "caress");
288 assert_eq!(stem("cats"), "cat");
289 assert_eq!(stem("feed"), "feed");
290 assert_eq!(stem("agreed"), "agre");
291 assert_eq!(stem("conflated"), "conflat");
292 assert_eq!(stem("troubled"), "troubl");
293 assert_eq!(stem("happy"), "happi");
294 assert_eq!(stem("relational"), "relat");
295 assert_eq!(stem("conditional"), "condit");
296 assert_eq!(stem("rational"), "ration");
297 assert_eq!(stem("triplicate"), "triplic");
298 assert_eq!(stem("formative"), "form");
299 assert_eq!(stem("electrical"), "electr");
300 assert_eq!(stem("hopeful"), "hope");
301 assert_eq!(stem("goodness"), "good");
302 assert_eq!(stem("revival"), "reviv");
303 assert_eq!(stem("homologous"), "homolog");
304 assert_eq!(stem("controll"), "control");
305 }
306}