use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Mode {
Nltk,
Martin,
Original,
}
enum Cond {
Always,
Pos,
Gt1,
PosOfWord(usize),
Gt1OfWord(usize),
IonM1,
M1AndCvc,
Y,
NotLsz(char),
}
struct Rule {
suffix: &'static str,
repl: Vec<char>,
cond: Cond,
}
fn rule(suffix: &'static str, repl: &str, cond: Cond) -> Rule {
Rule {
suffix,
repl: repl.chars().collect(),
cond,
}
}
fn is_vowel(c: char) -> bool {
matches!(c, 'a' | 'e' | 'i' | 'o' | 'u')
}
fn is_consonant(word: &[char], i: usize) -> bool {
let c = word[i];
if is_vowel(c) {
return false;
}
if c == 'y' {
let mut negate = false;
let mut j = i;
while j > 0 && word[j] == 'y' {
negate = !negate;
j -= 1;
}
return is_vowel(word[j]) == negate;
}
true
}
fn contains_vowel(stem: &[char]) -> bool {
(0..stem.len()).any(|i| !is_consonant(stem, i))
}
fn ends_double_consonant(word: &[char]) -> bool {
word.len() >= 2 && word[word.len() - 1] == word[word.len() - 2] && is_consonant(word, word.len() - 1)
}
fn ends_with(word: &[char], suffix: &str) -> bool {
let s: Vec<char> = suffix.chars().collect();
word.len() >= s.len() && word[word.len() - s.len()..] == s[..]
}
fn concat(a: &[char], b: &[char]) -> Vec<char> {
let mut v = Vec::with_capacity(a.len() + b.len());
v.extend_from_slice(a);
v.extend_from_slice(b);
v
}
pub struct PorterStemmer {
mode: Mode,
pool: HashMap<String, String>,
}
impl PorterStemmer {
pub fn new(mode: Mode) -> Self {
let mut pool = HashMap::new();
if mode == Mode::Nltk {
let irregular: &[(&str, &[&str])] = &[
("sky", &["sky", "skies"]),
("die", &["dying"]),
("lie", &["lying"]),
("tie", &["tying"]),
("news", &["news"]),
("inning", &["innings", "inning"]),
("outing", &["outings", "outing"]),
("canning", &["cannings", "canning"]),
("howe", &["howe"]),
("proceed", &["proceed"]),
("exceed", &["exceed"]),
("succeed", &["succeed"]),
];
for (key, vals) in irregular {
for v in *vals {
pool.insert((*v).to_string(), (*key).to_string());
}
}
}
PorterStemmer { mode, pool }
}
fn measure(&self, stem: &[char]) -> usize {
let mut cv = String::with_capacity(stem.len());
for i in 0..stem.len() {
cv.push(if is_consonant(stem, i) { 'c' } else { 'v' });
}
cv.matches("vc").count()
}
fn has_positive_measure(&self, stem: &[char]) -> bool {
self.measure(stem) > 0
}
fn ends_cvc(&self, word: &[char]) -> bool {
let n = word.len();
(n >= 3
&& is_consonant(word, n - 3)
&& !is_consonant(word, n - 2)
&& is_consonant(word, n - 1)
&& !matches!(word[n - 1], 'w' | 'x' | 'y'))
|| (self.mode == Mode::Nltk
&& n == 2
&& !is_consonant(word, 0)
&& is_consonant(word, 1))
}
fn eval(&self, cond: &Cond, word: &[char], stem: &[char]) -> bool {
match cond {
Cond::Always => true,
Cond::Pos => self.measure(stem) > 0,
Cond::Gt1 => self.measure(stem) > 1,
Cond::PosOfWord(n) => self.measure(&word[..word.len() - n]) > 0,
Cond::Gt1OfWord(n) => self.measure(&word[..word.len() - n]) > 1,
Cond::IonM1 => self.measure(stem) > 1 && matches!(stem.last(), Some('s') | Some('t')),
Cond::M1AndCvc => self.measure(stem) == 1 && self.ends_cvc(stem),
Cond::Y => {
if self.mode == Mode::Nltk {
stem.len() > 1 && is_consonant(stem, stem.len() - 1)
} else {
contains_vowel(stem)
}
}
Cond::NotLsz(c) => !matches!(c, 'l' | 's' | 'z'),
}
}
fn apply_rules(&self, word: &[char], rules: &[Rule]) -> Vec<char> {
for r in rules {
if r.suffix == "*d" {
if ends_double_consonant(word) {
let stem = &word[..word.len() - 2];
return if self.eval(&r.cond, word, stem) {
concat(stem, &r.repl)
} else {
word.to_vec()
};
}
continue;
}
if ends_with(word, r.suffix) {
let stem = &word[..word.len() - r.suffix.chars().count()];
return if self.eval(&r.cond, word, stem) {
concat(stem, &r.repl)
} else {
word.to_vec()
};
}
}
word.to_vec()
}
fn step1a(&self, word: &[char]) -> Vec<char> {
if self.mode == Mode::Nltk && ends_with(word, "ies") && word.len() == 4 {
return concat(&word[..word.len() - 3], &['i', 'e']);
}
self.apply_rules(
word,
&[
rule("sses", "ss", Cond::Always),
rule("ies", "i", Cond::Always),
rule("ss", "ss", Cond::Always),
rule("s", "", Cond::Always),
],
)
}
fn step1b(&self, word: &[char]) -> Vec<char> {
if self.mode == Mode::Nltk && ends_with(word, "ied") {
return if word.len() == 4 {
concat(&word[..word.len() - 3], &['i', 'e'])
} else {
concat(&word[..word.len() - 3], &['i'])
};
}
if ends_with(word, "eed") {
let stem = &word[..word.len() - 3];
return if self.measure(stem) > 0 {
concat(stem, &['e', 'e'])
} else {
word.to_vec()
};
}
let mut intermediate: Option<Vec<char>> = None;
for suffix in ["ed", "ing"] {
if ends_with(word, suffix) {
let stem = word[..word.len() - suffix.len()].to_vec();
if contains_vowel(&stem) {
intermediate = Some(stem);
break;
}
}
}
let inter = match intermediate {
Some(s) => s,
None => return word.to_vec(),
};
let last = *inter.last().unwrap();
let rules = [
rule("at", "ate", Cond::Always),
rule("bl", "ble", Cond::Always),
rule("iz", "ize", Cond::Always),
Rule {
suffix: "*d",
repl: vec![last],
cond: Cond::NotLsz(last),
},
rule("", "e", Cond::M1AndCvc),
];
self.apply_rules(&inter, &rules)
}
fn step1c(&self, word: &[char]) -> Vec<char> {
self.apply_rules(word, &[rule("y", "i", Cond::Y)])
}
fn step2(&self, word: &[char]) -> Vec<char> {
if self.mode == Mode::Nltk
&& ends_with(word, "alli")
&& self.has_positive_measure(&word[..word.len() - 4])
{
let reduced = concat(&word[..word.len() - 4], &['a', 'l']);
return self.step2(&reduced);
}
let mut rules = vec![
rule("ational", "ate", Cond::Pos),
rule("tional", "tion", Cond::Pos),
rule("enci", "ence", Cond::Pos),
rule("anci", "ance", Cond::Pos),
rule("izer", "ize", Cond::Pos),
if self.mode == Mode::Original {
rule("abli", "able", Cond::Pos)
} else {
rule("bli", "ble", Cond::Pos)
},
rule("alli", "al", Cond::Pos),
rule("entli", "ent", Cond::Pos),
rule("eli", "e", Cond::Pos),
rule("ousli", "ous", Cond::Pos),
rule("ization", "ize", Cond::Pos),
rule("ation", "ate", Cond::Pos),
rule("ator", "ate", Cond::Pos),
rule("alism", "al", Cond::Pos),
rule("iveness", "ive", Cond::Pos),
rule("fulness", "ful", Cond::Pos),
rule("ousness", "ous", Cond::Pos),
rule("aliti", "al", Cond::Pos),
rule("iviti", "ive", Cond::Pos),
rule("biliti", "ble", Cond::Pos),
];
if self.mode == Mode::Nltk {
rules.push(rule("fulli", "ful", Cond::Pos));
rules.push(rule("logi", "log", Cond::PosOfWord(3)));
}
if self.mode == Mode::Martin {
rules.push(rule("logi", "log", Cond::Pos));
}
self.apply_rules(word, &rules)
}
fn step3(&self, word: &[char]) -> Vec<char> {
self.apply_rules(
word,
&[
rule("icate", "ic", Cond::Pos),
rule("ative", "", Cond::Pos),
rule("alize", "al", Cond::Pos),
rule("iciti", "ic", Cond::Pos),
rule("ical", "ic", Cond::Pos),
rule("ful", "", Cond::Pos),
rule("ness", "", Cond::Pos),
],
)
}
fn step4(&self, word: &[char]) -> Vec<char> {
self.apply_rules(
word,
&[
rule("al", "", Cond::Gt1),
rule("ance", "", Cond::Gt1),
rule("ence", "", Cond::Gt1),
rule("er", "", Cond::Gt1),
rule("ic", "", Cond::Gt1),
rule("able", "", Cond::Gt1),
rule("ible", "", Cond::Gt1),
rule("ant", "", Cond::Gt1),
rule("ement", "", Cond::Gt1),
rule("ment", "", Cond::Gt1),
rule("ent", "", Cond::Gt1),
rule("ion", "", Cond::IonM1),
rule("ou", "", Cond::Gt1),
rule("ism", "", Cond::Gt1),
rule("ate", "", Cond::Gt1),
rule("iti", "", Cond::Gt1),
rule("ous", "", Cond::Gt1),
rule("ive", "", Cond::Gt1),
rule("ize", "", Cond::Gt1),
],
)
}
fn step5a(&self, word: &[char]) -> Vec<char> {
if ends_with(word, "e") {
let stem = &word[..word.len() - 1];
let m = self.measure(stem);
if m > 1 || (m == 1 && !self.ends_cvc(stem)) {
return stem.to_vec();
}
}
word.to_vec()
}
fn step5b(&self, word: &[char]) -> Vec<char> {
self.apply_rules(word, &[rule("ll", "l", Cond::Gt1OfWord(1))])
}
pub fn stem(&self, word: &str) -> String {
let lowered = word.to_lowercase();
if self.mode == Mode::Nltk {
if let Some(s) = self.pool.get(&lowered) {
return s.clone();
}
}
if self.mode != Mode::Original && word.chars().count() <= 2 {
return lowered;
}
let mut chars: Vec<char> = lowered.chars().collect();
chars = self.step1a(&chars);
chars = self.step1b(&chars);
chars = self.step1c(&chars);
chars = self.step2(&chars);
chars = self.step3(&chars);
chars = self.step4(&chars);
chars = self.step5a(&chars);
chars = self.step5b(&chars);
chars.into_iter().collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn paper_examples() {
let p = PorterStemmer::new(Mode::Nltk);
assert_eq!(p.stem("caresses"), "caress");
assert_eq!(p.stem("ponies"), "poni");
assert_eq!(p.stem("cats"), "cat");
assert_eq!(p.stem("agreed"), "agre");
assert_eq!(p.stem("motoring"), "motor");
assert_eq!(p.stem("happy"), "happi");
assert_eq!(p.stem("relational"), "relat");
assert_eq!(p.stem("revival"), "reviv");
assert_eq!(p.stem("controll"), "control");
}
#[test]
fn nltk_pool_and_short_words() {
let p = PorterStemmer::new(Mode::Nltk);
assert_eq!(p.stem("skies"), "sky"); assert_eq!(p.stem("dying"), "die");
assert_eq!(p.stem("by"), "by"); }
#[test]
fn modes_differ() {
assert_eq!(PorterStemmer::new(Mode::Nltk).stem("enjoy"), "enjoy");
assert_eq!(PorterStemmer::new(Mode::Original).stem("enjoy"), "enjoi");
}
}