use std::fmt::Formatter;
use crate::{Error, RANDOM_BITS, RANDOM_MASK, TIMESTAMP_MAX, base32};
pub(crate) fn as_array<const N: usize>(bytes: &[u8]) -> Result<&[u8; N], Error> {
use std::cmp::Ordering;
match bytes.len().cmp(&N) {
Ordering::Equal => Ok(bytes.try_into().unwrap()),
Ordering::Less => Err(Error::TooShort),
Ordering::Greater => Err(Error::TooLong),
}
}
pub(crate) const fn from_parts(timestamp: u64, randomness: u128) -> Result<u128, Error> {
if timestamp > TIMESTAMP_MAX {
Err(Error::TimestampOutOfRange)
} else if randomness > RANDOM_MASK {
Err(Error::RandomnessOutOfRange)
} else {
let shifted_timestamp = (timestamp as u128) << RANDOM_BITS;
Ok(shifted_timestamp | randomness)
}
}
pub(crate) fn try_to_string(ulid: u128) -> Option<String> {
let mut s = String::new();
s.try_reserve_exact(26).ok()?;
let mut buffer = [0; 26];
s.push_str(base32::encode(ulid, &mut buffer));
Some(s)
}
pub(crate) fn debug_ulid(name: &str, ulid: u128, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
struct Timestamp(u64);
impl std::fmt::Debug for Timestamp {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
write!(f, "\"{ts}\"", ts = timestamp_to_string(self.0))
}
}
struct Randomness(u128);
impl std::fmt::Debug for Randomness {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
write!(f, "\"{:010X}\"", self.0)
}
}
let mut buffer = [0; 26];
let string = base32::encode(ulid, &mut buffer);
let timestamp = Timestamp((ulid >> RANDOM_BITS) as u64);
let randomness = Randomness(ulid & RANDOM_MASK);
f.debug_struct(name)
.field("string", &string)
.field("timestamp", ×tamp)
.field("randomness", &randomness)
.finish()
}
fn timestamp_to_string(millis: u64) -> String {
const DAYS_PER_YEAR: u64 = 365;
const DAYS_PER_LEAP_YEAR: u64 = DAYS_PER_YEAR + 1;
const DAYS_PER_QUAD_YEAR: u64 = 4 * DAYS_PER_YEAR + 1; const DAYS_PER_CENTURY: u64 = 25 * DAYS_PER_QUAD_YEAR - 1; const DAYS_PER_QUADRICENTENNIAL: u64 = 4 * DAYS_PER_CENTURY + 1;
const BASE: u64 = 1600;
const DAYS_BASE_TO_1970: u64 = 3 * DAYS_PER_CENTURY + 1 + 70 * DAYS_PER_YEAR + 70 / 4;
let (seconds, millis) = (millis / 1000, (millis % 1000) as u32);
let (minutes, seconds) = (seconds / 60, (seconds % 60) as u32);
let (hours, minutes) = (minutes / 60, (minutes % 60) as u32);
let (days, hours) = (hours / 24, (hours % 24) as u32);
let days = days + DAYS_BASE_TO_1970;
let (quadricentennials, days) = (days / DAYS_PER_QUADRICENTENNIAL, days % DAYS_PER_QUADRICENTENNIAL);
let (centuries, days) = (days / DAYS_PER_CENTURY, days % DAYS_PER_CENTURY);
let (quad_years, days) = (days / DAYS_PER_QUAD_YEAR, days % DAYS_PER_QUAD_YEAR);
let is_leap_year = days < DAYS_PER_LEAP_YEAR;
let (years, days) = if is_leap_year {
(0, days)
} else {
let days = days - DAYS_PER_LEAP_YEAR;
let (normal_years, days) = (days / DAYS_PER_YEAR, days % DAYS_PER_YEAR);
(normal_years + 1, days)
};
let year = BASE + quadricentennials * 400 + centuries * 100 + quad_years * 4 + years;
#[rustfmt::skip]
let days_in_month = [
31,
if is_leap_year { 29 } else { 28 },
31, 30, 31, 30, 31, 31, 30, 31, 30, 31,
];
let mut days = days;
let mut month = 0;
while days >= days_in_month[month] {
days -= days_in_month[month];
month += 1;
}
let month = month + 1;
let day = days + 1;
format!("{year:04}-{month:02}-{day:02}T{hours:02}:{minutes:02}:{seconds:02}.{millis:03}Z")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_timestamp_to_string() {
assert_eq!(timestamp_to_string(0), "1970-01-01T00:00:00.000Z");
assert_eq!(timestamp_to_string((1 << 48) - 1), "10889-08-02T05:31:50.655Z");
assert_eq!(timestamp_to_string(327_403_382_400_000), "12345-01-01T00:00:00.000Z");
assert_eq!(timestamp_to_string(1_709_164_799_999), "2024-02-28T23:59:59.999Z");
assert_eq!(timestamp_to_string(1_709_164_800_000), "2024-02-29T00:00:00.000Z");
assert_eq!(timestamp_to_string(1_740_787_199_999), "2025-02-28T23:59:59.999Z");
assert_eq!(timestamp_to_string(1_740_787_200_000), "2025-03-01T00:00:00.000Z");
assert_eq!(timestamp_to_string(1_735_689_599_000), "2024-12-31T23:59:59.000Z");
assert_eq!(timestamp_to_string(1_735_689_599_999), "2024-12-31T23:59:59.999Z");
assert_eq!(timestamp_to_string(1_735_689_600_000), "2025-01-01T00:00:00.000Z");
}
}