memorable-ids 0.1.1

A flexible library for generating human-readable, memorable identifiers.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
//! Memorable ID Generator
//!
//! A flexible library for generating human-readable, memorable identifiers.
//! Uses combinations of adjectives, nouns, verbs, adverbs, and prepositions
//! with optional numeric/custom suffixes.
//!
//! @author Aris Ripandi
//! @license MIT

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};

/// Word arrays indexed by component position (adjective → noun → verb → adverb → preposition)
const COMPONENT_ARRAYS: &[&[&str]] =
    &[ADJECTIVES, NOUNS, VERBS, ADVERBS, PREPOSITIONS];

/// Dictionary sizes for combination math (computed at compile time)
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,
];

/// Error types for memorable ID operations
#[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),
}

/// Type alias for suffix generator function
pub type SuffixGenerator = fn() -> Option<String>;

/// Configuration options for ID generation
#[derive(Debug, Clone)]
pub struct GenerateOptions {
    /// Number of word components (1-5, default: 2)
    pub components: usize,
    /// Suffix generator function (default: None)
    pub suffix: Option<SuffixGenerator>,
    /// Separator between parts (default: "-")
    pub separator: String,
}

impl Default for GenerateOptions {
    fn default() -> Self {
        Self {
            components: 2,
            suffix: None,
            separator: "-".to_string(),
        }
    }
}

/// Parsed ID components structure
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ParsedId {
    /// Array of word components
    pub components: Vec<String>,
    /// Suffix part if detected, None otherwise
    pub suffix: Option<String>,
}

/// Collision scenario analysis
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CollisionScenario {
    /// Number of IDs in scenario
    pub ids: usize,
    /// Collision probability (0-1)
    pub probability: f64,
    /// Formatted percentage string
    pub percentage: String,
}

/// Collision analysis result
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CollisionAnalysis {
    /// Total possible combinations
    pub total_combinations: u64,
    /// Array of collision scenarios
    pub scenarios: Vec<CollisionScenario>,
}

/// Generate a memorable ID
///
/// # Arguments
/// * `options` - Configuration options
///
/// # Returns
/// Generated memorable ID
///
/// # Examples
/// ```rust
/// use memorable_ids::{generate, GenerateOptions, suffix_generators};
///
/// // Default: 2 components, no suffix
/// let id = generate(GenerateOptions::default()).unwrap();
/// // Example: "cute-rabbit"
///
/// // 3 components
/// let id = generate(GenerateOptions {
///     components: 3,
///     ..Default::default()
/// }).unwrap();
/// // Example: "large-fox-swim"
///
/// // With numeric suffix
/// let id = generate(GenerateOptions {
///     components: 2,
///     suffix: Some(suffix_generators::number),
///     ..Default::default()
/// }).unwrap();
/// // Example: "quick-mouse-042"
///
/// // Custom separator
/// let id = generate(GenerateOptions {
///     components: 2,
///     separator: "_".to_string(),
///     ..Default::default()
/// }).unwrap();
/// // Example: "warm_duck"
/// ```
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;

    // Pre-allocate: ~8 chars per word + separators
    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)
}

/// Default suffix generator - random 3-digit number
///
/// # Returns
/// Random number suffix (000-999)
///
/// # Examples
/// ```rust
/// use memorable_ids::default_suffix;
///
/// let suffix = default_suffix().unwrap(); // "042"
/// let suffix = default_suffix().unwrap(); // "789"
/// ```
pub fn default_suffix() -> Option<String> {
    suffix_generators::number()
}

/// Parse a memorable ID back to its components
///
/// # Arguments
/// * `id` - The memorable ID to parse
/// * `separator` - Separator used (default: "-")
///
/// # Returns
/// Parsed components with structure
///
/// # Examples
/// ```rust
/// use memorable_ids::parse;
///
/// let parsed = parse("cute-rabbit-042", "-").unwrap();
/// // ParsedId { components: ["cute", "rabbit"], suffix: Some("042") }
///
/// let parsed = parse("large-fox-swim", "-").unwrap();
/// // ParsedId { components: ["large", "fox", "swim"], suffix: None }
/// ```
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()));
    }

    // Last part is suffix when fully numeric (e.g. "cute-rabbit-042")
    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,
    })
}

/// Calculate total possible combinations for given configuration
///
/// # Arguments
/// * `components` - Number of word components (1-5)
/// * `suffix_range` - Range of suffix values (default: 1 for no suffix)
///
/// # Returns
/// Total possible unique combinations
///
/// # Examples
/// ```rust
/// use memorable_ids::calculate_combinations;
///
/// let total = calculate_combinations(2, 1); // 5,304 (2 components, no suffix)
/// let total = calculate_combinations(2, 1000); // 5,304,000 (2 components + 3-digit suffix)
/// let total = calculate_combinations(3, 1); // 212,160 (3 components, no suffix)
/// ```
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)
}

/// Calculate collision probability using Birthday Paradox
///
/// # Arguments
/// * `total_combinations` - Total possible combinations
/// * `generated_ids` - Number of IDs to generate
///
/// # Returns
/// Collision probability (0-1)
///
/// # Examples
/// ```rust
/// use memorable_ids::calculate_collision_probability;
///
/// // For 2 components (5,304 total), generating 100 IDs
/// let prob = calculate_collision_probability(5304, 100); // ~0.0093 (0.93%)
///
/// // For 3 components (212,160 total), generating 10,000 IDs
/// let prob = calculate_collision_probability(212160, 10000); // ~0.00235 (0.235%)
/// ```
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;
    }

    // Birthday paradox approximation: 1 - e^(-n²/2N)
    let n = generated_ids as f64;
    let total = total_combinations as f64;
    let exponent = -(n * n) / (2.0 * total);
    1.0 - exponent.exp()
}

/// Get collision analysis for different ID generation scenarios
///
/// # Arguments
/// * `components` - Number of components
/// * `suffix_range` - Suffix range (1 for no suffix)
///
/// # Returns
/// Analysis with total combinations and collision probabilities
///
/// # Examples
/// ```rust
/// use memorable_ids::get_collision_analysis;
///
/// let analysis = get_collision_analysis(2, 1);
/// // CollisionAnalysis {
/// //   total_combinations: 5304,
/// //   scenarios: [
/// //     CollisionScenario { ids: 100, probability: 0.0093, percentage: "0.93%" },
/// //     CollisionScenario { ids: 500, probability: 0.218, percentage: "21.8%" },
/// //     ...
/// //   ]
/// // }
/// ```
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)) // Only show realistic scenarios
        .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,
    }
}

/// Collection of predefined suffix generators
pub mod suffix_generators {
    use super::*;

    fn padded_decimal(value: u32, width: usize) -> String {
        format!("{:0width$}", value, width = width)
    }

    /// Random 3-digit number (000-999)
    /// Adds 1,000x multiplier to total combinations
    pub fn number() -> Option<String> {
        let mut rng = rand::rng();
        Some(padded_decimal(rng.random_range(0..1000), 3))
    }

    /// Random 4-digit number (0000-9999)
    /// Adds 10,000x multiplier to total combinations
    pub fn number4() -> Option<String> {
        let mut rng = rand::rng();
        Some(padded_decimal(rng.random_range(0..10000), 4))
    }

    /// Random 2-digit hex (00-ff)
    /// Adds 256x multiplier to total combinations
    pub fn hex() -> Option<String> {
        let mut rng = rand::rng();
        Some(format!("{:02x}", rng.random_range(0..256)))
    }

    /// Last 4 digits of current timestamp
    /// Adds ~10,000x multiplier (time-based, not truly random)
    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))
    }

    /// Random lowercase letter (a-z)
    /// Adds 26x multiplier to total combinations
    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())
    }
}

// Re-export dictionary for external use
pub use dictionary::{
    get_dictionary, get_dictionary_stats, Dictionary, DictionaryStats,
};