Skip to main content

triblespace_core/value/schemas/
time.rs

1use crate::id::ExclusiveId;
2use crate::id::Id;
3use crate::id_hex;
4use crate::macros::entity;
5use crate::metadata;
6use crate::metadata::{ConstDescribe, ConstId};
7use crate::repo::BlobStore;
8use crate::trible::Fragment;
9use crate::value::schemas::hash::Blake3;
10use crate::value::TryFromValue;
11use crate::value::TryToValue;
12use crate::value::Value;
13use crate::value::ValueSchema;
14use std::convert::Infallible;
15
16use std::convert::TryInto;
17
18use hifitime::prelude::*;
19
20/// A value schema for a TAI interval (order-preserving big-endian).
21///
22/// A TAI interval is a pair of TAI epochs stored as two 128-bit signed
23/// integers (lower, upper) in **order-preserving big-endian** byte order.
24/// Both bounds are inclusive.
25///
26/// Each i128 is XOR'd with the sign bit (mapping i128::MIN to 0, 0 to 2^127,
27/// i128::MAX to u128::MAX) then written big-endian. Byte-lexicographic order
28/// matches numeric order across the full i128 range, enabling efficient range
29/// scans on the trie.
30pub struct NsTAIInterval;
31
32impl ConstId for NsTAIInterval {
33    const ID: Id = id_hex!("2170014368272A2B1B18B86B1F1F1CB5");
34}
35
36impl ConstDescribe for NsTAIInterval {
37    fn describe<B>(blobs: &mut B) -> Result<Fragment, B::PutError>
38    where
39        B: BlobStore<Blake3>,
40    {
41        let id = Self::ID;
42        let description = blobs.put(
43            "Inclusive TAI interval encoded as two offset-big-endian i128 nanosecond bounds. Each i128 is XOR'd with i128::MIN then stored big-endian, so byte-lexicographic order matches numeric order. This enables efficient range scans on ordered indexes.\n\nSemantically identical to the legacy LE encoding — same inclusive bounds, same TAI monotonic time.",
44        )?;
45        Ok(entity! {
46            ExclusiveId::force_ref(&id) @
47                metadata::name: blobs.put("nstai_interval_be")?,
48                metadata::description: description,
49                metadata::tag: metadata::KIND_VALUE_SCHEMA,
50        })
51    }
52}
53
54const SIGN_BIT: u128 = 1u128 << 127;
55
56/// Encode i128 as order-preserving big-endian: flip sign bit, then BE.
57/// Maps i128::MIN→0, 0→2^127, i128::MAX→u128::MAX.
58fn i128_to_ordered_be(v: i128) -> [u8; 16] {
59    ((v as u128) ^ SIGN_BIT).to_be_bytes()
60}
61
62/// Decode order-preserving big-endian back to i128.
63fn i128_from_ordered_be(bytes: [u8; 16]) -> i128 {
64    (u128::from_be_bytes(bytes) ^ SIGN_BIT) as i128
65}
66
67impl ValueSchema for NsTAIInterval {
68    type ValidationError = InvertedIntervalError;
69
70    fn validate(value: Value<Self>) -> Result<Value<Self>, Self::ValidationError> {
71        let lower = i128_from_ordered_be(value.raw[0..16].try_into().unwrap());
72        let upper = i128_from_ordered_be(value.raw[16..32].try_into().unwrap());
73        if lower > upper {
74            Err(InvertedIntervalError { lower, upper })
75        } else {
76            Ok(value)
77        }
78    }
79}
80
81impl TryToValue<NsTAIInterval> for (Epoch, Epoch) {
82    type Error = InvertedIntervalError;
83    fn try_to_value(self) -> Result<Value<NsTAIInterval>, InvertedIntervalError> {
84        let lower = self.0.to_tai_duration().total_nanoseconds();
85        let upper = self.1.to_tai_duration().total_nanoseconds();
86        if lower > upper {
87            return Err(InvertedIntervalError { lower, upper });
88        }
89        let mut value = [0; 32];
90        value[0..16].copy_from_slice(&i128_to_ordered_be(lower));
91        value[16..32].copy_from_slice(&i128_to_ordered_be(upper));
92        Ok(Value::new(value))
93    }
94}
95
96impl TryFromValue<'_, NsTAIInterval> for (Epoch, Epoch) {
97    type Error = InvertedIntervalError;
98    fn try_from_value(v: &Value<NsTAIInterval>) -> Result<Self, InvertedIntervalError> {
99        let lower = i128_from_ordered_be(v.raw[0..16].try_into().unwrap());
100        let upper = i128_from_ordered_be(v.raw[16..32].try_into().unwrap());
101        if lower > upper {
102            return Err(InvertedIntervalError { lower, upper });
103        }
104        Ok((
105            Epoch::from_tai_duration(Duration::from_total_nanoseconds(lower)),
106            Epoch::from_tai_duration(Duration::from_total_nanoseconds(upper)),
107        ))
108    }
109}
110
111impl TryFromValue<'_, NsTAIInterval> for (i128, i128) {
112    type Error = InvertedIntervalError;
113    fn try_from_value(v: &Value<NsTAIInterval>) -> Result<Self, InvertedIntervalError> {
114        let lower = i128_from_ordered_be(v.raw[0..16].try_into().unwrap());
115        let upper = i128_from_ordered_be(v.raw[16..32].try_into().unwrap());
116        if lower > upper {
117            return Err(InvertedIntervalError { lower, upper });
118        }
119        Ok((lower, upper))
120    }
121}
122
123/// The lower bound of a TAI interval in nanoseconds.
124/// Use this when you want to sort or compare by interval start time.
125///
126/// ```rust,ignore
127/// find!(t: Lower, pattern!(&space, [{ entity @ attr: ?t }]))
128///     .max_by_key(|t| *t)  // latest start time
129/// ```
130#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
131pub struct Lower(pub i128);
132
133/// The upper bound of a TAI interval in nanoseconds.
134/// Use this when you want to sort or compare by interval end time.
135#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
136pub struct Upper(pub i128);
137
138/// The midpoint of a TAI interval in nanoseconds.
139/// Use this when you want to sort or compare by interval center.
140#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
141pub struct Midpoint(pub i128);
142
143/// The width of a TAI interval in nanoseconds.
144/// Use this when you want to sort or compare by interval duration.
145#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
146pub struct Width(pub i128);
147
148impl TryFromValue<'_, NsTAIInterval> for Lower {
149    type Error = Infallible;
150    fn try_from_value(v: &Value<NsTAIInterval>) -> Result<Self, Infallible> {
151        Ok(Lower(i128_from_ordered_be(v.raw[0..16].try_into().unwrap())))
152    }
153}
154
155impl TryFromValue<'_, NsTAIInterval> for Upper {
156    type Error = Infallible;
157    fn try_from_value(v: &Value<NsTAIInterval>) -> Result<Self, Infallible> {
158        Ok(Upper(i128_from_ordered_be(v.raw[16..32].try_into().unwrap())))
159    }
160}
161
162impl TryFromValue<'_, NsTAIInterval> for Midpoint {
163    type Error = InvertedIntervalError;
164    fn try_from_value(v: &Value<NsTAIInterval>) -> Result<Self, InvertedIntervalError> {
165        let lower = i128_from_ordered_be(v.raw[0..16].try_into().unwrap());
166        let upper = i128_from_ordered_be(v.raw[16..32].try_into().unwrap());
167        if lower > upper {
168            return Err(InvertedIntervalError { lower, upper });
169        }
170        Ok(Midpoint(lower + (upper - lower) / 2))
171    }
172}
173
174impl TryFromValue<'_, NsTAIInterval> for Width {
175    type Error = InvertedIntervalError;
176    fn try_from_value(v: &Value<NsTAIInterval>) -> Result<Self, InvertedIntervalError> {
177        let lower = i128_from_ordered_be(v.raw[0..16].try_into().unwrap());
178        let upper = i128_from_ordered_be(v.raw[16..32].try_into().unwrap());
179        if lower > upper {
180            return Err(InvertedIntervalError { lower, upper });
181        }
182        Ok(Width(upper - lower))
183    }
184}
185
186/// The lower bound exceeds the upper bound.
187#[derive(Debug, Clone, Copy, PartialEq, Eq)]
188pub struct InvertedIntervalError {
189    /// The lower bound that was greater than `upper`.
190    pub lower: i128,
191    /// The upper bound that was less than `lower`.
192    pub upper: i128,
193}
194
195impl std::fmt::Display for InvertedIntervalError {
196    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
197        write!(f, "inverted interval: lower {} > upper {}", self.lower, self.upper)
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204
205    #[test]
206    fn hifitime_conversion() {
207        let epoch = Epoch::from_tai_duration(Duration::from_total_nanoseconds(0));
208        let time_in: (Epoch, Epoch) = (epoch, epoch);
209        let interval: Value<NsTAIInterval> = time_in.try_to_value().unwrap();
210        let time_out: (Epoch, Epoch) = interval.try_from_value().unwrap();
211
212        assert_eq!(time_in, time_out);
213    }
214
215    #[test]
216    fn projection_types() {
217        let lower_ns: i128 = 1_000_000_000;
218        let upper_ns: i128 = 3_000_000_000;
219        let lower = Epoch::from_tai_duration(Duration::from_total_nanoseconds(lower_ns));
220        let upper = Epoch::from_tai_duration(Duration::from_total_nanoseconds(upper_ns));
221        let interval: Value<NsTAIInterval> = (lower, upper).try_to_value().unwrap();
222
223        let l: Lower = interval.from_value();
224        let u: Upper = interval.from_value();
225        let m: Midpoint = interval.try_from_value().unwrap();
226        let w: Width = interval.try_from_value().unwrap();
227
228        assert_eq!(l.0, lower_ns);
229        assert_eq!(u.0, upper_ns);
230        assert_eq!(m.0, 2_000_000_000); // midpoint
231        assert_eq!(w.0, 2_000_000_000); // width
232        assert!(l < Lower(upper_ns)); // Ord works
233    }
234
235    #[test]
236    fn try_to_value_rejects_inverted() {
237        let lower = Epoch::from_tai_duration(Duration::from_total_nanoseconds(2_000_000_000));
238        let upper = Epoch::from_tai_duration(Duration::from_total_nanoseconds(1_000_000_000));
239        let result: Result<Value<NsTAIInterval>, _> = (lower, upper).try_to_value();
240        assert!(result.is_err());
241    }
242
243    #[test]
244    fn validate_accepts_equal() {
245        let t = Epoch::from_tai_duration(Duration::from_total_nanoseconds(1_000_000_000));
246        let interval: Value<NsTAIInterval> = (t, t).try_to_value().unwrap();
247        assert!(NsTAIInterval::validate(interval).is_ok());
248    }
249
250    #[test]
251    fn nanosecond_conversion() {
252        let lower_ns: i128 = 1_000_000_000;
253        let upper_ns: i128 = 2_000_000_000;
254        let lower = Epoch::from_tai_duration(Duration::from_total_nanoseconds(lower_ns));
255        let upper = Epoch::from_tai_duration(Duration::from_total_nanoseconds(upper_ns));
256        let interval: Value<NsTAIInterval> = (lower, upper).try_to_value().unwrap();
257
258        let (out_lower, out_upper): (i128, i128) = interval.try_from_value().unwrap();
259        assert_eq!(out_lower, lower_ns);
260        assert_eq!(out_upper, upper_ns);
261    }
262
263    #[test]
264    fn byte_order_matches_numeric_order() {
265        // Order-preserving BE: byte order = i128 numeric order.
266        let times = [i128::MIN, -1_000_000_000, -1, 0, 1, 1_000_000_000, i128::MAX];
267        for pair in times.windows(2) {
268            let a = i128_to_ordered_be(pair[0]);
269            let b = i128_to_ordered_be(pair[1]);
270            assert!(a < b, "{} should sort before {} in bytes", pair[0], pair[1]);
271        }
272    }
273
274    #[test]
275    fn roundtrip_edge_cases() {
276        for v in [i128::MIN, -1, 0, 1, i128::MAX] {
277            assert_eq!(i128_from_ordered_be(i128_to_ordered_be(v)), v);
278        }
279    }
280}