use std::fmt;
use std::str::FromStr;
const ENCODED_LEN: usize = 26;
const MAX_TS_MS: u128 = (1u128 << 48) - 1;
pub const RANDOM_BITS: u32 = 80;
const RANDOM_MASK: u128 = (1u128 << RANDOM_BITS) - 1;
const CROCKFORD_ALPHABET: &[u8; 32] = b"0123456789ABCDEFGHJKMNPQRSTVWXYZ";
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Ulid(u128);
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum UlidParseError {
BadLength { actual: usize },
BadChar { position: usize, ch: char },
Overflow,
}
impl fmt::Display for UlidParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
UlidParseError::BadLength { actual } => write!(
f,
"ULID must be exactly {ENCODED_LEN} characters, got {actual}"
),
UlidParseError::BadChar { position, ch } => write!(
f,
"invalid Crockford base32 character {ch:?} at position {position}"
),
UlidParseError::Overflow => write!(
f,
"ULID overflows u128: leftmost character's top two bits must be zero"
),
}
}
}
impl std::error::Error for UlidParseError {}
impl Ulid {
pub fn from_raw(value: u128) -> Self {
Ulid(value)
}
pub fn as_u128(self) -> u128 {
self.0
}
pub fn from_millis_with_random(unix_ms: i64, random_80_bits: u128) -> Self {
let ts = (unix_ms.max(0) as u128) & MAX_TS_MS;
let rand = random_80_bits & RANDOM_MASK;
Ulid((ts << RANDOM_BITS) | rand)
}
pub fn timestamp_millis(self) -> i64 {
(self.0 >> RANDOM_BITS) as i64
}
pub fn random(self) -> u128 {
self.0 & RANDOM_MASK
}
pub fn encode(self) -> String {
let mut buf = [0u8; ENCODED_LEN];
let mut value = self.0;
for i in (0..ENCODED_LEN).rev() {
let bits = (value & 0x1F) as usize;
buf[i] = CROCKFORD_ALPHABET[bits];
value >>= 5;
}
String::from_utf8(buf.to_vec()).expect("alphabet is ASCII")
}
pub fn parse(s: &str) -> Result<Self, UlidParseError> {
if s.len() != ENCODED_LEN {
return Err(UlidParseError::BadLength { actual: s.len() });
}
let mut value: u128 = 0;
for (position, ch) in s.chars().enumerate() {
let bits = decode_char(ch).ok_or(UlidParseError::BadChar { position, ch })?;
if position == 0 && bits >= 8 {
return Err(UlidParseError::Overflow);
}
value = (value << 5) | bits as u128;
}
Ok(Ulid(value))
}
}
impl fmt::Display for Ulid {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.encode())
}
}
impl FromStr for Ulid {
type Err = UlidParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse(s)
}
}
impl serde::Serialize for Ulid {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
s.serialize_str(&self.encode())
}
}
impl<'de> serde::Deserialize<'de> for Ulid {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
let s = String::deserialize(d)?;
Self::parse(&s).map_err(serde::de::Error::custom)
}
}
fn decode_char(ch: char) -> Option<u8> {
let upper = ch.to_ascii_uppercase();
match upper {
'0' | 'O' => Some(0),
'1' | 'I' | 'L' => Some(1),
'2'..='9' => Some(upper as u8 - b'0'),
'A'..='H' => Some(upper as u8 - b'A' + 10),
'J' => Some(18),
'K' => Some(19),
'M' => Some(20),
'N' => Some(21),
'P'..='T' => Some(upper as u8 - b'P' + 22),
'V'..='Z' => Some(upper as u8 - b'V' + 27),
_ => None,
}
}
#[cfg(test)]
pub mod strategy {
use super::Ulid;
use proptest::prelude::*;
pub fn any_ulid() -> impl Strategy<Value = Ulid> {
(any::<u64>(), any::<u64>())
.prop_map(|(hi, lo)| Ulid::from_raw(((hi as u128) << 64) | lo as u128))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn encode_has_length_26_and_uppercase() {
let u = Ulid::from_raw(0);
let s = u.encode();
assert_eq!(s.len(), ENCODED_LEN);
assert_eq!(s, "00000000000000000000000000");
assert!(s
.chars()
.all(|c| c.is_ascii_uppercase() || c.is_ascii_digit()));
}
#[test]
fn parse_canonical_round_trips() {
for value in [
0u128,
1,
42,
MAX_TS_MS,
(MAX_TS_MS << RANDOM_BITS) | RANDOM_MASK,
u128::MAX >> 2, ] {
let u = Ulid::from_raw(value);
assert_eq!(Ulid::parse(&u.encode()), Ok(u), "value = {value:#x}");
}
}
#[test]
fn parse_is_case_insensitive() {
let u = Ulid::from_raw(0xABCDEF123456789);
let canonical = u.encode();
assert_eq!(Ulid::parse(&canonical.to_lowercase()), Ok(u));
}
#[test]
fn parse_accepts_ambiguous_crockford_chars() {
let u = Ulid::from_raw(1);
let mangled = u.encode().replace('1', "L");
assert_eq!(Ulid::parse(&mangled), Ok(u));
let mangled = u.encode().replace('1', "I");
assert_eq!(Ulid::parse(&mangled), Ok(u));
assert_eq!(
Ulid::parse("OOOOOOOOOOOOOOOOOOOOOOOOOO"),
Ok(Ulid::from_raw(0))
);
}
#[test]
fn parse_rejects_wrong_length() {
assert!(matches!(
Ulid::parse("ABCD"),
Err(UlidParseError::BadLength { actual: 4 })
));
assert!(matches!(
Ulid::parse("000000000000000000000000000"),
Err(UlidParseError::BadLength { actual: 27 })
));
}
#[test]
fn parse_rejects_invalid_char() {
assert!(matches!(
Ulid::parse("U0000000000000000000000000"),
Err(UlidParseError::BadChar {
position: 0,
ch: 'U'
})
));
}
#[test]
fn parse_rejects_overflow_at_top_char() {
assert_eq!(
Ulid::parse("80000000000000000000000000"),
Err(UlidParseError::Overflow)
);
}
#[test]
fn from_millis_uses_unix_epoch() {
let u = Ulid::from_millis_with_random(0, 0);
assert_eq!(
u.as_u128(),
0,
"Unix epoch ms with zero random == zero ULID"
);
let u = Ulid::from_millis_with_random(1, 0);
assert_eq!(u.as_u128(), 1u128 << RANDOM_BITS);
}
#[test]
fn from_millis_clamps_negative_input_to_zero() {
let u = Ulid::from_millis_with_random(-1_000_000, 42);
assert_eq!(u.as_u128(), 42u128, "negative ms collapses to ts=0");
}
#[test]
fn timestamp_millis_extracts_ts_portion() {
let u = Ulid::from_millis_with_random(1_700_000_000_000, 0xABCDEF);
assert_eq!(u.timestamp_millis(), 1_700_000_000_000);
}
#[test]
fn random_extracts_tail() {
let u = Ulid::from_millis_with_random(1_700_000_000_000, 0xABCDEF);
assert_eq!(u.random(), 0xABCDEF);
}
#[test]
fn serde_round_trips_via_string() {
let u = Ulid::from_raw(0xDEAD_BEEF_CAFE_BABE_1234_5678_90AB_CDEF);
let s = serde_json::to_string(&u).unwrap();
assert_eq!(s, format!("\"{u}\""));
let back: Ulid = serde_json::from_str(&s).unwrap();
assert_eq!(back, u);
}
use proptest::prelude::*;
proptest! {
#[test]
fn prop_round_trip(hi: u64, lo: u64) {
let raw = (((hi as u128) << 64) | lo as u128) >> 2;
let u = Ulid::from_raw(raw);
prop_assert_eq!(Ulid::parse(&u.encode()), Ok(u));
}
#[test]
fn prop_lexicographic_sort_matches_value_sort(
values in proptest::collection::vec((any::<u64>(), any::<u64>()), 0..50)
) {
let raws: Vec<u128> = values
.into_iter()
.map(|(hi, lo)| (((hi as u128) << 64) | lo as u128) >> 2)
.collect();
let mut ulids: Vec<Ulid> = raws.iter().copied().map(Ulid::from_raw).collect();
ulids.sort();
let encoded: Vec<String> = ulids.iter().map(|u| u.encode()).collect();
let mut sorted_encoded = encoded.clone();
sorted_encoded.sort();
prop_assert_eq!(encoded, sorted_encoded);
}
#[test]
fn prop_increasing_timestamp_yields_increasing_ulid(
ts1 in 0i64..(1i64 << 47),
delta in 1i64..1_000_000_000_i64,
) {
let a = Ulid::from_millis_with_random(ts1, 0);
let b = Ulid::from_millis_with_random(ts1 + delta, 0);
prop_assert!(a < b);
prop_assert!(a.encode() < b.encode());
}
#[test]
fn prop_timestamp_round_trips(
ts in 0i64..(1i64 << 47),
rand_hi: u32,
rand_lo: u64,
) {
let random = ((rand_hi as u128) << 64) | rand_lo as u128;
let u = Ulid::from_millis_with_random(ts, random);
prop_assert_eq!(u.timestamp_millis(), ts);
}
}
}