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;
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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81pub struct InvertedIntervalError {
82 pub lower: i128,
84 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#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
158pub struct Lower(pub i128);
159
160#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
163pub struct Upper(pub i128);
164
165#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
168pub struct Midpoint(pub i128);
169
170#[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); assert_eq!(w.0, 2_000_000_000); assert!(l < Lower(upper_ns)); }
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}