arms/core/
id.rs

1//! # Id
2//!
3//! Unique identifier for placed points.
4//!
5//! Format: 128 bits = [timestamp_ms:48][counter:16][random:64]
6//! - Timestamp provides natural temporal ordering
7//! - Counter prevents collisions within same millisecond
8//! - Random portion adds uniqueness
9//! - Sortable by time when compared
10//! - No external dependencies (not UUID, just bytes)
11
12use std::sync::atomic::{AtomicU64, Ordering};
13use std::time::{SystemTime, UNIX_EPOCH};
14
15/// Global counter for uniqueness within same millisecond
16static COUNTER: AtomicU64 = AtomicU64::new(0);
17
18/// Unique identifier for a placed point
19///
20/// 128 bits, timestamp-prefixed for natural time ordering.
21#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)]
22pub struct Id([u8; 16]);
23
24impl Id {
25    /// Generate a new Id for the current moment
26    ///
27    /// Uses current timestamp + counter + random bytes for uniqueness.
28    pub fn now() -> Self {
29        let timestamp = SystemTime::now()
30            .duration_since(UNIX_EPOCH)
31            .unwrap()
32            .as_millis() as u64;
33
34        // Atomically increment counter for uniqueness
35        let counter = COUNTER.fetch_add(1, Ordering::Relaxed);
36
37        let mut bytes = [0u8; 16];
38
39        // First 6 bytes: timestamp (48 bits)
40        bytes[0] = (timestamp >> 40) as u8;
41        bytes[1] = (timestamp >> 32) as u8;
42        bytes[2] = (timestamp >> 24) as u8;
43        bytes[3] = (timestamp >> 16) as u8;
44        bytes[4] = (timestamp >> 8) as u8;
45        bytes[5] = timestamp as u8;
46
47        // Next 2 bytes: counter (16 bits) - ensures uniqueness within millisecond
48        bytes[6] = (counter >> 8) as u8;
49        bytes[7] = counter as u8;
50
51        // Remaining 8 bytes: pseudo-random based on timestamp and counter
52        let random_seed = timestamp
53            .wrapping_mul(6364136223846793005)
54            .wrapping_add(counter);
55        bytes[8] = (random_seed >> 56) as u8;
56        bytes[9] = (random_seed >> 48) as u8;
57        bytes[10] = (random_seed >> 40) as u8;
58        bytes[11] = (random_seed >> 32) as u8;
59        bytes[12] = (random_seed >> 24) as u8;
60        bytes[13] = (random_seed >> 16) as u8;
61        bytes[14] = (random_seed >> 8) as u8;
62        bytes[15] = random_seed as u8;
63
64        Self(bytes)
65    }
66
67    /// Create an Id from raw bytes
68    pub fn from_bytes(bytes: [u8; 16]) -> Self {
69        Self(bytes)
70    }
71
72    /// Get the raw bytes
73    pub fn as_bytes(&self) -> &[u8; 16] {
74        &self.0
75    }
76
77    /// Extract the timestamp component (milliseconds since epoch)
78    pub fn timestamp_ms(&self) -> u64 {
79        ((self.0[0] as u64) << 40)
80            | ((self.0[1] as u64) << 32)
81            | ((self.0[2] as u64) << 24)
82            | ((self.0[3] as u64) << 16)
83            | ((self.0[4] as u64) << 8)
84            | (self.0[5] as u64)
85    }
86
87    /// Create a nil/zero Id (useful for testing)
88    pub fn nil() -> Self {
89        Self([0u8; 16])
90    }
91
92    /// Check if this is a nil Id
93    pub fn is_nil(&self) -> bool {
94        self.0 == [0u8; 16]
95    }
96}
97
98impl std::fmt::Display for Id {
99    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
100        // Display as hex string
101        for byte in &self.0 {
102            write!(f, "{:02x}", byte)?;
103        }
104        Ok(())
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111    use std::thread;
112    use std::time::Duration;
113
114    #[test]
115    fn test_id_creation() {
116        let id = Id::now();
117        assert!(!id.is_nil());
118    }
119
120    #[test]
121    fn test_id_timestamp() {
122        let before = SystemTime::now()
123            .duration_since(UNIX_EPOCH)
124            .unwrap()
125            .as_millis() as u64;
126
127        let id = Id::now();
128
129        let after = SystemTime::now()
130            .duration_since(UNIX_EPOCH)
131            .unwrap()
132            .as_millis() as u64;
133
134        let ts = id.timestamp_ms();
135        assert!(ts >= before);
136        assert!(ts <= after);
137    }
138
139    #[test]
140    fn test_id_ordering() {
141        let id1 = Id::now();
142        thread::sleep(Duration::from_millis(2));
143        let id2 = Id::now();
144
145        // id2 should be greater (later timestamp)
146        assert!(id2 > id1);
147    }
148
149    #[test]
150    fn test_id_from_bytes() {
151        let bytes = [1u8, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
152        let id = Id::from_bytes(bytes);
153        assert_eq!(id.as_bytes(), &bytes);
154    }
155
156    #[test]
157    fn test_id_nil() {
158        let nil = Id::nil();
159        assert!(nil.is_nil());
160        assert_eq!(nil.timestamp_ms(), 0);
161    }
162
163    #[test]
164    fn test_id_display() {
165        let id = Id::from_bytes([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]);
166        let display = format!("{}", id);
167        assert_eq!(display, "000102030405060708090a0b0c0d0e0f");
168    }
169}