use crate::certainty::Certainty;
use crate::error::BticError;
use crate::granularity::Granularity;
use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
use std::fmt;
pub const NEG_INF: i64 = i64::MIN;
pub const POS_INF: i64 = i64::MAX;
pub const SIGN_FLIP: u64 = 0x8000_0000_0000_0000;
#[derive(Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Btic {
lo: i64,
hi: i64,
meta: u64,
}
impl Btic {
pub fn new(lo: i64, hi: i64, meta: u64) -> Result<Self, BticError> {
let btic = Self { lo, hi, meta };
btic.validate()?;
Ok(btic)
}
pub fn validate(&self) -> Result<(), BticError> {
if self.lo >= self.hi {
return Err(BticError::BoundOrdering {
lo: self.lo,
hi: self.hi,
});
}
let lo_gran_code = ((self.meta >> 60) & 0xF) as u8;
let hi_gran_code = ((self.meta >> 56) & 0xF) as u8;
if lo_gran_code > 0xA {
return Err(BticError::GranularityRange(lo_gran_code));
}
if hi_gran_code > 0xA {
return Err(BticError::GranularityRange(hi_gran_code));
}
let version = ((self.meta >> 48) & 0xF) as u8;
let flags = ((self.meta >> 32) & 0xFFFF) as u16;
let reserved = (self.meta & 0xFFFF_FFFF) as u32;
if version != 0 || flags != 0 || reserved != 0 {
return Err(BticError::ReservedBits);
}
if self.lo == NEG_INF {
let lo_cert_code = ((self.meta >> 54) & 0x3) as u8;
if lo_gran_code != 0 || lo_cert_code != 0 {
return Err(BticError::SentinelMetadata);
}
}
if self.hi == POS_INF {
let hi_cert_code = ((self.meta >> 52) & 0x3) as u8;
if hi_gran_code != 0 || hi_cert_code != 0 {
return Err(BticError::SentinelMetadata);
}
}
Ok(())
}
pub fn build_meta(
lo_gran: Granularity,
hi_gran: Granularity,
lo_cert: Certainty,
hi_cert: Certainty,
) -> u64 {
((lo_gran.code() as u64) << 60)
| ((hi_gran.code() as u64) << 56)
| ((lo_cert.code() as u64) << 54)
| ((hi_cert.code() as u64) << 52)
}
pub fn lo_granularity(&self) -> Granularity {
Granularity::from_code(((self.meta >> 60) & 0xF) as u8).expect("validated on construction")
}
pub fn hi_granularity(&self) -> Granularity {
Granularity::from_code(((self.meta >> 56) & 0xF) as u8).expect("validated on construction")
}
pub fn lo_certainty(&self) -> Certainty {
Certainty::from_code(((self.meta >> 54) & 0x3) as u8).expect("validated on construction")
}
pub fn hi_certainty(&self) -> Certainty {
Certainty::from_code(((self.meta >> 52) & 0x3) as u8).expect("validated on construction")
}
pub fn lo(&self) -> i64 {
self.lo
}
pub fn hi(&self) -> i64 {
self.hi
}
pub fn meta(&self) -> u64 {
self.meta
}
pub fn duration_ms(&self) -> Option<i64> {
if self.lo == NEG_INF || self.hi == POS_INF {
None
} else {
Some(self.hi - self.lo)
}
}
pub fn is_instant(&self) -> bool {
self.hi == self.lo + 1
}
pub fn is_unbounded(&self) -> bool {
self.lo == NEG_INF || self.hi == POS_INF
}
pub fn is_finite(&self) -> bool {
!self.is_unbounded()
}
}
impl PartialOrd for Btic {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Btic {
fn cmp(&self, other: &Self) -> Ordering {
self.lo
.cmp(&other.lo)
.then(self.hi.cmp(&other.hi))
.then(self.meta.cmp(&other.meta))
}
}
impl fmt::Display for Btic {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let lo_str = if self.lo == NEG_INF {
"-inf".to_string()
} else {
format_ms_as_datetime(self.lo)
};
let hi_str = if self.hi == POS_INF {
"+inf".to_string()
} else {
format_ms_as_datetime(self.hi)
};
write!(f, "[{lo_str}, {hi_str})")?;
if self.lo != NEG_INF && self.hi != POS_INF {
let lg = self.lo_granularity();
let hg = self.hi_granularity();
if lg == hg {
write!(f, " ~{}", lg.name())?;
} else {
write!(f, " {}/{}", lg.name(), hg.name())?;
}
} else if self.lo != NEG_INF {
write!(f, " {}/", self.lo_granularity().name())?;
} else if self.hi != POS_INF {
write!(f, " /{}", self.hi_granularity().name())?;
}
let lc = self.lo_certainty();
let hc = self.hi_certainty();
if lc != Certainty::Definite || hc != Certainty::Definite {
if lc == hc {
write!(f, " [{}]", lc.name())?;
} else {
write!(f, " [{}/{}]", lc.name(), hc.name())?;
}
}
Ok(())
}
}
impl fmt::Debug for Btic {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Btic")
.field("lo", &self.lo)
.field("hi", &self.hi)
.field("meta", &format_args!("{:#018x}", self.meta))
.finish()
}
}
fn format_ms_as_datetime(ms: i64) -> String {
use chrono::{DateTime, Utc};
let secs = ms.div_euclid(1000);
let nanos = (ms.rem_euclid(1000) * 1_000_000) as u32;
match DateTime::<Utc>::from_timestamp(secs, nanos) {
Some(dt) => dt.format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string(),
None => format!("{ms}ms"),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn valid_instant_at_epoch() {
let meta = Btic::build_meta(
Granularity::Millisecond,
Granularity::Millisecond,
Certainty::Definite,
Certainty::Definite,
);
let b = Btic::new(0, 1, meta).unwrap();
assert!(b.is_instant());
assert!(b.is_finite());
assert!(!b.is_unbounded());
assert_eq!(b.duration_ms(), Some(1));
}
#[test]
fn inv1_lo_ge_hi_rejected() {
let meta = Btic::build_meta(
Granularity::Day,
Granularity::Day,
Certainty::Definite,
Certainty::Definite,
);
assert!(Btic::new(100, 100, meta).is_err());
assert!(Btic::new(200, 100, meta).is_err());
}
#[test]
fn inv5_bad_granularity_rejected() {
let bad_meta = 0xF700_0000_0000_0000u64; assert!(Btic::new(0, 1, bad_meta).is_err());
}
#[test]
fn inv6_sentinel_metadata_must_be_zero() {
let bad_meta = Btic::build_meta(
Granularity::Year,
Granularity::Month,
Certainty::Definite,
Certainty::Definite,
);
assert!(Btic::new(NEG_INF, 1000, bad_meta).is_err());
}
#[test]
fn unbounded_intervals() {
let meta_lo_zero = Btic::build_meta(
Granularity::Millisecond,
Granularity::Month,
Certainty::Definite,
Certainty::Definite,
);
let left_unbounded = Btic::new(NEG_INF, 1000, meta_lo_zero).unwrap();
assert!(left_unbounded.is_unbounded());
assert!(!left_unbounded.is_finite());
assert_eq!(left_unbounded.duration_ms(), None);
let meta_hi_zero = Btic::build_meta(
Granularity::Month,
Granularity::Millisecond,
Certainty::Definite,
Certainty::Definite,
);
let right_unbounded = Btic::new(1000, POS_INF, meta_hi_zero).unwrap();
assert!(right_unbounded.is_unbounded());
assert_eq!(right_unbounded.duration_ms(), None);
}
#[test]
fn ordering_lo_first() {
let meta = Btic::build_meta(
Granularity::Day,
Granularity::Day,
Certainty::Definite,
Certainty::Definite,
);
let a = Btic::new(100, 200, meta).unwrap();
let b = Btic::new(150, 200, meta).unwrap();
assert!(a < b);
}
#[test]
fn ordering_hi_second() {
let meta = Btic::build_meta(
Granularity::Day,
Granularity::Day,
Certainty::Definite,
Certainty::Definite,
);
let a = Btic::new(100, 200, meta).unwrap();
let b = Btic::new(100, 300, meta).unwrap();
assert!(a < b);
}
#[test]
fn ordering_meta_third() {
let meta_a = Btic::build_meta(
Granularity::Day,
Granularity::Day,
Certainty::Definite,
Certainty::Definite,
);
let meta_b = Btic::build_meta(
Granularity::Year,
Granularity::Year,
Certainty::Definite,
Certainty::Definite,
);
let a = Btic::new(100, 200, meta_a).unwrap();
let b = Btic::new(100, 200, meta_b).unwrap();
assert!(a < b); }
#[test]
fn display_finite_interval() {
let meta = Btic::build_meta(
Granularity::Year,
Granularity::Year,
Certainty::Definite,
Certainty::Definite,
);
let b = Btic::new(473_385_600_000, 504_921_600_000, meta).unwrap();
let s = b.to_string();
assert!(s.contains("1985-01-01"));
assert!(s.contains("1986-01-01"));
assert!(s.contains("~year"));
}
#[test]
fn build_meta_roundtrip() {
let meta = Btic::build_meta(
Granularity::Month,
Granularity::Day,
Certainty::Approximate,
Certainty::Uncertain,
);
let b = Btic::new(0, 1000, meta).unwrap();
assert_eq!(b.lo_granularity(), Granularity::Month);
assert_eq!(b.hi_granularity(), Granularity::Day);
assert_eq!(b.lo_certainty(), Certainty::Approximate);
assert_eq!(b.hi_certainty(), Certainty::Uncertain);
}
}