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