Skip to main content

nodedb_types/temporal/
interval.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Bitemporal interval types.
4//!
5//! Two orthogonal time dimensions:
6//!
7//! - **Valid time** — application time. Client/device-assigned. Stored in value payload.
8//! - **System time** — Origin time. Derived from WAL LSN at Raft commit. Stored in key.
9//!
10//! Both use closed-open semantics `[from, to)`. The open-upper sentinel is
11//! [`OPEN_UPPER`] (`i64::MAX`), which represents "still current / never closed".
12
13use serde::{Deserialize, Serialize};
14
15/// Sentinel value marking an interval's upper bound as open ("until further notice").
16pub const OPEN_UPPER: i64 = i64::MAX;
17
18/// A two-dimensional interval carrying both valid-time and system-time bounds.
19///
20/// All fields are milliseconds since the Unix epoch (signed). Upper bounds
21/// equal to [`OPEN_UPPER`] mean "currently open".
22#[derive(
23    Debug,
24    Clone,
25    Copy,
26    PartialEq,
27    Eq,
28    Hash,
29    Serialize,
30    Deserialize,
31    zerompk::ToMessagePack,
32    zerompk::FromMessagePack,
33)]
34pub struct BitemporalInterval {
35    pub valid_from_ms: i64,
36    pub valid_until_ms: i64,
37    pub system_from_ms: i64,
38    pub system_until_ms: i64,
39}
40
41impl BitemporalInterval {
42    /// Construct an interval that is open on both upper bounds ("current, still valid").
43    pub const fn current(valid_from_ms: i64, system_from_ms: i64) -> Self {
44        Self {
45            valid_from_ms,
46            valid_until_ms: OPEN_UPPER,
47            system_from_ms,
48            system_until_ms: OPEN_UPPER,
49        }
50    }
51
52    /// Whether the system-time upper bound is still open
53    /// (i.e., this version has not been superseded).
54    pub const fn is_system_current(&self) -> bool {
55        self.system_until_ms == OPEN_UPPER
56    }
57
58    /// Whether the valid-time upper bound is still open
59    /// (i.e., the fact is still asserted as applicable).
60    pub const fn is_valid_current(&self) -> bool {
61        self.valid_until_ms == OPEN_UPPER
62    }
63
64    /// Whether both bounds are open — the tuple is live and currently asserted.
65    pub const fn is_current(&self) -> bool {
66        self.is_system_current() && self.is_valid_current()
67    }
68
69    /// Whether `t` falls within `[valid_from_ms, valid_until_ms)`.
70    pub const fn contains_valid(&self, t: i64) -> bool {
71        t >= self.valid_from_ms && t < self.valid_until_ms
72    }
73
74    /// Whether `t` falls within `[system_from_ms, system_until_ms)`.
75    pub const fn contains_system(&self, t: i64) -> bool {
76        t >= self.system_from_ms && t < self.system_until_ms
77    }
78
79    /// Whether this interval's valid-time range overlaps `[from, to)`.
80    pub const fn overlaps_valid(&self, from: i64, to: i64) -> bool {
81        self.valid_from_ms < to && from < self.valid_until_ms
82    }
83
84    /// Whether this interval's system-time range overlaps `[from, to)`.
85    pub const fn overlaps_system(&self, from: i64, to: i64) -> bool {
86        self.system_from_ms < to && from < self.system_until_ms
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93
94    #[test]
95    fn current_is_both_open() {
96        let i = BitemporalInterval::current(100, 200);
97        assert!(i.is_current());
98        assert!(i.is_valid_current());
99        assert!(i.is_system_current());
100    }
101
102    #[test]
103    fn closed_open_contains() {
104        let i = BitemporalInterval {
105            valid_from_ms: 10,
106            valid_until_ms: 20,
107            system_from_ms: 100,
108            system_until_ms: 200,
109        };
110        assert!(i.contains_valid(10));
111        assert!(i.contains_valid(19));
112        assert!(!i.contains_valid(20));
113        assert!(!i.contains_valid(9));
114        assert!(i.contains_system(150));
115        assert!(!i.contains_system(200));
116    }
117
118    #[test]
119    fn overlap_semantics() {
120        let i = BitemporalInterval {
121            valid_from_ms: 10,
122            valid_until_ms: 20,
123            system_from_ms: 100,
124            system_until_ms: 200,
125        };
126        assert!(i.overlaps_valid(15, 25));
127        assert!(i.overlaps_valid(5, 15));
128        // Touching at the upper bound is not overlap (closed-open).
129        assert!(!i.overlaps_valid(20, 30));
130        // Touching at the lower bound IS overlap.
131        assert!(i.overlaps_valid(0, 11));
132    }
133
134    #[test]
135    fn open_upper_is_i64_max() {
136        assert_eq!(OPEN_UPPER, i64::MAX);
137        let i = BitemporalInterval::current(0, 0);
138        assert!(i.contains_valid(i64::MAX - 1));
139        assert!(!i.contains_valid(i64::MAX));
140    }
141}