use rand::RngExt;
use serde::{Deserialize, Serialize};
use std::time::{SystemTime, UNIX_EPOCH};
use thiserror::Error;
pub mod dictionary;
use dictionary::{ADJECTIVES, ADVERBS, NOUNS, PREPOSITIONS, VERBS};
const COMPONENT_ARRAYS: &[&[&str]] =
&[ADJECTIVES, NOUNS, VERBS, ADVERBS, PREPOSITIONS];
const COMPONENT_SIZES: [u64; 5] = [
ADJECTIVES.len() as u64,
NOUNS.len() as u64,
VERBS.len() as u64,
ADVERBS.len() as u64,
PREPOSITIONS.len() as u64,
];
#[derive(Error, Debug)]
pub enum MemorableIdError {
#[error("Components must be between 1 and 5, got {0}")]
InvalidComponentCount(usize),
#[error("Invalid separator: cannot be empty")]
InvalidSeparator,
#[error("Failed to parse ID: {0}")]
ParseError(String),
}
pub type SuffixGenerator = fn() -> Option<String>;
#[derive(Debug, Clone)]
pub struct GenerateOptions {
pub components: usize,
pub suffix: Option<SuffixGenerator>,
pub separator: String,
}
impl Default for GenerateOptions {
fn default() -> Self {
Self {
components: 2,
suffix: None,
separator: "-".to_string(),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ParsedId {
pub components: Vec<String>,
pub suffix: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CollisionScenario {
pub ids: usize,
pub probability: f64,
pub percentage: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CollisionAnalysis {
pub total_combinations: u64,
pub scenarios: Vec<CollisionScenario>,
}
pub fn generate(options: GenerateOptions) -> Result<String, MemorableIdError> {
if options.components < 1 || options.components > 5 {
return Err(MemorableIdError::InvalidComponentCount(
options.components,
));
}
if options.separator.is_empty() {
return Err(MemorableIdError::InvalidSeparator);
}
let mut rng = rand::rng();
let component_count = options.components;
let mut result = String::with_capacity(
component_count * 8 + options.separator.len() * component_count,
);
for i in 0..component_count {
if i > 0 {
result.push_str(&options.separator);
}
let array = COMPONENT_ARRAYS[i];
let index = rng.random_range(0..array.len());
result.push_str(array[index]);
}
if let Some(suffix_fn) = options.suffix {
if let Some(suffix_value) = suffix_fn() {
result.push_str(&options.separator);
result.push_str(&suffix_value);
}
}
Ok(result)
}
pub fn default_suffix() -> Option<String> {
suffix_generators::number()
}
pub fn parse(id: &str, separator: &str) -> Result<ParsedId, MemorableIdError> {
if id.is_empty() {
return Err(MemorableIdError::ParseError(
"ID cannot be empty".to_string(),
));
}
let parts: Vec<&str> = id.split(separator).collect();
if parts.is_empty() {
return Err(MemorableIdError::ParseError("No parts found".to_string()));
}
if let Some(last) = parts.last() {
if last.chars().all(|c| c.is_ascii_digit()) {
return Ok(ParsedId {
components: parts[..parts.len() - 1]
.iter()
.map(|s| (*s).to_string())
.collect(),
suffix: Some((*last).to_string()),
});
}
}
Ok(ParsedId {
components: parts.iter().map(|s| (*s).to_string()).collect(),
suffix: None,
})
}
pub fn calculate_combinations(components: usize, suffix_range: u64) -> u64 {
let mut total = 1u64;
for &size in &COMPONENT_SIZES[..components.min(5)] {
total = total.saturating_mul(size);
}
total.saturating_mul(suffix_range)
}
pub fn calculate_collision_probability(
total_combinations: u64,
generated_ids: usize,
) -> f64 {
if generated_ids >= total_combinations as usize {
return 1.0;
}
if generated_ids <= 1 {
return 0.0;
}
let n = generated_ids as f64;
let total = total_combinations as f64;
let exponent = -(n * n) / (2.0 * total);
1.0 - exponent.exp()
}
pub fn get_collision_analysis(
components: usize,
suffix_range: u64,
) -> CollisionAnalysis {
let total = calculate_combinations(components, suffix_range);
let test_sizes = [50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 50000];
let scenarios: Vec<CollisionScenario> = test_sizes
.iter()
.filter(|&&size| (size as u64) < (total * 80 / 100)) .map(|&size| {
let probability = calculate_collision_probability(total, size);
CollisionScenario {
ids: size,
probability,
percentage: format!("{:.2}%", probability * 100.0),
}
})
.collect();
CollisionAnalysis {
total_combinations: total,
scenarios,
}
}
pub mod suffix_generators {
use super::*;
fn padded_decimal(value: u32, width: usize) -> String {
format!("{:0width$}", value, width = width)
}
pub fn number() -> Option<String> {
let mut rng = rand::rng();
Some(padded_decimal(rng.random_range(0..1000), 3))
}
pub fn number4() -> Option<String> {
let mut rng = rand::rng();
Some(padded_decimal(rng.random_range(0..10000), 4))
}
pub fn hex() -> Option<String> {
let mut rng = rand::rng();
Some(format!("{:02x}", rng.random_range(0..256)))
}
pub fn timestamp() -> Option<String> {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis();
Some(padded_decimal((now % 10000) as u32, 4))
}
pub fn letter() -> Option<String> {
let mut rng = rand::rng();
let letter = (b'a' + rng.random_range(0..26)) as char;
Some(letter.to_string())
}
}
pub use dictionary::{
get_dictionary, get_dictionary_stats, Dictionary, DictionaryStats,
};