use super::chars::{US_TAUS, char_len, drop_last};
use super::lexicon::{Lexicon, Lookup};
use super::token::TokenContext;
fn last_char(s: &str) -> Option<char> {
s.chars().next_back()
}
fn second_last_char(s: &str) -> Option<char> {
s.chars().rev().nth(1)
}
pub(crate) fn suffix_s(stem: Option<&str>, british: bool) -> Option<String> {
let stem = stem.filter(|s| !s.is_empty())?;
let last = last_char(stem)?;
Some(if "ptkfθ".contains(last) {
format!("{stem}s")
} else if "szʃʒʧʤ".contains(last) {
format!("{stem}{}z", if british { 'ɪ' } else { 'ᵻ' })
} else {
format!("{stem}z")
})
}
pub(crate) fn suffix_ed(stem: Option<&str>, british: bool) -> Option<String> {
let stem = stem.filter(|s| !s.is_empty())?;
let last = last_char(stem)?;
Some(if "pkfθʃsʧ".contains(last) {
format!("{stem}t")
} else if last == 'd' {
format!("{stem}{}d", if british { 'ɪ' } else { 'ᵻ' })
} else if last != 't' {
format!("{stem}d")
} else if british || char_len(stem) < 2 {
format!("{stem}ɪd")
} else if second_last_char(stem).is_some_and(|c| US_TAUS.contains(c)) {
format!("{}ɾᵻd", drop_last(stem, 1))
} else {
format!("{stem}ᵻd")
})
}
pub(crate) fn suffix_ing(stem: Option<&str>, british: bool) -> Option<String> {
let stem = stem.filter(|s| !s.is_empty())?;
let last = last_char(stem)?;
if british {
if last == 'ə' || last == 'ː' {
return None;
}
} else if char_len(stem) > 1
&& last == 't'
&& second_last_char(stem).is_some_and(|c| US_TAUS.contains(c))
{
return Some(format!("{}ɾɪŋ", drop_last(stem, 1)));
}
Some(format!("{stem}ɪŋ"))
}
fn doubled_consonant_ing(word: &str) -> bool {
if word.ends_with("cking") {
return true;
}
let Some(stem) = word.strip_suffix("ing") else {
return false;
};
let mut it = stem.chars().rev();
match (it.next(), it.next()) {
(Some(a), Some(b)) => a == b && "bcdgklmnprstvxz".contains(a),
_ => false,
}
}
impl Lexicon {
pub(crate) fn stem_s(
&self,
word: &str,
tag: Option<&str>,
stress: Option<f64>,
ctx: Option<&TokenContext>,
) -> Lookup {
let n = char_len(word);
if n < 3 || !word.ends_with('s') {
return (None, None);
}
let stem = if !word.ends_with("ss") && self.is_known(drop_last(word, 1)) {
drop_last(word, 1).to_string()
} else if (word.ends_with("'s")
|| (n > 4 && word.ends_with("es") && !word.ends_with("ies")))
&& self.is_known(drop_last(word, 2))
{
drop_last(word, 2).to_string()
} else if n > 4
&& word.ends_with("ies")
&& self.is_known(&format!("{}y", drop_last(word, 3)))
{
format!("{}y", drop_last(word, 3))
} else {
return (None, None);
};
let (ps, rating) = self.lookup(&stem, tag, stress, ctx);
(suffix_s(ps.as_deref(), self.british), rating)
}
pub(crate) fn stem_ed(
&self,
word: &str,
tag: Option<&str>,
stress: Option<f64>,
ctx: Option<&TokenContext>,
) -> Lookup {
let n = char_len(word);
if n < 4 || !word.ends_with('d') {
return (None, None);
}
let stem = if !word.ends_with("dd") && self.is_known(drop_last(word, 1)) {
drop_last(word, 1)
} else if n > 4
&& word.ends_with("ed")
&& !word.ends_with("eed")
&& self.is_known(drop_last(word, 2))
{
drop_last(word, 2)
} else {
return (None, None);
};
let (ps, rating) = self.lookup(stem, tag, stress, ctx);
(suffix_ed(ps.as_deref(), self.british), rating)
}
pub(crate) fn stem_ing(
&self,
word: &str,
tag: Option<&str>,
stress: Option<f64>,
ctx: Option<&TokenContext>,
) -> Lookup {
let n = char_len(word);
if n < 5 || !word.ends_with("ing") {
return (None, None);
}
let base = drop_last(word, 3);
let stem = if n > 5 && self.is_known(base) {
base.to_string()
} else if self.is_known(&format!("{base}e")) {
format!("{base}e")
} else if n > 5 && doubled_consonant_ing(word) && self.is_known(drop_last(word, 4)) {
drop_last(word, 4).to_string()
} else {
return (None, None);
};
let (ps, rating) = self.lookup(&stem, tag, stress, ctx);
(suffix_ing(ps.as_deref(), self.british), rating)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn phoneme_suffixes() {
assert_eq!(suffix_s(Some("kˈæt"), false).as_deref(), Some("kˈæts"));
assert_eq!(suffix_s(Some("bˈʌs"), false).as_deref(), Some("bˈʌsᵻz"));
assert_eq!(suffix_s(Some("bˈʌs"), true).as_deref(), Some("bˈʌsɪz"));
assert_eq!(suffix_s(Some(""), false), None);
assert_eq!(suffix_ed(Some("wˈɔk"), false).as_deref(), Some("wˈɔkt"));
assert_eq!(suffix_ed(Some("nˈid"), false).as_deref(), Some("nˈidᵻd"));
assert_eq!(suffix_ed(Some("wˈɑnt"), false).as_deref(), Some("wˈɑntᵻd"));
assert_eq!(suffix_ed(Some("sˈit"), false).as_deref(), Some("sˈiɾᵻd"));
assert_eq!(suffix_ed(Some("sˈit"), true).as_deref(), Some("sˈitɪd"));
assert_eq!(suffix_ing(Some("sˈɪt"), false).as_deref(), Some("sˈɪɾɪŋ"));
assert_eq!(suffix_ing(Some("sˈɪt"), true).as_deref(), Some("sˈɪtɪŋ"));
assert_eq!(suffix_ing(Some("kˈʌlə"), true), None);
}
#[test]
fn doubled_consonants() {
assert!(doubled_consonant_ing("running") && doubled_consonant_ing("kicking"));
assert!(!doubled_consonant_ing("seeing") && !doubled_consonant_ing("ing"));
}
#[test]
fn stems_match_misaki() {
let ctx = TokenContext::default();
for (british, cases) in [
(
false,
[
("cats", "kˈæts"),
("buses", "bˈʌsᵻz"),
("flies", "flˈIz"),
("walked", "wˈɔkt"),
("wanted", "wˈɑntᵻd"),
("sitting", "sˈɪɾɪŋ"),
("making", "mˌAkɪŋ"),
],
),
(
true,
[
("cats", "kˈats"),
("buses", "bˈʌsɪz"),
("flies", "flˈIz"),
("walked", "wˈɔːkt"),
("wanted", "wˈɒntɪd"),
("sitting", "sˈɪtɪŋ"),
("making", "mˌAkɪŋ"),
],
),
] {
let lex = match Lexicon::new(british) {
Ok(l) => l,
Err(e) => panic!("lexicon: {e}"),
};
for (word, want) in cases {
let got = if word.ends_with("ing") {
lex.stem_ing(word, Some("VBG"), Some(0.5), Some(&ctx)).0
} else if word.ends_with('d') {
lex.stem_ed(word, Some("VBD"), None, Some(&ctx)).0
} else {
lex.stem_s(word, Some("NNS"), None, Some(&ctx)).0
};
assert_eq!(got.as_deref(), Some(want), "{word} british={british}");
}
}
}
}