Skip to main content

nexus_id/
snowflake_id.rs

1//! Snowflake ID newtypes with field extraction and mixing.
2//!
3//! These types wrap raw integer values produced by snowflake generators,
4//! providing methods for field extraction, hash-friendly mixing, and
5//! string encoding.
6
7use core::cmp::Ordering;
8use core::fmt;
9use core::hash::{Hash, Hasher};
10
11use crate::types::{Base36Id, Base62Id, HexId64};
12
13/// Fibonacci hashing constant (golden ratio * 2^64, truncated).
14/// Bijective permutation for uniform hash distribution.
15const GOLDEN_64: u64 = 0x9E37_79B9_7F4A_7C15;
16
17/// Multiplicative inverse of GOLDEN_64 mod 2^64.
18/// Satisfies: GOLDEN_64.wrapping_mul(GOLDEN_64_INV) == 1
19const GOLDEN_64_INV: u64 = 0xF1DE_83E1_9937_733D;
20
21/// Fibonacci hashing constant for 32-bit (golden ratio * 2^32, truncated).
22const GOLDEN_32: u32 = 0x9E37_79B9;
23
24/// Multiplicative inverse of GOLDEN_32 mod 2^32.
25const GOLDEN_32_INV: u32 = 0x144C_BC89;
26
27// =============================================================================
28// SnowflakeId64
29// =============================================================================
30
31/// 64-bit Snowflake ID with compile-time layout.
32///
33/// Wraps a u64 containing packed `[timestamp: TS][worker: WK][sequence: SQ]` fields.
34/// Provides extraction, mixing, and formatting methods.
35///
36/// # Type Parameters
37/// - `TS`: Timestamp bits
38/// - `WK`: Worker bits
39/// - `SQ`: Sequence bits
40///
41/// # Example
42///
43/// ```rust
44/// use nexus_id::{Snowflake64, SnowflakeId64};
45///
46/// let mut generator: Snowflake64<42, 6, 16> = Snowflake64::new(5);
47/// let id: SnowflakeId64<42, 6, 16> = generator.next_id(0).unwrap();
48/// assert_eq!(id.worker(), 5);
49/// assert_eq!(id.sequence(), 0);
50/// ```
51#[derive(Clone, Copy, PartialEq, Eq)]
52#[repr(transparent)]
53pub struct SnowflakeId64<const TS: u8, const WK: u8, const SQ: u8>(pub(crate) u64);
54
55impl<const TS: u8, const WK: u8, const SQ: u8> SnowflakeId64<TS, WK, SQ> {
56    const TS_SHIFT: u8 = WK + SQ;
57    const WK_SHIFT: u8 = SQ;
58    const SEQUENCE_MASK: u64 = (1u64 << SQ) - 1;
59    const WORKER_MASK: u64 = if WK == 0 { 0 } else { (1u64 << WK) - 1 };
60
61    /// Create from a raw u64 value.
62    #[inline]
63    pub const fn from_raw(raw: u64) -> Self {
64        Self(raw)
65    }
66
67    /// Raw u64 value.
68    #[inline]
69    pub const fn raw(&self) -> u64 {
70        self.0
71    }
72
73    /// Extract the timestamp field.
74    #[inline]
75    pub const fn timestamp(&self) -> u64 {
76        self.0 >> Self::TS_SHIFT
77    }
78
79    /// Extract the worker field.
80    #[inline]
81    pub const fn worker(&self) -> u64 {
82        (self.0 >> Self::WK_SHIFT) & Self::WORKER_MASK
83    }
84
85    /// Extract the sequence field.
86    #[inline]
87    pub const fn sequence(&self) -> u64 {
88        self.0 & Self::SEQUENCE_MASK
89    }
90
91    /// Unpack into (timestamp, worker, sequence).
92    #[inline]
93    pub const fn unpack(&self) -> (u64, u64, u64) {
94        (self.timestamp(), self.worker(), self.sequence())
95    }
96
97    /// Mix bits for uniform hash distribution via Fibonacci multiply.
98    ///
99    /// Produces output suitable for identity hashers (e.g., `nohash-hasher`).
100    /// The mixing is bijective and reversible via [`MixedId64::unmix()`].
101    ///
102    /// Cost: 1 multiply (~1 cycle).
103    #[inline]
104    pub const fn mixed(&self) -> MixedId64<TS, WK, SQ> {
105        MixedId64(self.0.wrapping_mul(GOLDEN_64))
106    }
107
108    /// Encode as 16-char lowercase hex.
109    #[inline]
110    pub fn to_hex(&self) -> HexId64 {
111        HexId64::encode(self.0)
112    }
113
114    /// Encode as 11-char base62.
115    #[inline]
116    pub fn to_base62(&self) -> Base62Id {
117        Base62Id::encode(self.0)
118    }
119
120    /// Encode as 13-char base36.
121    #[inline]
122    pub fn to_base36(&self) -> Base36Id {
123        Base36Id::encode(self.0)
124    }
125}
126
127impl<const TS: u8, const WK: u8, const SQ: u8> Ord for SnowflakeId64<TS, WK, SQ> {
128    #[inline]
129    fn cmp(&self, other: &Self) -> Ordering {
130        // Raw comparison preserves time ordering (timestamp in MSB)
131        self.0.cmp(&other.0)
132    }
133}
134
135impl<const TS: u8, const WK: u8, const SQ: u8> PartialOrd for SnowflakeId64<TS, WK, SQ> {
136    #[inline]
137    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
138        Some(self.cmp(other))
139    }
140}
141
142impl<const TS: u8, const WK: u8, const SQ: u8> Hash for SnowflakeId64<TS, WK, SQ> {
143    #[inline]
144    fn hash<H: Hasher>(&self, state: &mut H) {
145        state.write_u64(self.0);
146    }
147}
148
149impl<const TS: u8, const WK: u8, const SQ: u8> fmt::Debug for SnowflakeId64<TS, WK, SQ> {
150    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
151        write!(
152            f,
153            "SnowflakeId64({}, ts={}, w={}, s={})",
154            self.0,
155            self.timestamp(),
156            self.worker(),
157            self.sequence()
158        )
159    }
160}
161
162impl<const TS: u8, const WK: u8, const SQ: u8> fmt::Display for SnowflakeId64<TS, WK, SQ> {
163    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
164        write!(f, "{}", self.0)
165    }
166}
167
168// =============================================================================
169// MixedId64
170// =============================================================================
171
172/// 64-bit Snowflake ID with Fibonacci-mixed bits.
173///
174/// Created by [`SnowflakeId64::mixed()`]. The bits have been permuted for
175/// uniform distribution with identity hashers. Use [`unmix()`](Self::unmix)
176/// to recover the original `SnowflakeId64`.
177///
178/// # Hash Behavior
179///
180/// The `Hash` impl writes the mixed value directly, making this type
181/// safe for use with identity hashers (e.g., `nohash-hasher`).
182#[derive(Clone, Copy, PartialEq, Eq)]
183#[repr(transparent)]
184pub struct MixedId64<const TS: u8, const WK: u8, const SQ: u8>(pub(crate) u64);
185
186impl<const TS: u8, const WK: u8, const SQ: u8> MixedId64<TS, WK, SQ> {
187    /// Create from a raw mixed value.
188    #[inline]
189    pub const fn from_raw(raw: u64) -> Self {
190        Self(raw)
191    }
192
193    /// Raw mixed u64 value.
194    #[inline]
195    pub const fn raw(&self) -> u64 {
196        self.0
197    }
198
199    /// Recover the original Snowflake ID by reversing the Fibonacci multiply.
200    #[inline]
201    pub const fn unmix(&self) -> SnowflakeId64<TS, WK, SQ> {
202        SnowflakeId64(self.0.wrapping_mul(GOLDEN_64_INV))
203    }
204}
205
206impl<const TS: u8, const WK: u8, const SQ: u8> Hash for MixedId64<TS, WK, SQ> {
207    #[inline]
208    fn hash<H: Hasher>(&self, state: &mut H) {
209        // Already mixed — write directly for identity hashers
210        state.write_u64(self.0);
211    }
212}
213
214impl<const TS: u8, const WK: u8, const SQ: u8> fmt::Debug for MixedId64<TS, WK, SQ> {
215    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
216        write!(f, "MixedId64(0x{:016x})", self.0)
217    }
218}
219
220impl<const TS: u8, const WK: u8, const SQ: u8> fmt::Display for MixedId64<TS, WK, SQ> {
221    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
222        write!(f, "{}", self.0)
223    }
224}
225
226// =============================================================================
227// SnowflakeId32
228// =============================================================================
229
230/// 32-bit Snowflake ID with compile-time layout.
231///
232/// Same as [`SnowflakeId64`] but for 32-bit snowflake generators.
233#[derive(Clone, Copy, PartialEq, Eq)]
234#[repr(transparent)]
235pub struct SnowflakeId32<const TS: u8, const WK: u8, const SQ: u8>(pub(crate) u32);
236
237impl<const TS: u8, const WK: u8, const SQ: u8> SnowflakeId32<TS, WK, SQ> {
238    const TS_SHIFT: u8 = WK + SQ;
239    const WK_SHIFT: u8 = SQ;
240    const SEQUENCE_MASK: u32 = (1u32 << SQ) - 1;
241    const WORKER_MASK: u32 = if WK == 0 { 0 } else { (1u32 << WK) - 1 };
242
243    /// Create from a raw u32 value.
244    #[inline]
245    pub const fn from_raw(raw: u32) -> Self {
246        Self(raw)
247    }
248
249    /// Raw u32 value.
250    #[inline]
251    pub const fn raw(&self) -> u32 {
252        self.0
253    }
254
255    /// Extract the timestamp field.
256    #[inline]
257    pub const fn timestamp(&self) -> u32 {
258        self.0 >> Self::TS_SHIFT
259    }
260
261    /// Extract the worker field.
262    #[inline]
263    pub const fn worker(&self) -> u32 {
264        (self.0 >> Self::WK_SHIFT) & Self::WORKER_MASK
265    }
266
267    /// Extract the sequence field.
268    #[inline]
269    pub const fn sequence(&self) -> u32 {
270        self.0 & Self::SEQUENCE_MASK
271    }
272
273    /// Unpack into (timestamp, worker, sequence).
274    #[inline]
275    pub const fn unpack(&self) -> (u32, u32, u32) {
276        (self.timestamp(), self.worker(), self.sequence())
277    }
278
279    /// Mix bits for uniform hash distribution via Fibonacci multiply (32-bit).
280    #[inline]
281    pub const fn mixed(&self) -> MixedId32<TS, WK, SQ> {
282        MixedId32(self.0.wrapping_mul(GOLDEN_32))
283    }
284
285    /// Encode as 16-char lowercase hex (zero-padded u64).
286    #[inline]
287    pub fn to_hex(&self) -> HexId64 {
288        HexId64::encode(self.0 as u64)
289    }
290
291    /// Encode as 11-char base62 (zero-padded u64).
292    #[inline]
293    pub fn to_base62(&self) -> Base62Id {
294        Base62Id::encode(self.0 as u64)
295    }
296
297    /// Encode as 13-char base36 (zero-padded u64).
298    #[inline]
299    pub fn to_base36(&self) -> Base36Id {
300        Base36Id::encode(self.0 as u64)
301    }
302}
303
304impl<const TS: u8, const WK: u8, const SQ: u8> Ord for SnowflakeId32<TS, WK, SQ> {
305    #[inline]
306    fn cmp(&self, other: &Self) -> Ordering {
307        self.0.cmp(&other.0)
308    }
309}
310
311impl<const TS: u8, const WK: u8, const SQ: u8> PartialOrd for SnowflakeId32<TS, WK, SQ> {
312    #[inline]
313    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
314        Some(self.cmp(other))
315    }
316}
317
318impl<const TS: u8, const WK: u8, const SQ: u8> Hash for SnowflakeId32<TS, WK, SQ> {
319    #[inline]
320    fn hash<H: Hasher>(&self, state: &mut H) {
321        state.write_u32(self.0);
322    }
323}
324
325impl<const TS: u8, const WK: u8, const SQ: u8> fmt::Debug for SnowflakeId32<TS, WK, SQ> {
326    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
327        write!(
328            f,
329            "SnowflakeId32({}, ts={}, w={}, s={})",
330            self.0,
331            self.timestamp(),
332            self.worker(),
333            self.sequence()
334        )
335    }
336}
337
338impl<const TS: u8, const WK: u8, const SQ: u8> fmt::Display for SnowflakeId32<TS, WK, SQ> {
339    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
340        write!(f, "{}", self.0)
341    }
342}
343
344// =============================================================================
345// MixedId32
346// =============================================================================
347
348/// 32-bit Snowflake ID with Fibonacci-mixed bits.
349#[derive(Clone, Copy, PartialEq, Eq)]
350#[repr(transparent)]
351pub struct MixedId32<const TS: u8, const WK: u8, const SQ: u8>(pub(crate) u32);
352
353impl<const TS: u8, const WK: u8, const SQ: u8> MixedId32<TS, WK, SQ> {
354    /// Create from a raw mixed value.
355    #[inline]
356    pub const fn from_raw(raw: u32) -> Self {
357        Self(raw)
358    }
359
360    /// Raw mixed u32 value.
361    #[inline]
362    pub const fn raw(&self) -> u32 {
363        self.0
364    }
365
366    /// Recover the original Snowflake ID.
367    #[inline]
368    pub const fn unmix(&self) -> SnowflakeId32<TS, WK, SQ> {
369        SnowflakeId32(self.0.wrapping_mul(GOLDEN_32_INV))
370    }
371}
372
373impl<const TS: u8, const WK: u8, const SQ: u8> Hash for MixedId32<TS, WK, SQ> {
374    #[inline]
375    fn hash<H: Hasher>(&self, state: &mut H) {
376        state.write_u32(self.0);
377    }
378}
379
380impl<const TS: u8, const WK: u8, const SQ: u8> fmt::Debug for MixedId32<TS, WK, SQ> {
381    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
382        write!(f, "MixedId32(0x{:08x})", self.0)
383    }
384}
385
386impl<const TS: u8, const WK: u8, const SQ: u8> fmt::Display for MixedId32<TS, WK, SQ> {
387    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
388        write!(f, "{}", self.0)
389    }
390}
391
392// =============================================================================
393// From impls — transparent newtype extraction
394// =============================================================================
395
396impl<const TS: u8, const WK: u8, const SQ: u8> From<SnowflakeId64<TS, WK, SQ>> for u64 {
397    #[inline]
398    fn from(id: SnowflakeId64<TS, WK, SQ>) -> Self {
399        id.0
400    }
401}
402
403impl<const TS: u8, const WK: u8, const SQ: u8> From<MixedId64<TS, WK, SQ>> for u64 {
404    #[inline]
405    fn from(id: MixedId64<TS, WK, SQ>) -> Self {
406        id.0
407    }
408}
409
410impl<const TS: u8, const WK: u8, const SQ: u8> From<SnowflakeId32<TS, WK, SQ>> for u32 {
411    #[inline]
412    fn from(id: SnowflakeId32<TS, WK, SQ>) -> Self {
413        id.0
414    }
415}
416
417impl<const TS: u8, const WK: u8, const SQ: u8> From<MixedId32<TS, WK, SQ>> for u32 {
418    #[inline]
419    fn from(id: MixedId32<TS, WK, SQ>) -> Self {
420        id.0
421    }
422}
423
424// =============================================================================
425// Tests
426// =============================================================================
427
428#[cfg(all(test, feature = "std"))]
429mod tests {
430    use super::*;
431
432    type Id64 = SnowflakeId64<42, 6, 16>;
433    type Id32 = SnowflakeId32<20, 4, 8>;
434
435    #[test]
436    fn golden_64_inverse_correct() {
437        assert_eq!(GOLDEN_64.wrapping_mul(GOLDEN_64_INV), 1u64);
438    }
439
440    #[test]
441    fn golden_32_inverse_correct() {
442        assert_eq!(GOLDEN_32.wrapping_mul(GOLDEN_32_INV), 1u32);
443    }
444
445    #[test]
446    fn unpack_64() {
447        // ts=100, worker=5, seq=42 for <42, 6, 16>
448        let raw = (100u64 << 22) | (5u64 << 16) | 0x2A_u64;
449        let id = Id64::from_raw(raw);
450
451        assert_eq!(id.timestamp(), 100);
452        assert_eq!(id.worker(), 5);
453        assert_eq!(id.sequence(), 42);
454        assert_eq!(id.unpack(), (100, 5, 42));
455    }
456
457    #[test]
458    fn unpack_32() {
459        // ts=50, worker=7, seq=200 for <20, 4, 8>
460        let raw = (50u32 << 12) | (7u32 << 8) | 0xC8_u32;
461        let id = Id32::from_raw(raw);
462
463        assert_eq!(id.timestamp(), 50);
464        assert_eq!(id.worker(), 7);
465        assert_eq!(id.sequence(), 200);
466    }
467
468    #[test]
469    fn mix_unmix_roundtrip_64() {
470        for raw in [0u64, 1, 12345, 0xDEAD_BEEF_CAFE_BABE, u64::MAX] {
471            let id = Id64::from_raw(raw);
472            let mixed = id.mixed();
473            let recovered = mixed.unmix();
474            assert_eq!(recovered.raw(), raw);
475        }
476    }
477
478    #[test]
479    fn mix_unmix_roundtrip_32() {
480        for raw in [0u32, 1, 12345, 0xDEAD_BEEF, u32::MAX] {
481            let id = Id32::from_raw(raw);
482            let mixed = id.mixed();
483            let recovered = mixed.unmix();
484            assert_eq!(recovered.raw(), raw);
485        }
486    }
487
488    #[test]
489    fn mixed_differs_from_raw() {
490        let id = Id64::from_raw(12345);
491        let mixed = id.mixed();
492        assert_ne!(mixed.raw(), id.raw());
493    }
494
495    #[test]
496    fn ordering_preserves_time() {
497        let id1 = Id64::from_raw((100u64 << 22) | (5u64 << 16));
498        let id2 = Id64::from_raw((101u64 << 22) | (5u64 << 16));
499        let id3 = Id64::from_raw((100u64 << 22) | (5u64 << 16) | 1);
500
501        // Later timestamp > earlier timestamp
502        assert!(id2 > id1);
503        // Same timestamp, higher sequence > lower sequence
504        assert!(id3 > id1);
505    }
506
507    #[test]
508    fn to_hex_roundtrip() {
509        let id = Id64::from_raw(0xDEAD_BEEF_CAFE_BABE);
510        let hex = id.to_hex();
511        assert_eq!(hex.decode(), id.raw());
512    }
513
514    #[test]
515    fn to_base62_roundtrip() {
516        let id = Id64::from_raw(12_345_678);
517        let b62 = id.to_base62();
518        assert_eq!(b62.decode(), id.raw());
519    }
520
521    #[test]
522    fn to_base36_roundtrip() {
523        let id = Id64::from_raw(12_345_678);
524        let b36 = id.to_base36();
525        assert_eq!(b36.decode(), id.raw());
526    }
527
528    #[test]
529    fn snowflake32_to_hex_roundtrip() {
530        let id = Id32::from_raw(0xDEAD_BEEF);
531        let hex = id.to_hex();
532        assert_eq!(hex.decode(), 0xDEAD_BEEF_u64);
533    }
534
535    #[test]
536    fn snowflake32_to_base62_roundtrip() {
537        let id = Id32::from_raw(12_345_678);
538        let b62 = id.to_base62();
539        assert_eq!(b62.decode(), 12_345_678_u64);
540    }
541
542    #[test]
543    fn snowflake32_to_base36_roundtrip() {
544        let id = Id32::from_raw(12_345_678);
545        let b36 = id.to_base36();
546        assert_eq!(b36.decode(), 12_345_678_u64);
547    }
548
549    #[test]
550    fn display_shows_raw() {
551        let id = Id64::from_raw(42);
552        assert_eq!(format!("{}", id), "42");
553    }
554
555    #[test]
556    fn debug_shows_fields() {
557        let raw = (100u64 << 22) | (5u64 << 16) | 7u64;
558        let id = Id64::from_raw(raw);
559        let dbg = format!("{:?}", id);
560        assert!(dbg.contains("ts=100"));
561        assert!(dbg.contains("w=5"));
562        assert!(dbg.contains("s=7"));
563    }
564}