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::ToValue;
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
79impl ValueSchema for NsTAIInterval {
80    type ValidationError = Infallible;
81}
82
83impl ToValue<NsTAIInterval> for (Epoch, Epoch) {
84    fn to_value(self) -> Value<NsTAIInterval> {
85        let lower = self.0.to_tai_duration().total_nanoseconds();
86        let upper = self.1.to_tai_duration().total_nanoseconds();
87
88        let mut value = [0; 32];
89        value[0..16].copy_from_slice(&lower.to_le_bytes());
90        value[16..32].copy_from_slice(&upper.to_le_bytes());
91        Value::new(value)
92    }
93}
94
95impl TryFromValue<'_, NsTAIInterval> for (Epoch, Epoch) {
96    type Error = Infallible;
97    fn try_from_value(v: &Value<NsTAIInterval>) -> Result<Self, Infallible> {
98        let (lower, upper): (i128, i128) = v.from_value();
99        let lower = Epoch::from_tai_duration(Duration::from_total_nanoseconds(lower));
100        let upper = Epoch::from_tai_duration(Duration::from_total_nanoseconds(upper));
101        Ok((lower, upper))
102    }
103}
104
105impl TryFromValue<'_, NsTAIInterval> for (i128, i128) {
106    type Error = Infallible;
107    fn try_from_value(v: &Value<NsTAIInterval>) -> Result<Self, Infallible> {
108        let lower = i128::from_le_bytes(v.raw[0..16].try_into().unwrap());
109        let upper = i128::from_le_bytes(v.raw[16..32].try_into().unwrap());
110        Ok((lower, upper))
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117
118    #[test]
119    fn hifitime_conversion() {
120        let epoch = Epoch::from_tai_duration(Duration::from_total_nanoseconds(0));
121        let time_in: (Epoch, Epoch) = (epoch, epoch);
122        let interval: Value<NsTAIInterval> = time_in.to_value();
123        let time_out: (Epoch, Epoch) = interval.from_value();
124
125        assert_eq!(time_in, time_out);
126    }
127
128    #[test]
129    fn nanosecond_conversion() {
130        let lower_ns: i128 = 1_000_000_000;
131        let upper_ns: i128 = 2_000_000_000;
132        let lower = Epoch::from_tai_duration(Duration::from_total_nanoseconds(lower_ns));
133        let upper = Epoch::from_tai_duration(Duration::from_total_nanoseconds(upper_ns));
134        let interval: Value<NsTAIInterval> = (lower, upper).to_value();
135
136        let (out_lower, out_upper): (i128, i128) = interval.from_value();
137        assert_eq!(out_lower, lower_ns);
138        assert_eq!(out_upper, upper_ns);
139    }
140}