Skip to main content

relmath/
temporal.rs

1//! Deterministic temporal interval and valid-time foundations.
2//!
3//! This first G5 slice introduces half-open intervals `[start, end)` over
4//! endpoint domains with total ordering. Intervals use deterministic
5//! lexicographic `(start, end)` ordering when stored in ordered collections,
6//! which makes later coalescing behavior easy to audit.
7//!
8//! Half-open semantics mean:
9//!
10//! - `start` is included;
11//! - `end` is excluded;
12//! - directly adjacent intervals such as `[1, 3)` and `[3, 5)` do not
13//!   overlap, but they can be coalesced;
14//! - invalid intervals with `start >= end` are rejected explicitly.
15//!
16//! On top of the interval substrate, this module also provides a first
17//! deterministic valid-time relation foundation:
18//!
19//! - `ValidTimeSupport<T>` stores the canonical interval support for one fact;
20//! - `ValidTimeRelation<F, T>` maps exact facts to deterministic valid-time
21//!   support and materializes exact support back into the published unary,
22//!   binary, and n-ary relation surface.
23//! - `ValidTimeRelation::snapshot_at` and `ValidTimeRelation::restrict_to`
24//!   provide the first narrow temporal operations over stored facts.
25//!
26//! This module remains intentionally narrower than a full temporal algebra.
27//! Temporal joins, temporal composition, transaction time, and bitemporal APIs
28//! are later work.
29
30use std::{collections::BTreeMap, fmt};
31
32use crate::{
33    BinaryRelation, NaryRelation, NaryRelationError, UnaryRelation, traits::FiniteRelation,
34};
35
36/// Validation errors for deterministic half-open intervals.
37///
38/// # Examples
39///
40/// ```rust
41/// use relmath::temporal::{Interval, IntervalError};
42///
43/// assert_eq!(
44///     Interval::new(4, 4),
45///     Err(IntervalError::InvalidBounds { start: 4, end: 4 })
46/// );
47/// ```
48#[derive(Clone, Debug, PartialEq, Eq)]
49pub enum IntervalError<T> {
50    /// The interval is invalid because `start >= end`.
51    InvalidBounds {
52        /// The requested inclusive lower bound.
53        start: T,
54        /// The requested exclusive upper bound.
55        end: T,
56    },
57}
58
59impl<T: fmt::Debug> fmt::Display for IntervalError<T> {
60    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61        match self {
62            Self::InvalidBounds { start, end } => {
63                write!(
64                    f,
65                    "invalid half-open interval: expected start < end, got start={start:?}, end={end:?}"
66                )
67            }
68        }
69    }
70}
71
72impl<T: fmt::Debug> std::error::Error for IntervalError<T> {}
73
74/// Deterministic half-open interval over an ordered endpoint domain.
75///
76/// `Interval<T>` represents `[start, end)`: `start` is included and `end` is
77/// excluded. The interval is valid only when `start < end`.
78///
79/// Intervals derive `Ord`, so ordered collections such as `BTreeSet` keep
80/// them in lexicographic `(start, end)` order. That is the deterministic order
81/// later temporal coalescing code should use.
82///
83/// # Examples
84///
85/// ```rust
86/// use std::collections::BTreeSet;
87///
88/// use relmath::temporal::Interval;
89///
90/// let windows = BTreeSet::from([
91///     Interval::new(3, 5).expect("expected valid interval"),
92///     Interval::new(1, 4).expect("expected valid interval"),
93///     Interval::new(1, 3).expect("expected valid interval"),
94/// ]);
95///
96/// assert_eq!(
97///     windows.into_iter().collect::<Vec<_>>(),
98///     vec![
99///         Interval::new(1, 3).expect("expected valid interval"),
100///         Interval::new(1, 4).expect("expected valid interval"),
101///         Interval::new(3, 5).expect("expected valid interval"),
102///     ]
103/// );
104/// ```
105#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
106pub struct Interval<T: Ord> {
107    start: T,
108    end: T,
109}
110
111impl<T: Ord> Interval<T> {
112    /// Creates one valid half-open interval `[start, end)`.
113    ///
114    /// Returns an explicit error when `start >= end`.
115    ///
116    /// # Examples
117    ///
118    /// ```rust
119    /// use relmath::temporal::{Interval, IntervalError};
120    ///
121    /// let valid = Interval::new(2, 5).expect("expected valid interval");
122    /// assert_eq!(valid.start(), &2);
123    /// assert_eq!(valid.end(), &5);
124    ///
125    /// assert_eq!(
126    ///     Interval::new(5, 2),
127    ///     Err(IntervalError::InvalidBounds { start: 5, end: 2 })
128    /// );
129    /// ```
130    pub fn new(start: T, end: T) -> Result<Self, IntervalError<T>> {
131        if start < end {
132            Ok(Self { start, end })
133        } else {
134            Err(IntervalError::InvalidBounds { start, end })
135        }
136    }
137
138    /// Returns the inclusive lower bound of the interval.
139    #[must_use]
140    pub fn start(&self) -> &T {
141        &self.start
142    }
143
144    /// Returns the exclusive upper bound of the interval.
145    #[must_use]
146    pub fn end(&self) -> &T {
147        &self.end
148    }
149
150    /// Returns `true` when `point` lies inside `[start, end)`.
151    ///
152    /// # Examples
153    ///
154    /// ```rust
155    /// use relmath::temporal::Interval;
156    ///
157    /// let window = Interval::new(10, 12).expect("expected valid interval");
158    ///
159    /// assert!(window.contains(&10));
160    /// assert!(window.contains(&11));
161    /// assert!(!window.contains(&12));
162    /// ```
163    #[must_use]
164    pub fn contains(&self, point: &T) -> bool {
165        &self.start <= point && point < &self.end
166    }
167
168    /// Returns `true` when the intervals overlap with non-empty intersection.
169    ///
170    /// Directly adjacent intervals do not overlap under half-open semantics.
171    ///
172    /// # Examples
173    ///
174    /// ```rust
175    /// use relmath::temporal::Interval;
176    ///
177    /// let left = Interval::new(1, 3).expect("expected valid interval");
178    /// let overlap = Interval::new(2, 4).expect("expected valid interval");
179    /// let adjacent = Interval::new(3, 5).expect("expected valid interval");
180    ///
181    /// assert!(left.overlaps(&overlap));
182    /// assert!(!left.overlaps(&adjacent));
183    /// ```
184    #[must_use]
185    pub fn overlaps(&self, other: &Self) -> bool {
186        self.start < other.end && other.start < self.end
187    }
188
189    /// Returns `true` when the intervals touch at exactly one boundary.
190    ///
191    /// Adjacent intervals are not overlapping, but later valid-time layers may
192    /// still coalesce them into one canonical support fragment.
193    #[must_use]
194    pub fn is_adjacent_to(&self, other: &Self) -> bool {
195        self.end == other.start || other.end == self.start
196    }
197
198    /// Returns `true` when the intervals can be coalesced.
199    ///
200    /// This is true for overlapping or directly adjacent intervals.
201    #[must_use]
202    pub fn can_coalesce_with(&self, other: &Self) -> bool {
203        self.overlaps(other) || self.is_adjacent_to(other)
204    }
205}
206
207impl<T: Ord + Clone> Interval<T> {
208    /// Returns the non-empty intersection of two intervals.
209    ///
210    /// When the intervals are disjoint or only touch at one boundary, this
211    /// returns `None`.
212    ///
213    /// # Examples
214    ///
215    /// ```rust
216    /// use relmath::temporal::Interval;
217    ///
218    /// let left = Interval::new(1, 4).expect("expected valid interval");
219    /// let right = Interval::new(3, 6).expect("expected valid interval");
220    ///
221    /// assert_eq!(
222    ///     left.intersection(&right),
223    ///     Some(Interval::new(3, 4).expect("expected valid interval"))
224    /// );
225    /// ```
226    #[must_use]
227    pub fn intersection(&self, other: &Self) -> Option<Self> {
228        let start = if self.start >= other.start {
229            self.start.clone()
230        } else {
231            other.start.clone()
232        };
233        let end = if self.end <= other.end {
234            self.end.clone()
235        } else {
236            other.end.clone()
237        };
238
239        Self::new(start, end).ok()
240    }
241
242    /// Restricts this interval to the overlap with `constraint`.
243    ///
244    /// This is an interval-level helper for later temporal `restrict_to`
245    /// behavior on valid-time relations.
246    #[must_use]
247    pub fn restrict_to(&self, constraint: &Self) -> Option<Self> {
248        self.intersection(constraint)
249    }
250
251    /// Coalesces two overlapping or directly adjacent intervals.
252    ///
253    /// Returns `None` when the intervals are strictly disjoint.
254    ///
255    /// # Examples
256    ///
257    /// ```rust
258    /// use relmath::temporal::Interval;
259    ///
260    /// let morning = Interval::new(9, 12).expect("expected valid interval");
261    /// let afternoon = Interval::new(12, 15).expect("expected valid interval");
262    ///
263    /// assert_eq!(
264    ///     morning.coalesce(&afternoon),
265    ///     Some(Interval::new(9, 15).expect("expected valid interval"))
266    /// );
267    /// ```
268    #[must_use]
269    pub fn coalesce(&self, other: &Self) -> Option<Self> {
270        if !self.can_coalesce_with(other) {
271            return None;
272        }
273
274        let start = if self.start <= other.start {
275            self.start.clone()
276        } else {
277            other.start.clone()
278        };
279        let end = if self.end >= other.end {
280            self.end.clone()
281        } else {
282            other.end.clone()
283        };
284
285        Some(Self { start, end })
286    }
287}
288
289fn canonicalize_intervals<T: Ord + Clone>(mut intervals: Vec<Interval<T>>) -> Vec<Interval<T>> {
290    intervals.sort();
291
292    let mut canonical: Vec<Interval<T>> = Vec::with_capacity(intervals.len());
293    for interval in intervals {
294        if let Some(last) = canonical.last_mut() {
295            if let Some(coalesced) = last.coalesce(&interval) {
296                *last = coalesced;
297                continue;
298            }
299        }
300
301        canonical.push(interval);
302    }
303
304    canonical
305}
306
307/// Deterministic canonical valid-time support for one fact.
308///
309/// `ValidTimeSupport<T>` stores half-open intervals in canonical
310/// lexicographic order. Overlapping or directly adjacent intervals are
311/// coalesced, so user-visible support never contains redundant fragments.
312/// Support can then be queried for overlap with one interval or restricted to
313/// one interval window without losing deterministic canonical order.
314///
315/// This is the interval support attached to one stored fact. It is distinct
316/// from the exact support of a relation, which forgets time and keeps only
317/// which facts are present.
318///
319/// # Examples
320///
321/// ```rust
322/// use relmath::temporal::{Interval, ValidTimeSupport};
323///
324/// let support = ValidTimeSupport::from_intervals([
325///     Interval::new(3, 5).expect("expected valid interval"),
326///     Interval::new(1, 3).expect("expected valid interval"),
327///     Interval::new(6, 7).expect("expected valid interval"),
328///     Interval::new(4, 6).expect("expected valid interval"),
329/// ]);
330///
331/// assert_eq!(
332///     support.to_vec(),
333///     vec![Interval::new(1, 7).expect("expected valid interval")]
334/// );
335/// assert!(support.contains(&4));
336/// assert!(!support.contains(&7));
337/// ```
338#[derive(Clone, Debug, PartialEq, Eq)]
339pub struct ValidTimeSupport<T: Ord + Clone> {
340    intervals: Vec<Interval<T>>,
341}
342
343impl<T: Ord + Clone> ValidTimeSupport<T> {
344    /// Creates empty valid-time support.
345    ///
346    /// A `ValidTimeRelation` does not store empty support for a fact, so
347    /// absent facts return `None` rather than an empty support value. This
348    /// constructor is still useful for standalone inspection and testing.
349    ///
350    /// # Examples
351    ///
352    /// ```rust
353    /// use relmath::temporal::ValidTimeSupport;
354    ///
355    /// let empty = ValidTimeSupport::<i32>::new();
356    ///
357    /// assert!(empty.is_empty());
358    /// assert_eq!(empty.len(), 0);
359    /// ```
360    #[must_use]
361    pub fn new() -> Self {
362        Self {
363            intervals: Vec::new(),
364        }
365    }
366
367    /// Creates canonical valid-time support from intervals.
368    ///
369    /// The resulting support is sorted and coalesced deterministically.
370    ///
371    /// # Examples
372    ///
373    /// ```rust
374    /// use relmath::temporal::{Interval, ValidTimeSupport};
375    ///
376    /// let support = ValidTimeSupport::from_intervals([
377    ///     Interval::new(2, 4).expect("expected valid interval"),
378    ///     Interval::new(1, 2).expect("expected valid interval"),
379    /// ]);
380    ///
381    /// assert_eq!(
382    ///     support.to_vec(),
383    ///     vec![Interval::new(1, 4).expect("expected valid interval")]
384    /// );
385    /// ```
386    #[must_use]
387    pub fn from_intervals<I>(intervals: I) -> Self
388    where
389        I: IntoIterator<Item = Interval<T>>,
390    {
391        intervals.into_iter().collect()
392    }
393
394    /// Returns the number of canonical support intervals.
395    ///
396    /// For one stored fact, this counts canonical support fragments rather than
397    /// time points or inserted raw intervals.
398    #[must_use]
399    pub fn len(&self) -> usize {
400        self.intervals.len()
401    }
402
403    /// Returns `true` when the support contains no intervals.
404    #[must_use]
405    pub fn is_empty(&self) -> bool {
406        self.intervals.is_empty()
407    }
408
409    /// Returns `true` when `point` is covered by some support interval.
410    ///
411    /// # Examples
412    ///
413    /// ```rust
414    /// use relmath::temporal::{Interval, ValidTimeSupport};
415    ///
416    /// let support =
417    ///     ValidTimeSupport::from_intervals([Interval::new(2, 4).expect("expected valid interval")]);
418    ///
419    /// assert!(support.contains(&2));
420    /// assert!(support.contains(&3));
421    /// assert!(!support.contains(&4));
422    /// ```
423    #[must_use]
424    pub fn contains(&self, point: &T) -> bool {
425        self.intervals
426            .iter()
427            .any(|interval| interval.contains(point))
428    }
429
430    /// Returns `true` when some support interval overlaps `constraint`.
431    ///
432    /// Direct boundary-only contact does not count as overlap under half-open
433    /// semantics.
434    ///
435    /// # Examples
436    ///
437    /// ```rust
438    /// use relmath::temporal::{Interval, ValidTimeSupport};
439    ///
440    /// let support = ValidTimeSupport::from_intervals([
441    ///     Interval::new(1, 3).expect("expected valid interval"),
442    ///     Interval::new(5, 7).expect("expected valid interval"),
443    /// ]);
444    ///
445    /// assert!(support.overlaps(&Interval::new(2, 6).expect("expected valid interval")));
446    /// assert!(!support.overlaps(&Interval::new(3, 5).expect("expected valid interval")));
447    /// ```
448    #[must_use]
449    pub fn overlaps(&self, constraint: &Interval<T>) -> bool {
450        self.intervals
451            .iter()
452            .any(|interval| interval.overlaps(constraint))
453    }
454
455    /// Restricts this support to the overlap with `constraint`.
456    ///
457    /// The returned support remains canonical: overlapping or directly adjacent
458    /// fragments are coalesced deterministically. When no interval overlaps the
459    /// constraint, this returns empty support.
460    ///
461    /// # Examples
462    ///
463    /// ```rust
464    /// use relmath::temporal::{Interval, ValidTimeSupport};
465    ///
466    /// let support = ValidTimeSupport::from_intervals([
467    ///     Interval::new(1, 3).expect("expected valid interval"),
468    ///     Interval::new(5, 7).expect("expected valid interval"),
469    /// ]);
470    ///
471    /// assert_eq!(
472    ///     support
473    ///         .restrict_to(&Interval::new(2, 6).expect("expected valid interval"))
474    ///         .to_vec(),
475    ///     vec![
476    ///         Interval::new(2, 3).expect("expected valid interval"),
477    ///         Interval::new(5, 6).expect("expected valid interval"),
478    ///     ]
479    /// );
480    /// assert!(
481    ///     support
482    ///         .restrict_to(&Interval::new(3, 5).expect("expected valid interval"))
483    ///         .is_empty()
484    /// );
485    /// ```
486    #[must_use]
487    pub fn restrict_to(&self, constraint: &Interval<T>) -> Self {
488        self.intervals
489            .iter()
490            .filter_map(|interval| interval.restrict_to(constraint))
491            .collect()
492    }
493
494    /// Returns an iterator over canonical support intervals.
495    ///
496    /// # Examples
497    ///
498    /// ```rust
499    /// use relmath::temporal::{Interval, ValidTimeSupport};
500    ///
501    /// let support = ValidTimeSupport::from_intervals([
502    ///     Interval::new(4, 6).expect("expected valid interval"),
503    ///     Interval::new(1, 3).expect("expected valid interval"),
504    /// ]);
505    ///
506    /// assert_eq!(
507    ///     support.iter().cloned().collect::<Vec<_>>(),
508    ///     vec![
509    ///         Interval::new(1, 3).expect("expected valid interval"),
510    ///         Interval::new(4, 6).expect("expected valid interval"),
511    ///     ]
512    /// );
513    /// ```
514    pub fn iter(&self) -> impl Iterator<Item = &Interval<T>> {
515        self.intervals.iter()
516    }
517
518    /// Returns the canonical interval list as a vector.
519    #[must_use]
520    pub fn to_vec(&self) -> Vec<Interval<T>> {
521        self.intervals.clone()
522    }
523
524    fn insert_interval(&mut self, interval: Interval<T>) -> bool {
525        let mut updated = self.intervals.clone();
526        updated.push(interval);
527        updated = canonicalize_intervals(updated);
528
529        if updated == self.intervals {
530            false
531        } else {
532            self.intervals = updated;
533            true
534        }
535    }
536}
537
538impl<T: Ord + Clone> Default for ValidTimeSupport<T> {
539    fn default() -> Self {
540        Self::new()
541    }
542}
543
544impl<T: Ord + Clone> FromIterator<Interval<T>> for ValidTimeSupport<T> {
545    fn from_iter<I: IntoIterator<Item = Interval<T>>>(iter: I) -> Self {
546        let intervals = canonicalize_intervals(iter.into_iter().collect());
547        Self { intervals }
548    }
549}
550
551impl<T: Ord + Clone> Extend<Interval<T>> for ValidTimeSupport<T> {
552    fn extend<I: IntoIterator<Item = Interval<T>>>(&mut self, iter: I) {
553        let mut updated = self.intervals.clone();
554        updated.extend(iter);
555        self.intervals = canonicalize_intervals(updated);
556    }
557}
558
559/// Deterministic valid-time support attached to exact facts.
560///
561/// `ValidTimeRelation<F, T>` keeps exact facts in deterministic fact order and
562/// stores canonical valid-time support for each fact. Repeated insertion of a
563/// fact with overlapping or directly adjacent intervals coalesces that fact's
564/// support. The first temporal operations on top of this surface are
565/// [`Self::snapshot_at`] and [`Self::restrict_to`].
566///
567/// In this first G5 slice, only facts with non-empty valid-time support are
568/// stored. Exact support materialization forgets time and keeps only which
569/// facts are present at all.
570///
571/// # Examples
572///
573/// ```rust
574/// use relmath::temporal::{Interval, ValidTimeRelation};
575///
576/// let assignments = ValidTimeRelation::from_facts([
577///     (("alice", "review"), Interval::new(1, 3).expect("expected valid interval")),
578///     (("alice", "review"), Interval::new(3, 5).expect("expected valid interval")),
579///     (("bob", "approve"), Interval::new(2, 4).expect("expected valid interval")),
580/// ]);
581///
582/// assert_eq!(
583///     assignments
584///         .valid_time_of(&("alice", "review"))
585///         .expect("expected support")
586///         .to_vec(),
587///     vec![Interval::new(1, 5).expect("expected valid interval")]
588/// );
589/// assert!(assignments.is_active_at(&("alice", "review"), &4));
590/// assert!(!assignments.is_active_at(&("alice", "review"), &5));
591/// ```
592#[derive(Clone, Debug, PartialEq, Eq)]
593pub struct ValidTimeRelation<F: Ord, T: Ord + Clone> {
594    facts: BTreeMap<F, ValidTimeSupport<T>>,
595}
596
597impl<F: Ord, T: Ord + Clone> ValidTimeRelation<F, T> {
598    /// Creates an empty valid-time relation.
599    ///
600    /// # Examples
601    ///
602    /// ```rust
603    /// use relmath::{FiniteRelation, temporal::ValidTimeRelation};
604    ///
605    /// let relation = ValidTimeRelation::<(&str, &str), i32>::new();
606    ///
607    /// assert!(relation.is_empty());
608    /// assert!(relation.snapshot_at(&0).is_empty());
609    /// ```
610    #[must_use]
611    pub fn new() -> Self {
612        Self {
613            facts: BTreeMap::new(),
614        }
615    }
616
617    /// Creates a valid-time relation from `(fact, interval)` entries.
618    ///
619    /// Repeated facts combine by deterministic canonical coalescing of valid
620    /// time support.
621    ///
622    /// # Examples
623    ///
624    /// ```rust
625    /// use relmath::temporal::{Interval, ValidTimeRelation};
626    ///
627    /// let relation = ValidTimeRelation::from_facts([
628    ///     ("alice", Interval::new(1, 3).expect("expected valid interval")),
629    ///     ("alice", Interval::new(3, 5).expect("expected valid interval")),
630    /// ]);
631    ///
632    /// assert_eq!(
633    ///     relation
634    ///         .valid_time_of(&"alice")
635    ///         .expect("expected support")
636    ///         .to_vec(),
637    ///     vec![Interval::new(1, 5).expect("expected valid interval")]
638    /// );
639    /// ```
640    #[must_use]
641    pub fn from_facts<I>(facts: I) -> Self
642    where
643        I: IntoIterator<Item = (F, Interval<T>)>,
644    {
645        facts.into_iter().collect()
646    }
647
648    /// Inserts one valid interval for a fact.
649    ///
650    /// Returns `true` when the fact's stored valid-time support changes.
651    ///
652    /// # Examples
653    ///
654    /// ```rust
655    /// use relmath::temporal::{Interval, ValidTimeRelation};
656    ///
657    /// let mut relation = ValidTimeRelation::new();
658    ///
659    /// assert!(relation.insert("alice", Interval::new(1, 3).expect("expected valid interval")));
660    /// assert!(!relation.insert("alice", Interval::new(2, 3).expect("already covered")));
661    /// ```
662    pub fn insert(&mut self, fact: F, interval: Interval<T>) -> bool {
663        match self.facts.entry(fact) {
664            std::collections::btree_map::Entry::Vacant(entry) => {
665                entry.insert(ValidTimeSupport::from_intervals([interval]));
666                true
667            }
668            std::collections::btree_map::Entry::Occupied(mut entry) => {
669                entry.get_mut().insert_interval(interval)
670            }
671        }
672    }
673
674    /// Inserts one interval for a fact from raw bounds.
675    ///
676    /// Returns an explicit error when `start >= end`.
677    ///
678    /// # Examples
679    ///
680    /// ```rust
681    /// use relmath::temporal::{IntervalError, ValidTimeRelation};
682    ///
683    /// let mut relation = ValidTimeRelation::new();
684    ///
685    /// assert!(relation.insert_bounds("alice", 1, 3).expect("expected valid bounds"));
686    /// assert_eq!(
687    ///     relation.insert_bounds("alice", 3, 3),
688    ///     Err(IntervalError::InvalidBounds { start: 3, end: 3 })
689    /// );
690    /// ```
691    pub fn insert_bounds(&mut self, fact: F, start: T, end: T) -> Result<bool, IntervalError<T>> {
692        Ok(self.insert(fact, Interval::new(start, end)?))
693    }
694
695    /// Returns `true` when the relation contains the given fact with non-empty
696    /// valid-time support.
697    ///
698    /// # Examples
699    ///
700    /// ```rust
701    /// use relmath::temporal::{Interval, ValidTimeRelation};
702    ///
703    /// let relation = ValidTimeRelation::from_facts([(
704    ///     "alice",
705    ///     Interval::new(1, 3).expect("expected valid interval"),
706    /// )]);
707    ///
708    /// assert!(relation.contains_fact(&"alice"));
709    /// assert!(!relation.contains_fact(&"bob"));
710    /// ```
711    #[must_use]
712    pub fn contains_fact(&self, fact: &F) -> bool {
713        self.facts.contains_key(fact)
714    }
715
716    /// Returns the canonical valid-time support for one fact.
717    ///
718    /// When a fact is absent, this returns `None`. In this first G5 slice, the
719    /// relation does not store empty support as a sentinel value, so `None`
720    /// also means the fact has no stored valid-time support.
721    ///
722    /// # Examples
723    ///
724    /// ```rust
725    /// use relmath::temporal::{Interval, ValidTimeRelation};
726    ///
727    /// let relation = ValidTimeRelation::from_facts([(
728    ///     "alice",
729    ///     Interval::new(1, 3).expect("expected valid interval"),
730    /// )]);
731    ///
732    /// assert_eq!(
733    ///     relation.valid_time_of(&"alice").expect("expected support").to_vec(),
734    ///     vec![Interval::new(1, 3).expect("expected valid interval")]
735    /// );
736    /// assert!(relation.valid_time_of(&"bob").is_none());
737    /// ```
738    #[must_use]
739    pub fn valid_time_of(&self, fact: &F) -> Option<&ValidTimeSupport<T>> {
740        self.facts.get(fact)
741    }
742
743    /// Returns `true` when `fact` is active at `point`.
744    ///
745    /// Absent facts are never active.
746    ///
747    /// # Examples
748    ///
749    /// ```rust
750    /// use relmath::temporal::{Interval, ValidTimeRelation};
751    ///
752    /// let relation = ValidTimeRelation::from_facts([(
753    ///     "alice",
754    ///     Interval::new(1, 3).expect("expected valid interval"),
755    /// )]);
756    ///
757    /// assert!(relation.is_active_at(&"alice", &1));
758    /// assert!(!relation.is_active_at(&"alice", &3));
759    /// assert!(!relation.is_active_at(&"bob", &1));
760    /// ```
761    #[must_use]
762    pub fn is_active_at(&self, fact: &F, point: &T) -> bool {
763        self.facts
764            .get(fact)
765            .is_some_and(|support| support.contains(point))
766    }
767
768    /// Returns an iterator over facts and valid-time support in deterministic
769    /// fact order.
770    ///
771    /// # Examples
772    ///
773    /// ```rust
774    /// use relmath::temporal::{Interval, ValidTimeRelation};
775    ///
776    /// let relation = ValidTimeRelation::from_facts([
777    ///     ("bob", Interval::new(2, 4).expect("expected valid interval")),
778    ///     ("alice", Interval::new(1, 3).expect("expected valid interval")),
779    /// ]);
780    ///
781    /// assert_eq!(
782    ///     relation
783    ///         .iter()
784    ///         .map(|(fact, support)| (*fact, support.to_vec()))
785    ///         .collect::<Vec<_>>(),
786    ///     vec![
787    ///         ("alice", vec![Interval::new(1, 3).expect("expected valid interval")]),
788    ///         ("bob", vec![Interval::new(2, 4).expect("expected valid interval")]),
789    ///     ]
790    /// );
791    /// ```
792    pub fn iter(&self) -> impl Iterator<Item = (&F, &ValidTimeSupport<T>)> {
793        self.facts.iter()
794    }
795
796    /// Returns the exact support relation of stored facts as a unary relation.
797    ///
798    /// This materialization forgets time and keeps only which facts have
799    /// non-empty valid-time support.
800    ///
801    /// # Examples
802    ///
803    /// ```rust
804    /// use relmath::temporal::{Interval, ValidTimeRelation};
805    ///
806    /// let relation = ValidTimeRelation::from_facts([
807    ///     ("Closure", Interval::new(1, 4).expect("expected valid interval")),
808    ///     ("Relations", Interval::new(2, 5).expect("expected valid interval")),
809    /// ]);
810    ///
811    /// assert_eq!(relation.support().to_vec(), vec!["Closure", "Relations"]);
812    /// ```
813    #[must_use]
814    pub fn support(&self) -> UnaryRelation<F>
815    where
816        F: Clone,
817    {
818        self.facts.keys().cloned().collect()
819    }
820
821    /// Returns the exact snapshot of facts active at `point`.
822    ///
823    /// The result is a unary relation over stored facts. Facts are present in
824    /// the snapshot exactly when their valid-time support contains `point`.
825    /// When no facts are active, this returns an empty unary relation.
826    ///
827    /// # Examples
828    ///
829    /// ```rust
830    /// use relmath::{FiniteRelation, temporal::{Interval, ValidTimeRelation}};
831    ///
832    /// let assignments = ValidTimeRelation::from_facts([
833    ///     (("alice", "review"), Interval::new(1, 3).expect("expected valid interval")),
834    ///     (("alice", "review"), Interval::new(3, 5).expect("expected valid interval")),
835    ///     (("bob", "approve"), Interval::new(2, 4).expect("expected valid interval")),
836    /// ]);
837    ///
838    /// assert_eq!(
839    ///     assignments.snapshot_at(&3).to_vec(),
840    ///     vec![("alice", "review"), ("bob", "approve")]
841    /// );
842    /// assert!(assignments.snapshot_at(&5).is_empty());
843    /// ```
844    #[must_use]
845    pub fn snapshot_at(&self, point: &T) -> UnaryRelation<F>
846    where
847        F: Clone,
848    {
849        self.facts
850            .iter()
851            .filter(|(_, support)| support.contains(point))
852            .map(|(fact, _)| fact.clone())
853            .collect()
854    }
855
856    /// Restricts every fact's valid-time support to `constraint`.
857    ///
858    /// Facts whose support has no overlap with `constraint` are omitted from
859    /// the returned relation. Remaining support fragments stay in deterministic
860    /// fact order and canonical interval order.
861    ///
862    /// # Examples
863    ///
864    /// ```rust
865    /// use relmath::temporal::{Interval, ValidTimeRelation};
866    ///
867    /// let assignments = ValidTimeRelation::from_facts([
868    ///     (("alice", "review"), Interval::new(1, 3).expect("expected valid interval")),
869    ///     (("alice", "review"), Interval::new(5, 7).expect("expected valid interval")),
870    ///     (("bob", "approve"), Interval::new(2, 4).expect("expected valid interval")),
871    ///     (("carol", "audit"), Interval::new(7, 9).expect("expected valid interval")),
872    /// ]);
873    ///
874    /// let audit_window = Interval::new(2, 6).expect("expected valid interval");
875    /// let restricted = assignments.restrict_to(&audit_window);
876    ///
877    /// assert_eq!(
878    ///     restricted
879    ///         .valid_time_of(&("alice", "review"))
880    ///         .expect("expected support")
881    ///         .to_vec(),
882    ///     vec![
883    ///         Interval::new(2, 3).expect("expected valid interval"),
884    ///         Interval::new(5, 6).expect("expected valid interval"),
885    ///     ]
886    /// );
887    /// assert!(!restricted.contains_fact(&("carol", "audit")));
888    /// ```
889    #[must_use]
890    pub fn restrict_to(&self, constraint: &Interval<T>) -> Self
891    where
892        F: Clone,
893    {
894        let facts = self
895            .facts
896            .iter()
897            .filter_map(|(fact, support)| {
898                let restricted = support.restrict_to(constraint);
899
900                if restricted.is_empty() {
901                    None
902                } else {
903                    Some((fact.clone(), restricted))
904                }
905            })
906            .collect();
907
908        Self { facts }
909    }
910}
911
912impl<F: Ord, T: Ord + Clone> Default for ValidTimeRelation<F, T> {
913    fn default() -> Self {
914        Self::new()
915    }
916}
917
918impl<F: Ord, T: Ord + Clone> FiniteRelation for ValidTimeRelation<F, T> {
919    fn len(&self) -> usize {
920        self.facts.len()
921    }
922}
923
924impl<F: Ord, T: Ord + Clone> FromIterator<(F, Interval<T>)> for ValidTimeRelation<F, T> {
925    fn from_iter<I: IntoIterator<Item = (F, Interval<T>)>>(iter: I) -> Self {
926        let mut relation = Self::new();
927        relation.extend(iter);
928        relation
929    }
930}
931
932impl<F: Ord, T: Ord + Clone> Extend<(F, Interval<T>)> for ValidTimeRelation<F, T> {
933    fn extend<I: IntoIterator<Item = (F, Interval<T>)>>(&mut self, iter: I) {
934        for (fact, interval) in iter {
935            self.insert(fact, interval);
936        }
937    }
938}
939
940impl<Value: Ord + Clone, Time: Ord + Clone> ValidTimeRelation<Value, Time> {
941    /// Converts scalar facts into a unary relation support set.
942    ///
943    /// # Examples
944    ///
945    /// ```rust
946    /// use relmath::temporal::{Interval, ValidTimeRelation};
947    ///
948    /// let relation = ValidTimeRelation::from_facts([(
949    ///     "Closure",
950    ///     Interval::new(1, 4).expect("expected valid interval"),
951    /// )]);
952    ///
953    /// assert_eq!(relation.to_unary_relation().to_vec(), vec!["Closure"]);
954    /// ```
955    #[must_use]
956    pub fn to_unary_relation(&self) -> UnaryRelation<Value> {
957        self.support()
958    }
959}
960
961impl<A: Ord + Clone, B: Ord + Clone, Time: Ord + Clone> ValidTimeRelation<(A, B), Time> {
962    /// Converts pair facts into a binary relation support set.
963    ///
964    /// # Examples
965    ///
966    /// ```rust
967    /// use relmath::temporal::{Interval, ValidTimeRelation};
968    ///
969    /// let relation = ValidTimeRelation::from_facts([(
970    ///     ("alice", "review"),
971    ///     Interval::new(1, 3).expect("expected valid interval"),
972    /// )]);
973    ///
974    /// assert_eq!(relation.to_binary_relation().to_vec(), vec![("alice", "review")]);
975    /// ```
976    #[must_use]
977    pub fn to_binary_relation(&self) -> BinaryRelation<A, B> {
978        self.facts.keys().cloned().collect()
979    }
980}
981
982impl<Value: Ord + Clone, Time: Ord + Clone> ValidTimeRelation<Vec<Value>, Time> {
983    /// Converts row facts into an n-ary relation with the given schema.
984    ///
985    /// This method reuses the current `NaryRelation` validation rules, so
986    /// duplicate or blank column names and row-arity mismatches return
987    /// `NaryRelationError`.
988    ///
989    /// # Examples
990    ///
991    /// ```rust
992    /// use relmath::temporal::{Interval, ValidTimeRelation};
993    ///
994    /// let rows = ValidTimeRelation::from_facts([(
995    ///     vec!["Alice", "Math", "passed"],
996    ///     Interval::new(1, 3).expect("expected valid interval"),
997    /// )]);
998    ///
999    /// let relation = rows
1000    ///     .to_nary_relation(["student", "course", "status"])
1001    ///     .expect("expected valid n-ary relation");
1002    ///
1003    /// assert_eq!(relation.to_rows(), vec![vec!["Alice", "Math", "passed"]]);
1004    /// ```
1005    pub fn to_nary_relation<I, S>(
1006        &self,
1007        schema: I,
1008    ) -> Result<NaryRelation<Value>, NaryRelationError>
1009    where
1010        I: IntoIterator<Item = S>,
1011        S: Into<String>,
1012    {
1013        NaryRelation::from_rows(schema, self.facts.keys().cloned())
1014    }
1015}