memorable_ids/lib.rs
1//! Memorable ID Generator
2//!
3//! A flexible library for generating human-readable, memorable identifiers.
4//! Uses combinations of adjectives, nouns, verbs, adverbs, and prepositions
5//! with optional numeric/custom suffixes.
6//!
7//! @author Aris Ripandi
8//! @license MIT
9
10use rand::Rng;
11use serde::{Deserialize, Serialize};
12use std::time::{SystemTime, UNIX_EPOCH};
13use thiserror::Error;
14
15pub mod dictionary;
16
17use dictionary::{ADJECTIVES, ADVERBS, NOUNS, PREPOSITIONS, VERBS};
18
19/// Error types for memorable ID operations
20#[derive(Error, Debug)]
21pub enum MemorableIdError {
22 #[error("Components must be between 1 and 5, got {0}")]
23 InvalidComponentCount(usize),
24 #[error("Invalid separator: cannot be empty")]
25 InvalidSeparator,
26 #[error("Failed to parse ID: {0}")]
27 ParseError(String),
28}
29
30/// Type alias for suffix generator function
31pub type SuffixGenerator = fn() -> Option<String>;
32
33/// Configuration options for ID generation
34#[derive(Debug, Clone)]
35pub struct GenerateOptions {
36 /// Number of word components (1-5, default: 2)
37 pub components: usize,
38 /// Suffix generator function (default: None)
39 pub suffix: Option<SuffixGenerator>,
40 /// Separator between parts (default: "-")
41 pub separator: String,
42}
43
44impl Default for GenerateOptions {
45 fn default() -> Self {
46 Self {
47 components: 2,
48 suffix: None,
49 separator: "-".to_string(),
50 }
51 }
52}
53
54/// Parsed ID components structure
55#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
56pub struct ParsedId {
57 /// Array of word components
58 pub components: Vec<String>,
59 /// Suffix part if detected, None otherwise
60 pub suffix: Option<String>,
61}
62
63/// Collision scenario analysis
64#[derive(Debug, Clone, Serialize, Deserialize)]
65pub struct CollisionScenario {
66 /// Number of IDs in scenario
67 pub ids: usize,
68 /// Collision probability (0-1)
69 pub probability: f64,
70 /// Formatted percentage string
71 pub percentage: String,
72}
73
74/// Collision analysis result
75#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct CollisionAnalysis {
77 /// Total possible combinations
78 pub total_combinations: u64,
79 /// Array of collision scenarios
80 pub scenarios: Vec<CollisionScenario>,
81}
82
83/// Generate a memorable ID
84///
85/// # Arguments
86/// * `options` - Configuration options
87///
88/// # Returns
89/// Generated memorable ID
90///
91/// # Examples
92/// ```rust
93/// use memorable_ids::{generate, GenerateOptions, suffix_generators};
94///
95/// // Default: 2 components, no suffix
96/// let id = generate(GenerateOptions::default()).unwrap();
97/// // Example: "cute-rabbit"
98///
99/// // 3 components
100/// let id = generate(GenerateOptions {
101/// components: 3,
102/// ..Default::default()
103/// }).unwrap();
104/// // Example: "large-fox-swim"
105///
106/// // With numeric suffix
107/// let id = generate(GenerateOptions {
108/// components: 2,
109/// suffix: Some(suffix_generators::number),
110/// ..Default::default()
111/// }).unwrap();
112/// // Example: "quick-mouse-042"
113///
114/// // Custom separator
115/// let id = generate(GenerateOptions {
116/// components: 2,
117/// separator: "_".to_string(),
118/// ..Default::default()
119/// }).unwrap();
120/// // Example: "warm_duck"
121/// ```
122pub fn generate(options: GenerateOptions) -> Result<String, MemorableIdError> {
123 if options.components < 1 || options.components > 5 {
124 return Err(MemorableIdError::InvalidComponentCount(
125 options.components,
126 ));
127 }
128
129 if options.separator.is_empty() {
130 return Err(MemorableIdError::InvalidSeparator);
131 }
132
133 let mut rng = rand::rng();
134 let mut parts = Vec::new();
135
136 // Component generators in order
137 let component_arrays = [ADJECTIVES, NOUNS, VERBS, ADVERBS, PREPOSITIONS];
138
139 // Generate requested number of components
140 for i in 0..options.components {
141 let array = component_arrays[i];
142 let index = rng.random_range(0..array.len());
143 parts.push(array[index].to_string());
144 }
145
146 // Add suffix if provided
147 if let Some(suffix_fn) = options.suffix {
148 if let Some(suffix_value) = suffix_fn() {
149 parts.push(suffix_value);
150 }
151 }
152
153 Ok(parts.join(&options.separator))
154}
155
156/// Default suffix generator - random 3-digit number
157///
158/// # Returns
159/// Random number suffix (000-999)
160///
161/// # Examples
162/// ```rust
163/// use memorable_ids::default_suffix;
164///
165/// let suffix = default_suffix().unwrap(); // "042"
166/// let suffix = default_suffix().unwrap(); // "789"
167/// ```
168pub fn default_suffix() -> Option<String> {
169 let mut rng = rand::rng();
170 Some(format!("{:03}", rng.random_range(0..1000)))
171}
172
173/// Parse a memorable ID back to its components
174///
175/// # Arguments
176/// * `id` - The memorable ID to parse
177/// * `separator` - Separator used (default: "-")
178///
179/// # Returns
180/// Parsed components with structure
181///
182/// # Examples
183/// ```rust
184/// use memorable_ids::parse;
185///
186/// let parsed = parse("cute-rabbit-042", "-").unwrap();
187/// // ParsedId { components: ["cute", "rabbit"], suffix: Some("042") }
188///
189/// let parsed = parse("large-fox-swim", "-").unwrap();
190/// // ParsedId { components: ["large", "fox", "swim"], suffix: None }
191/// ```
192pub fn parse(id: &str, separator: &str) -> Result<ParsedId, MemorableIdError> {
193 if id.is_empty() {
194 return Err(MemorableIdError::ParseError(
195 "ID cannot be empty".to_string(),
196 ));
197 }
198
199 let parts: Vec<String> =
200 id.split(separator).map(|s| s.to_string()).collect();
201
202 if parts.is_empty() {
203 return Err(MemorableIdError::ParseError("No parts found".to_string()));
204 }
205
206 let mut result = ParsedId {
207 components: Vec::new(),
208 suffix: None,
209 };
210
211 // Last part is likely suffix if it's numeric
212 if let Some(last_part) = parts.last() {
213 if last_part.chars().all(|c| c.is_ascii_digit()) {
214 result.suffix = Some(last_part.clone());
215 result.components = parts[..parts.len() - 1].to_vec();
216 } else {
217 result.components = parts;
218 }
219 }
220
221 Ok(result)
222}
223
224/// Calculate total possible combinations for given configuration
225///
226/// # Arguments
227/// * `components` - Number of word components (1-5)
228/// * `suffix_range` - Range of suffix values (default: 1 for no suffix)
229///
230/// # Returns
231/// Total possible unique combinations
232///
233/// # Examples
234/// ```rust
235/// use memorable_ids::calculate_combinations;
236///
237/// let total = calculate_combinations(2, 1); // 5,304 (2 components, no suffix)
238/// let total = calculate_combinations(2, 1000); // 5,304,000 (2 components + 3-digit suffix)
239/// let total = calculate_combinations(3, 1); // 212,160 (3 components, no suffix)
240/// ```
241pub fn calculate_combinations(components: usize, suffix_range: u64) -> u64 {
242 let stats = get_dictionary_stats();
243 let component_sizes = [
244 stats.adjectives as u64,
245 stats.nouns as u64,
246 stats.verbs as u64,
247 stats.adverbs as u64,
248 stats.prepositions as u64,
249 ];
250
251 let mut total = 1u64;
252 for i in 0..components.min(5) {
253 total = total.saturating_mul(component_sizes[i]);
254 }
255
256 total.saturating_mul(suffix_range)
257}
258
259/// Calculate collision probability using Birthday Paradox
260///
261/// # Arguments
262/// * `total_combinations` - Total possible combinations
263/// * `generated_ids` - Number of IDs to generate
264///
265/// # Returns
266/// Collision probability (0-1)
267///
268/// # Examples
269/// ```rust
270/// use memorable_ids::calculate_collision_probability;
271///
272/// // For 2 components (5,304 total), generating 100 IDs
273/// let prob = calculate_collision_probability(5304, 100); // ~0.0093 (0.93%)
274///
275/// // For 3 components (212,160 total), generating 10,000 IDs
276/// let prob = calculate_collision_probability(212160, 10000); // ~0.00235 (0.235%)
277/// ```
278pub fn calculate_collision_probability(
279 total_combinations: u64,
280 generated_ids: usize,
281) -> f64 {
282 if generated_ids >= total_combinations as usize {
283 return 1.0;
284 }
285 if generated_ids <= 1 {
286 return 0.0;
287 }
288
289 // Birthday paradox approximation: 1 - e^(-n²/2N)
290 let n = generated_ids as f64;
291 let total = total_combinations as f64;
292 let exponent = -(n * n) / (2.0 * total);
293 1.0 - exponent.exp()
294}
295
296/// Get collision analysis for different ID generation scenarios
297///
298/// # Arguments
299/// * `components` - Number of components
300/// * `suffix_range` - Suffix range (1 for no suffix)
301///
302/// # Returns
303/// Analysis with total combinations and collision probabilities
304///
305/// # Examples
306/// ```rust
307/// use memorable_ids::get_collision_analysis;
308///
309/// let analysis = get_collision_analysis(2, 1);
310/// // CollisionAnalysis {
311/// // total_combinations: 5304,
312/// // scenarios: [
313/// // CollisionScenario { ids: 100, probability: 0.0093, percentage: "0.93%" },
314/// // CollisionScenario { ids: 500, probability: 0.218, percentage: "21.8%" },
315/// // ...
316/// // ]
317/// // }
318/// ```
319pub fn get_collision_analysis(
320 components: usize,
321 suffix_range: u64,
322) -> CollisionAnalysis {
323 let total = calculate_combinations(components, suffix_range);
324 let test_sizes = [50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 50000];
325
326 let scenarios: Vec<CollisionScenario> = test_sizes
327 .iter()
328 .filter(|&&size| (size as u64) < (total * 80 / 100)) // Only show realistic scenarios
329 .map(|&size| {
330 let probability = calculate_collision_probability(total, size);
331 CollisionScenario {
332 ids: size,
333 probability,
334 percentage: format!("{:.2}%", probability * 100.0),
335 }
336 })
337 .collect();
338
339 CollisionAnalysis {
340 total_combinations: total,
341 scenarios,
342 }
343}
344
345/// Collection of predefined suffix generators
346pub mod suffix_generators {
347 use super::*;
348
349 /// Random 3-digit number (000-999)
350 /// Adds 1,000x multiplier to total combinations
351 pub fn number() -> Option<String> {
352 let mut rng = rand::rng();
353 Some(format!("{:03}", rng.random_range(0..1000)))
354 }
355
356 /// Random 4-digit number (0000-9999)
357 /// Adds 10,000x multiplier to total combinations
358 pub fn number4() -> Option<String> {
359 let mut rng = rand::rng();
360 Some(format!("{:04}", rng.random_range(0..10000)))
361 }
362
363 /// Random 2-digit hex (00-ff)
364 /// Adds 256x multiplier to total combinations
365 pub fn hex() -> Option<String> {
366 let mut rng = rand::rng();
367 Some(format!("{:02x}", rng.random_range(0..256)))
368 }
369
370 /// Last 4 digits of current timestamp
371 /// Adds ~10,000x multiplier (time-based, not truly random)
372 pub fn timestamp() -> Option<String> {
373 let now = SystemTime::now()
374 .duration_since(UNIX_EPOCH)
375 .unwrap()
376 .as_millis();
377 Some(format!("{:04}", now % 10000))
378 }
379
380 /// Random lowercase letter (a-z)
381 /// Adds 26x multiplier to total combinations
382 pub fn letter() -> Option<String> {
383 let mut rng = rand::rng();
384 let letter = (b'a' + rng.random_range(0..26)) as char;
385 Some(letter.to_string())
386 }
387}
388
389// Re-export dictionary for external use
390pub use dictionary::{
391 get_dictionary, get_dictionary_stats, Dictionary, DictionaryStats,
392};