Skip to main content

solverforge_scoring/stream/joiner/
equal.rs

1// Equal joiner for matching on property equality.
2
3use std::marker::PhantomData;
4
5use super::Joiner;
6
7// Marker for same-source joins where each unordered pair is considered once.
8pub struct Symmetric;
9
10// Marker for left/right joins where pair orientation is part of the semantics.
11pub struct Directed;
12
13/* Creates a joiner that matches when a property is equal on both sides.
14
15This is the primary joiner for self-joins where you're matching
16entities from the same collection on a shared property.
17
18# Example
19
20```
21use solverforge_scoring::stream::joiner::{Joiner, equal};
22
23#[derive(Clone)]
24struct Shift { employee_id: Option<usize>, start: i64 }
25
26// Match shifts with the same employee (self-join)
27let same_employee = equal(|s: &Shift| s.employee_id);
28
29let a = Shift { employee_id: Some(5), start: 0 };
30let b = Shift { employee_id: Some(5), start: 8 };
31let c = Shift { employee_id: Some(3), start: 16 };
32
33assert!(same_employee.matches(&a, &b));
34assert!(!same_employee.matches(&a, &c));
35```
36*/
37pub fn equal<A, T, F>(key: F) -> EqualJoiner<F, F, T, Symmetric>
38where
39    T: PartialEq,
40    F: Fn(&A) -> T + Clone + Send + Sync,
41{
42    EqualJoiner {
43        left: key.clone(),
44        right: key,
45        _phantom: PhantomData,
46    }
47}
48
49/* Creates a joiner that matches when extracted values are equal.
50
51Use this for cross-joins between different entity types.
52
53# Example
54
55```
56use solverforge_scoring::stream::joiner::{Joiner, equal_bi};
57
58#[derive(Clone)]
59struct Employee { id: usize, department: String }
60#[derive(Clone)]
61struct Task { assigned_to: usize, name: String }
62
63// Match employees to their assigned tasks
64let by_id = equal_bi(
65|e: &Employee| e.id,
66|t: &Task| t.assigned_to
67);
68
69let emp = Employee { id: 5, department: "Engineering".into() };
70let task1 = Task { assigned_to: 5, name: "Review".into() };
71let task2 = Task { assigned_to: 3, name: "Test".into() };
72
73assert!(by_id.matches(&emp, &task1));
74assert!(!by_id.matches(&emp, &task2));
75```
76*/
77pub fn equal_bi<T, Fa, Fb>(left: Fa, right: Fb) -> EqualJoiner<Fa, Fb, T, Directed>
78where
79    T: PartialEq,
80    Fa: Send + Sync,
81    Fb: Send + Sync,
82{
83    EqualJoiner {
84        left,
85        right,
86        _phantom: PhantomData,
87    }
88}
89
90/* Builds an `EqualJoiner` from raw key extractors (crate-internal adapter
91for fluent dispatch sites that lift unary keys onto row views). */
92pub fn equal_raw<Fa, Fb, T>(left: Fa, right: Fb) -> EqualJoiner<Fa, Fb, T, Directed>
93where
94    T: PartialEq,
95    Fa: Send + Sync,
96    Fb: Send + Sync,
97{
98    EqualJoiner {
99        left,
100        right,
101        _phantom: PhantomData,
102    }
103}
104
105/* Creates a joiner whose left key inspects the whole joined left row.
106
107The left closure receives the borrowed left row (recursive `Concat` of
108`Leaf` views), so the second relationship in a chain can inspect any
109earlier binding or combine several. The right closure sees only the new
110right entity. Successive joins keep independent key types: each uniform
111`.join()` carries its own `K2` domain rather than inheriting the first
112join's key.
113
114Row and right-input types stay method-generic (never impl generics), so
115successive joins keep heterogeneous key types without unifying domains.
116See `constraint::tests::relational::fluent_chain` for the exercised shapes.
117*/
118pub fn equal_on<K2, LK, KC>(left: LK, right: KC) -> EqualJoiner<LK, KC, K2, Directed>
119where
120    K2: PartialEq,
121    LK: Send + Sync,
122    KC: Send + Sync,
123{
124    EqualJoiner {
125        left,
126        right,
127        _phantom: PhantomData,
128    }
129}
130
131/* A joiner that matches when extracted values are equal.
132
133Created by the [`equal()`] or [`equal_bi()`] functions.
134*/
135pub struct EqualJoiner<Fa, Fb, T, Mode = Directed> {
136    left: Fa,
137    right: Fb,
138    _phantom: PhantomData<(fn() -> T, fn() -> Mode)>,
139}
140
141impl<Fa, Fb, T, Mode> EqualJoiner<Fa, Fb, T, Mode> {
142    /// Compose conditions without fixing a borrowed row's lifetime during construction.
143    pub fn and<J>(self, other: J) -> super::AndJoiner<Self, J> {
144        super::AndJoiner {
145            first: self,
146            second: other,
147        }
148    }
149    // Extracts the join key from an A entity.
150    #[inline]
151    pub fn key_a<A>(&self, a: &A) -> T
152    where
153        Fa: Fn(&A) -> T,
154    {
155        (self.left)(a)
156    }
157
158    // Extracts the join key from a B entity.
159    #[inline]
160    pub fn key_b<B>(&self, b: &B) -> T
161    where
162        Fb: Fn(&B) -> T,
163    {
164        (self.right)(b)
165    }
166
167    /* Consumes the joiner and returns the key extractors.
168
169    This is useful for zero-erasure constraint creation where
170    the key extractors need to be stored as concrete types.
171    */
172    #[inline]
173    pub fn into_keys(self) -> (Fa, Fb) {
174        (self.left, self.right)
175    }
176
177    // Returns references to the key extractors.
178    #[inline]
179    pub fn key_extractors(&self) -> (&Fa, &Fb) {
180        (&self.left, &self.right)
181    }
182}
183
184impl<A, B, T, Fa, Fb, Mode> Joiner<A, B> for EqualJoiner<Fa, Fb, T, Mode>
185where
186    T: PartialEq,
187    Fa: Fn(&A) -> T + Send + Sync,
188    Fb: Fn(&B) -> T + Send + Sync,
189{
190    #[inline]
191    fn matches(&self, a: &A, b: &B) -> bool {
192        (self.left)(a) == (self.right)(b)
193    }
194}