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/// Creates a joiner that matches when a property is equal on both sides.
8///
9/// This is the primary joiner for self-joins where you're matching
10/// entities from the same collection on a shared property.
11///
12/// # Example
13///
14/// ```
15/// use solverforge_scoring::stream::joiner::{Joiner, equal};
16///
17/// #[derive(Clone)]
18/// struct Shift { employee_id: Option<usize>, start: i64 }
19///
20/// // Match shifts with the same employee (self-join)
21/// let same_employee = equal(|s: &Shift| s.employee_id);
22///
23/// let a = Shift { employee_id: Some(5), start: 0 };
24/// let b = Shift { employee_id: Some(5), start: 8 };
25/// let c = Shift { employee_id: Some(3), start: 16 };
26///
27/// assert!(same_employee.matches(&a, &b));
28/// assert!(!same_employee.matches(&a, &c));
29/// ```
30pub fn equal<A, T, F>(key: F) -> EqualJoiner<F, F, T>
31where
32 T: PartialEq,
33 F: Fn(&A) -> T + Clone + Send + Sync,
34{
35 EqualJoiner {
36 left: key.clone(),
37 right: key,
38 _phantom: PhantomData,
39 }
40}
41
42/// Creates a joiner that matches when extracted values are equal.
43///
44/// Use this for cross-joins between different entity types.
45///
46/// # Example
47///
48/// ```
49/// use solverforge_scoring::stream::joiner::{Joiner, equal_bi};
50///
51/// #[derive(Clone)]
52/// struct Employee { id: usize, department: String }
53/// #[derive(Clone)]
54/// struct Task { assigned_to: usize, name: String }
55///
56/// // Match employees to their assigned tasks
57/// let by_id = equal_bi(
58/// |e: &Employee| e.id,
59/// |t: &Task| t.assigned_to
60/// );
61///
62/// let emp = Employee { id: 5, department: "Engineering".into() };
63/// let task1 = Task { assigned_to: 5, name: "Review".into() };
64/// let task2 = Task { assigned_to: 3, name: "Test".into() };
65///
66/// assert!(by_id.matches(&emp, &task1));
67/// assert!(!by_id.matches(&emp, &task2));
68/// ```
69pub fn equal_bi<A, B, T, Fa, Fb>(left: Fa, right: Fb) -> EqualJoiner<Fa, Fb, T>
70where
71 T: PartialEq,
72 Fa: Fn(&A) -> T + Send + Sync,
73 Fb: Fn(&B) -> T + Send + Sync,
74{
75 EqualJoiner {
76 left,
77 right,
78 _phantom: PhantomData,
79 }
80}
81
82/// A joiner that matches when extracted values are equal.
83///
84/// Created by the [`equal()`] or [`equal_bi()`] functions.
85pub struct EqualJoiner<Fa, Fb, T> {
86 left: Fa,
87 right: Fb,
88 _phantom: PhantomData<fn() -> T>,
89}
90
91impl<Fa, Fb, T> EqualJoiner<Fa, Fb, T> {
92 /// Extracts the join key from an A entity.
93 #[inline]
94 pub fn key_a<A>(&self, a: &A) -> T
95 where
96 Fa: Fn(&A) -> T,
97 {
98 (self.left)(a)
99 }
100
101 /// Extracts the join key from a B entity.
102 #[inline]
103 pub fn key_b<B>(&self, b: &B) -> T
104 where
105 Fb: Fn(&B) -> T,
106 {
107 (self.right)(b)
108 }
109
110 /// Consumes the joiner and returns the key extractors.
111 ///
112 /// This is useful for zero-erasure constraint creation where
113 /// the key extractors need to be stored as concrete types.
114 #[inline]
115 pub fn into_keys(self) -> (Fa, Fb) {
116 (self.left, self.right)
117 }
118
119 /// Returns references to the key extractors.
120 #[inline]
121 pub fn key_extractors(&self) -> (&Fa, &Fb) {
122 (&self.left, &self.right)
123 }
124}
125
126impl<A, B, T, Fa, Fb> Joiner<A, B> for EqualJoiner<Fa, Fb, T>
127where
128 T: PartialEq,
129 Fa: Fn(&A) -> T + Send + Sync,
130 Fb: Fn(&B) -> T + Send + Sync,
131{
132 #[inline]
133 fn matches(&self, a: &A, b: &B) -> bool {
134 (self.left)(a) == (self.right)(b)
135 }
136}