1use std::time::{SystemTime, UNIX_EPOCH};
9
10const DIGITS: &[u8; 36] = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
11const SUFFIX_LEN: usize = 5;
12
13pub fn new_id() -> String {
15 id_at(now_ms(), &mut fastrand::Rng::new())
16}
17
18pub fn id_at(ms: u64, rng: &mut fastrand::Rng) -> String {
20 let mut id = to_base36(ms);
21 id.extend((0..SUFFIX_LEN).map(|_| DIGITS[rng.usize(..36)] as char));
22 id
23}
24
25pub fn timestamp(id: &str) -> Option<u64> {
29 let prefix = id.len().checked_sub(SUFFIX_LEN).filter(|_| id.len() != 36)?;
30 u64::from_str_radix(&id[..prefix], 36).ok()
31}
32
33pub fn is_valid(id: &str) -> bool {
35 matches!(id.len(), 13 | 36)
36}
37
38fn to_base36(mut n: u64) -> String {
39 let mut out = Vec::new();
40 loop {
41 out.push(DIGITS[(n % 36) as usize]);
42 n /= 36;
43 if n == 0 {
44 break;
45 }
46 }
47 out.reverse();
48 String::from_utf8(out).expect("base-36 digits are ASCII")
49}
50
51fn now_ms() -> u64 {
52 let since = SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default();
53 since.as_millis() as u64
54}
55
56#[cfg(test)]
57mod tests {
58 use super::*;
59
60 #[test]
61 fn prefix_matches_fixture_ids() {
62 assert_eq!(timestamp("MVP1BZ832CIJ1"), Some(1793005755411));
65 assert_eq!(timestamp("MVP1JTFJI1HWU"), Some(1793006121151));
66 assert_eq!(to_base36(1793005755411), "MVP1BZ83");
67 }
68
69 #[test]
70 fn generated_ids_round_trip() {
71 let mut rng = fastrand::Rng::with_seed(7);
72 let id = id_at(1793005755411, &mut rng);
73 assert_eq!(id.len(), 13);
74 assert!(id.starts_with("MVP1BZ83"));
75 assert!(is_valid(&id));
76 assert_eq!(timestamp(&id), Some(1793005755411));
77 }
78
79 #[test]
80 fn new_ids_sort_by_time() {
81 let mut rng = fastrand::Rng::with_seed(1);
82 assert!(id_at(1_000_000_000_000, &mut rng) < id_at(1_000_000_000_001, &mut rng));
83 }
84}