Skip to main content

lunaris_core/
bitemporal.rs

1//! Bi-temporal validity stamp per blueprint ยง3.3.
2//!
3//!   valid : when the fact is true in the world
4//!   sys   : when the system observed/recorded the fact
5//!
6//! Half-open intervals: [from, to). `to = None` means "still valid".
7
8use serde::{Deserialize, Serialize};
9
10use crate::hlc::{Hlc, HlcClock};
11
12#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
13pub struct BiTemporal {
14    pub valid: (Hlc, Option<Hlc>),
15    pub sys: (Hlc, Option<Hlc>),
16}
17
18impl BiTemporal {
19    /// New record stamped with `clock.tick()` for both valid_from and sys_from.
20    pub fn now(clock: &HlcClock) -> Self {
21        let t = clock.tick();
22        Self { valid: (t, None), sys: (t, None) }
23    }
24
25    pub fn at(valid_from: Hlc, sys_from: Hlc) -> Self {
26        Self { valid: (valid_from, None), sys: (sys_from, None) }
27    }
28
29    pub fn invalidate_valid(&mut self, t: Hlc) {
30        self.valid.1 = Some(t);
31    }
32    pub fn invalidate_sys(&mut self, t: Hlc) {
33        self.sys.1 = Some(t);
34    }
35
36    /// Was the fact true in the world at `t`?
37    pub fn valid_at(&self, t: Hlc) -> bool {
38        self.valid.0 <= t && self.valid.1.is_none_or(|end| t < end)
39    }
40    /// Was the fact recorded in the system at `t`?
41    pub fn system_at(&self, t: Hlc) -> bool {
42        self.sys.0 <= t && self.sys.1.is_none_or(|end| t < end)
43    }
44    /// AS_OF query: both axes hold at `t`.
45    pub fn overlaps(&self, t: Hlc) -> bool {
46        self.valid_at(t) && self.system_at(t)
47    }
48}