use std::sync::LazyLock;
use aho_corasick::{AhoCorasick, MatchKind};
static AUTOMATON: LazyLock<AhoCorasick> = LazyLock::new(|| {
let patterns: Vec<&str> = crate::tables::contraction_rules()
.iter()
.map(|(source, _)| *source)
.collect();
AhoCorasick::builder()
.match_kind(MatchKind::LeftmostLongest)
.build(&patterns)
.expect("the contraction rule set is validated at build time")
});
pub(crate) fn contract(text: &str) -> std::borrow::Cow<'_, str> {
let rules = crate::tables::contraction_rules();
let mut matches = AUTOMATON.find_iter(text).peekable();
if matches.peek().is_none() {
return std::borrow::Cow::Borrowed(text);
}
let mut out = String::with_capacity(text.len());
let mut last = 0;
for m in matches {
out.push_str(&text[last..m.start()]);
out.push_str(rules[m.pattern().as_usize()].1);
last = m.end();
}
out.push_str(&text[last..]);
std::borrow::Cow::Owned(out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn borrows_when_nothing_matches() {
assert!(matches!(contract("example"), std::borrow::Cow::Borrowed(_)));
}
#[test]
fn contracts_each_rule() {
assert_eq!(contract("arnazon"), "amazon");
assert_eq!(contract("vvikipedia"), "wikipedia");
assert_eq!(contract("clropbox"), "dropbox");
}
#[test]
fn leftmost_longest_on_overlaps() {
assert_eq!(contract("vvv"), "wv");
assert_eq!(contract("vvvv"), "ww");
}
#[test]
fn adjacent_distinct_rules_both_fire() {
assert_eq!(contract("rnvv"), "mw");
}
#[test]
fn is_idempotent() {
for input in ["arnazon", "vvvv", "rnrn", "clcl", "example", "", "rn", "v"] {
let once = contract(input).into_owned();
assert_eq!(contract(&once), once, "not idempotent for {input:?}");
}
}
#[test]
fn no_rule_chains_into_another() {
let rules = crate::tables::contraction_rules();
for (_, target) in rules {
for (source, _) in rules {
assert!(
!source.contains(target),
"output {target:?} occurs inside source {source:?}"
);
}
}
}
#[test]
fn prose_words_that_must_not_be_touched_by_the_general_fold() {
assert_eq!(contract("earnings"), "eamings");
assert_eq!(contract("turnip"), "tumip");
assert_eq!(contract("born"), "bom");
}
}