Skip to main content

tsoracle_core/
timestamp.rs

1//! 64-bit packed timestamp: 46 high bits for `physical_ms`, 18 low bits for `logical`.
2
3const 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        // Roundtrip: pack then unpack returns the original fields for any valid
113        // input in the 46-bit physical / 18-bit logical domain. A single failure
114        // here means the bit layout is wrong (mask, shift, or width).
115        #[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        // Order preservation: the packed u64 ordering equals lexicographic
126        // ordering on (physical_ms, logical). This is the property that lets
127        // every consumer compare Timestamps directly without unpacking.
128        #[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(&timestamp_b), pair_ordering);
139        }
140
141        // try_pack accepts exactly the values pack accepts and rejects everything
142        // else, with the right error variant. Catches drift between the two APIs.
143        #[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}