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