tsoracle_core/
timestamp.rs1const LOGICAL_BITS: u32 = 18;
28const LOGICAL_MASK: u64 = (1 << LOGICAL_BITS) - 1;
29pub const LOGICAL_MAX: u32 = (1 << LOGICAL_BITS) - 1;
30pub const PHYSICAL_MS_MAX: u64 = (1 << (64 - LOGICAL_BITS)) - 1;
31
32#[derive(Copy, Clone, Debug, thiserror::Error, PartialEq, Eq)]
33pub enum TimestampError {
34 #[error("physical_ms {physical_ms} exceeds 46-bit maximum {max}")]
35 PhysicalMsOutOfRange { physical_ms: u64, max: u64 },
36 #[error("logical {logical} exceeds 18-bit maximum {max}")]
37 LogicalOutOfRange { logical: u32, max: u32 },
38}
39
40#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
41#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
42pub struct Timestamp(pub u64);
43
44impl Timestamp {
45 pub const fn pack(physical_ms: u64, logical: u32) -> Self {
46 assert!(
47 physical_ms <= PHYSICAL_MS_MAX,
48 "physical_ms exceeds 46-bit timestamp field"
49 );
50 assert!(
51 logical <= LOGICAL_MAX,
52 "logical exceeds 18-bit timestamp field"
53 );
54 Timestamp((physical_ms << LOGICAL_BITS) | (logical as u64))
55 }
56
57 pub const fn try_pack(physical_ms: u64, logical: u32) -> Result<Self, TimestampError> {
58 if physical_ms > PHYSICAL_MS_MAX {
59 return Err(TimestampError::PhysicalMsOutOfRange {
60 physical_ms,
61 max: PHYSICAL_MS_MAX,
62 });
63 }
64 if logical > LOGICAL_MAX {
65 return Err(TimestampError::LogicalOutOfRange {
66 logical,
67 max: LOGICAL_MAX,
68 });
69 }
70 Ok(Timestamp((physical_ms << LOGICAL_BITS) | (logical as u64)))
71 }
72
73 pub const fn physical_ms(self) -> u64 {
74 self.0 >> LOGICAL_BITS
75 }
76 pub const fn logical(self) -> u32 {
77 (self.0 & LOGICAL_MASK) as u32
78 }
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84
85 #[test]
86 fn pack_unpack_roundtrip() {
87 let ts = Timestamp::pack(1_700_000_000_000, 12345);
88 assert_eq!(ts.physical_ms(), 1_700_000_000_000);
89 assert_eq!(ts.logical(), 12345);
90 }
91
92 #[test]
93 fn pack_zero() {
94 let ts = Timestamp::pack(0, 0);
95 assert_eq!(ts.0, 0);
96 assert_eq!(ts.physical_ms(), 0);
97 assert_eq!(ts.logical(), 0);
98 }
99
100 #[test]
101 fn pack_max_logical() {
102 let ts = Timestamp::pack(1000, LOGICAL_MAX);
103 assert_eq!(ts.physical_ms(), 1000);
104 assert_eq!(ts.logical(), LOGICAL_MAX);
105 }
106
107 #[test]
108 fn try_pack_rejects_out_of_range_physical_ms() {
109 assert!(matches!(
110 Timestamp::try_pack(PHYSICAL_MS_MAX + 1, 0),
111 Err(TimestampError::PhysicalMsOutOfRange { .. })
112 ));
113 }
114
115 #[test]
116 fn try_pack_rejects_out_of_range_logical() {
117 assert!(matches!(
118 Timestamp::try_pack(0, LOGICAL_MAX + 1),
119 Err(TimestampError::LogicalOutOfRange { .. })
120 ));
121 }
122
123 #[test]
124 #[should_panic(expected = "physical_ms exceeds 46-bit timestamp field")]
125 fn pack_panics_on_out_of_range_physical_ms() {
126 let _ = Timestamp::pack(PHYSICAL_MS_MAX + 1, 0);
127 }
128
129 #[test]
130 #[should_panic(expected = "logical exceeds 18-bit timestamp field")]
131 fn pack_panics_on_out_of_range_logical() {
132 let _ = Timestamp::pack(0, LOGICAL_MAX + 1);
133 }
134
135 #[test]
136 fn ordering_follows_packed_value() {
137 let earliest = Timestamp::pack(1000, 5);
138 let middle = Timestamp::pack(1000, 6);
139 let latest = Timestamp::pack(1001, 0);
140 assert!(earliest < middle);
141 assert!(middle < latest);
142 assert!(earliest < latest);
143 }
144
145 use proptest::prelude::*;
146
147 proptest! {
148 #[test]
152 fn pack_unpack_roundtrip_property(
153 physical_ms in 0u64..=PHYSICAL_MS_MAX,
154 logical in 0u32..=LOGICAL_MAX,
155 ) {
156 let packed = Timestamp::pack(physical_ms, logical);
157 prop_assert_eq!(packed.physical_ms(), physical_ms);
158 prop_assert_eq!(packed.logical(), logical);
159 }
160
161 #[test]
165 fn ordering_matches_lexicographic_pair(
166 physical_a in 0u64..=PHYSICAL_MS_MAX,
167 logical_a in 0u32..=LOGICAL_MAX,
168 physical_b in 0u64..=PHYSICAL_MS_MAX,
169 logical_b in 0u32..=LOGICAL_MAX,
170 ) {
171 let timestamp_a = Timestamp::pack(physical_a, logical_a);
172 let timestamp_b = Timestamp::pack(physical_b, logical_b);
173 let pair_ordering = (physical_a, logical_a).cmp(&(physical_b, logical_b));
174 prop_assert_eq!(timestamp_a.cmp(×tamp_b), pair_ordering);
175 }
176
177 #[test]
180 fn try_pack_accepts_iff_in_range(
181 physical_ms in 0u64..=u64::MAX >> 10,
182 logical in 0u32..=u32::MAX >> 10,
183 ) {
184 let result = Timestamp::try_pack(physical_ms, logical);
185 if physical_ms > PHYSICAL_MS_MAX {
186 let rejected_physical =
187 matches!(result, Err(TimestampError::PhysicalMsOutOfRange { .. }));
188 prop_assert!(rejected_physical);
189 } else if logical > LOGICAL_MAX {
190 let rejected_logical =
191 matches!(result, Err(TimestampError::LogicalOutOfRange { .. }));
192 prop_assert!(rejected_logical);
193 } else {
194 let timestamp = result.unwrap();
195 prop_assert_eq!(timestamp.physical_ms(), physical_ms);
196 prop_assert_eq!(timestamp.logical(), logical);
197 }
198 }
199 }
200}