1use unicode_normalization::UnicodeNormalization;
5
6#[derive(Clone, Debug)]
8pub struct Preprocessor {
9 pub normalize_unicode: bool,
11 pub collapse_whitespace: bool,
13 pub trim: bool,
15 pub to_lowercase: bool,
17 pub remove_stopwords: bool,
19 pub stopwords: Vec<String>,
21 pub max_length: usize,
23}
24
25impl Default for Preprocessor {
26 fn default() -> Self {
27 Self {
28 normalize_unicode: true,
29 collapse_whitespace: true,
30 trim: true,
31 to_lowercase: true,
32 remove_stopwords: false,
33 stopwords: Vec::new(),
34 max_length: 0,
35 }
36 }
37}
38
39impl Preprocessor {
40 #[inline]
42 pub fn new() -> Self {
43 Self::default()
44 }
45
46 #[inline]
48 pub fn with_normalize_unicode(mut self, v: bool) -> Self {
49 self.normalize_unicode = v;
50 self
51 }
52
53 #[inline]
55 pub fn with_collapse_whitespace(mut self, v: bool) -> Self {
56 self.collapse_whitespace = v;
57 self
58 }
59
60 #[inline]
62 pub fn with_trim(mut self, v: bool) -> Self {
63 self.trim = v;
64 self
65 }
66
67 #[inline]
69 pub fn with_lowercase(mut self, v: bool) -> Self {
70 self.to_lowercase = v;
71 self
72 }
73
74 #[inline]
76 pub fn with_remove_stopwords(mut self, v: bool) -> Self {
77 self.remove_stopwords = v;
78 self
79 }
80
81 #[inline]
83 pub fn with_stopwords(mut self, words: Vec<String>) -> Self {
84 self.stopwords = words;
85 self
86 }
87
88 #[inline]
90 pub fn with_max_length(mut self, max: usize) -> Self {
91 self.max_length = max;
92 self
93 }
94
95 #[inline]
99 pub fn process(&self, text: &str) -> String {
100 let mut s = text.to_string();
101
102 if self.normalize_unicode {
104 s = s.nfc().collect();
105 }
106
107 if self.to_lowercase {
109 s = s.to_lowercase();
110 }
111
112 if self.collapse_whitespace {
114 let mut result = String::with_capacity(s.len());
115 let mut prev_was_space = false;
116 for c in s.chars() {
117 if c.is_whitespace() {
118 if !prev_was_space {
119 result.push(' ');
120 prev_was_space = true;
121 }
122 } else {
123 result.push(c);
124 prev_was_space = false;
125 }
126 }
127 s = result;
128 }
129
130 if self.trim {
132 s = s.trim().to_string();
133 }
134
135 if self.remove_stopwords {
137 let result: Vec<&str> = s
138 .split_whitespace()
139 .filter(|word| !is_stopword(word, &self.stopwords))
140 .collect();
141 s = result.join(" ");
142 }
143
144 if self.max_length > 0 && s.len() > self.max_length {
146 s.truncate(self.max_length);
147 }
148
149 s
150 }
151}
152
153const DEFAULT_STOPWORDS: &[&str] = &[
155 "a",
156 "an",
157 "the",
158 "and",
159 "or",
160 "but",
161 "if",
162 "because",
163 "as",
164 "until",
165 "while",
166 "of",
167 "at",
168 "by",
169 "for",
170 "with",
171 "about",
172 "between",
173 "into",
174 "through",
175 "during",
176 "before",
177 "after",
178 "above",
179 "below",
180 "to",
181 "from",
182 "up",
183 "down",
184 "in",
185 "out",
186 "on",
187 "off",
188 "over",
189 "under",
190 "again",
191 "further",
192 "then",
193 "once",
194 "here",
195 "there",
196 "when",
197 "where",
198 "why",
199 "how",
200 "all",
201 "each",
202 "every",
203 "both",
204 "few",
205 "more",
206 "most",
207 "other",
208 "some",
209 "such",
210 "no",
211 "nor",
212 "not",
213 "only",
214 "own",
215 "same",
216 "so",
217 "than",
218 "too",
219 "very",
220 "just",
221 "also",
222 "am",
223 "is",
224 "are",
225 "was",
226 "were",
227 "be",
228 "been",
229 "being",
230 "have",
231 "has",
232 "had",
233 "having",
234 "do",
235 "does",
236 "did",
237 "doing",
238 "would",
239 "could",
240 "should",
241 "might",
242 "must",
243 "shall",
244 "can",
245 "will",
246 "may",
247 "need",
248 "dare",
249 "ought",
250 "used",
251 "this",
252 "that",
253 "these",
254 "those",
255 "i",
256 "me",
257 "my",
258 "myself",
259 "we",
260 "our",
261 "ours",
262 "ourselves",
263 "you",
264 "your",
265 "yours",
266 "yourself",
267 "yourselves",
268 "he",
269 "him",
270 "his",
271 "himself",
272 "she",
273 "her",
274 "hers",
275 "herself",
276 "it",
277 "its",
278 "itself",
279 "they",
280 "them",
281 "their",
282 "theirs",
283 "themselves",
284 "what",
285 "which",
286 "who",
287 "whom",
288 "whose",
289 "any",
290 "anyone",
291 "anything",
292 "anybody",
293 "everyone",
294 "everything",
295 "everybody",
296 "someone",
297 "something",
298 "somebody",
299 "nobody",
300 "nothing",
301 "none",
302 "neither",
303 "one",
304 "two",
305 "three",
306 "get",
307 "got",
308 "getting",
309 "make",
310 "made",
311 "making",
312 "take",
313 "took",
314 "taking",
315 "let",
316 "lets",
317 "letting",
318 "like",
319 "liked",
320 "likes",
321 "really",
322 "actually",
323 "basically",
324 "probably",
325 "maybe",
326 "perhaps",
327 "quite",
328 "rather",
329 "pretty",
330 "almost",
331 "nearly",
332 "hardly",
333 "scarcely",
334 "barely",
335 "already",
336 "yet",
337 "still",
338];
339
340#[inline]
342fn is_stopword(word: &str, custom: &[String]) -> bool {
343 if custom.is_empty() {
344 DEFAULT_STOPWORDS.contains(&word)
345 } else {
346 custom.iter().any(|w| w == word)
347 }
348}
349
350#[inline]
352pub fn clean(text: &str) -> String {
353 Preprocessor::default().process(text)
354}
355
356#[inline]
358pub fn clean_with_stopwords(text: &str) -> String {
359 Preprocessor::default()
360 .with_remove_stopwords(true)
361 .process(text)
362}
363
364#[cfg(test)]
365mod tests {
366 use super::*;
367
368 #[test]
369 fn default_cleanup() {
370 let cleaned = clean(" Hello World! ");
371 assert_eq!(cleaned, "hello world!");
372 }
373
374 #[test]
375 fn unicode_normalization() {
376 let s = clean("\u{0065}\u{0301}"); assert_eq!(s, "\u{00e9}"); }
380
381 #[test]
382 fn stopword_removal() {
383 let result = clean_with_stopwords("the quick brown fox jumps over the lazy dog");
384 assert!(!result.contains("the "));
386 assert!(result.contains("quick"));
387 assert!(result.contains("fox"));
388 assert!(result.contains("jumps"));
389 assert!(result.contains("lazy"));
390 assert!(result.contains("dog"));
391 }
392
393 #[test]
394 fn lowercase() {
395 let pre = Preprocessor::default().with_lowercase(true);
396 assert_eq!(pre.process("Hello WORLD"), "hello world");
397 }
398
399 #[test]
400 fn no_lowercase() {
401 let pre = Preprocessor::default().with_lowercase(false);
402 assert_eq!(pre.process("Hello WORLD"), "Hello WORLD");
403 }
404
405 #[test]
406 fn custom_stopwords() {
407 let words = vec!["hello".to_string(), "world".to_string()];
408 let pre = Preprocessor::default()
409 .with_remove_stopwords(true)
410 .with_stopwords(words);
411 let result = pre.process("hello wonderful world");
412 assert_eq!(result, "wonderful");
413 }
414
415 #[test]
416 fn trim_input() {
417 let pre = Preprocessor::default().with_trim(true);
418 assert_eq!(pre.process(" spaced "), "spaced");
419 }
420
421 #[test]
422 fn max_length() {
423 let pre = Preprocessor::default().with_max_length(5);
424 assert_eq!(pre.process("hello world"), "hello");
425 }
426
427 #[test]
428 fn builder_pattern() {
429 let pre = Preprocessor::new()
430 .with_normalize_unicode(true)
431 .with_collapse_whitespace(true)
432 .with_trim(true)
433 .with_lowercase(true);
434 assert_eq!(pre.process(" Hello World "), "hello world");
435 }
436}