Skip to main content

mempill_types/
time.rs

1//! Temporal types: bi-temporal model support.
2
3/// Transaction-time stamp: machine-assigned, monotone, reliable. Engine-assigned; host cannot supply this as truth.
4#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
5#[serde(transparent)]
6pub struct TransactionTime(pub chrono::DateTime<chrono::Utc>);
7
8impl TransactionTime {
9    /// Stamp the current UTC instant.
10    pub fn now() -> Self {
11        Self(chrono::Utc::now())
12    }
13}
14
15/// Valid-time interval — fallible and host-extracted (confidence-tagged).
16/// When start/end are None, belief ordering falls back to TransactionTime.
17#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
18pub struct ValidTime {
19    /// Start of the valid-time window (`None` = unknown / open-ended).
20    pub start: Option<chrono::DateTime<chrono::Utc>>,
21    /// End of the valid-time window (`None` = unknown / open-ended).
22    pub end: Option<chrono::DateTime<chrono::Utc>>,
23    /// Confidence in the valid-time extraction itself (mirrors Confidence.valid_time_confidence).
24    pub valid_time_confidence: f32,
25}
26
27impl ValidTime {
28    /// Returns true iff both start and end are None (unknown valid-time window).
29    pub fn is_unknown(&self) -> bool {
30        self.start.is_none() && self.end.is_none()
31    }
32
33    /// Returns true iff the interval is temporally incoherent: start > end,
34    /// or start > tx_time (valid-time boundary must predate or equal the time it was learned).
35    pub fn is_temporally_incoherent(&self, tx_time: &TransactionTime) -> bool {
36        if let (Some(s), Some(e)) = (self.start, self.end) {
37            if s > e {
38                return true;
39            }
40        }
41        if let Some(s) = self.start {
42            if s > tx_time.0 {
43                return true;
44            }
45        }
46        false
47    }
48}
49
50#[cfg(test)]
51mod tests {
52    use super::*;
53    use chrono::Utc;
54
55    #[test]
56    fn valid_time_unknown_when_both_none() {
57        let vt = ValidTime { start: None, end: None, valid_time_confidence: 0.0 };
58        assert!(vt.is_unknown());
59    }
60
61    #[test]
62    fn valid_time_not_unknown_when_start_set() {
63        let vt = ValidTime { start: Some(Utc::now()), end: None, valid_time_confidence: 0.8 };
64        assert!(!vt.is_unknown());
65    }
66
67    #[test]
68    fn incoherent_when_start_after_end() {
69        let now = Utc::now();
70        let tx = TransactionTime(now);
71        let vt = ValidTime {
72            start: Some(now + chrono::Duration::hours(1)),
73            end: Some(now),
74            valid_time_confidence: 1.0,
75        };
76        assert!(vt.is_temporally_incoherent(&tx));
77    }
78
79    #[test]
80    fn incoherent_when_valid_start_after_tx_time() {
81        let now = Utc::now();
82        let tx = TransactionTime(now);
83        let vt = ValidTime {
84            start: Some(now + chrono::Duration::hours(1)),
85            end: None,
86            valid_time_confidence: 1.0,
87        };
88        assert!(vt.is_temporally_incoherent(&tx));
89    }
90
91    #[test]
92    fn coherent_normal_interval() {
93        let now = Utc::now();
94        let tx = TransactionTime(now);
95        let vt = ValidTime {
96            start: Some(now - chrono::Duration::days(1)),
97            end: Some(now),
98            valid_time_confidence: 0.9,
99        };
100        assert!(!vt.is_temporally_incoherent(&tx));
101    }
102
103    #[test]
104    fn transaction_time_ordering() {
105        let t1 = TransactionTime(Utc::now());
106        let t2 = TransactionTime(Utc::now() + chrono::Duration::seconds(1));
107        assert!(t1 < t2);
108    }
109
110    #[test]
111    fn transaction_time_serializes_as_bare_iso8601_string() {
112        use chrono::TimeZone;
113        // Fixed timestamp: 2024-01-15T12:00:00Z
114        let dt = Utc.with_ymd_and_hms(2024, 1, 15, 12, 0, 0).unwrap();
115        let tt = TransactionTime(dt);
116        let json = serde_json::to_string(&tt).unwrap();
117        // chrono serializes DateTime<Utc> as RFC3339/ISO-8601 string
118        assert!(json.starts_with('"'), "expected a bare JSON string, got: {json}");
119        assert!(json.contains("2024-01-15"), "expected date in serialized form, got: {json}");
120        let back: TransactionTime = serde_json::from_str(&json).unwrap();
121        assert_eq!(tt, back);
122    }
123
124    #[test]
125    fn valid_time_round_trip_serde() {
126        let vt = ValidTime { start: Some(Utc::now()), end: None, valid_time_confidence: 0.7 };
127        let json = serde_json::to_string(&vt).unwrap();
128        let back: ValidTime = serde_json::from_str(&json).unwrap();
129        assert_eq!(vt.start, back.start);
130        assert_eq!(vt.end, back.end);
131    }
132}