triblespace_core/value/schemas/
time.rs1use 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
20pub 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
56fn i128_to_ordered_be(v: i128) -> [u8; 16] {
59 ((v as u128) ^ SIGN_BIT).to_be_bytes()
60}
61
62fn 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#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
131pub struct Lower(pub i128);
132
133#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
136pub struct Upper(pub i128);
137
138#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
141pub struct Midpoint(pub i128);
142
143#[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
188pub struct InvertedIntervalError {
189 pub lower: i128,
191 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); assert_eq!(w.0, 2_000_000_000); assert!(l < Lower(upper_ns)); }
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 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}