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