pub const DEFAULT_ALPHABET: &str = "asdfghjkl";
#[derive(Debug, Clone)]
pub struct HintEngine {
alphabet: Vec<char>,
}
impl Default for HintEngine {
fn default() -> Self {
Self::new(DEFAULT_ALPHABET)
}
}
impl HintEngine {
pub fn new(alphabet: &str) -> Self {
assert!(!alphabet.is_empty(), "hint alphabet must not be empty");
Self {
alphabet: alphabet.chars().collect(),
}
}
pub fn alphabet(&self) -> &[char] {
&self.alphabet
}
pub fn generate(&self, count: usize) -> Vec<String> {
if count == 0 {
return Vec::new();
}
let n = self.alphabet.len();
let mut len = 1usize;
let mut cap = n;
while cap < count {
len += 1;
cap = cap.saturating_mul(n);
if cap == usize::MAX {
break;
}
}
(0..count).map(|i| self.encode(i, len)).collect()
}
fn encode(&self, mut idx: usize, len: usize) -> String {
let n = self.alphabet.len();
let mut chars = vec![self.alphabet[0]; len];
for pos in (0..len).rev() {
chars[pos] = self.alphabet[idx % n];
idx /= n;
}
chars.into_iter().collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashSet;
#[test]
fn empty_count_returns_empty() {
let e = HintEngine::default();
assert!(e.generate(0).is_empty());
}
#[test]
fn single_char_labels_when_count_fits_alphabet() {
let e = HintEngine::new("abc");
let labels = e.generate(3);
assert_eq!(labels, vec!["a", "b", "c"]);
}
#[test]
fn promotes_to_two_chars_when_count_exceeds_alphabet() {
let e = HintEngine::new("ab");
let labels = e.generate(4);
assert_eq!(labels, vec!["aa", "ab", "ba", "bb"]);
}
#[test]
fn all_labels_are_unique_and_same_length() {
let e = HintEngine::default();
let labels = e.generate(120);
let len = labels[0].len();
assert!(labels.iter().all(|l| l.len() == len));
let set: HashSet<_> = labels.iter().collect();
assert_eq!(set.len(), labels.len());
}
#[test]
#[should_panic]
fn empty_alphabet_panics() {
let _ = HintEngine::new("");
}
}