solverforge_scoring/stream/joiner/mod.rs
1//! Joiner functions for constraint stream joins.
2//!
3//! Joiners define matching conditions between entities in different streams
4//! (cross-joins) or the same stream (self-joins).
5//!
6//! # Self-joins
7//!
8//! Use [`equal()`] with a single extractor for self-joins:
9//!
10//! ```
11//! use solverforge_scoring::stream::joiner::{Joiner, equal};
12//!
13//! #[derive(Clone)]
14//! struct Shift { employee_id: usize, start: i64, end: i64 }
15//!
16//! // Match shifts with the same employee
17//! let same_employee = equal(|s: &Shift| s.employee_id);
18//! assert!(same_employee.matches(
19//! &Shift { employee_id: 1, start: 0, end: 8 },
20//! &Shift { employee_id: 1, start: 8, end: 16 }
21//! ));
22//! ```
23//!
24//! # Cross-joins
25//!
26//! Use [`equal_bi()`] for cross-joins between different types:
27//!
28//! ```
29//! use solverforge_scoring::stream::joiner::{Joiner, equal_bi};
30//!
31//! struct Employee { id: usize }
32//! struct Shift { employee_id: Option<usize> }
33//!
34//! let by_id = equal_bi(
35//! |shift: &Shift| shift.employee_id,
36//! |emp: &Employee| Some(emp.id)
37//! );
38//! ```
39
40mod comparison;
41mod equal;
42mod filtering;
43mod overlapping;
44
45pub use comparison::{
46 greater_than, greater_than_or_equal, less_than, less_than_or_equal, GreaterThanJoiner,
47 GreaterThanOrEqualJoiner, LessThanJoiner, LessThanOrEqualJoiner,
48};
49pub use equal::{equal, equal_bi, EqualJoiner};
50pub use filtering::{filtering, FilteringJoiner};
51pub use overlapping::{overlapping, OverlappingJoiner};
52
53/// A joiner defines matching conditions between two entities.
54///
55/// Joiners are used in stream operations like `join()` and `join_self()`
56/// to determine which entity pairs should be matched.
57///
58/// # Example
59///
60/// ```
61/// use solverforge_scoring::stream::joiner::{Joiner, equal};
62///
63/// let joiner = equal(|x: &i32| *x % 10);
64/// assert!(joiner.matches(&15, &25)); // Both end in 5
65/// assert!(!joiner.matches(&15, &26)); // 5 != 6
66/// ```
67pub trait Joiner<A, B>: Send + Sync {
68 /// Returns true if the two entities should be joined.
69 fn matches(&self, a: &A, b: &B) -> bool;
70
71 /// Combines this joiner with another using AND semantics.
72 ///
73 /// The resulting joiner matches only if both joiners match.
74 ///
75 /// # Example
76 ///
77 /// ```
78 /// use solverforge_scoring::stream::joiner::{Joiner, equal};
79 ///
80 /// #[derive(Clone)]
81 /// struct Item { category: u32, priority: u32 }
82 ///
83 /// let same_category = equal(|i: &Item| i.category);
84 /// let same_priority = equal(|i: &Item| i.priority);
85 ///
86 /// let combined = same_category.and(same_priority);
87 ///
88 /// let a = Item { category: 1, priority: 5 };
89 /// let b = Item { category: 1, priority: 5 };
90 /// let c = Item { category: 1, priority: 3 };
91 ///
92 /// assert!(combined.matches(&a, &b)); // Same category AND priority
93 /// assert!(!combined.matches(&a, &c)); // Same category but different priority
94 /// ```
95 fn and<J>(self, other: J) -> AndJoiner<Self, J>
96 where
97 Self: Sized,
98 J: Joiner<A, B>,
99 {
100 AndJoiner {
101 first: self,
102 second: other,
103 }
104 }
105}
106
107/// A joiner that combines two joiners with AND semantics.
108///
109/// Created by calling `joiner.and(other)`.
110pub struct AndJoiner<J1, J2> {
111 first: J1,
112 second: J2,
113}
114
115impl<A, B, J1, J2> Joiner<A, B> for AndJoiner<J1, J2>
116where
117 J1: Joiner<A, B>,
118 J2: Joiner<A, B>,
119{
120 #[inline]
121 fn matches(&self, a: &A, b: &B) -> bool {
122 self.first.matches(a, b) && self.second.matches(a, b)
123 }
124}
125
126/// A joiner wrapping a closure for testing and simple cases.
127pub struct FnJoiner<F> {
128 f: F,
129}
130
131impl<F> FnJoiner<F> {
132 /// Creates a joiner from a closure.
133 pub fn new(f: F) -> Self {
134 Self { f }
135 }
136}
137
138impl<A, B, F> Joiner<A, B> for FnJoiner<F>
139where
140 F: Fn(&A, &B) -> bool + Send + Sync,
141{
142 #[inline]
143 fn matches(&self, a: &A, b: &B) -> bool {
144 (self.f)(a, b)
145 }
146}