use crate::error::Result;
use crate::stemming::Stemmer;
use lazy_static::lazy_static;
use std::collections::HashMap;
lazy_static! {
static ref LANCASTER_RULES: Vec<(String, String, char, i32)> = vec![
("ai".to_string(), "".to_string(), '.', -1), ("a".to_string(), "".to_string(), '.', -1),
("bb".to_string(), "b".to_string(), '.', -1), ("ble".to_string(), "".to_string(), 'c', -1), ("bly".to_string(), "b".to_string(), 'l', -1),
("cie".to_string(), "".to_string(), '.', -1), ("ci".to_string(), "".to_string(), '.', -1), ("ce".to_string(), "".to_string(), '.', -1), ("c".to_string(), "".to_string(), '.', -1),
("dd".to_string(), "d".to_string(), '.', -1), ("ied".to_string(), "y".to_string(), '.', -1), ("ded".to_string(), "".to_string(), '.', -1), ("d".to_string(), "".to_string(), '.', -1),
("eer".to_string(), "".to_string(), '.', -1), ("ese".to_string(), "".to_string(), '.', -1), ("ely".to_string(), "".to_string(), 'e', -1), ("ee".to_string(), "".to_string(), '.', -1), ("e".to_string(), "".to_string(), '.', -1),
("ff".to_string(), "f".to_string(), '.', -1),
("gger".to_string(), "g".to_string(), '.', 1), ("gging".to_string(), "g".to_string(), '.', 1), ("gg".to_string(), "g".to_string(), '.', -1), ("ger".to_string(), "".to_string(), '.', -1), ("gy".to_string(), "".to_string(), '.', -1), ("ges".to_string(), "".to_string(), '.', -1), ("gly".to_string(), "g".to_string(), '.', -1),
("ht".to_string(), "".to_string(), '.', -1),
("izing".to_string(), "iz".to_string(), '.', -1), ("izing".to_string(), "iz".to_string(), '.', -1), ("ity".to_string(), "".to_string(), '.', -1), ("ie".to_string(), "".to_string(), '.', -1), ("ied".to_string(), "".to_string(), '.', -1), ("ies".to_string(), "".to_string(), '.', -1), ("i".to_string(), "".to_string(), '.', -1),
("j".to_string(), "".to_string(), '.', -1),
("lyte".to_string(), "l".to_string(), 'y', -1), ("ll".to_string(), "l".to_string(), '.', -1), ("lands".to_string(), "land".to_string(), '.', -1), ("lely".to_string(), "le".to_string(), '.', -1), ("ly".to_string(), "".to_string(), 'l', -1), ("less".to_string(), "".to_string(), '.', -1), ("li".to_string(), "".to_string(), '.', 1),
("mm".to_string(), "m".to_string(), '.', -1), ("ment".to_string(), "".to_string(), '.', -1), ("ments".to_string(), "".to_string(), '.', -1),
("nn".to_string(), "n".to_string(), '.', -1),
("oid".to_string(), "".to_string(), '.', -1), ("ology".to_string(), "o".to_string(), '.', -1), ("or".to_string(), "".to_string(), '.', -1), ("ous".to_string(), "".to_string(), '.', -1), ("ously".to_string(), "".to_string(), '.', -1),
("pp".to_string(), "p".to_string(), '.', -1),
("rr".to_string(), "r".to_string(), '.', -1), ("ry".to_string(), "".to_string(), 'r', -1), ("rs".to_string(), "".to_string(), '.', -1),
("ss".to_string(), "".to_string(), '.', -1), ("ssen".to_string(), "".to_string(), '.', 1), ("sses".to_string(), "".to_string(), '.', -1), ("ssed".to_string(), "".to_string(), '.', -1), ("ses".to_string(), "s".to_string(), '.', -1), ("sing".to_string(), "".to_string(), '.', -1), ("s".to_string(), "".to_string(), '.', -1),
("tting".to_string(), "t".to_string(), '.', -1), ("tt".to_string(), "t".to_string(), '.', -1), ("tly".to_string(), "t".to_string(), '.', -1), ("ty".to_string(), "".to_string(), '.', -1), ("ting".to_string(), "".to_string(), '.', -1), ("ted".to_string(), "".to_string(), '.', -1), ("th".to_string(), "".to_string(), '.', 1), ("t".to_string(), "".to_string(), '.', -1),
("uly".to_string(), "".to_string(), '.', -1), ("ul".to_string(), "".to_string(), '.', -1), ("um".to_string(), "".to_string(), '.', -1), ("uous".to_string(), "".to_string(), '.', -1), ("u".to_string(), "".to_string(), '.', -1),
("vas".to_string(), "".to_string(), '.', -1), ("v".to_string(), "".to_string(), '.', -1),
("wise".to_string(), "".to_string(), '.', -1),
("xes".to_string(), "".to_string(), '.', -1), ("x".to_string(), "".to_string(), '.', -1),
("ying".to_string(), "y".to_string(), '.', -1), ("yingly".to_string(), "".to_string(), '.', -1), ("y".to_string(), "".to_string(), '.', -1),
("zes".to_string(), "".to_string(), '.', -1), ("zed".to_string(), "".to_string(), '.', -1), ("zing".to_string(), "".to_string(), '.', -1), ];
static ref RULES_BY_LETTER: HashMap<char, Vec<usize>> = {
let mut map: HashMap<char, Vec<usize>> = HashMap::new();
for (i, (suffix, _, _, _)) in LANCASTER_RULES.iter().enumerate() {
if let Some(first_char) = suffix.chars().next() {
map.entry(first_char).or_default().push(i);
}
}
map
};
}
#[derive(Debug, Clone)]
pub struct LancasterStemmer {
check_acceptable: bool,
min_stemmed_length: usize,
}
impl LancasterStemmer {
pub fn new() -> Self {
Self {
check_acceptable: true,
min_stemmed_length: 2,
}
}
pub fn with_acceptable_check(mut self, check: bool) -> Self {
self.check_acceptable = check;
self
}
pub fn with_min_stemmed_length(mut self, length: usize) -> Self {
self.min_stemmed_length = length;
self
}
fn is_acceptable(&self, word: &str) -> bool {
if !self.check_acceptable {
return true;
}
word.len() >= 3
}
fn apply_rules(&self, word: &str) -> String {
if word.len() <= self.min_stemmed_length {
return word.to_string();
}
let mut stem = word.to_string();
let mut intact = true;
let mut continue_stemming = true;
while continue_stemming {
continue_stemming = false;
if let Some(last_char) = stem.chars().last() {
if let Some(rule_indices) = RULES_BY_LETTER.get(&last_char) {
for &rule_idx in rule_indices {
let (suffix, replacement, next_rule, intact_flag) =
&LANCASTER_RULES[rule_idx];
if stem.ends_with(suffix) && (intact || *intact_flag == -1) {
let new_stem =
format!("{}{}", &stem[..stem.len() - suffix.len()], replacement);
if new_stem.len() >= self.min_stemmed_length {
stem = new_stem;
intact = false;
match next_rule {
'.' => continue_stemming = false, '$' => continue_stemming = false, '*' => {
continue_stemming = false;
}
_ => {
continue_stemming = true;
}
}
break;
}
}
}
}
}
}
stem
}
}
impl Default for LancasterStemmer {
fn default() -> Self {
Self::new()
}
}
impl Stemmer for LancasterStemmer {
fn stem(&self, word: &str) -> Result<String> {
if word.is_empty() || !self.is_acceptable(word) {
return Ok(word.to_string());
}
let lowercase = word.to_lowercase();
Ok(self.apply_rules(&lowercase))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_lancaster_stemmer() {
let stemmer = LancasterStemmer::new();
let test_cases = vec![
("maximum", "maximum"), ("presumably", "presumabl"), ("multiply", "multipl"), ("provision", "provision"), ("owed", "owe"), ("necessity", "necessit"), ("opposition", "opposition"), ("organization", "organization"), ("running", "running"), ("ran", "ran"),
("easily", "easil"), ("fishing", "fishing"), ("fished", "fishe"), ("troubled", "trouble"), ("troubling", "troubling"), ("troubles", "trouble"), ("trouble", "troubl"), ("ear", "ear"), ("a", "a"), ];
for (word, expected) in test_cases {
let stemmed = stemmer.stem(word).expect("Operation failed");
assert_eq!(stemmed, expected, "Failed for word: {word}");
}
}
#[test]
fn test_lancaster_with_min_length() {
let stemmer = LancasterStemmer::new().with_min_stemmed_length(3);
let test_cases = vec![
("provision", "provision"), ("maximum", "maximum"), ("multiply", "multipl"), ("running", "running"), ];
for (word, expected) in test_cases {
let stemmed = stemmer.stem(word).expect("Operation failed");
assert_eq!(stemmed, expected, "Failed for word: {word}");
}
}
#[test]
fn test_lancaster_no_acceptability_check() {
let stemmer = LancasterStemmer::new().with_acceptable_check(false);
let test_cases = vec![
("ear", "ear"), ("me", "me"), ];
for (word, expected) in test_cases {
let stemmed = stemmer.stem(word).expect("Operation failed");
assert_eq!(stemmed, expected, "Failed for word: {word}");
}
}
}