use unicode_normalization::UnicodeNormalization;
#[derive(Clone, Debug)]
pub struct Preprocessor {
pub normalize_unicode: bool,
pub collapse_whitespace: bool,
pub trim: bool,
pub to_lowercase: bool,
pub remove_stopwords: bool,
pub stopwords: Vec<String>,
pub max_length: usize,
}
impl Default for Preprocessor {
fn default() -> Self {
Self {
normalize_unicode: true,
collapse_whitespace: true,
trim: true,
to_lowercase: true,
remove_stopwords: false,
stopwords: Vec::new(),
max_length: 0,
}
}
}
impl Preprocessor {
#[inline]
pub fn new() -> Self {
Self::default()
}
#[inline]
pub fn with_normalize_unicode(mut self, v: bool) -> Self {
self.normalize_unicode = v;
self
}
#[inline]
pub fn with_collapse_whitespace(mut self, v: bool) -> Self {
self.collapse_whitespace = v;
self
}
#[inline]
pub fn with_trim(mut self, v: bool) -> Self {
self.trim = v;
self
}
#[inline]
pub fn with_lowercase(mut self, v: bool) -> Self {
self.to_lowercase = v;
self
}
#[inline]
pub fn with_remove_stopwords(mut self, v: bool) -> Self {
self.remove_stopwords = v;
self
}
#[inline]
pub fn with_stopwords(mut self, words: Vec<String>) -> Self {
self.stopwords = words;
self
}
#[inline]
pub fn with_max_length(mut self, max: usize) -> Self {
self.max_length = max;
self
}
#[inline]
pub fn process(&self, text: &str) -> String {
let mut s = text.to_string();
if self.normalize_unicode {
s = s.nfc().collect();
}
if self.to_lowercase {
s = s.to_lowercase();
}
if self.collapse_whitespace {
let mut result = String::with_capacity(s.len());
let mut prev_was_space = false;
for c in s.chars() {
if c.is_whitespace() {
if !prev_was_space {
result.push(' ');
prev_was_space = true;
}
} else {
result.push(c);
prev_was_space = false;
}
}
s = result;
}
if self.trim {
s = s.trim().to_string();
}
if self.remove_stopwords {
let result: Vec<&str> = s
.split_whitespace()
.filter(|word| !is_stopword(word, &self.stopwords))
.collect();
s = result.join(" ");
}
if self.max_length > 0 && s.len() > self.max_length {
s.truncate(self.max_length);
}
s
}
}
const DEFAULT_STOPWORDS: &[&str] = &[
"a",
"an",
"the",
"and",
"or",
"but",
"if",
"because",
"as",
"until",
"while",
"of",
"at",
"by",
"for",
"with",
"about",
"between",
"into",
"through",
"during",
"before",
"after",
"above",
"below",
"to",
"from",
"up",
"down",
"in",
"out",
"on",
"off",
"over",
"under",
"again",
"further",
"then",
"once",
"here",
"there",
"when",
"where",
"why",
"how",
"all",
"each",
"every",
"both",
"few",
"more",
"most",
"other",
"some",
"such",
"no",
"nor",
"not",
"only",
"own",
"same",
"so",
"than",
"too",
"very",
"just",
"also",
"am",
"is",
"are",
"was",
"were",
"be",
"been",
"being",
"have",
"has",
"had",
"having",
"do",
"does",
"did",
"doing",
"would",
"could",
"should",
"might",
"must",
"shall",
"can",
"will",
"may",
"need",
"dare",
"ought",
"used",
"this",
"that",
"these",
"those",
"i",
"me",
"my",
"myself",
"we",
"our",
"ours",
"ourselves",
"you",
"your",
"yours",
"yourself",
"yourselves",
"he",
"him",
"his",
"himself",
"she",
"her",
"hers",
"herself",
"it",
"its",
"itself",
"they",
"them",
"their",
"theirs",
"themselves",
"what",
"which",
"who",
"whom",
"whose",
"any",
"anyone",
"anything",
"anybody",
"everyone",
"everything",
"everybody",
"someone",
"something",
"somebody",
"nobody",
"nothing",
"none",
"neither",
"one",
"two",
"three",
"get",
"got",
"getting",
"make",
"made",
"making",
"take",
"took",
"taking",
"let",
"lets",
"letting",
"like",
"liked",
"likes",
"really",
"actually",
"basically",
"probably",
"maybe",
"perhaps",
"quite",
"rather",
"pretty",
"almost",
"nearly",
"hardly",
"scarcely",
"barely",
"already",
"yet",
"still",
];
#[inline]
fn is_stopword(word: &str, custom: &[String]) -> bool {
if custom.is_empty() {
DEFAULT_STOPWORDS.contains(&word)
} else {
custom.iter().any(|w| w == word)
}
}
#[inline]
pub fn clean(text: &str) -> String {
Preprocessor::default().process(text)
}
#[inline]
pub fn clean_with_stopwords(text: &str) -> String {
Preprocessor::default()
.with_remove_stopwords(true)
.process(text)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_cleanup() {
let cleaned = clean(" Hello World! ");
assert_eq!(cleaned, "hello world!");
}
#[test]
fn unicode_normalization() {
let s = clean("\u{0065}\u{0301}"); assert_eq!(s, "\u{00e9}"); }
#[test]
fn stopword_removal() {
let result = clean_with_stopwords("the quick brown fox jumps over the lazy dog");
assert!(!result.contains("the "));
assert!(result.contains("quick"));
assert!(result.contains("fox"));
assert!(result.contains("jumps"));
assert!(result.contains("lazy"));
assert!(result.contains("dog"));
}
#[test]
fn lowercase() {
let pre = Preprocessor::default().with_lowercase(true);
assert_eq!(pre.process("Hello WORLD"), "hello world");
}
#[test]
fn no_lowercase() {
let pre = Preprocessor::default().with_lowercase(false);
assert_eq!(pre.process("Hello WORLD"), "Hello WORLD");
}
#[test]
fn custom_stopwords() {
let words = vec!["hello".to_string(), "world".to_string()];
let pre = Preprocessor::default()
.with_remove_stopwords(true)
.with_stopwords(words);
let result = pre.process("hello wonderful world");
assert_eq!(result, "wonderful");
}
#[test]
fn trim_input() {
let pre = Preprocessor::default().with_trim(true);
assert_eq!(pre.process(" spaced "), "spaced");
}
#[test]
fn max_length() {
let pre = Preprocessor::default().with_max_length(5);
assert_eq!(pre.process("hello world"), "hello");
}
#[test]
fn builder_pattern() {
let pre = Preprocessor::new()
.with_normalize_unicode(true)
.with_collapse_whitespace(true)
.with_trim(true)
.with_lowercase(true);
assert_eq!(pre.process(" Hello World "), "hello world");
}
}