use super::*;
fn sugg(items: &[&str]) -> Vec<String> {
items.iter().map(|s| (*s).to_string()).collect()
}
fn query<'a>(token: &'a str, text: &'a str, suggestions: &'a [String]) -> NameQuery<'a> {
let start = text.find(token).expect("token must occur in text");
NameQuery {
token,
text,
start_byte: start,
end_byte: start + token.len(),
suggestions,
}
}
fn english() -> NameFilter {
NameFilter::new(Aggressiveness::Balanced, "en-US")
}
#[test]
fn surname_close_to_a_real_word_is_still_recognised() {
let s = sugg(&["Hare", "Hoar", "Hoard"]);
let verdict = english().evaluate(&query("Hoare", "The logic of Hoare is central.", &s));
assert!(verdict.is_name, "signals: {:?}", verdict.signals);
assert!(verdict.signals.contains(&NameSignal::Gazetteer));
assert!(verdict.signals.contains(&NameSignal::Shape));
}
#[test]
fn surname_one_edit_from_a_variant_spelling_is_recognised() {
let s = sugg(&["Ackerman"]);
let verdict = english().evaluate(&query("Ackermann", "See Ackermann for details.", &s));
assert!(verdict.is_name, "signals: {:?}", verdict.signals);
}
#[test]
fn unknown_surname_with_distant_suggestions_is_recognised() {
let s = sugg(&["Aymaran", "Beltran", "Elara"]);
let verdict = english().evaluate(&query("Oyelaran", "The paper by Oyelaran argues.", &s));
assert!(verdict.is_name, "signals: {:?}", verdict.signals);
assert!(verdict.signals.contains(&NameSignal::Orphan));
}
#[test]
fn honorific_supplies_context() {
let s = sugg(&["Nemesis"]);
let verdict = english().evaluate(&query("Nkemdirim", "We met Dr. Nkemdirim today.", &s));
assert!(verdict.is_name, "signals: {:?}", verdict.signals);
assert!(verdict.signals.contains(&NameSignal::Context));
}
#[test]
fn repeated_token_gains_a_signal() {
let text = "Szymanski proved it. Later Szymanski extended the result.";
let s = sugg(&["Szymanowski"]);
let verdict = english().evaluate(&query("Szymanski", text, &s));
assert!(verdict.is_name);
assert!(verdict.signals.contains(&NameSignal::Repetition));
}
#[test]
fn engine_offering_no_correction_counts_as_evidence() {
let verdict = english().evaluate(&query("Bhattacharya", "As Bhattacharya notes,", &[]));
assert!(verdict.is_name, "signals: {:?}", verdict.signals);
assert!(verdict.signals.contains(&NameSignal::NoSuggestions));
}
#[test]
fn common_misspellings_are_never_suppressed() {
let cases: &[(&str, &[&str])] = &[
("recieve", &["relieve", "receive"]),
("seperate", &["separate"]),
("definately", &["definitely"]),
("occured", &["occurred"]),
("adress", &["address", "dress"]),
("begining", &["beginning"]),
("enviroment", &["environment"]),
("succesful", &["successful"]),
("neccessary", &["necessary"]),
("similiar", &["similar"]),
("tommorow", &["tomorrow"]),
];
let filter = english();
for (token, suggestions) in cases {
let text = format!("I will {token} the document later.");
let s = sugg(suggestions);
let verdict = filter.evaluate(&query(token, &text, &s));
assert!(
!verdict.is_name,
"{token} was wrongly treated as a name (signals {:?}, score {})",
verdict.signals, verdict.score
);
}
}
#[test]
fn thier_is_not_treated_as_a_name() {
assert!(
!gazetteer_contains("thier"),
"build-time typo subtraction regressed"
);
let s = sugg(&["their", "there"]);
let verdict = english().evaluate(&query("thier", "I read thier paper.", &s));
assert!(!verdict.is_name);
}
#[test]
fn capitalised_misspelling_of_a_real_word_is_not_suppressed() {
let s = sugg(&["Germany"]);
let verdict = english().evaluate(&query("Germny", "Germny is in Europe.", &s));
assert!(!verdict.is_name, "signals: {:?}", verdict.signals);
}
#[test]
fn lowercase_jargon_is_not_a_name() {
let s = sugg(&["fumble", "flyable", "burble"]);
let verdict = english().evaluate(&query("flurble", "the flurble operator commutes", &s));
assert!(!verdict.is_name, "signals: {:?}", verdict.signals);
}
#[test]
fn a_gazetteer_hit_alone_never_suppresses() {
let s = sugg(&["starling"]);
let verdict = english().evaluate(&query("sterlin", "the sterlin rate today", &s));
assert!(!verdict.is_name);
}
#[test]
fn german_does_not_use_capitalisation_as_evidence() {
let german = NameFilter::new(Aggressiveness::Balanced, "de-DE");
let s = sugg(&["Backer"]);
let verdict = german.evaluate(&query("Bäcker", "Der Bäcker hat geöffnet.", &s));
assert!(
!verdict.signals.contains(&NameSignal::Shape),
"shape must not vote in German"
);
assert!(!verdict.is_name, "signals: {:?}", verdict.signals);
}
#[test]
fn english_capitalisation_does_vote() {
let verdict = english().evaluate(&query("Baker", "I saw Baker yesterday.", &[]));
assert!(verdict.signals.contains(&NameSignal::Shape));
}
#[test]
fn split_suggestions_are_ignored_for_distance() {
assert_eq!(
min_suggestion_distance("Abramsky", &sugg(&["Abram sky"])),
None
);
assert_eq!(
min_suggestion_distance("recieve", &sugg(&["receive"])),
Some(1)
);
}
#[test]
fn distance_is_case_insensitive() {
assert_eq!(min_suggestion_distance("Hoare", &sugg(&["hoar"])), Some(1));
}
#[test]
fn morphological_bases_cover_possessive_and_genitive() {
let bases = morphological_bases("merkels");
assert!(bases.contains(&"merkels".to_string()));
assert!(bases.contains(&"merkel".to_string()));
let possessive = morphological_bases("merkel's");
assert!(possessive.contains(&"merkel".to_string()));
}
#[test]
fn gazetteer_matches_inflected_forms() {
assert!(gazetteer_contains("Hoare"));
assert!(gazetteer_contains("hoare"));
assert!(gazetteer_contains("Hoare's"));
}
#[test]
fn occurrences_counts_whole_words_only() {
assert_eq!(occurrences("Hoare and Hoare again", "Hoare"), 2);
assert_eq!(occurrences("Hoarefrost is not Hoare", "Hoare"), 1);
assert_eq!(occurrences("nothing here", "Hoare"), 0);
}
#[test]
fn occurrences_terminates_on_overlapping_and_empty_input() {
assert_eq!(occurrences("aaaa", ""), 0);
assert_eq!(occurrences("aaaa", "aa"), 0, "not whole words");
assert_eq!(occurrences("aa aa", "aa"), 2);
}
#[test]
fn sentence_start_detection() {
let text = "Hello. Baker arrived.";
assert!(starts_sentence(text, 0));
assert!(starts_sentence(text, text.find("Baker").unwrap()));
assert!(!starts_sentence("I saw Baker there.", 6));
}
#[test]
fn windows_respect_char_boundaries() {
let text = "Grüße von Müller und Schröder überall";
let start = text.find("Müller").unwrap();
let _ = preceding_window(text, start);
let _ = following_window(text, start + "Müller".len());
}
#[test]
fn aggressiveness_changes_the_bar() {
let s = sugg(&["Hare"]);
let text = "The logic of Hoare is central.";
assert!(
NameFilter::new(Aggressiveness::Balanced, "en-US")
.evaluate(&query("Hoare", text, &s))
.is_name
);
assert!(
!NameFilter::new(Aggressiveness::Conservative, "en-US")
.evaluate(&query("Hoare", text, &s))
.is_name
);
}
#[test]
fn verdict_reports_its_signals() {
let verdict = english().evaluate(&query("Bhattacharya", "As Bhattacharya notes,", &[]));
let tags = verdict.signal_tags();
assert!(tags.contains("no-suggestions"), "tags were {tags}");
}
#[test]
fn gazetteer_loads() {
assert!(
GAZETTEER.is_some(),
"embedded names.fst failed to parse — regenerate via examples/build-name-fst.rs"
);
}