1use crate::certainty::Certainty;
2use crate::error::BticError;
3use crate::granularity::Granularity;
4use serde::{Deserialize, Serialize};
5use std::cmp::Ordering;
6use std::fmt;
7
8pub const NEG_INF: i64 = i64::MIN;
10pub const POS_INF: i64 = i64::MAX;
12pub const SIGN_FLIP: u64 = 0x8000_0000_0000_0000;
14
15#[derive(Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
24pub struct Btic {
25 lo: i64,
26 hi: i64,
27 meta: u64,
28}
29
30impl Btic {
31 pub fn new(lo: i64, hi: i64, meta: u64) -> Result<Self, BticError> {
33 let btic = Self { lo, hi, meta };
34 btic.validate()?;
35 Ok(btic)
36 }
37
38 pub fn validate(&self) -> Result<(), BticError> {
40 if self.lo >= self.hi {
42 return Err(BticError::BoundOrdering {
43 lo: self.lo,
44 hi: self.hi,
45 });
46 }
47
48 let lo_gran_code = ((self.meta >> 60) & 0xF) as u8;
50 let hi_gran_code = ((self.meta >> 56) & 0xF) as u8;
51 if lo_gran_code > 0xA {
52 return Err(BticError::GranularityRange(lo_gran_code));
53 }
54 if hi_gran_code > 0xA {
55 return Err(BticError::GranularityRange(hi_gran_code));
56 }
57
58 let version = ((self.meta >> 48) & 0xF) as u8;
60 let flags = ((self.meta >> 32) & 0xFFFF) as u16;
61 let reserved = (self.meta & 0xFFFF_FFFF) as u32;
62 if version != 0 || flags != 0 || reserved != 0 {
63 return Err(BticError::ReservedBits);
64 }
65
66 if self.lo == NEG_INF {
68 let lo_cert_code = ((self.meta >> 54) & 0x3) as u8;
69 if lo_gran_code != 0 || lo_cert_code != 0 {
70 return Err(BticError::SentinelMetadata);
71 }
72 }
73 if self.hi == POS_INF {
74 let hi_cert_code = ((self.meta >> 52) & 0x3) as u8;
75 if hi_gran_code != 0 || hi_cert_code != 0 {
76 return Err(BticError::SentinelMetadata);
77 }
78 }
79
80 Ok(())
84 }
85
86 pub fn build_meta(
92 lo_gran: Granularity,
93 hi_gran: Granularity,
94 lo_cert: Certainty,
95 hi_cert: Certainty,
96 ) -> u64 {
97 ((lo_gran.code() as u64) << 60)
98 | ((hi_gran.code() as u64) << 56)
99 | ((lo_cert.code() as u64) << 54)
100 | ((hi_cert.code() as u64) << 52)
101 }
103
104 pub fn lo_granularity(&self) -> Granularity {
110 Granularity::from_code(((self.meta >> 60) & 0xF) as u8).expect("validated on construction")
111 }
112
113 pub fn hi_granularity(&self) -> Granularity {
115 Granularity::from_code(((self.meta >> 56) & 0xF) as u8).expect("validated on construction")
116 }
117
118 pub fn lo_certainty(&self) -> Certainty {
120 Certainty::from_code(((self.meta >> 54) & 0x3) as u8).expect("validated on construction")
121 }
122
123 pub fn hi_certainty(&self) -> Certainty {
125 Certainty::from_code(((self.meta >> 52) & 0x3) as u8).expect("validated on construction")
126 }
127
128 pub fn lo(&self) -> i64 {
134 self.lo
135 }
136
137 pub fn hi(&self) -> i64 {
139 self.hi
140 }
141
142 pub fn meta(&self) -> u64 {
144 self.meta
145 }
146
147 pub fn duration_ms(&self) -> Option<i64> {
149 if self.lo == NEG_INF || self.hi == POS_INF {
150 None
151 } else {
152 Some(self.hi - self.lo)
153 }
154 }
155
156 pub fn is_instant(&self) -> bool {
158 self.hi == self.lo + 1
159 }
160
161 pub fn is_unbounded(&self) -> bool {
163 self.lo == NEG_INF || self.hi == POS_INF
164 }
165
166 pub fn is_finite(&self) -> bool {
168 !self.is_unbounded()
169 }
170}
171
172impl PartialOrd for Btic {
177 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
178 Some(self.cmp(other))
179 }
180}
181
182impl Ord for Btic {
183 fn cmp(&self, other: &Self) -> Ordering {
184 self.lo
185 .cmp(&other.lo)
186 .then(self.hi.cmp(&other.hi))
187 .then(self.meta.cmp(&other.meta))
188 }
189}
190
191impl fmt::Display for Btic {
196 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
197 let lo_str = if self.lo == NEG_INF {
198 "-inf".to_string()
199 } else {
200 format_ms_as_datetime(self.lo)
201 };
202 let hi_str = if self.hi == POS_INF {
203 "+inf".to_string()
204 } else {
205 format_ms_as_datetime(self.hi)
206 };
207
208 write!(f, "[{lo_str}, {hi_str})")?;
209
210 if self.lo != NEG_INF && self.hi != POS_INF {
212 let lg = self.lo_granularity();
213 let hg = self.hi_granularity();
214 if lg == hg {
215 write!(f, " ~{}", lg.name())?;
216 } else {
217 write!(f, " {}/{}", lg.name(), hg.name())?;
218 }
219 } else if self.lo != NEG_INF {
220 write!(f, " {}/", self.lo_granularity().name())?;
221 } else if self.hi != POS_INF {
222 write!(f, " /{}", self.hi_granularity().name())?;
223 }
224
225 let lc = self.lo_certainty();
227 let hc = self.hi_certainty();
228 if lc != Certainty::Definite || hc != Certainty::Definite {
229 if lc == hc {
230 write!(f, " [{}]", lc.name())?;
231 } else {
232 write!(f, " [{}/{}]", lc.name(), hc.name())?;
233 }
234 }
235
236 Ok(())
237 }
238}
239
240impl fmt::Debug for Btic {
241 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
242 f.debug_struct("Btic")
243 .field("lo", &self.lo)
244 .field("hi", &self.hi)
245 .field("meta", &format_args!("{:#018x}", self.meta))
246 .finish()
247 }
248}
249
250fn format_ms_as_datetime(ms: i64) -> String {
256 use chrono::{DateTime, Utc};
257
258 let secs = ms.div_euclid(1000);
259 let nanos = (ms.rem_euclid(1000) * 1_000_000) as u32;
260 match DateTime::<Utc>::from_timestamp(secs, nanos) {
261 Some(dt) => dt.format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string(),
262 None => format!("{ms}ms"),
263 }
264}
265
266#[cfg(test)]
267mod tests {
268 use super::*;
269
270 #[test]
271 fn valid_instant_at_epoch() {
272 let meta = Btic::build_meta(
273 Granularity::Millisecond,
274 Granularity::Millisecond,
275 Certainty::Definite,
276 Certainty::Definite,
277 );
278 let b = Btic::new(0, 1, meta).unwrap();
279 assert!(b.is_instant());
280 assert!(b.is_finite());
281 assert!(!b.is_unbounded());
282 assert_eq!(b.duration_ms(), Some(1));
283 }
284
285 #[test]
286 fn inv1_lo_ge_hi_rejected() {
287 let meta = Btic::build_meta(
288 Granularity::Day,
289 Granularity::Day,
290 Certainty::Definite,
291 Certainty::Definite,
292 );
293 assert!(Btic::new(100, 100, meta).is_err());
294 assert!(Btic::new(200, 100, meta).is_err());
295 }
296
297 #[test]
298 fn inv5_bad_granularity_rejected() {
299 let bad_meta = 0xF700_0000_0000_0000u64; assert!(Btic::new(0, 1, bad_meta).is_err());
302 }
303
304 #[test]
305 fn inv6_sentinel_metadata_must_be_zero() {
306 let bad_meta = Btic::build_meta(
308 Granularity::Year,
309 Granularity::Month,
310 Certainty::Definite,
311 Certainty::Definite,
312 );
313 assert!(Btic::new(NEG_INF, 1000, bad_meta).is_err());
314 }
315
316 #[test]
317 fn unbounded_intervals() {
318 let meta_lo_zero = Btic::build_meta(
319 Granularity::Millisecond,
320 Granularity::Month,
321 Certainty::Definite,
322 Certainty::Definite,
323 );
324 let left_unbounded = Btic::new(NEG_INF, 1000, meta_lo_zero).unwrap();
325 assert!(left_unbounded.is_unbounded());
326 assert!(!left_unbounded.is_finite());
327 assert_eq!(left_unbounded.duration_ms(), None);
328
329 let meta_hi_zero = Btic::build_meta(
330 Granularity::Month,
331 Granularity::Millisecond,
332 Certainty::Definite,
333 Certainty::Definite,
334 );
335 let right_unbounded = Btic::new(1000, POS_INF, meta_hi_zero).unwrap();
336 assert!(right_unbounded.is_unbounded());
337 assert_eq!(right_unbounded.duration_ms(), None);
338 }
339
340 #[test]
341 fn ordering_lo_first() {
342 let meta = Btic::build_meta(
343 Granularity::Day,
344 Granularity::Day,
345 Certainty::Definite,
346 Certainty::Definite,
347 );
348 let a = Btic::new(100, 200, meta).unwrap();
349 let b = Btic::new(150, 200, meta).unwrap();
350 assert!(a < b);
351 }
352
353 #[test]
354 fn ordering_hi_second() {
355 let meta = Btic::build_meta(
356 Granularity::Day,
357 Granularity::Day,
358 Certainty::Definite,
359 Certainty::Definite,
360 );
361 let a = Btic::new(100, 200, meta).unwrap();
362 let b = Btic::new(100, 300, meta).unwrap();
363 assert!(a < b);
364 }
365
366 #[test]
367 fn ordering_meta_third() {
368 let meta_a = Btic::build_meta(
369 Granularity::Day,
370 Granularity::Day,
371 Certainty::Definite,
372 Certainty::Definite,
373 );
374 let meta_b = Btic::build_meta(
375 Granularity::Year,
376 Granularity::Year,
377 Certainty::Definite,
378 Certainty::Definite,
379 );
380 let a = Btic::new(100, 200, meta_a).unwrap();
381 let b = Btic::new(100, 200, meta_b).unwrap();
382 assert!(a < b); }
384
385 #[test]
386 fn display_finite_interval() {
387 let meta = Btic::build_meta(
388 Granularity::Year,
389 Granularity::Year,
390 Certainty::Definite,
391 Certainty::Definite,
392 );
393 let b = Btic::new(473_385_600_000, 504_921_600_000, meta).unwrap();
395 let s = b.to_string();
396 assert!(s.contains("1985-01-01"));
397 assert!(s.contains("1986-01-01"));
398 assert!(s.contains("~year"));
399 }
400
401 #[test]
402 fn build_meta_roundtrip() {
403 let meta = Btic::build_meta(
404 Granularity::Month,
405 Granularity::Day,
406 Certainty::Approximate,
407 Certainty::Uncertain,
408 );
409 let b = Btic::new(0, 1000, meta).unwrap();
410 assert_eq!(b.lo_granularity(), Granularity::Month);
411 assert_eq!(b.hi_granularity(), Granularity::Day);
412 assert_eq!(b.lo_certainty(), Certainty::Approximate);
413 assert_eq!(b.hi_certainty(), Certainty::Uncertain);
414 }
415}