use crate::normalizer::Normalizer;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct NicknameTable {
classes: Vec<Vec<String>>,
}
impl NicknameTable {
pub fn empty() -> Self {
Self {
classes: Vec::new(),
}
}
pub fn with_class<I, S>(mut self, names: I) -> Self
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
let mut entries: Vec<String> = Vec::new();
for name in names {
let normalised = Normalizer::normalize_name(name.as_ref());
if !normalised.is_empty() && !entries.contains(&normalised) {
entries.push(normalised);
}
}
if entries.len() >= 2 {
self.classes.push(entries);
}
self
}
pub fn are_equivalent(&self, a: &str, b: &str) -> bool {
let na = Normalizer::normalize_name(a);
let nb = Normalizer::normalize_name(b);
if na == nb {
return true;
}
self.classes
.iter()
.any(|cls| cls.iter().any(|n| n == &na) && cls.iter().any(|n| n == &nb))
}
pub fn is_empty(&self) -> bool {
self.classes.is_empty()
}
pub fn len(&self) -> usize {
self.classes.len()
}
pub fn english() -> Self {
let pairs: &[&[&str]] = &[
&["michael", "mike", "mick", "mickey"],
&["robert", "bob", "rob", "robbie", "bobby"],
&["william", "will", "bill", "billy", "willy"],
&["james", "jim", "jimmy", "jamie"],
&["richard", "rick", "dick", "rich", "richie"],
&["thomas", "tom", "tommy"],
&[
"elizabeth",
"liz",
"beth",
"betty",
"eliza",
"lizzy",
"betsy",
],
&[
"katherine",
"kate",
"kathy",
"katy",
"kat",
"cathy",
"katie",
],
&[
"catherine",
"kate",
"kathy",
"katy",
"kat",
"cathy",
"katie",
],
&["margaret", "maggie", "meg", "peggy", "marge"],
&["jennifer", "jen", "jenny", "jenn"],
&["patricia", "pat", "patty", "tricia", "trish"],
&["susan", "sue", "suzie", "susie"],
&["barbara", "barb", "babs"],
&["anthony", "tony"],
&["christopher", "chris", "kris"],
&["charles", "charlie", "chuck", "chas"],
&["daniel", "dan", "danny"],
&["david", "dave", "davy"],
&["edward", "ed", "eddie", "ted", "ned"],
&["joseph", "joe", "joey"],
&["kenneth", "ken", "kenny"],
&["nicholas", "nick", "nico"],
&["peter", "pete"],
&["samuel", "sam", "sammy"],
&["stephen", "steve", "stevie"],
&["steven", "steve", "stevie"],
&["timothy", "tim", "timmy"],
&["alexander", "alex", "xander"],
&["alexandra", "alex", "alexa", "sandy"],
&["sandra", "sandy"],
&["benjamin", "ben", "benny"],
&["rebecca", "becca", "becky"],
&["sarah", "sara", "sally"],
&["victoria", "vicky", "vic", "tori"],
&["matthew", "matt", "matty"],
&["jonathan", "jon", "jonny", "jonathon"],
&["frederick", "fred", "freddy", "freddie"],
&["lawrence", "larry"],
&["henry", "hank", "harry"],
&["ronald", "ron", "ronnie"],
&["donald", "don", "donnie"],
&["andrew", "andy", "drew"],
&["abigail", "abby", "gail"],
&["amanda", "mandy"],
&["isabella", "izzy", "bella"],
&["isabel", "izzy", "bella"],
&["olivia", "liv", "livy"],
&["nicole", "nikki"],
&["samantha", "sam", "sammy"],
&["pamela", "pam"],
&["deborah", "deb", "debbie"],
&["kimberly", "kim"],
&["jessica", "jess", "jessie"],
&["stephanie", "steph"],
&["madeline", "maddy", "maddie"],
];
let mut table = Self::empty();
for class in pairs {
table = table.with_class(*class);
}
table
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_table_treats_distinct_strings_as_inequivalent() {
let t = NicknameTable::empty();
assert!(!t.are_equivalent("Mike", "Michael"));
assert!(!t.are_equivalent("Liz", "Elizabeth"));
}
#[test]
fn identical_normalised_strings_are_trivially_equivalent_even_when_empty() {
let t = NicknameTable::empty();
assert!(t.are_equivalent("Mike", "mike"));
assert!(t.are_equivalent("MICHAEL", "michael"));
assert!(t.are_equivalent("", ""));
}
#[test]
fn with_class_normalises_entries_at_insertion() {
let t = NicknameTable::empty().with_class(["Robert", "Bob", "Rob"]);
assert!(t.are_equivalent("BOB", "robert"));
assert!(t.are_equivalent("Rob", "Robert"));
}
#[test]
fn with_class_dedupes_after_normalisation() {
let t = NicknameTable::empty().with_class(["mike", "MIKE", "Mike"]);
assert_eq!(t.len(), 0);
}
#[test]
fn with_class_drops_classes_with_fewer_than_two_distinct_entries() {
let t = NicknameTable::empty().with_class(["Mike"]);
assert!(t.is_empty());
}
#[test]
fn with_class_drops_empty_strings_silently() {
let t = NicknameTable::empty().with_class(["", "Mike", ""]);
assert!(t.is_empty());
}
#[test]
fn english_table_covers_acceptance_criterion() {
let t = NicknameTable::english();
for (a, b) in [
("Mike", "Michael"),
("Liz", "Elizabeth"),
("Bob", "Robert"),
("Bill", "William"),
("Dick", "Richard"),
] {
assert!(t.are_equivalent(a, b), "{a:?} ↮ {b:?} in english()");
}
}
#[test]
fn english_table_treats_unrelated_names_as_inequivalent() {
let t = NicknameTable::english();
assert!(!t.are_equivalent("Mike", "Robert"));
assert!(!t.are_equivalent("Liz", "Tom"));
}
#[test]
fn english_table_handles_shared_nicknames_across_classes() {
let t = NicknameTable::english();
assert!(t.are_equivalent("Sandy", "Alexandra"));
assert!(t.are_equivalent("Sandy", "Sandra"));
assert!(t.are_equivalent("Steve", "Stephen"));
assert!(t.are_equivalent("Steve", "Steven"));
}
#[test]
fn with_class_composes_on_top_of_english() {
let t = NicknameTable::english().with_class(["Reginald", "Reggie"]);
assert!(t.are_equivalent("Reggie", "Reginald"));
assert!(t.are_equivalent("Mike", "Michael"));
}
#[test]
fn lookup_is_case_and_punctuation_insensitive() {
let t = NicknameTable::english();
assert!(t.are_equivalent("MIKE", "michael"));
assert!(t.are_equivalent(" Mike ", "Michael"));
}
#[test]
fn default_is_empty() {
let t = NicknameTable::default();
assert!(t.is_empty());
}
}