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}