use phf::phf_map;
use super::contraction::{ContractionMatch, ContractionRule};
use crate::unicode::decode_unicode;
static FINAL_GROUPSIGNS: phf::Map<&'static str, [u8; 2]> = phf_map! {
"ound" => [decode_unicode('⠨'), decode_unicode('⠙')],
"ance" => [decode_unicode('⠨'), decode_unicode('⠑')],
"sion" => [decode_unicode('⠨'), decode_unicode('⠝')],
"less" => [decode_unicode('⠨'), decode_unicode('⠎')],
"ount" => [decode_unicode('⠨'), decode_unicode('⠞')],
"ence" => [decode_unicode('⠰'), decode_unicode('⠑')],
"ong" => [decode_unicode('⠰'), decode_unicode('⠛')],
"ful" => [decode_unicode('⠰'), decode_unicode('⠇')],
"tion" => [decode_unicode('⠰'), decode_unicode('⠝')],
"ness" => [decode_unicode('⠰'), decode_unicode('⠎')],
"ment" => [decode_unicode('⠰'), decode_unicode('⠞')],
"ity" => [decode_unicode('⠰'), decode_unicode('⠽')],
};
pub(crate) fn final_groupsign_cells(cluster: &str) -> Option<[u8; 2]> {
FINAL_GROUPSIGNS.get(cluster).copied()
}
fn ity_exception(word: &[char]) -> bool {
matches!(
word,
['b', 'i', 's', 'c', 'u', 'i', 't', 'y']
| ['d', 'a', 'c', 'o', 'i', 't', 'y']
| ['f', 'r', 'u', 'i', 't', 'y']
| ['p', 'i', 't', 'y', 'a', 'r', 'd']
| ['r', 'a', 'b', 'b', 'i', 't', 'y']
)
}
fn ness_exception(word: &[char]) -> bool {
matches!(word, ['h', 'e', 'a', 't', 'h', 'e', 'n', 'e', 's', 's'])
}
pub struct FinalGroupsignRule;
impl ContractionRule for FinalGroupsignRule {
fn try_match(&self, word: &[char], pos: usize) -> Option<ContractionMatch> {
if pos == 0 {
return None;
}
let mut best: Option<(usize, [u8; 2])> = None;
for (key, &cells) in FINAL_GROUPSIGNS.entries() {
let klen = key.chars().count();
if pos + klen <= word.len()
&& key
.chars()
.zip(&word[pos..pos + klen])
.all(|(k, w)| k == *w)
&& !(*key == "ity" && ity_exception(word))
&& !(*key == "ness" && ness_exception(word))
&& best.is_none_or(|(bl, _)| klen > bl)
{
best = Some((klen, cells));
}
}
best.map(|(klen, cells)| ContractionMatch {
cells: cells.to_vec(),
consumed: klen,
priority: 80,
protect_span: false,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[rstest::rstest]
#[case::tion_final("bastion", 3, Some((vec![decode_unicode('⠰'), decode_unicode('⠝')], 4)))]
#[case::ment_final("comment", 3, Some((vec![decode_unicode('⠰'), decode_unicode('⠞')], 4)))]
#[case::ount_mid("amount", 2, Some((vec![decode_unicode('⠨'), decode_unicode('⠞')], 4)))]
#[case::ness_final("baroness", 4, Some((vec![decode_unicode('⠰'), decode_unicode('⠎')], 4)))]
#[case::ity_final("circuity", 5, Some((vec![decode_unicode('⠰'), decode_unicode('⠽')], 3)))]
#[case::no_match_at_start("tion", 0, None)]
#[case::no_cluster("cat", 1, None)]
fn matches_final_groupsigns(
#[case] word: &str,
#[case] pos: usize,
#[case] expected: Option<(Vec<u8>, usize)>,
) {
let chars: Vec<char> = word.chars().collect();
let got = FinalGroupsignRule
.try_match(&chars, pos)
.map(|m| (m.cells, m.consumed));
assert_eq!(got, expected);
}
#[test]
fn runtime_start_position_never_uses_final_groupsign() {
let word: Vec<char> = std::hint::black_box("tion").chars().collect();
let pos = std::hint::black_box(0);
assert!(FinalGroupsignRule.try_match(&word, pos).is_none());
}
}