1use std::collections::HashSet;
7
8#[derive(Debug, Clone)]
20pub struct SimpleTokenizer {
21 lowercase: bool,
22 remove_punctuation: bool,
23}
24
25impl Default for SimpleTokenizer {
26 fn default() -> Self {
27 Self::new()
28 }
29}
30
31impl SimpleTokenizer {
32 pub fn new() -> Self {
34 Self {
35 lowercase: true,
36 remove_punctuation: true,
37 }
38 }
39
40 pub fn lowercase(mut self, lowercase: bool) -> Self {
42 self.lowercase = lowercase;
43 self
44 }
45
46 pub fn remove_punctuation(mut self, remove_punctuation: bool) -> Self {
48 self.remove_punctuation = remove_punctuation;
49 self
50 }
51
52 pub fn tokenize(&self, text: &str) -> Vec<String> {
54 let mut text = text.to_string();
55
56 if self.lowercase {
57 text = text.to_lowercase();
58 }
59
60 if self.remove_punctuation {
61 text = text
62 .chars()
63 .filter(|c| c.is_alphanumeric() || c.is_whitespace())
64 .collect();
65 }
66
67 text.split_whitespace()
68 .filter(|s| !s.is_empty())
69 .map(|s| s.to_string())
70 .collect()
71 }
72}
73
74#[derive(Debug, Clone)]
89pub struct StopWordRemover {
90 stop_words: HashSet<String>,
91}
92
93impl StopWordRemover {
94 pub fn new(stop_words: HashSet<String>) -> Self {
96 Self { stop_words }
97 }
98
99 pub fn english() -> Self {
101 let stop_words = [
102 "a", "an", "and", "are", "as", "at", "be", "been", "by", "for", "from", "has", "he",
103 "in", "is", "it", "its", "of", "on", "that", "the", "to", "was", "will", "with", "the",
104 "this", "but", "they", "have", "had", "what", "said", "each", "which", "she", "do",
105 "how", "their", "if", "up", "out", "many", "then", "them", "these", "so", "some",
106 "her", "would", "make", "like", "into", "him", "time", "two", "more", "very", "when",
107 "come", "can", "could", "say", "get", "use", "your", "may", "way", "work", "just",
108 "now", "think", "also", "after", "back", "other", "good", "new", "first", "well",
109 "year", "see", "own", "want", "over",
110 ]
111 .iter()
112 .map(|s| s.to_string())
113 .collect();
114
115 Self { stop_words }
116 }
117
118 pub fn remove_stop_words(&self, tokens: &[impl AsRef<str>]) -> Vec<String> {
120 tokens
121 .iter()
122 .filter_map(|token| {
123 let token_str = token.as_ref().to_lowercase();
124 if self.stop_words.contains(&token_str) {
125 None
126 } else {
127 Some(token.as_ref().to_string())
128 }
129 })
130 .collect()
131 }
132
133 pub fn add_stop_words<I>(&mut self, words: I)
135 where
136 I: IntoIterator<Item = String>,
137 {
138 for word in words {
139 self.stop_words.insert(word.to_lowercase());
140 }
141 }
142}
143
144#[derive(Debug, Clone)]
159pub struct PorterStemmer;
160
161impl Default for PorterStemmer {
162 fn default() -> Self {
163 Self::new()
164 }
165}
166
167impl PorterStemmer {
168 pub fn new() -> Self {
170 Self
171 }
172
173 pub fn stem(&self, word: &str) -> String {
175 let word = word.to_lowercase();
176 let mut result = word.clone();
177
178 #[allow(clippy::if_same_then_else)]
181 if result.ends_with("sses") {
182 result = result[..result.len() - 2].to_string();
183 } else if result.ends_with("ies") {
184 result = result[..result.len() - 2].to_string();
185 } else if result.ends_with("ss") {
186 } else if result.ends_with("s") && result.len() > 1 {
188 result = result[..result.len() - 1].to_string();
189 }
190
191 if result.ends_with("eed") && self.get_measure(&result[..result.len() - 3]) > 0 {
193 result = result[..result.len() - 1].to_string();
194 } else if result.ends_with("ed") && self.contains_vowel(&result[..result.len() - 2]) {
195 result = result[..result.len() - 2].to_string();
196 result = self.post_process_1b(result);
197 } else if result.ends_with("ing") && self.contains_vowel(&result[..result.len() - 3]) {
198 result = result[..result.len() - 3].to_string();
199 result = self.post_process_1b(result);
200 }
201
202 result = self.step_2(result);
204
205 result = self.step_3(result);
207
208 result = self.step_4(result);
210
211 result = self.step_5(result);
213
214 result
215 }
216
217 fn is_vowel(&self, c: char) -> bool {
218 matches!(c, 'a' | 'e' | 'i' | 'o' | 'u')
219 }
220
221 fn contains_vowel(&self, word: &str) -> bool {
222 word.chars().any(|c| self.is_vowel(c))
223 }
224
225 fn get_measure(&self, word: &str) -> usize {
226 let mut measure = 0;
227 let mut prev_was_vowel = false;
228
229 for c in word.chars() {
230 let is_vowel = self.is_vowel(c);
231 if !is_vowel && prev_was_vowel {
232 measure += 1;
233 }
234 prev_was_vowel = is_vowel;
235 }
236
237 measure
238 }
239
240 fn post_process_1b(&self, mut word: String) -> String {
241 if word.ends_with("at") || word.ends_with("bl") || word.ends_with("iz") {
242 word.push('e');
243 } else if self.ends_with_double_consonant(&word)
244 && !word.ends_with("l")
245 && !word.ends_with("s")
246 && !word.ends_with("z")
247 {
248 word.pop();
249 } else if self.get_measure(&word) == 1 && self.ends_with_cvc(&word) {
250 word.push('e');
251 }
252 word
253 }
254
255 fn ends_with_double_consonant(&self, word: &str) -> bool {
256 if word.len() < 2 {
257 return false;
258 }
259 let chars: Vec<char> = word.chars().collect();
260 let last = chars[chars.len() - 1];
261 let second_last = chars[chars.len() - 2];
262 !self.is_vowel(last) && last == second_last
263 }
264
265 fn ends_with_cvc(&self, word: &str) -> bool {
266 if word.len() < 3 {
267 return false;
268 }
269 let chars: Vec<char> = word.chars().collect();
270 let len = chars.len();
271 !self.is_vowel(chars[len - 3])
272 && self.is_vowel(chars[len - 2])
273 && !self.is_vowel(chars[len - 1])
274 && !matches!(chars[len - 1], 'w' | 'x' | 'y')
275 }
276
277 fn step_2(&self, mut word: String) -> String {
278 let suffixes = [
279 ("ational", "ate"),
280 ("tional", "tion"),
281 ("enci", "ence"),
282 ("anci", "ance"),
283 ("izer", "ize"),
284 ("abli", "able"),
285 ("alli", "al"),
286 ("entli", "ent"),
287 ("eli", "e"),
288 ("ousli", "ous"),
289 ("ization", "ize"),
290 ("ation", "ate"),
291 ("ator", "ate"),
292 ("alism", "al"),
293 ("iveness", "ive"),
294 ("fulness", "ful"),
295 ("ousness", "ous"),
296 ("aliti", "al"),
297 ("iviti", "ive"),
298 ("biliti", "ble"),
299 ];
300
301 for (suffix, replacement) in &suffixes {
302 if word.ends_with(suffix) {
303 let stem = &word[..word.len() - suffix.len()];
304 if self.get_measure(stem) > 0 {
305 word = format!("{}{}", stem, replacement);
306 break;
307 }
308 }
309 }
310 word
311 }
312
313 fn step_3(&self, mut word: String) -> String {
314 let suffixes = [
315 ("icate", "ic"),
316 ("ative", ""),
317 ("alize", "al"),
318 ("iciti", "ic"),
319 ("ical", "ic"),
320 ("ful", ""),
321 ("ness", ""),
322 ];
323
324 for (suffix, replacement) in &suffixes {
325 if word.ends_with(suffix) {
326 let stem = &word[..word.len() - suffix.len()];
327 if self.get_measure(stem) > 0 {
328 word = format!("{}{}", stem, replacement);
329 break;
330 }
331 }
332 }
333 word
334 }
335
336 fn step_4(&self, mut word: String) -> String {
337 let suffixes = [
338 "al", "ance", "ence", "er", "ic", "able", "ible", "ant", "ement", "ment", "ent", "ion",
339 "ou", "ism", "ate", "iti", "ous", "ive", "ize",
340 ];
341
342 for suffix in &suffixes {
343 if word.ends_with(suffix) {
344 let stem = &word[..word.len() - suffix.len()];
345 if self.get_measure(stem) > 1 {
346 #[allow(clippy::if_same_then_else)]
348 if *suffix == "ion" && stem.ends_with("s") || stem.ends_with("t") {
349 word = stem.to_string();
350 } else if *suffix != "ion" {
351 word = stem.to_string();
352 }
353 break;
354 }
355 }
356 }
357 word
358 }
359
360 fn step_5(&self, mut word: String) -> String {
361 if word.ends_with("e") {
362 let stem = &word[..word.len() - 1];
363 let measure = self.get_measure(stem);
364 if measure > 1 || (measure == 1 && !self.ends_with_cvc(stem)) {
365 word = stem.to_string();
366 }
367 }
368
369 if word.ends_with("ll") && self.get_measure(&word) > 1 {
370 word.pop();
371 }
372
373 word
374 }
375}
376
377#[derive(Debug, Clone)]
392pub struct TextPreprocessor {
393 tokenizer: SimpleTokenizer,
394 stop_word_remover: Option<StopWordRemover>,
395 stemmer: Option<PorterStemmer>,
396 enable_stemming: bool,
397 enable_stop_word_removal: bool,
398}
399
400impl Default for TextPreprocessor {
401 fn default() -> Self {
402 Self::new()
403 }
404}
405
406impl TextPreprocessor {
407 pub fn new() -> Self {
409 Self {
410 tokenizer: SimpleTokenizer::new(),
411 stop_word_remover: Some(StopWordRemover::english()),
412 stemmer: Some(PorterStemmer::new()),
413 enable_stemming: false,
414 enable_stop_word_removal: false,
415 }
416 }
417
418 pub fn enable_stemming(mut self, enable: bool) -> Self {
420 self.enable_stemming = enable;
421 self
422 }
423
424 pub fn enable_stop_word_removal(mut self, enable: bool) -> Self {
426 self.enable_stop_word_removal = enable;
427 self
428 }
429
430 pub fn stop_words(mut self, stop_words: HashSet<String>) -> Self {
432 self.stop_word_remover = Some(StopWordRemover::new(stop_words));
433 self
434 }
435
436 pub fn tokenizer(mut self, tokenizer: SimpleTokenizer) -> Self {
438 self.tokenizer = tokenizer;
439 self
440 }
441
442 pub fn preprocess(&self, text: &str) -> Vec<String> {
444 let mut tokens = self.tokenizer.tokenize(text);
446
447 if self.enable_stop_word_removal {
449 if let Some(ref remover) = self.stop_word_remover {
450 tokens = remover.remove_stop_words(&tokens);
451 }
452 }
453
454 if self.enable_stemming {
456 if let Some(ref stemmer) = self.stemmer {
457 tokens = tokens.iter().map(|token| stemmer.stem(token)).collect();
458 }
459 }
460
461 tokens
462 }
463}