use derive_more::{Display, Error, From};
use std::fmt;
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Display, Error, Debug, Clone, PartialEq, Eq)]
pub enum BitLayoutError {
#[display("Bit allocation must sum to 64, got {actual}")]
InvalidSum { actual: u8 },
#[display("Timestamp bits must be greater than 0")]
ZeroTimestampBits,
#[display("Sequence bits must be greater than 0")]
ZeroSequenceBits,
}
impl BitLayoutError {
pub const fn as_str(&self) -> &'static str {
match self {
Self::InvalidSum { .. } => "Bit allocation must sum to 64",
Self::ZeroTimestampBits => "Timestamp bits must be greater than 0",
Self::ZeroSequenceBits => "Sequence bits must be greater than 0",
}
}
}
#[derive(Display, Error, Debug, Clone, PartialEq, Eq)]
pub enum EpochError {
#[display("Invalid year {year}, must be between 1970 and 2100")]
InvalidYear { year: u16 },
#[display("Invalid month {month}, must be between 1 and 12")]
InvalidMonth { month: u8 },
#[display("Invalid day {day} for month {month}")]
InvalidDay { day: u8, month: u8 },
}
impl EpochError {
pub const fn as_str(&self) -> &'static str {
match self {
Self::InvalidYear { .. } => "Invalid year, must be between 1970 and 2100",
Self::InvalidMonth { .. } => "Invalid month, must be between 1 and 12",
Self::InvalidDay { .. } => "Invalid day for month",
}
}
}
#[derive(Display, Error, Debug, Clone, PartialEq, Eq)]
pub enum ConfigError {
#[display("Epoch {epoch}ms is in the future (current time: {current}ms)")]
FutureEpoch { epoch: u64, current: u64 },
#[display(
"Epoch is too old: {elapsed}ms elapsed exceeds maximum {max_duration}ms (configured with {bits} timestamp bits)"
)]
EpochExceedsCapacity {
elapsed: u64,
max_duration: u64,
bits: u8,
},
}
impl ConfigError {
pub const fn as_str(&self) -> &'static str {
match self {
Self::FutureEpoch { .. } => "Epoch is in the future",
Self::EpochExceedsCapacity { .. } => "Epoch is too old for timestamp bit allocation",
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Epoch {
millis: u64,
}
impl Epoch {
pub const TWITTER: Self = Self::new(1288834974657);
pub const DISCORD: Self = Self::new(1420070400000);
pub const INSTAGRAM: Self = Self::new(1314220021721);
pub const DEFAULT: Self = Self::new(1735689600000);
pub const fn new(millis: u64) -> Self {
Self { millis }
}
pub const fn try_from_date(year: u16, month: u8, day: u8) -> Result<Self, EpochError> {
if year < 1970 || year > 2100 {
return Err(EpochError::InvalidYear { year });
}
if month < 1 || month > 12 {
return Err(EpochError::InvalidMonth { month });
}
let days_in_month = match month {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
2 => {
if (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0) {
29
} else {
28
}
}
_ => unreachable!(),
};
if day < 1 || day > days_in_month {
return Err(EpochError::InvalidDay { day, month });
}
let mut days = 0i64;
let mut y: u16 = 1970;
while y < year {
days += if (y % 4 == 0 && y % 100 != 0) || (y % 400 == 0) {
366
} else {
365
};
y += 1;
}
let mut m = 1;
while m < month {
days += match m {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
2 => {
if (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0) {
29
} else {
28
}
}
_ => unreachable!(),
} as i64;
m += 1;
}
days += (day - 1) as i64;
let millis = (days * 24 * 60 * 60 * 1000) as u64;
Ok(Self::new(millis))
}
pub const fn from_date(year: u16, month: u8, day: u8) -> Self {
match Self::try_from_date(year, month, day) {
Ok(epoch) => epoch,
Err(error) => panic!("{}", error.as_str()),
}
}
pub const fn from_seconds(secs: i64) -> Self {
Self::new((secs * 1000) as u64)
}
pub const fn as_millis(&self) -> u64 {
self.millis
}
}
impl fmt::Display for Epoch {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}ms", self.millis)
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct BitLayout {
timestamp: u8,
worker: u8,
process: u8,
sequence: u8,
}
impl BitLayout {
pub const TWITTER: Self = Self::new(42, 5, 5, 12);
pub const DISCORD: Self = Self::new(42, 5, 5, 12);
pub const DEFAULT: Self = Self::new(42, 5, 5, 12);
pub const fn new(timestamp: u8, worker: u8, process: u8, sequence: u8) -> Self {
match Self::try_new(timestamp, worker, process, sequence) {
Ok(layout) => layout,
Err(error) => panic!("{}", error.as_str()),
}
}
pub const fn try_new(
timestamp: u8,
worker: u8,
process: u8,
sequence: u8,
) -> Result<Self, BitLayoutError> {
let sum = timestamp as u16 + worker as u16 + process as u16 + sequence as u16;
if sum != 64 {
return Err(BitLayoutError::InvalidSum { actual: sum as u8 });
}
if timestamp == 0 {
return Err(BitLayoutError::ZeroTimestampBits);
}
if sequence == 0 {
return Err(BitLayoutError::ZeroSequenceBits);
}
Ok(Self {
timestamp,
worker,
process,
sequence,
})
}
pub const fn timestamp(&self) -> u8 {
self.timestamp
}
pub const fn worker(&self) -> u8 {
self.worker
}
pub const fn process(&self) -> u8 {
self.process
}
pub const fn sequence(&self) -> u8 {
self.sequence
}
pub const fn total_instances(&self) -> u64 {
(1u64 << self.worker) * (1u64 << self.process)
}
pub const fn ids_per_millisecond(&self) -> u64 {
1u64 << self.sequence
}
pub const fn timestamp_duration_ms(&self) -> u64 {
self.timestamp_max()
}
pub fn timestamp_duration_years(&self) -> f64 {
let ms = self.timestamp_max() as f64;
ms / 1000.0 / 60.0 / 60.0 / 24.0 / 365.25
}
pub const fn sequence_shift(&self) -> u8 {
0
}
pub const fn process_shift(&self) -> u8 {
self.sequence
}
pub const fn worker_shift(&self) -> u8 {
self.sequence + self.process
}
pub const fn timestamp_shift(&self) -> u8 {
self.sequence + self.process + self.worker
}
pub const fn sequence_max(&self) -> u64 {
(1u64 << self.sequence) - 1
}
pub const fn process_max(&self) -> u64 {
(1u64 << self.process) - 1
}
pub const fn worker_max(&self) -> u64 {
(1u64 << self.worker) - 1
}
pub const fn timestamp_max(&self) -> u64 {
(1u64 << self.timestamp) - 1
}
}
impl Default for BitLayout {
fn default() -> Self {
Self::DEFAULT
}
}
impl fmt::Debug for BitLayout {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"BitLayout {{ timestamp: {}, worker: {}, process: {}, sequence: {} }}\n\
Capacity: {:.1} years, {} instances, {} IDs/ms",
self.timestamp(),
self.worker(),
self.process(),
self.sequence(),
self.timestamp_duration_years(),
self.total_instances(),
self.ids_per_millisecond()
)
}
}
impl fmt::Display for BitLayout {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"BitLayout[{}t|{}w|{}p|{}s]",
self.timestamp(),
self.worker(),
self.process(),
self.sequence()
)
}
}
impl From<(u8, u8, u8, u8)> for BitLayout {
fn from((timestamp, worker, process, sequence): (u8, u8, u8, u8)) -> Self {
Self::new(timestamp, worker, process, sequence)
}
}
#[derive(Display, Error, From, Debug, Clone, PartialEq, Eq)]
pub enum ValidationError {
#[display("Timestamp {provided} exceeds maximum {maximum} (configured with {bits} bits)")]
TimestampOutOfRange {
provided: u64,
maximum: u64,
bits: u8,
},
#[display("Worker ID {provided} exceeds maximum {maximum} (configured with {bits} bits)")]
WorkerIdOutOfRange {
provided: u64,
maximum: u64,
bits: u8,
},
#[display("Process ID {provided} exceeds maximum {maximum} (configured with {bits} bits)")]
ProcessIdOutOfRange {
provided: u64,
maximum: u64,
bits: u8,
},
#[display("Sequence {provided} exceeds maximum {maximum} (configured with {bits} bits)")]
SequenceOutOfRange {
provided: u64,
maximum: u64,
bits: u8,
},
#[display("Failed to parse ID from string")]
#[from]
StringParseError(std::num::ParseIntError),
}
impl ValidationError {
pub const fn as_str(&self) -> &'static str {
match self {
Self::TimestampOutOfRange { .. } => "Timestamp exceeds maximum",
Self::WorkerIdOutOfRange { .. } => "Worker ID exceeds maximum",
Self::ProcessIdOutOfRange { .. } => "Process ID exceeds maximum",
Self::SequenceOutOfRange { .. } => "Sequence exceeds maximum",
Self::StringParseError(_) => "Failed to parse ID from string",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Config {
layout: BitLayout,
epoch: Epoch,
}
impl Config {
pub const TWITTER: Self = Self::new_unchecked(BitLayout::TWITTER, Epoch::TWITTER);
pub const DISCORD: Self = Self::new_unchecked(BitLayout::DISCORD, Epoch::DISCORD);
pub const DEFAULT: Self = Self::new_unchecked(BitLayout::DEFAULT, Epoch::DEFAULT);
pub const fn new_unchecked(layout: BitLayout, epoch: Epoch) -> Self {
Config { layout, epoch }
}
pub fn try_new(layout: BitLayout, epoch: Epoch) -> Result<Self, ConfigError> {
let current_ms = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("System time is before UNIX epoch")
.as_millis() as u64;
let epoch_ms = epoch.as_millis();
if epoch_ms > current_ms {
return Err(ConfigError::FutureEpoch {
epoch: epoch_ms,
current: current_ms,
});
}
let elapsed = current_ms - epoch_ms;
let max_duration = layout.timestamp_max();
if elapsed > max_duration {
return Err(ConfigError::EpochExceedsCapacity {
elapsed,
max_duration,
bits: layout.timestamp(),
});
}
Ok(Config { layout, epoch })
}
pub const fn layout(&self) -> BitLayout {
self.layout
}
pub const fn epoch(&self) -> Epoch {
self.epoch
}
pub(crate) fn current_timestamp_ms(&self) -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Time went backwards")
.as_millis() as u64
- self.epoch().as_millis()
}
pub fn validate_instance(
&self,
worker_id: u64,
process_id: u64,
) -> Result<(), ValidationError> {
if worker_id > self.layout().worker_max() {
return Err(ValidationError::WorkerIdOutOfRange {
provided: worker_id,
maximum: self.layout().worker_max(),
bits: self.layout().worker(),
});
}
if process_id > self.layout().process_max() {
return Err(ValidationError::ProcessIdOutOfRange {
provided: process_id,
maximum: self.layout().process_max(),
bits: self.layout().process(),
});
}
Ok(())
}
pub fn validate_components(
&self,
timestamp: u64,
worker_id: u64,
process_id: u64,
sequence: u64,
) -> Result<(), ValidationError> {
if timestamp > self.layout().timestamp_max() {
return Err(ValidationError::TimestampOutOfRange {
provided: timestamp,
maximum: self.layout().timestamp_max(),
bits: self.layout().timestamp(),
});
}
if worker_id > self.layout().worker_max() {
return Err(ValidationError::WorkerIdOutOfRange {
provided: worker_id,
maximum: self.layout().worker_max(),
bits: self.layout().worker(),
});
}
if process_id > self.layout().process_max() {
return Err(ValidationError::ProcessIdOutOfRange {
provided: process_id,
maximum: self.layout().process_max(),
bits: self.layout().process(),
});
}
if sequence > self.layout().sequence_max() {
return Err(ValidationError::SequenceOutOfRange {
provided: sequence,
maximum: self.layout().sequence_max(),
bits: self.layout().sequence(),
});
}
Ok(())
}
pub fn validate_id(&self, id: u64) -> Result<(), ValidationError> {
let timestamp = (id >> self.layout().timestamp_shift()) & self.layout().timestamp_max();
let worker_id = (id >> self.layout().worker_shift()) & self.layout().worker_max();
let process_id = (id >> self.layout().process_shift()) & self.layout().process_max();
let sequence = (id >> self.layout().sequence_shift()) & self.layout().sequence_max();
self.validate_components(timestamp, worker_id, process_id, sequence)
}
}
impl Default for Config {
fn default() -> Self {
Self::new_unchecked(BitLayout::DEFAULT, Epoch::DEFAULT)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::BitLayout;
#[test]
fn config_default() {
let config = Config::default();
let layout = config.layout();
assert_eq!(
layout.timestamp() + layout.worker() + layout.process() + layout.sequence(),
64
);
assert!(layout.timestamp() > 0);
assert!(layout.sequence() > 0);
}
#[test]
fn config_new() {
let config =
Config::new_unchecked(BitLayout::new(42, 8, 4, 10), Epoch::new(1_600_000_000_000));
assert_eq!(config.layout().timestamp(), 42);
assert_eq!(config.layout().worker(), 8);
assert_eq!(config.layout().process(), 4);
assert_eq!(config.layout().sequence(), 10);
assert_eq!(config.epoch().as_millis(), 1_600_000_000_000);
assert_eq!(config.layout().worker_max(), (1u64 << 8) - 1); assert_eq!(config.layout().process_max(), (1u64 << 4) - 1); assert_eq!(config.layout().sequence_max(), (1u64 << 10) - 1); }
#[test]
fn config_zero_process_bits() {
let config = Config::new_unchecked(
BitLayout::new(41, 10, 0, 13), Epoch::DEFAULT,
);
assert_eq!(config.layout().process(), 0);
assert_eq!(config.layout().process_max(), 0);
}
#[test]
fn validate_instance_boundaries_and_errors() {
let layout = BitLayout::new(42, 8, 4, 10); let config = Config::new_unchecked(layout, Epoch::new(1_600_000_000_000));
assert!(config.validate_instance(255, 15).is_ok());
assert!(config.validate_instance(0, 0).is_ok());
let worker_error = config.validate_instance(256, 0);
assert!(worker_error.is_err());
if let Err(ValidationError::WorkerIdOutOfRange {
provided,
maximum,
bits,
}) = worker_error
{
assert_eq!(provided, 256);
assert_eq!(maximum, 255);
assert_eq!(bits, 8);
let error_msg = format!(
"{}",
ValidationError::WorkerIdOutOfRange {
provided,
maximum,
bits,
}
);
assert!(error_msg.contains("Worker ID"));
assert!(error_msg.contains("256"));
assert!(error_msg.contains("255"));
assert!(error_msg.contains("8 bits"));
} else {
panic!("Expected WorkerIdOutOfRange error");
}
let process_error = config.validate_instance(0, 16);
assert!(process_error.is_err());
if let Err(ValidationError::ProcessIdOutOfRange {
provided,
maximum,
bits,
}) = process_error
{
assert_eq!(provided, 16);
assert_eq!(maximum, 15);
assert_eq!(bits, 4);
let error_msg = format!(
"{}",
ValidationError::ProcessIdOutOfRange {
provided,
maximum,
bits,
}
);
assert!(error_msg.contains("Process ID"));
assert!(error_msg.contains("16"));
assert!(error_msg.contains("15"));
assert!(error_msg.contains("4 bits"));
} else {
panic!("Expected ProcessIdOutOfRange error");
}
}
#[test]
fn validate_components_success() {
let layout = BitLayout::new(42, 8, 4, 10);
let config = Config::new_unchecked(layout, Epoch::new(1_600_000_000_000));
let max_timestamp = (1u64 << 42) - 1;
let max_worker = (1u64 << 8) - 1;
let max_process = (1u64 << 4) - 1;
let max_sequence = (1u64 << 10) - 1;
assert!(
config
.validate_components(max_timestamp, max_worker, max_process, max_sequence)
.is_ok()
);
assert!(config.validate_components(0, 0, 0, 0).is_ok());
assert!(config.validate_components(1000000, 50, 5, 100).is_ok());
}
#[test]
fn validate_components_errors() {
let layout = BitLayout::new(42, 8, 4, 10);
let config = Config::new_unchecked(layout, Epoch::new(1_600_000_000_000));
let timestamp_err = config.validate_components(1u64 << 42, 0, 0, 0);
assert!(matches!(
timestamp_err,
Err(ValidationError::TimestampOutOfRange { .. })
));
let worker_err = config.validate_components(0, 256, 0, 0);
assert!(matches!(
worker_err,
Err(ValidationError::WorkerIdOutOfRange { .. })
));
let process_err = config.validate_components(0, 0, 16, 0);
assert!(matches!(
process_err,
Err(ValidationError::ProcessIdOutOfRange { .. })
));
let sequence_err = config.validate_components(0, 0, 0, 1024);
assert!(matches!(
sequence_err,
Err(ValidationError::SequenceOutOfRange { .. })
));
}
#[test]
fn validate_id_from_raw_u64() {
let layout = BitLayout::new(42, 8, 4, 10);
let config = Config::new_unchecked(layout, Epoch::new(1_600_000_000_000));
let timestamp = 1000u64;
let worker_id = 50u64;
let process_id = 5u64;
let sequence = 100u64;
let valid_id = (timestamp << layout.timestamp_shift())
| (worker_id << layout.worker_shift())
| (process_id << layout.process_shift())
| (sequence << layout.sequence_shift());
assert!(config.validate_id(valid_id).is_ok());
assert!(config.validate_id(0).is_ok());
let max_timestamp = (1u64 << 42) - 1;
let max_worker = (1u64 << 8) - 1;
let max_process = (1u64 << 4) - 1;
let max_sequence = (1u64 << 10) - 1;
let max_valid_id = (max_timestamp << layout.timestamp_shift())
| (max_worker << layout.worker_shift())
| (max_process << layout.process_shift())
| (max_sequence << layout.sequence_shift());
assert!(config.validate_id(max_valid_id).is_ok());
}
#[test]
fn epoch_const_from_date() {
const EPOCH_2025: Epoch = Epoch::from_date(2025, 1, 1);
assert_eq!(EPOCH_2025.as_millis(), 1735689600000);
const EPOCH_2024: Epoch = Epoch::from_date(2024, 6, 15);
assert!(EPOCH_2024.as_millis() > 0);
const EPOCH_RESULT: Result<Epoch, EpochError> = Epoch::try_from_date(2025, 12, 31);
assert!(EPOCH_RESULT.is_ok());
const CUSTOM_CONFIG: Config =
Config::new_unchecked(BitLayout::DEFAULT, Epoch::from_date(2025, 3, 15));
assert!(CUSTOM_CONFIG.epoch().as_millis() > 0);
}
#[test]
fn epoch_try_from_date_errors() {
assert!(Epoch::try_from_date(1969, 1, 1).is_err());
assert!(Epoch::try_from_date(2101, 1, 1).is_err());
assert!(Epoch::try_from_date(2025, 0, 1).is_err());
assert!(Epoch::try_from_date(2025, 13, 1).is_err());
assert!(Epoch::try_from_date(2025, 2, 30).is_err());
assert!(Epoch::try_from_date(2025, 4, 31).is_err());
assert!(Epoch::try_from_date(2024, 2, 29).is_ok()); assert!(Epoch::try_from_date(2025, 2, 29).is_err()); }
#[test]
fn config_try_new_valid() {
let layout = BitLayout::DEFAULT;
let epoch = Epoch::new(1_600_000_000_000); let config = Config::try_new(layout, epoch);
assert!(config.is_ok());
let config = config.unwrap();
assert_eq!(config.layout(), layout);
assert_eq!(config.epoch(), epoch);
}
#[test]
fn config_try_new_future_epoch() {
let layout = BitLayout::DEFAULT;
let future_epoch = Epoch::new(9_999_999_999_999);
let result = Config::try_new(layout, future_epoch);
assert!(result.is_err());
if let Err(ConfigError::FutureEpoch { epoch, current }) = result {
assert_eq!(epoch, 9_999_999_999_999);
assert!(current < epoch);
} else {
panic!("Expected FutureEpoch error");
}
}
#[test]
fn config_try_new_epoch_exceeds_capacity() {
let layout = BitLayout::new(20, 10, 10, 24);
let current = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis() as u64;
let two_years_ago = current - (2 * 365 * 24 * 60 * 60 * 1000);
let old_epoch = Epoch::new(two_years_ago);
let result = Config::try_new(layout, old_epoch);
assert!(result.is_err());
if let Err(ConfigError::EpochExceedsCapacity {
elapsed,
max_duration,
bits,
}) = result
{
assert!(elapsed > max_duration);
assert_eq!(bits, 20);
assert_eq!(max_duration, (1u64 << 20) - 1);
} else {
panic!("Expected EpochExceedsCapacity error");
}
}
#[test]
fn config_try_new_edge_cases() {
let layout = BitLayout::DEFAULT;
let current = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis() as u64;
let now_epoch = Epoch::new(current);
assert!(Config::try_new(layout, now_epoch).is_ok());
let one_ms_ago = Epoch::new(current - 1);
assert!(Config::try_new(layout, one_ms_ago).is_ok());
}
#[test]
fn config_try_new_with_presets() {
assert!(Config::try_new(BitLayout::TWITTER, Epoch::TWITTER).is_ok());
assert!(Config::try_new(BitLayout::DISCORD, Epoch::DISCORD).is_ok());
assert!(Config::try_new(BitLayout::TWITTER, Epoch::DISCORD).is_ok());
assert!(Config::try_new(BitLayout::DISCORD, Epoch::TWITTER).is_ok());
}
}