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.
21/// A TAI interval is a pair of TAI epochs.
22/// The interval is stored as two 128-bit signed integers, the lower and upper bounds.
23/// The lower bound is stored in the first 16 bytes and the upper bound is stored in the last 16 bytes.
24/// Both the lower and upper bounds are stored in little-endian byte order.
25/// Both the lower and upper bounds are inclusive. That is, the interval contains all TAI epochs between the lower and upper bounds.
26pub struct NsTAIInterval;
27
28impl ConstId for NsTAIInterval {
29    const ID: Id = id_hex!("675A2E885B12FCBC0EEC01E6AEDD8AA8");
30}
31
32impl ConstDescribe for NsTAIInterval {
33    fn describe<B>(blobs: &mut B) -> Result<Fragment, B::PutError>
34    where
35        B: BlobStore<Blake3>,
36    {
37        let id = Self::ID;
38        let description = blobs.put(
39            "Inclusive TAI interval encoded as two little-endian i128 nanosecond bounds. TAI is monotonic and does not include leap seconds, making it ideal for precise ordering.\n\nUse for time windows, scheduling, or event ranges where monotonic time matters. If you need civil time, time zones, or calendar semantics, store a separate representation alongside this interval.\n\nIntervals are inclusive on both ends. If you need half-open intervals or offsets, consider RangeU128 with your own epoch mapping.",
40        )?;
41        let tribles = entity! {
42            ExclusiveId::force_ref(&id) @
43                metadata::name: blobs.put("nstai_interval")?,
44                metadata::description: description,
45                metadata::tag: metadata::KIND_VALUE_SCHEMA,
46        };
47
48        #[cfg(feature = "wasm")]
49        let tribles = {
50            let mut tribles = tribles;
51            tribles += entity! { ExclusiveId::force_ref(&id) @
52                metadata::value_formatter: blobs.put(wasm_formatter::NSTAI_INTERVAL_WASM)?,
53            };
54            tribles
55        };
56        Ok(tribles)
57    }
58}
59
60#[cfg(feature = "wasm")]
61mod wasm_formatter {
62    use core::fmt::Write;
63
64    use triblespace_core_macros::value_formatter;
65
66    #[value_formatter]
67    pub(crate) fn nstai_interval(raw: &[u8; 32], out: &mut impl Write) -> Result<(), u32> {
68        let mut buf = [0u8; 16];
69        buf.copy_from_slice(&raw[0..16]);
70        let lower = i128::from_le_bytes(buf);
71        buf.copy_from_slice(&raw[16..32]);
72        let upper = i128::from_le_bytes(buf);
73
74        write!(out, "{lower}..={upper}").map_err(|_| 1u32)?;
75        Ok(())
76    }
77}
78
79/// The lower bound exceeds the upper bound.
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81pub struct InvertedIntervalError {
82    /// The lower bound that was greater than `upper`.
83    pub lower: i128,
84    /// The upper bound that was less than `lower`.
85    pub upper: i128,
86}
87
88impl std::fmt::Display for InvertedIntervalError {
89    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90        write!(f, "inverted interval: lower {} > upper {}", self.lower, self.upper)
91    }
92}
93
94impl ValueSchema for NsTAIInterval {
95    type ValidationError = InvertedIntervalError;
96
97    fn validate(value: Value<Self>) -> Result<Value<Self>, Self::ValidationError> {
98        let lower = i128::from_le_bytes(value.raw[0..16].try_into().unwrap());
99        let upper = i128::from_le_bytes(value.raw[16..32].try_into().unwrap());
100        if lower > upper {
101            Err(InvertedIntervalError { lower, upper })
102        } else {
103            Ok(value)
104        }
105    }
106}
107
108impl TryToValue<NsTAIInterval> for (Epoch, Epoch) {
109    type Error = InvertedIntervalError;
110    fn try_to_value(self) -> Result<Value<NsTAIInterval>, InvertedIntervalError> {
111        let lower = self.0.to_tai_duration().total_nanoseconds();
112        let upper = self.1.to_tai_duration().total_nanoseconds();
113        if lower > upper {
114            return Err(InvertedIntervalError { lower, upper });
115        }
116        let mut value = [0; 32];
117        value[0..16].copy_from_slice(&lower.to_le_bytes());
118        value[16..32].copy_from_slice(&upper.to_le_bytes());
119        Ok(Value::new(value))
120    }
121}
122
123impl TryFromValue<'_, NsTAIInterval> for (Epoch, Epoch) {
124    type Error = InvertedIntervalError;
125    fn try_from_value(v: &Value<NsTAIInterval>) -> Result<Self, InvertedIntervalError> {
126        let lower = i128::from_le_bytes(v.raw[0..16].try_into().unwrap());
127        let upper = i128::from_le_bytes(v.raw[16..32].try_into().unwrap());
128        if lower > upper {
129            return Err(InvertedIntervalError { lower, upper });
130        }
131        Ok((
132            Epoch::from_tai_duration(Duration::from_total_nanoseconds(lower)),
133            Epoch::from_tai_duration(Duration::from_total_nanoseconds(upper)),
134        ))
135    }
136}
137
138impl TryFromValue<'_, NsTAIInterval> for (i128, i128) {
139    type Error = InvertedIntervalError;
140    fn try_from_value(v: &Value<NsTAIInterval>) -> Result<Self, InvertedIntervalError> {
141        let lower = i128::from_le_bytes(v.raw[0..16].try_into().unwrap());
142        let upper = i128::from_le_bytes(v.raw[16..32].try_into().unwrap());
143        if lower > upper {
144            return Err(InvertedIntervalError { lower, upper });
145        }
146        Ok((lower, upper))
147    }
148}
149
150/// The lower bound of a TAI interval in nanoseconds.
151/// Use this when you want to sort or compare by interval start time.
152///
153/// ```rust,ignore
154/// find!(t: Lower, pattern!(&space, [{ entity @ attr: ?t }]))
155///     .max_by_key(|t| *t)  // latest start time
156/// ```
157#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
158pub struct Lower(pub i128);
159
160/// The upper bound of a TAI interval in nanoseconds.
161/// Use this when you want to sort or compare by interval end time.
162#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
163pub struct Upper(pub i128);
164
165/// The midpoint of a TAI interval in nanoseconds.
166/// Use this when you want to sort or compare by interval center.
167#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
168pub struct Midpoint(pub i128);
169
170/// The width of a TAI interval in nanoseconds.
171/// Use this when you want to sort or compare by interval duration.
172#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
173pub struct Width(pub i128);
174
175impl TryFromValue<'_, NsTAIInterval> for Lower {
176    type Error = Infallible;
177    fn try_from_value(v: &Value<NsTAIInterval>) -> Result<Self, Infallible> {
178        let lower = i128::from_le_bytes(v.raw[0..16].try_into().unwrap());
179        Ok(Lower(lower))
180    }
181}
182
183impl TryFromValue<'_, NsTAIInterval> for Upper {
184    type Error = Infallible;
185    fn try_from_value(v: &Value<NsTAIInterval>) -> Result<Self, Infallible> {
186        let upper = i128::from_le_bytes(v.raw[16..32].try_into().unwrap());
187        Ok(Upper(upper))
188    }
189}
190
191impl TryFromValue<'_, NsTAIInterval> for Midpoint {
192    type Error = InvertedIntervalError;
193    fn try_from_value(v: &Value<NsTAIInterval>) -> Result<Self, InvertedIntervalError> {
194        let lower = i128::from_le_bytes(v.raw[0..16].try_into().unwrap());
195        let upper = i128::from_le_bytes(v.raw[16..32].try_into().unwrap());
196        if lower > upper {
197            return Err(InvertedIntervalError { lower, upper });
198        }
199        Ok(Midpoint(lower + (upper - lower) / 2))
200    }
201}
202
203impl TryFromValue<'_, NsTAIInterval> for Width {
204    type Error = InvertedIntervalError;
205    fn try_from_value(v: &Value<NsTAIInterval>) -> Result<Self, InvertedIntervalError> {
206        let lower = i128::from_le_bytes(v.raw[0..16].try_into().unwrap());
207        let upper = i128::from_le_bytes(v.raw[16..32].try_into().unwrap());
208        if lower > upper {
209            return Err(InvertedIntervalError { lower, upper });
210        }
211        Ok(Width(upper - lower))
212    }
213}
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218
219    #[test]
220    fn hifitime_conversion() {
221        let epoch = Epoch::from_tai_duration(Duration::from_total_nanoseconds(0));
222        let time_in: (Epoch, Epoch) = (epoch, epoch);
223        let interval: Value<NsTAIInterval> = time_in.try_to_value().unwrap();
224        let time_out: (Epoch, Epoch) = interval.try_from_value().unwrap();
225
226        assert_eq!(time_in, time_out);
227    }
228
229    #[test]
230    fn projection_types() {
231        let lower_ns: i128 = 1_000_000_000;
232        let upper_ns: i128 = 3_000_000_000;
233        let lower = Epoch::from_tai_duration(Duration::from_total_nanoseconds(lower_ns));
234        let upper = Epoch::from_tai_duration(Duration::from_total_nanoseconds(upper_ns));
235        let interval: Value<NsTAIInterval> = (lower, upper).try_to_value().unwrap();
236
237        let l: Lower = interval.from_value();
238        let u: Upper = interval.from_value();
239        let m: Midpoint = interval.try_from_value().unwrap();
240        let w: Width = interval.try_from_value().unwrap();
241
242        assert_eq!(l.0, lower_ns);
243        assert_eq!(u.0, upper_ns);
244        assert_eq!(m.0, 2_000_000_000); // midpoint
245        assert_eq!(w.0, 2_000_000_000); // width
246        assert!(l < Lower(upper_ns)); // Ord works
247    }
248
249    #[test]
250    fn try_to_value_rejects_inverted() {
251        let lower = Epoch::from_tai_duration(Duration::from_total_nanoseconds(2_000_000_000));
252        let upper = Epoch::from_tai_duration(Duration::from_total_nanoseconds(1_000_000_000));
253        let result: Result<Value<NsTAIInterval>, _> = (lower, upper).try_to_value();
254        assert!(result.is_err());
255    }
256
257    #[test]
258    fn validate_accepts_equal() {
259        let t = Epoch::from_tai_duration(Duration::from_total_nanoseconds(1_000_000_000));
260        let interval: Value<NsTAIInterval> = (t, t).try_to_value().unwrap();
261        assert!(NsTAIInterval::validate(interval).is_ok());
262    }
263
264    #[test]
265    fn nanosecond_conversion() {
266        let lower_ns: i128 = 1_000_000_000;
267        let upper_ns: i128 = 2_000_000_000;
268        let lower = Epoch::from_tai_duration(Duration::from_total_nanoseconds(lower_ns));
269        let upper = Epoch::from_tai_duration(Duration::from_total_nanoseconds(upper_ns));
270        let interval: Value<NsTAIInterval> = (lower, upper).try_to_value().unwrap();
271
272        let (out_lower, out_upper): (i128, i128) = interval.try_from_value().unwrap();
273        assert_eq!(out_lower, lower_ns);
274        assert_eq!(out_upper, upper_ns);
275    }
276}