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