use relmath::{
BinaryRelation, FiniteRelation, NaryRelationError, UnaryRelation,
temporal::{Interval, IntervalError, ValidTimeRelation, ValidTimeSupport},
};
#[test]
fn stores_canonical_valid_time_support_in_deterministic_fact_order() {
let relation = ValidTimeRelation::from_facts([
(
("bob", "reviewer"),
Interval::new(2, 4).expect("expected valid interval"),
),
(
("alice", "admin"),
Interval::new(1, 3).expect("expected valid interval"),
),
(
("alice", "admin"),
Interval::new(3, 5).expect("expected valid interval"),
),
(
("alice", "admin"),
Interval::new(4, 6).expect("expected valid interval"),
),
(
("alice", "admin"),
Interval::new(2, 3).expect("interval already covered"),
),
]);
assert_eq!(relation.len(), 2);
assert!(relation.contains_fact(&("alice", "admin")));
assert_eq!(
relation
.valid_time_of(&("alice", "admin"))
.expect("expected support")
.to_vec(),
vec![Interval::new(1, 6).expect("expected valid interval")]
);
assert_eq!(
relation
.iter()
.map(|(fact, support)| (*fact, support.to_vec()))
.collect::<Vec<_>>(),
vec![
(
("alice", "admin"),
vec![Interval::new(1, 6).expect("expected valid interval")],
),
(
("bob", "reviewer"),
vec![Interval::new(2, 4).expect("expected valid interval")],
),
]
);
}
#[test]
fn empty_and_singleton_relations_cover_absent_and_boundary_cases() {
let empty = ValidTimeRelation::<(&str, &str), i32>::new();
assert!(empty.is_empty());
assert!(!empty.contains_fact(&("alice", "admin")));
assert!(empty.valid_time_of(&("alice", "admin")).is_none());
assert!(!empty.is_active_at(&("alice", "admin"), &3));
let singleton = ValidTimeRelation::from_facts([(
("alice", "admin"),
Interval::new(2, 4).expect("expected valid interval"),
)]);
assert_eq!(singleton.len(), 1);
assert!(singleton.contains_fact(&("alice", "admin")));
assert!(singleton.is_active_at(&("alice", "admin"), &2));
assert!(singleton.is_active_at(&("alice", "admin"), &3));
assert!(!singleton.is_active_at(&("alice", "admin"), &4));
assert!(!singleton.is_active_at(&("alice", "editor"), &3));
assert_eq!(
singleton
.valid_time_of(&("alice", "admin"))
.expect("expected support")
.len(),
1
);
assert!(singleton.valid_time_of(&("alice", "editor")).is_none());
}
#[test]
fn insert_and_insert_bounds_report_whether_support_changed() {
let mut relation = ValidTimeRelation::new();
assert!(relation.insert(
("alice", "review"),
Interval::new(1, 3).expect("expected valid interval")
));
assert!(!relation.insert(
("alice", "review"),
Interval::new(2, 3).expect("interval already covered")
));
assert!(
relation
.insert_bounds(("alice", "review"), 3, 5)
.expect("expected valid bounds")
);
assert_eq!(
relation.insert_bounds(("alice", "review"), 5, 5),
Err(IntervalError::InvalidBounds { start: 5, end: 5 })
);
assert_eq!(
relation
.valid_time_of(&("alice", "review"))
.expect("expected support")
.to_vec(),
vec![Interval::new(1, 5).expect("expected valid interval")]
);
}
#[test]
fn support_empty_and_singleton_cases_preserve_iteration_views() {
let empty = ValidTimeSupport::<i32>::new();
assert!(empty.is_empty());
assert_eq!(empty.len(), 0);
assert_eq!(empty.iter().count(), 0);
assert_eq!(empty.to_vec(), Vec::<Interval<i32>>::new());
assert!(!empty.contains(&0));
let singleton =
ValidTimeSupport::from_intervals([Interval::new(2, 4).expect("expected valid interval")]);
assert_eq!(singleton.len(), 1);
assert!(!singleton.is_empty());
assert_eq!(
singleton.iter().cloned().collect::<Vec<_>>(),
vec![Interval::new(2, 4).expect("expected valid interval")]
);
assert_eq!(
singleton.to_vec(),
vec![Interval::new(2, 4).expect("expected valid interval")]
);
}
#[test]
fn support_restriction_distinguishes_overlap_from_boundary_only_contact() {
let empty = ValidTimeSupport::<i32>::new();
let window = Interval::new(2, 6).expect("expected valid interval");
assert!(!empty.overlaps(&window));
assert!(empty.restrict_to(&window).is_empty());
let support = ValidTimeSupport::from_intervals([
Interval::new(1, 3).expect("expected valid interval"),
Interval::new(5, 7).expect("expected valid interval"),
]);
assert!(support.overlaps(&window));
assert_eq!(
support.restrict_to(&window).to_vec(),
vec![
Interval::new(2, 3).expect("expected valid interval"),
Interval::new(5, 6).expect("expected valid interval"),
]
);
let boundary_only = Interval::new(3, 5).expect("expected valid interval");
assert!(!support.overlaps(&boundary_only));
assert!(support.restrict_to(&boundary_only).is_empty());
}
#[test]
fn snapshot_at_uses_half_open_boundaries_and_deterministic_fact_order() {
let empty = ValidTimeRelation::<(&str, &str), i32>::new();
assert!(empty.snapshot_at(&3).is_empty());
let assignments = ValidTimeRelation::from_facts([
(
("alice", "review"),
Interval::new(1, 3).expect("expected valid interval"),
),
(
("alice", "review"),
Interval::new(3, 5).expect("expected valid interval"),
),
(
("bob", "approve"),
Interval::new(2, 4).expect("expected valid interval"),
),
(
("carol", "audit"),
Interval::new(5, 7).expect("expected valid interval"),
),
]);
assert_eq!(
assignments.snapshot_at(&1).to_vec(),
vec![("alice", "review")]
);
assert_eq!(
assignments.snapshot_at(&3).to_vec(),
vec![("alice", "review"), ("bob", "approve")]
);
assert_eq!(
assignments.snapshot_at(&5).to_vec(),
vec![("carol", "audit")]
);
assert!(assignments.snapshot_at(&7).is_empty());
}
#[test]
fn restrict_to_keeps_only_overlapping_facts_and_preserves_disconnected_fragments() {
let assignments = ValidTimeRelation::from_facts([
(
("alice", "review"),
Interval::new(1, 3).expect("expected valid interval"),
),
(
("alice", "review"),
Interval::new(5, 7).expect("expected valid interval"),
),
(
("bob", "approve"),
Interval::new(2, 6).expect("expected valid interval"),
),
(
("carol", "audit"),
Interval::new(7, 9).expect("expected valid interval"),
),
]);
let audit_window = Interval::new(2, 6).expect("expected valid interval");
let restricted = assignments.restrict_to(&audit_window);
assert_eq!(restricted.len(), 2);
assert_eq!(
restricted
.iter()
.map(|(fact, support)| (*fact, support.to_vec()))
.collect::<Vec<_>>(),
vec![
(
("alice", "review"),
vec![
Interval::new(2, 3).expect("expected valid interval"),
Interval::new(5, 6).expect("expected valid interval"),
],
),
(
("bob", "approve"),
vec![Interval::new(2, 6).expect("expected valid interval")],
),
]
);
assert_eq!(
restricted.to_binary_relation().to_vec(),
vec![("alice", "review"), ("bob", "approve")]
);
let singleton = ValidTimeRelation::from_facts([(
("dora", "observe"),
Interval::new(2, 4).expect("expected valid interval"),
)]);
assert!(
singleton
.restrict_to(&Interval::new(4, 6).expect("expected valid interval"))
.is_empty()
);
}
#[test]
fn restrict_to_with_no_overlap_returns_empty_relation_and_empty_exact_support() {
let assignments = ValidTimeRelation::from_facts([
(
("alice", "review"),
Interval::new(1, 3).expect("expected valid interval"),
),
(
("bob", "approve"),
Interval::new(3, 5).expect("expected valid interval"),
),
]);
let restricted =
assignments.restrict_to(&Interval::new(5, 7).expect("expected valid interval"));
assert!(restricted.is_empty());
assert!(restricted.snapshot_at(&5).is_empty());
assert!(restricted.support().is_empty());
assert!(restricted.to_binary_relation().is_empty());
assert!(restricted.valid_time_of(&("alice", "review")).is_none());
}
#[test]
fn materializes_exact_binary_support_without_changing_binary_semantics() {
let assignments = ValidTimeRelation::from_facts([
(
("alice", "review"),
Interval::new(1, 3).expect("expected valid interval"),
),
(
("alice", "review"),
Interval::new(3, 5).expect("expected valid interval"),
),
(
("bob", "approve"),
Interval::new(2, 4).expect("expected valid interval"),
),
]);
let role_task = BinaryRelation::from_pairs([("review", "submit"), ("approve", "publish")]);
let exact_assignments = assignments.to_binary_relation();
let alice = UnaryRelation::singleton("alice");
let tasks = exact_assignments.compose(&role_task);
assert_eq!(
assignments.support().to_vec(),
vec![("alice", "review"), ("bob", "approve")]
);
assert_eq!(
exact_assignments.to_vec(),
vec![("alice", "review"), ("bob", "approve")]
);
assert_eq!(tasks.image(&alice).to_vec(), vec!["submit"]);
}
#[test]
fn scalar_facts_cover_unary_support_and_absent_queries() {
let concepts = ValidTimeRelation::from_facts([
(
"Relations",
Interval::new(1, 2).expect("expected valid interval"),
),
(
"Relations",
Interval::new(2, 4).expect("expected valid interval"),
),
(
"Closure",
Interval::new(3, 5).expect("expected valid interval"),
),
]);
assert_eq!(
concepts
.valid_time_of(&"Relations")
.expect("expected support")
.to_vec(),
vec![Interval::new(1, 4).expect("expected valid interval")]
);
assert!(concepts.is_active_at(&"Closure", &4));
assert!(!concepts.is_active_at(&"Closure", &5));
assert!(concepts.valid_time_of(&"Transitivity").is_none());
assert_eq!(concepts.support().to_vec(), vec!["Closure", "Relations"]);
assert_eq!(
concepts.to_unary_relation().to_vec(),
vec!["Closure", "Relations"]
);
}
#[test]
fn row_facts_can_be_materialized_back_into_nary_relations() {
let rows = ValidTimeRelation::from_facts([
(
vec!["Alice", "Math", "passed"],
Interval::new(1, 3).expect("expected valid interval"),
),
(
vec!["Alice", "Math", "passed"],
Interval::new(3, 4).expect("expected valid interval"),
),
(
vec!["Bob", "Physics", "passed"],
Interval::new(2, 5).expect("expected valid interval"),
),
]);
let relation = rows
.to_nary_relation(["student", "course", "status"])
.expect("expected valid n-ary support relation");
assert_eq!(
relation.to_rows(),
vec![
vec!["Alice", "Math", "passed"],
vec!["Bob", "Physics", "passed"],
]
);
assert_eq!(
rows.valid_time_of(&vec!["Alice", "Math", "passed"])
.expect("expected support")
.to_vec(),
vec![Interval::new(1, 4).expect("expected valid interval")]
);
}
#[test]
fn row_fact_materialization_reuses_nary_validation_rules() {
let rows = ValidTimeRelation::from_facts([(
vec!["Alice", "Math", "passed"],
Interval::new(1, 3).expect("expected valid interval"),
)]);
assert_eq!(
rows.to_nary_relation(["student", "student", "status"])
.expect_err("expected duplicate column error"),
NaryRelationError::DuplicateColumn {
column: "student".to_string(),
}
);
assert_eq!(
rows.to_nary_relation(["student", "course"])
.expect_err("expected row arity mismatch"),
NaryRelationError::RowArityMismatch {
expected: 2,
actual: 3,
}
);
}