use super::contraction::{ContractionMatch, ContractionRule};
use super::pronunciation::PronunciationProvider;
use super::rule_10_3::StrongContractionRule;
use super::rule_10_4::StrongGroupsignRule;
use super::rule_10_6::middle_lower_groupsign;
use super::rule_10_8::FinalGroupsignRule;
const A_INITIAL_SUFFIXES: &[&str] = &["ability", "able", "ably", "age", "al", "ate"];
const CONSONANT_DOUBLING_SUFFIXES: &[&str] = &["ing", "ed", "er", "est", "y", "ish", "able"];
const ROOTLIKE_A_FORMS: &[&str] = &["ade"];
const EA_BRIDGING_PREFIXES: &[&str] = &["de", "fore", "ge", "pre", "re"];
pub struct MiddleLowerGroupsignRule {
provider: Box<dyn PronunciationProvider>,
}
impl MiddleLowerGroupsignRule {
pub fn new(provider: Box<dyn PronunciationProvider>) -> Self {
Self { provider }
}
fn ea_allowed(&self, word: &[char], pos: usize) -> bool {
let b = pos + 1;
if self.is_ea_suffix_seam(word, b) {
return true;
}
let derived_compound_seams = self.derived_ea_compound_seams(word, pos);
if bridges_known_compound_seam(word, pos, 2)
|| bridges_any_seam(&derived_compound_seams, pos, 2)
{
return false;
}
if sign_outranks_ea(word, b, &derived_compound_seams) {
return false;
}
if starts_at_known_compound_component(word, pos) {
return true;
}
!self.is_ea_prefix_seam(word, b)
}
fn doubled_allowed(&self, word: &[char], pos: usize) -> bool {
if strong_sign_outranks_doubled(word, pos + 1) {
return false;
}
if self.is_doubled_before_suffix(word, pos) {
return true;
}
!self.is_doubled_compound_seam(word, pos)
}
fn is_ea_suffix_seam(&self, word: &[char], b: usize) -> bool {
let right = collect(&word[b..]);
A_INITIAL_SUFFIXES.iter().any(|s| right.starts_with(s))
&& (self.is_word(&word[..b]) || self.is_word(word))
}
fn is_ea_prefix_seam(&self, word: &[char], b: usize) -> bool {
let right = &word[b..];
if right.starts_with(&['a', 'c', 'h']) {
return false;
}
if ROOTLIKE_A_FORMS.contains(&collect(right).as_str()) {
return true;
}
let prefix = collect(&word[..b]);
if !EA_BRIDGING_PREFIXES.contains(&prefix.as_str()) {
return false;
}
right.len() >= 4 && self.is_word(right)
}
fn derived_ea_compound_seams(&self, word: &[char], pos: usize) -> Vec<usize> {
let b = pos + 1;
let mut seams = Vec::with_capacity(2);
if b >= 3 && self.is_word(&word[..b]) && self.is_compound_component(&word[b..]) {
seams.push(b);
}
if b + 1 < word.len()
&& self.is_word(&word[..=b])
&& self.is_compound_component(&word[b + 1..])
{
seams.push(b + 1);
}
seams
}
fn is_compound_component(&self, chars: &[char]) -> bool {
let text = collect(chars);
if ROOTLIKE_A_FORMS.contains(&text.as_str()) {
return true;
}
if chars.len() < 4 {
return false;
}
let pronunciations = self.provider.pronunciations(&text);
!pronunciations.is_empty()
&& pronunciations.iter().all(|pron| {
pron.iter()
.find(|phoneme| phoneme.is_vowel())
.is_some_and(|phoneme| phoneme.stress == Some(1))
})
}
fn is_doubled_before_suffix(&self, word: &[char], pos: usize) -> bool {
let base = &word[..=pos];
let after = collect(&word[pos + 2..]);
self.is_word(base)
&& CONSONANT_DOUBLING_SUFFIXES
.iter()
.any(|s| after.starts_with(s))
}
fn is_doubled_compound_seam(&self, word: &[char], pos: usize) -> bool {
let left = &word[..=pos];
let right = &word[pos + 1..];
if left.len() < 3 || !self.is_word(left) {
return false;
}
if self.is_lexicalized_doubled_form(word) {
return false;
}
(right.len() >= 4 && self.is_word(right)) || right.len() >= 5
}
fn is_lexicalized_doubled_form(&self, word: &[char]) -> bool {
let pronunciations = self.provider.pronunciations(&collect(word));
!pronunciations.is_empty()
&& pronunciations.iter().all(|pron| {
let mut vowels = pron.iter().filter(|phoneme| phoneme.is_vowel());
let _first_stressed_head = vowels.next();
vowels.all(|phoneme| phoneme.stress == Some(0))
})
}
fn is_word(&self, chars: &[char]) -> bool {
!self.provider.pronunciations(&collect(chars)).is_empty()
}
}
fn collect(chars: &[char]) -> String {
chars.iter().collect()
}
fn compound_seams_for(word: &[char]) -> Vec<usize> {
super::compound::compound_seams(&collect(word))
}
fn starts_at_known_compound_component(word: &[char], pos: usize) -> bool {
compound_seams_for(word).contains(&pos)
}
fn bridges_known_compound_seam(word: &[char], pos: usize, consumed: usize) -> bool {
bridges_any_seam(&compound_seams_for(word), pos, consumed)
}
fn bridges_any_seam(seams: &[usize], pos: usize, consumed: usize) -> bool {
seams
.iter()
.any(|&seam| pos < seam && seam < pos + consumed)
}
pub(crate) fn outranked_at(word: &[char], at: usize) -> bool {
StrongContractionRule.try_match(word, at).is_some()
|| StrongGroupsignRule.try_match(word, at).is_some()
|| FinalGroupsignRule.try_match(word, at).is_some()
}
fn sign_outranks_ea(word: &[char], at: usize, derived_compound_seams: &[usize]) -> bool {
[
StrongContractionRule.try_match(word, at),
StrongGroupsignRule.try_match(word, at),
FinalGroupsignRule.try_match(word, at),
]
.into_iter()
.flatten()
.any(|m| {
!bridges_known_compound_seam(word, at, m.consumed)
&& !bridges_any_seam(derived_compound_seams, at, m.consumed)
})
}
fn strong_sign_outranks_doubled(word: &[char], at: usize) -> bool {
[
StrongContractionRule.try_match(word, at),
StrongGroupsignRule.try_match(word, at),
]
.into_iter()
.flatten()
.any(|m| !bridges_compound_seam(word, at, m.consumed))
}
fn bridges_compound_seam(word: &[char], pos: usize, consumed: usize) -> bool {
compound_seams_for(word)
.iter()
.any(|&seam| pos < seam && seam < pos + consumed)
}
impl ContractionRule for MiddleLowerGroupsignRule {
fn try_match(&self, word: &[char], pos: usize) -> Option<ContractionMatch> {
let m = middle_lower_groupsign(word, pos)?;
let allowed = if (word[pos], word[pos + 1]) == ('e', 'a') {
self.ea_allowed(word, pos)
} else {
self.doubled_allowed(word, pos)
};
allowed.then_some(ContractionMatch {
protect_span: true,
..m
})
}
}
#[cfg(test)]
mod tests {
use super::super::pronunciation::cmudict::CmuDictProvider;
use super::*;
use crate::unicode::decode_unicode;
fn rule() -> MiddleLowerGroupsignRule {
MiddleLowerGroupsignRule::new(Box::new(CmuDictProvider::new()))
}
fn try_at(word: &str, pos: usize) -> Option<(Vec<u8>, usize)> {
let chars: Vec<char> = word.chars().collect();
rule().try_match(&chars, pos).map(|m| (m.cells, m.consumed))
}
#[rstest::rstest]
#[case::oceanic("oceanic", 2)] #[case::head("head", 1)] #[case::beat("beat", 1)]
#[case::peanut("peanut", 1)] #[case::agreeable("agreeable", 4)] #[case::european("european", 5)] #[case::lineage("lineage", 3)] #[case::lineal("lineal", 3)] #[case::peaceable("peaceable", 4)] #[case::caveat("caveat", 3)] #[case::seashore("seashore", 1)] #[case::genealogy("genealogy", 3)] #[case::read("read", 1)] #[case::ready("ready", 1)] #[case::leader("leader", 1)] #[case::motheaten("motheaten", 4)] #[case::toreador("toreador", 3)] #[case::flearidden("flearidden", 2)] #[case::tearoom("tearoom", 1)] fn ea_contracts(#[case] word: &str, #[case] pos: usize) {
assert_eq!(try_at(word, pos), Some((vec![decode_unicode('⠂')], 2)));
}
#[rstest::rstest]
#[case::pineapple("pineapple", 3)] #[case::hideaway("hideaway", 3)] #[case::limeade("limeade", 3)] #[case::reaction("reaction", 1)] #[case::preamble("preamble", 3)] #[case::geanticline("geanticline", 1)] #[case::wiseacre("wiseacre", 3)] #[case::bear("bear", 1)] #[case::meander("meander", 1)] #[case::vengeance("vengeance", 4)] fn ea_spells_out(#[case] word: &str, #[case] pos: usize) {
assert_eq!(try_at(word, pos), None);
}
#[test]
fn ea_prefix_seam_recognizes_rootlike_a_form() {
let chars: Vec<char> = "limeade".chars().collect();
assert!(rule().is_ea_prefix_seam(&chars, 4));
}
#[rstest::rstest]
#[case::bubble("bubble", 2, '⠆')] #[case::accept("accept", 1, '⠒')] #[case::account("account", 1, '⠒')] #[case::begging("begging", 2, '⠶')] #[case::doggone("doggone", 2, '⠶')] #[case::chifforobe("chifforobe", 3, '⠖')] #[case::rabbi("rabbi", 2, '⠆')] #[case::abbe("abbé", 1, '⠆')] fn doubled_contracts(#[case] word: &str, #[case] pos: usize, #[case] cell: char) {
assert_eq!(try_at(word, pos), Some((vec![decode_unicode(cell)], 2)));
}
#[rstest::rstest]
#[case::dumbbell("dumbbell", 3)] #[case::subbasement("subbasement", 2)] #[case::arccosine("arccosine", 2)] #[case::afford("afford", 1)] #[case::bacchanal("bacchanal", 2)] fn doubled_spells_out(#[case] word: &str, #[case] pos: usize) {
assert_eq!(try_at(word, pos), None);
}
}