use rand::Rng;
use serde::{Deserialize, Serialize};
use std::time::{SystemTime, UNIX_EPOCH};
use thiserror::Error;
pub mod dictionary;
use dictionary::{ADJECTIVES, ADVERBS, NOUNS, PREPOSITIONS, VERBS};
#[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 mut parts = Vec::new();
let component_arrays = [ADJECTIVES, NOUNS, VERBS, ADVERBS, PREPOSITIONS];
for i in 0..options.components {
let array = component_arrays[i];
let index = rng.random_range(0..array.len());
parts.push(array[index].to_string());
}
if let Some(suffix_fn) = options.suffix {
if let Some(suffix_value) = suffix_fn() {
parts.push(suffix_value);
}
}
Ok(parts.join(&options.separator))
}
pub fn default_suffix() -> Option<String> {
let mut rng = rand::rng();
Some(format!("{:03}", rng.random_range(0..1000)))
}
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<String> =
id.split(separator).map(|s| s.to_string()).collect();
if parts.is_empty() {
return Err(MemorableIdError::ParseError("No parts found".to_string()));
}
let mut result = ParsedId {
components: Vec::new(),
suffix: None,
};
if let Some(last_part) = parts.last() {
if last_part.chars().all(|c| c.is_ascii_digit()) {
result.suffix = Some(last_part.clone());
result.components = parts[..parts.len() - 1].to_vec();
} else {
result.components = parts;
}
}
Ok(result)
}
pub fn calculate_combinations(components: usize, suffix_range: u64) -> u64 {
let stats = get_dictionary_stats();
let component_sizes = [
stats.adjectives as u64,
stats.nouns as u64,
stats.verbs as u64,
stats.adverbs as u64,
stats.prepositions as u64,
];
let mut total = 1u64;
for i in 0..components.min(5) {
total = total.saturating_mul(component_sizes[i]);
}
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::*;
pub fn number() -> Option<String> {
let mut rng = rand::rng();
Some(format!("{:03}", rng.random_range(0..1000)))
}
pub fn number4() -> Option<String> {
let mut rng = rand::rng();
Some(format!("{:04}", rng.random_range(0..10000)))
}
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()
.as_millis();
Some(format!("{:04}", now % 10000))
}
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,
};