hora_id/
lib.rs

1#![cfg_attr(docsrs, feature(doc_cfg))]
2//! Time sorted unique ID generator
3//! IDs are time-sorted and 8 bytes long, which is half the length of a UUID and ULID
4//!
5//! ## Composition
6//! HoraID has 3 parts
7//! - 4 byte timestamp high
8//! - 1 byte timestamp low
9//! - 1 byte for machine ID (0-255)
10//! - 2 bytes for sequence number
11//!
12//! ## Usage
13//! Generate IDs in a distributed system
14//! ```no_run
15//! use hora_id::{HoraGenerator, HoraId};
16//!
17//! let machine_id = 1; // You'll ideally get this from environment variable or configuration
18//!  let mut generator: HoraGenerator = HoraGenerator::new(machine_id).unwrap();
19//!
20//! let id: HoraId = generator.next();
21//! println!("{}", id.to_string()); // example: '00cd01daff010002'
22//! println!("{}", id.to_u64()); // example: 57704355272392706
23//! println!("{}", id.to_datetime()); // example: 2025-03-20 00:00:00
24//! println!("{}", id.to_utc()); // example: 2025-03-20 00:00:00 UTC
25//! ```
26//!
27//! Quickly generate a new ID.
28//!
29//! ```no_run
30//! use hora_id::HoraId;
31//! let id = HoraId::rand().unwrap();
32//! ```
33
34#[cfg(feature = "chrono")]
35use chrono::{DateTime, NaiveDateTime, Utc};
36use std::time::{SystemTime, UNIX_EPOCH};
37
38/// Unix Epoch on Jan 01 2024 12:00:00 am
39const EPOCH: u64 = 1735689600000;
40
41/// Get the current epoch with base epoch starting at [EPOCH]
42///
43/// ## Fail condition
44/// If the system time is incorrect and before the [EPOCH] time
45///
46fn current_epoch() -> Result<u64, String> {
47    let mut now = SystemTime::now()
48        .duration_since(UNIX_EPOCH)
49        .unwrap()
50        .as_millis() as u64;
51    if now < EPOCH {
52        return Err("Your device time is incorrect.".to_owned());
53    }
54    now = now - EPOCH;
55    Ok(now)
56}
57
58pub(crate) struct HoraParams {
59    machine_id: u8,
60    epoch: u64,
61    sequence: u16,
62}
63
64/// ID Generator with guarantee to generate time-based unique IDs on a single machine
65///
66/// ## Usage
67/// ```no_run
68/// use hora_id::{HoraGenerator, HoraId};
69///
70/// let mut generator = HoraGenerator::new(1).unwrap();
71///
72/// // generate one ID
73/// let id: HoraId = generator.next();
74/// // generate another ID
75/// let another_id: HoraId = generator.next();
76/// ```
77pub struct HoraGenerator {
78    /// Unique Machine identifier with support for max 256 unique machines
79    machine_id: u8,
80    /// sequence number in the same epoch,
81    sequence: u16,
82    /// Last time an ID was generated
83    last_gen: u64,
84}
85
86impl HoraGenerator {
87    pub fn new(machine_id: u8) -> Result<Self, String> {
88        let epoch = current_epoch()?;
89        let epoch = rescale_epoch(epoch);
90        Ok(Self {
91            machine_id,
92            sequence: 0,
93            last_gen: epoch,
94        })
95    }
96
97    /// Generate a new [HoraId]
98    pub fn next(&mut self) -> HoraId {
99        loop {
100            let epoch = current_epoch().unwrap();
101            let scaled_epoch = rescale_epoch(epoch);
102            if scaled_epoch > self.last_gen {
103                self.sequence = 0;
104            }
105
106            // generate_id
107            self.sequence += 1;
108            let params = HoraParams {
109                machine_id: self.machine_id,
110                epoch,
111                sequence: self.sequence + 1,
112            };
113            let id = HoraId::with_params(params);
114            self.last_gen = scaled_epoch;
115            break id;
116        }
117    }
118}
119
120/// A time-sorted 8-byte (64-bit) unique identifier
121#[derive(Debug, Clone, PartialEq, Eq, Hash)]
122pub struct HoraId {
123    inner: [u8; 8],
124}
125
126impl HoraId {
127    /// Quickly generate a new [HoraId]
128    ///
129    /// ## Caution
130    /// Calling this method doesn't guarantee a unique ID for every call.
131    /// This method shall only be used when you need to generate a new id rapidly.
132    ///
133    pub fn new(machine_id: Option<u8>) -> Result<Self, String> {
134        let epoch = current_epoch()?;
135        let params = HoraParams {
136            machine_id: machine_id.unwrap_or(0),
137            epoch,
138            sequence: 0,
139        };
140        let id = Self::with_params(params);
141        Ok(id)
142    }
143
144    /// Quickly generate a new random [HoraId]
145    ///
146    /// ## More info
147    /// This method generates a random machine_id and sequence number
148    pub fn rand() -> Result<Self, String> {
149        let epoch = current_epoch()?;
150        let params = HoraParams {
151            machine_id: rand::random::<u8>(),
152            epoch,
153            sequence: rand::random::<u16>(),
154        };
155        let id = Self::with_params(params);
156        Ok(id)
157    }
158
159    /// Generate a new HoraId with custom epoch
160    ///
161    /// ## More info
162    /// This method is mainly used by the [HoraGenerator] generator to get a new [HoraId].
163    /// THe `HoraId::new` method also calls this method after getting the current epoch.
164    ///
165    fn with_params(params: HoraParams) -> Self {
166        let high = (params.epoch / 1000) as u32;
167        let low = (params.epoch % 1000) as u16;
168
169        // create a default bytes array
170        let mut tuid = [0u8; 8];
171
172        // set time high
173        let bytes = high.to_be_bytes();
174        tuid[0] = bytes[0];
175        tuid[1] = bytes[1];
176        tuid[2] = bytes[2];
177        tuid[3] = bytes[3];
178        // set time low
179        tuid[4] = rescale_low(low);
180
181        // add machine_id
182        tuid[5] = params.machine_id;
183
184        // add sequence
185        let sequence_high = ((params.sequence >> 8) & 0xFF) as u8;
186        let sequence_low = (params.sequence & 0xFF) as u8;
187
188        tuid[6] = sequence_high;
189        tuid[7] = sequence_low;
190
191        Self { inner: tuid }
192    }
193
194    /// Convert a [HoraId] to a number
195    pub fn to_u64(&self) -> u64 {
196        u64::from_be_bytes(self.inner)
197    }
198
199    /// Convert a number to [HoraId]
200    pub fn from_u64(num: u64) -> Option<Self> {
201        let d: [u8; 8] = num.to_be_bytes();
202        let id = Self { inner: d };
203        Some(id)
204    }
205
206    /// Convert a [HoraId] to a [String]
207    pub fn to_string(&self) -> String {
208        format!(
209            "{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
210            self.inner[0],
211            self.inner[1],
212            self.inner[2],
213            self.inner[3],
214            self.inner[4],
215            self.inner[5],
216            self.inner[6],
217            self.inner[7]
218        )
219    }
220
221    /// Create a [HoraId] from a string slice
222    pub fn from_str(s: &str) -> Option<Self> {
223        if s.len() != 16 {
224            return None;
225        }
226        let num = u64::from_str_radix(s, 16).ok()?;
227        let bytes: [u8; 8] = num.to_be_bytes();
228        let id = Self { inner: bytes };
229        Some(id)
230    }
231
232    /// Get the byte representation of [HoraId]
233    pub fn as_bytes(&self) -> &[u8] {
234        &self.inner
235    }
236
237    /// Retrieve a chrono [NaiveDateTime] from [HoraId]
238    #[cfg(feature = "chrono")]
239    #[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
240    pub fn to_datetime(&self) -> NaiveDateTime {
241        let mut high = [0; 4];
242        for i in 0..4 {
243            high[i] = self.inner[i];
244        }
245        let high = u32::from_be_bytes(high);
246        let low = u8::from_be_bytes([self.inner[4]]);
247        let low = upscale_low(low);
248
249        let timestamp = (high as u64 * 1000) + low as u64 + EPOCH;
250        NaiveDateTime::from_timestamp_millis(timestamp as i64).unwrap()
251    }
252
253    /// Retrieve a chrono [Utc] datetime from [HoraId]
254    #[cfg(feature = "chrono")]
255    #[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
256    pub fn to_utc(&self) -> DateTime<Utc> {
257        let timestamp = self.to_datetime();
258        DateTime::<Utc>::from_utc(timestamp, Utc)
259    }
260}
261
262fn rescale_epoch(value: u64) -> u64 {
263    let high = value / 1000;
264    let low = (value % 1000) as u16;
265    let low = (low as f32) * 0.256;
266    let low = low as u64;
267    high * 1000 + low
268}
269
270/// Convert u16 to u8 with rescaling process
271fn rescale_low(value: u16) -> u8 {
272    let new_val = (value as f32) * (256.0) / (1000.0);
273    new_val as u8
274}
275
276/// Convert a u8 to u16 with rescaling process
277#[allow(dead_code)]
278fn upscale_low(value: u8) -> u16 {
279    let new_val = (value as f32) * (1000.0) / 256.0;
280    new_val as u16
281}
282
283#[cfg(test)]
284mod tests {
285    use super::*;
286    #[cfg(feature = "chrono")]
287    use chrono::Timelike;
288
289    #[test]
290    fn it_works() {
291        let id = HoraId::new(None);
292        assert!(id.is_ok());
293    }
294
295    #[test]
296    fn random() {
297        let id1 = HoraId::rand();
298        assert!(id1.is_ok());
299        let id2 = HoraId::rand();
300        assert!(id2.is_ok());
301        assert_ne!(id1.unwrap(), id2.unwrap());
302    }
303
304    #[test]
305    fn strings() {
306        let source_id = HoraId::new(None).unwrap();
307        let s = source_id.to_string();
308        let id = HoraId::from_str(&s);
309        let derived_id = id.unwrap();
310        assert_eq!(source_id.to_string(), derived_id.to_string());
311    }
312
313    #[test]
314    fn u64s() {
315        let num = 57630818184577258;
316        let id = HoraId::from_u64(num);
317        assert!(id.is_some());
318        let id = id.unwrap();
319        assert_eq!(id.to_u64(), num);
320    }
321
322    #[test]
323    fn eq() {
324        let num = 57630818184577258;
325        let id = HoraId::from_u64(num).unwrap();
326        let id2 = HoraId::from_u64(num).unwrap();
327        assert_eq!(id, id2);
328    }
329
330    #[test]
331    fn clone() {
332        let num = 57630818184577258;
333        let id = HoraId::from_u64(num).unwrap();
334        let id2 = id.clone();
335        assert_eq!(id, id2);
336    }
337
338    #[cfg(feature = "chrono")]
339    #[test]
340    fn chrono() {
341        let id = HoraId::new(None).unwrap();
342        let time = id.to_utc();
343        let now = Utc::now();
344        assert_eq!(now.date_naive(), time.date_naive());
345        assert_eq!(now.hour(), time.hour());
346        assert_eq!(now.minute(), time.minute());
347        assert_eq!(now.second(), time.second());
348    }
349
350    #[test]
351    fn rescaling() {
352        assert_eq!(rescale_low(0), 0);
353        assert_eq!(rescale_low(1), 0);
354        assert_eq!(rescale_low(5), 1);
355        assert_eq!(rescale_low(498), 127);
356        assert_eq!(rescale_low(500), 128);
357        assert_eq!(rescale_low(995), 254);
358        assert_eq!(rescale_low(997), 255);
359        assert_eq!(rescale_low(999), 255);
360    }
361
362    #[test]
363    fn rescale() {
364        let value = upscale_low(rescale_low(500));
365        assert_eq!(value, 500);
366    }
367
368    #[test]
369    fn epoch_rescaling() {
370        // test 1
371        let value = 1672531200000;
372        assert_eq!(rescale_epoch(value), value);
373        // test 2
374        assert_eq!(rescale_epoch(1672531200003), 1672531200000);
375        // test 3
376        assert_eq!(rescale_epoch(1672531200005), 1672531200001);
377        assert_eq!(rescale_epoch(1672531200006), 1672531200001);
378        // test 4
379        assert_eq!(rescale_epoch(1672531200998), 1672531200255);
380        assert_eq!(rescale_epoch(1672531200999), 1672531200255);
381    }
382}
383
384#[cfg(test)]
385mod gen_tests {
386    use super::*;
387
388    #[cfg(feature = "chrono")]
389    #[test]
390    fn it_works() {
391        let generator = HoraGenerator::new(1);
392        assert!(generator.is_ok());
393        let mut generator = generator.unwrap();
394        generator.next();
395    }
396}