use std::collections::HashSet;
pub const TITLE_ABBREVIATIONS: &[&str] = &[
"Dr.", "Mr.", "Mrs.", "Ms.", "Prof.", "Sr.", "Jr.", "St.",
"A.", "B.", "C.", "D.", "E.", "F.", "G.", "H.", "I.", "J.", "K.", "L.", "M.",
"N.", "O.", "P.", "Q.", "R.", "S.", "T.", "U.", "V.", "W.", "X.", "Y.", "Z.",
"Rev.", "Fr.", "Rab.", "Msgr.",
"Gov.", "Sen.", "Rep.", "Amb.", "Hon.", "Ald.",
"Adm.", "Capt.", "Cmdr.", "Col.", "Gen.", "Lt.", "Maj.", "Sgt.", "Cpl.", "Pvt.", "Ens.",
"Pres.", "Supt.",
"Messrs.", "Mme.", "Mlle.", "M."
];
#[cfg(test)]
pub const ABBREVIATIONS: &[&str] = &[
"Dr.", "Mr.", "Mrs.", "Ms.", "Prof.", "Sr.", "Jr.",
"U.S.A.", "U.K.", "N.Y.C.", "L.A.", "D.C.",
"ft.", "in.", "lbs.", "oz.", "mi.", "km.",
"a.m.", "p.m.", "etc.", "vs.", "ea.", "deg.", "et al."
];
pub struct AbbreviationChecker {
#[cfg(test)]
abbreviations: HashSet<&'static str>,
title_abbreviations: HashSet<&'static str>,
}
impl AbbreviationChecker {
pub fn new() -> Self {
Self {
#[cfg(test)]
abbreviations: ABBREVIATIONS.iter().copied().collect(),
title_abbreviations: TITLE_ABBREVIATIONS.iter().copied().collect(),
}
}
#[cfg(test)]
pub fn is_abbreviation(&self, word: &str) -> bool {
self.abbreviations.contains(word)
}
pub fn is_title_abbreviation(&self, word: &str) -> bool {
self.title_abbreviations.contains(word)
}
#[cfg(test)]
pub fn ends_with_abbreviation(&self, text: &str) -> bool {
if let Some(last_word) = text.split_whitespace().last() {
let clean_word = last_word.trim_matches(|c: char| {
matches!(c, '"' | '\'' | '\u{201C}' | '\u{201D}' | '\u{2018}' | '\u{2019}')
});
self.is_abbreviation(clean_word)
} else {
false
}
}
pub fn ends_with_title_abbreviation(&self, text: &str) -> bool {
if let Some(last_word) = text.split_whitespace().last() {
let clean_word = last_word.trim_matches(|c: char| {
matches!(c, '"' | '\'' | '\u{201C}' | '\u{201D}' | '\u{2018}' | '\u{2019}')
});
self.is_title_abbreviation(clean_word)
} else {
false
}
}
}
impl Default for AbbreviationChecker {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::OnceLock;
static SHARED_CHECKER: OnceLock<AbbreviationChecker> = OnceLock::new();
fn get_checker() -> &'static AbbreviationChecker {
SHARED_CHECKER.get_or_init(AbbreviationChecker::new)
}
#[test]
fn test_abbreviation_detection_comprehensive() {
let checker = get_checker();
let abbreviations = ["Dr.", "U.S.A.", "p.m.", "Prof.", "mi."];
for abbr in &abbreviations {
assert!(checker.is_abbreviation(abbr), "Should detect {abbr} as abbreviation");
}
assert!(!checker.is_abbreviation("Hello"));
let title_abbreviations = ["Dr.", "Prof.", "Mr.", "Mrs."];
for abbr in &title_abbreviations {
assert!(checker.is_title_abbreviation(abbr), "Should detect {abbr} as title abbreviation");
}
assert!(!checker.is_title_abbreviation("U.S.A."));
assert!(!checker.is_title_abbreviation("p.m."));
let ending_tests = [
("Meeting at 5 p.m.", true),
("He lives in the U.S.A.", true),
("This is a sentence", false),
("Call Dr.", true),
("See Prof.", true),
];
for (text, should_end_with_abbr) in &ending_tests {
assert_eq!(checker.ends_with_abbreviation(text), *should_end_with_abbr,
"ends_with_abbreviation failed for: {text}");
}
let punctuation_tests = [
("He said 'Dr.'", true),
("She mentioned \"Prof.\"", true),
("Meeting at 5 p.m.?", false), ];
for (text, should_detect) in &punctuation_tests {
assert_eq!(checker.ends_with_abbreviation(text), *should_detect,
"Punctuation handling failed for: {text}");
}
let single_capital_tests = [
("Listener, S.", true),
("latitude, N.", true),
("longitude, W.", true),
("Direction E.", true),
("Point A.", true),
("Section Z.", true),
];
for (text, should_detect) in &single_capital_tests {
assert_eq!(checker.ends_with_title_abbreviation(text), *should_detect,
"Single capital abbreviation failed for: {text}");
}
}
}