//! Identifiers (spec ยง9.4): ULIDs, 26 characters of Crockford base32, time //! ordered and globally unique, so an id made on one machine merges into a //! hub without renaming. use rand::RngCore; const CROCKFORD: &[u8; 32] = b"0123456789ABCDEFGHJKMNPQRSTVWXYZ"; /// A new ULID for the current time. pub fn ulid() -> String { let mut r = [0u8; 16]; rand::rngs::OsRng.fill_bytes(&mut r); ulid_from(now_ms(), u128::from_be_bytes(r)) } /// A ULID from a millisecond time and 80 bits taken from `random`. pub fn ulid_from(ms: u64, random: u128) -> String { let v: u128 = ((ms as u128 & ((1 << 48) - 1)) << 80) | (random & ((1u128 << 80) - 1)); (0..26).map(|i| CROCKFORD[((v >> (125 - 5 * i)) & 31) as usize] as char).collect() } /// Milliseconds since the Unix epoch. pub fn now_ms() -> u64 { std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_millis() as u64).unwrap_or(0) } /// A time as people and models read it: `2026-11-02 21:43Z`, UTC. pub fn format_time(ms: u64) -> String { let secs = (ms / 1000) as i64; let days = secs.div_euclid(86_400); let rem = secs.rem_euclid(86_400); // Civil from days (Howard Hinnant). let z = days + 719_468; let era = z.div_euclid(146_097); let doe = z - era * 146_097; let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365; let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); let mp = (5 * doy + 2) / 153; let d = doy - (153 * mp + 2) / 5 + 1; let m = if mp < 10 { mp + 3 } else { mp - 9 }; let y = yoe + era * 400 + if m <= 2 { 1 } else { 0 }; format!("{y:04}-{m:02}-{d:02} {:02}:{:02}Z", rem / 3600, (rem % 3600) / 60) } #[cfg(test)] mod tests { use super::*; #[test] fn ulids_are_26_chars_and_sort_by_time() { let a = ulid_from(1_000, 5); let b = ulid_from(2_000, 0); assert_eq!(a.len(), 26); assert!(a < b); assert_eq!(ulid_from(0, 0), "00000000000000000000000000"); assert_ne!(ulid(), ulid()); assert_eq!(format_time(1_793_655_780_000), "2026-11-02 21:43Z"); } }