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}