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