#![allow(dead_code)]
use std::collections::HashSet;
use crate::models::ExpertId;
pub const LITERARY_NAMES: &[&str] = &[
"Alyosha",
"Ilyusha",
"Grigory",
"Katya",
"Dmitri",
"Ivan",
"Smerdyakov",
"Fyodor",
"Zosima",
"Lise",
"Varvara",
"Marfa",
];
#[derive(Debug, Clone, Copy)]
pub struct NamePool {
pool: &'static [&'static str],
}
impl NamePool {
pub const fn new() -> Self {
Self {
pool: LITERARY_NAMES,
}
}
pub fn pick_unused(&self, used: &HashSet<&str>) -> Option<&'static str> {
self.pool.iter().copied().find(|name| !used.contains(name))
}
pub fn fallback(&self, id: ExpertId) -> String {
format!("Expert{id:02}")
}
}
impl Default for NamePool {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
use regex::Regex;
fn name_regex() -> Regex {
Regex::new(r"^[A-Za-z][A-Za-z0-9_-]*$").expect("valid regex")
}
#[test]
fn pick_unused_returns_first_free_literary_name() {
let pool = NamePool::new();
let used: HashSet<&str> = HashSet::new();
let picked = pool.pick_unused(&used);
assert_eq!(
picked,
Some("Alyosha"),
"pick_unused: empty `used` should return the first pool entry"
);
}
#[test]
fn pick_unused_skips_taken_names() {
let pool = NamePool::new();
let mut used: HashSet<&str> = HashSet::new();
used.insert("Alyosha");
used.insert("Ilyusha");
let picked = pool.pick_unused(&used);
assert_eq!(
picked,
Some("Grigory"),
"pick_unused: should skip names present in `used`"
);
}
#[test]
fn pick_unused_returns_none_when_pool_exhausted() {
let pool = NamePool::new();
let used: HashSet<&str> = LITERARY_NAMES.iter().copied().collect();
let picked = pool.pick_unused(&used);
assert!(
picked.is_none(),
"pick_unused: full `used` set must yield None"
);
}
#[test]
fn fallback_matches_two_digit_pattern() {
let pool = NamePool::new();
assert_eq!(
pool.fallback(0),
"Expert00",
"fallback: id 0 should pad to two digits"
);
assert_eq!(
pool.fallback(7),
"Expert07",
"fallback: id 7 should pad to two digits"
);
assert_eq!(
pool.fallback(42),
"Expert42",
"fallback: id 42 should keep two-digit shape"
);
assert_eq!(
pool.fallback(1234),
"Expert1234",
"fallback: ids beyond two digits should grow naturally"
);
}
proptest! {
#[test]
fn name_pool_always_yields_valid_unused_name(
used_names in prop::collection::vec("[A-Za-z][A-Za-z0-9_-]{0,15}", 0..32),
id in 0u32..1_000_000,
) {
let regex = name_regex();
let used: HashSet<&str> = used_names.iter().map(String::as_str).collect();
let pool = NamePool::new();
let picked: String = match pool.pick_unused(&used) {
Some(literary) => literary.to_string(),
None => pool.fallback(id),
};
prop_assert!(
regex.is_match(&picked),
"selected name {picked:?} must satisfy naming regex"
);
if pool.pick_unused(&used).is_some() {
prop_assert!(
!used.contains(picked.as_str()),
"pick_unused returned a name present in used set: {picked:?}"
);
}
}
}
}