use crate::epoch::EpochType;
use crate::error::TiltflakeError;
use crate::id::TiltflakeId;
use chrono::{DateTime, Utc};
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Debug)]
pub struct Tiltflake {
pub machine_id: u16,
pub custom_epoch: DateTime<Utc>,
}
impl Tiltflake {
pub(crate) fn new(machine_id: u16, epoch: &EpochType) -> Self {
let datetime = epoch.base_datetime();
Self {
machine_id: machine_id & 0x3FF, custom_epoch: datetime,
}
}
pub fn generate_from_unix_millis(
&self,
millis: u64,
sequence: u16,
) -> Result<TiltflakeId, TiltflakeError> {
let delta = i64::try_from(millis).unwrap() - self.custom_epoch.timestamp_millis();
if delta < 0 {
return Err(TiltflakeError::TimestampBeforeEpoch);
}
if delta as u64 > (1 << 41) - 1 {
return Err(TiltflakeError::TimestampTooLarge);
}
let timestamp = delta as u64;
let sequence = u64::from(sequence) & 0xFFF;
Ok(TiltflakeId(
(timestamp << 22) | ((u64::from(self.machine_id)) << 12) | sequence,
))
}
pub fn generate_from_system_time(
&self,
time: SystemTime,
sequence: u16,
) -> Result<TiltflakeId, TiltflakeError> {
let millis = time
.duration_since(UNIX_EPOCH)
.map_err(|_| TiltflakeError::TimestampBeforeEpoch)?
.as_millis() as u64;
self.generate_from_unix_millis(millis, sequence)
}
pub fn generate_from_datetime(
&self,
datetime: DateTime<Utc>,
sequence: u16,
) -> Result<TiltflakeId, TiltflakeError> {
self.generate_from_unix_millis(
u64::try_from(datetime.timestamp_millis()).unwrap(),
sequence,
)
}
pub fn generate_from_rfc3339(
&self,
rfc3339: &str,
sequence: u16,
) -> Result<TiltflakeId, TiltflakeError> {
let datetime = rfc3339.parse::<DateTime<Utc>>()?;
self.generate_from_datetime(datetime, sequence)
}
pub fn parse(&self, id: TiltflakeId) -> (DateTime<Utc>, u16, u16) {
let raw = id.0;
let timestamp = raw >> 22;
let machine_id = ((raw >> 12) & 0x3FF) as u16;
let sequence = (raw & 0xFFF) as u16;
let datetime = self.custom_epoch
+ chrono::Duration::milliseconds(
timestamp
.try_into()
.map_err(|_| TiltflakeError::TimestampTooLarge)
.unwrap(),
);
(datetime, machine_id, sequence)
}
pub fn parse_id(id: u64, epoch: EpochType) -> (DateTime<Utc>, u16, u16) {
let timestamp = id >> 22; let machine_id = ((id >> 12) & 0x3FF) as u16; let sequence = (id & 0xFFF) as u16;
let base_datetime = epoch.base_datetime();
let datetime = base_datetime
+ chrono::Duration::milliseconds(
timestamp
.try_into()
.map_err(|_| TiltflakeError::TimestampTooLarge)
.unwrap(),
);
(datetime, machine_id, sequence)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::epoch::EpochType;
use chrono::{TimeZone, Utc};
use std::time::{Duration, SystemTime};
#[test]
fn generate_with_unix_epoch() {
let unix_epoch = EpochType::Unix;
let snowflake = Tiltflake::new(1, &unix_epoch);
let id = snowflake.generate_from_unix_millis(0, 0).unwrap(); let (datetime, machine_id, sequence) = snowflake.parse(id);
assert_eq!(datetime, Utc.with_ymd_and_hms(1970, 1, 1, 0, 0, 0).unwrap());
assert_eq!(machine_id, 1);
assert_eq!(sequence, 0);
}
#[test]
fn generate_with_discord_epoch() {
let discord_epoch = EpochType::Discord;
let snowflake = Tiltflake::new(2, &discord_epoch);
let id = snowflake
.generate_from_unix_millis(1420070400000, 0)
.unwrap(); let (datetime, machine_id, sequence) = snowflake.parse(id);
assert_eq!(datetime, Utc.with_ymd_and_hms(2015, 1, 1, 0, 0, 0).unwrap());
assert_eq!(machine_id, 2);
assert_eq!(sequence, 0);
}
#[test]
fn new_initializes_with_masked_machine_id() {
let custom_epoch = Utc.with_ymd_and_hms(2020, 1, 1, 0, 0, 0).unwrap();
let snowflake = Tiltflake::new(0xFFFF, &EpochType::Custom(custom_epoch));
assert_eq!(snowflake.machine_id, 0x3FF);
assert_eq!(snowflake.custom_epoch, custom_epoch);
}
#[test]
fn generate_from_unix_millis_creates_valid_id() {
let custom_epoch = Utc.with_ymd_and_hms(2020, 1, 1, 0, 0, 0).unwrap();
let snowflake = Tiltflake::new(1, &EpochType::Custom(custom_epoch));
let id = snowflake
.generate_from_unix_millis(1577836800000, 0)
.unwrap(); let (datetime, machine_id, sequence) = snowflake.parse(id);
assert_eq!(datetime, custom_epoch);
assert_eq!(machine_id, 1);
assert_eq!(sequence, 0);
}
#[test]
fn generate_from_system_time_creates_valid_id() {
let custom_epoch = Utc.with_ymd_and_hms(2020, 1, 1, 0, 0, 0).unwrap();
let snowflake = Tiltflake::new(2, &EpochType::Custom(custom_epoch));
let system_time = SystemTime::UNIX_EPOCH + Duration::from_millis(1577836800000); let id = snowflake.generate_from_system_time(system_time, 5).unwrap();
let (datetime, machine_id, sequence) = snowflake.parse(id);
assert_eq!(datetime, custom_epoch);
assert_eq!(machine_id, 2);
assert_eq!(sequence, 5);
}
#[test]
fn generate_from_rfc3339_parses_valid_id() {
let custom_epoch = Utc.with_ymd_and_hms(2020, 1, 1, 0, 0, 0).unwrap();
let snowflake = Tiltflake::new(3, &EpochType::Custom(custom_epoch));
let id = snowflake
.generate_from_rfc3339("2020-01-01T00:00:00Z", 10)
.unwrap();
let (datetime, machine_id, sequence) = snowflake.parse(id);
assert_eq!(datetime, custom_epoch);
assert_eq!(machine_id, 3);
assert_eq!(sequence, 10);
}
#[test]
fn parse_correctly_extracts_components() {
let custom_epoch = Utc.with_ymd_and_hms(2020, 1, 1, 0, 0, 0).unwrap();
let snowflake = Tiltflake::new(4, &EpochType::Custom(custom_epoch));
let id = snowflake
.generate_from_unix_millis(1577836800000, 15)
.unwrap(); let (datetime, machine_id, sequence) = snowflake.parse(id);
assert_eq!(datetime, custom_epoch);
assert_eq!(machine_id, 4);
assert_eq!(sequence, 15);
}
#[test]
fn generate_from_unix_millis_handles_max_range() {
let custom_epoch = Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap();
let snowflake = Tiltflake::new(5, &EpochType::Custom(custom_epoch));
let max_millis = custom_epoch.timestamp_millis() + (1i64 << 41) - 1;
let id = snowflake
.generate_from_unix_millis(max_millis as u64, 0)
.unwrap();
let (datetime, machine_id, sequence) = snowflake.parse(id);
assert_eq!(datetime.timestamp_millis(), max_millis);
assert_eq!(machine_id, 5);
assert_eq!(sequence, 0);
}
}