1pub mod builtin;
4pub mod compose;
5mod context;
6pub mod error;
7mod selection;
8mod traits;
9
10pub use context::ObjectiveContext;
11pub use error::{ObjectiveError, ObjectiveResult};
12pub use selection::Selection;
13pub use traits::{objective_fn, DeterministicObjective, Objective};
14
15#[cfg(test)]
16mod tests {
17 use super::*;
18 use crate::ordering::HasId;
19 use crate::ObjectiveError;
20 use uuid::Uuid;
21
22 #[test]
23 fn test_simple_objective() {
24 let objective = objective_fn(|n: &i32, _ctx: &ObjectiveContext| *n as f64);
25
26 let candidates = vec![1, 5, 3, 8, 2];
27 let selection = objective
28 .select(&candidates, &ObjectiveContext::new())
29 .unwrap();
30
31 assert_eq!(*selection.item, 8);
32 assert_eq!(selection.score, 8.0);
33 assert_eq!(selection.index, 3);
34 }
35
36 #[test]
37 fn test_threshold() {
38 let objective = objective_fn(|n: &i32, _ctx: &ObjectiveContext| *n as f64);
39
40 let candidates = vec![1, 5, 3, 8, 2];
41 let context = ObjectiveContext::new().with_min_score(4.0);
42 let selection = objective.select(&candidates, &context).unwrap();
43
44 assert_eq!(*selection.item, 8);
45 assert_eq!(selection.passed, 2);
46 }
47
48 #[test]
49 fn test_no_candidates() {
50 let objective = objective_fn(|n: &i32, _ctx: &ObjectiveContext| *n as f64);
51
52 let candidates: Vec<i32> = vec![];
53 let result = objective.select(&candidates, &ObjectiveContext::new());
54
55 assert!(matches!(result, Err(ObjectiveError::NoCandidates)));
56 }
57
58 #[test]
59 fn test_no_match() {
60 let objective = objective_fn(|n: &i32, _ctx: &ObjectiveContext| *n as f64);
61
62 let candidates = vec![1, 2, 3];
63 let context = ObjectiveContext::new().with_min_score(10.0);
64 let result = objective.select(&candidates, &context);
65
66 assert!(matches!(result, Err(ObjectiveError::NoMatch(_))));
67 }
68
69 #[test]
70 fn test_select_top() {
71 let objective = objective_fn(|n: &i32, _ctx: &ObjectiveContext| *n as f64);
72
73 let candidates = vec![1, 5, 3, 8, 2];
74 let top = objective.select_top(&candidates, 3, &ObjectiveContext::new());
75
76 assert_eq!(top.len(), 3);
77 assert_eq!(*top[0].item, 8);
78 assert_eq!(*top[1].item, 5);
79 assert_eq!(*top[2].item, 3);
80 }
81
82 #[test]
83 fn test_nan_score_never_selected() {
84 let objective = objective_fn(
85 |n: &i32, _ctx: &ObjectiveContext| {
86 if *n == 5 {
87 f64::NAN
88 } else {
89 *n as f64
90 }
91 },
92 );
93
94 let candidates = vec![1, 5, 3];
95 let selection = objective
96 .select(&candidates, &ObjectiveContext::new())
97 .unwrap();
98
99 assert_eq!(*selection.item, 3);
100 assert_eq!(selection.score, 3.0);
101 assert_eq!(selection.passed, 2);
102 }
103
104 #[test]
105 fn test_infinite_score_never_selected() {
106 let objective = objective_fn(
107 |n: &i32, _ctx: &ObjectiveContext| {
108 if *n == 5 {
109 f64::INFINITY
110 } else {
111 *n as f64
112 }
113 },
114 );
115
116 let candidates = vec![1, 5, 3];
117 let selection = objective
118 .select(&candidates, &ObjectiveContext::new())
119 .unwrap();
120
121 assert_eq!(*selection.item, 3);
122 assert_eq!(selection.score, 3.0);
123 assert_eq!(selection.passed, 2);
124 }
125
126 #[test]
127 fn test_max_candidates_respected() {
128 let objective = objective_fn(|n: &i32, _ctx: &ObjectiveContext| *n as f64);
129
130 let candidates = vec![1, 5, 3, 8, 2];
131 let context = ObjectiveContext::new().with_max_candidates(2);
132 let selection = objective.select(&candidates, &context).unwrap();
133
134 assert_eq!(*selection.item, 5);
135 assert_eq!(selection.considered, 2);
136 }
137
138 #[derive(Debug, Clone)]
143 struct TestCandidate {
144 id: Uuid,
145 value: i32,
146 }
147
148 impl TestCandidate {
149 fn new(value: i32) -> Self {
150 Self {
151 id: Uuid::new_v4(),
152 value,
153 }
154 }
155
156 fn with_id(id: Uuid, value: i32) -> Self {
157 Self { id, value }
158 }
159 }
160
161 impl HasId for TestCandidate {
162 fn id(&self) -> Uuid {
163 self.id
164 }
165 }
166
167 #[test]
168 fn test_deterministic_select_basic() {
169 let objective = objective_fn(|c: &TestCandidate, _ctx: &ObjectiveContext| c.value as f64);
170
171 let candidates = vec![
172 TestCandidate::new(1),
173 TestCandidate::new(5),
174 TestCandidate::new(3),
175 ];
176
177 let selection = objective
178 .select_deterministic(&candidates, &ObjectiveContext::new())
179 .unwrap();
180
181 assert_eq!(selection.item.value, 5);
182 assert_eq!(selection.score, 5.0);
183 }
184
185 #[test]
186 fn test_deterministic_select_equal_scores_uses_uuid_tiebreaker() {
187 let objective = objective_fn(|_c: &TestCandidate, _ctx: &ObjectiveContext| 1.0);
188
189 let id1 = Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap();
190 let id2 = Uuid::parse_str("00000000-0000-0000-0000-000000000002").unwrap();
191 let id3 = Uuid::parse_str("00000000-0000-0000-0000-000000000003").unwrap();
192
193 let candidates = vec![
194 TestCandidate::with_id(id2, 100),
195 TestCandidate::with_id(id3, 200),
196 TestCandidate::with_id(id1, 300),
197 ];
198
199 let selection = objective
200 .select_deterministic(&candidates, &ObjectiveContext::new())
201 .unwrap();
202
203 assert_eq!(selection.item.id, id1);
204 assert_eq!(selection.item.value, 300);
205 }
206
207 #[test]
208 fn test_deterministic_select_top_ordering() {
209 let objective = objective_fn(|_c: &TestCandidate, _ctx: &ObjectiveContext| 1.0);
210
211 let id1 = Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap();
212 let id2 = Uuid::parse_str("00000000-0000-0000-0000-000000000002").unwrap();
213 let id3 = Uuid::parse_str("00000000-0000-0000-0000-000000000003").unwrap();
214
215 let candidates = vec![
216 TestCandidate::with_id(id3, 300),
217 TestCandidate::with_id(id1, 100),
218 TestCandidate::with_id(id2, 200),
219 ];
220
221 let top = objective.select_top_deterministic(&candidates, 3, &ObjectiveContext::new());
222
223 assert_eq!(top.len(), 3);
224 assert_eq!(top[0].item.id, id1);
225 assert_eq!(top[1].item.id, id2);
226 assert_eq!(top[2].item.id, id3);
227 }
228
229 #[test]
230 fn test_deterministic_reproducibility() {
231 let objective = objective_fn(|_c: &TestCandidate, _ctx: &ObjectiveContext| 1.0);
232
233 let id1 = Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap();
234 let id2 = Uuid::parse_str("00000000-0000-0000-0000-000000000002").unwrap();
235 let id3 = Uuid::parse_str("00000000-0000-0000-0000-000000000003").unwrap();
236
237 let candidates = vec![
238 TestCandidate::with_id(id2, 1),
239 TestCandidate::with_id(id3, 2),
240 TestCandidate::with_id(id1, 3),
241 ];
242
243 for _ in 0..100 {
244 let selection = objective
245 .select_deterministic(&candidates, &ObjectiveContext::new())
246 .unwrap();
247 assert_eq!(selection.item.id, id1, "Determinism violated!");
248
249 let top = objective.select_top_deterministic(&candidates, 3, &ObjectiveContext::new());
250 assert_eq!(top[0].item.id, id1);
251 assert_eq!(top[1].item.id, id2);
252 assert_eq!(top[2].item.id, id3);
253 }
254 }
255
256 #[test]
261 fn precision_default_returns_one() {
262 let objective = objective_fn(|n: &i32, _ctx: &ObjectiveContext| *n as f64);
264 let ctx = ObjectiveContext::new();
265 assert_eq!(objective.precision(&42, &ctx), 1.0);
266 }
267
268 #[test]
269 fn precision_one_leaves_ranking_unchanged() {
270 let objective = objective_fn(|n: &i32, _ctx: &ObjectiveContext| *n as f64);
272 let candidates = vec![1, 5, 3, 8, 2];
273 let sel = objective
274 .select(&candidates, &ObjectiveContext::new())
275 .unwrap();
276 assert_eq!(*sel.item, 8);
277 assert_eq!(sel.precision, 1.0);
278 }
279
280 #[test]
281 fn precision_reorders_candidates_when_lower() {
282 struct PrecisionObjective;
286 impl Objective<(f64, f64)> for PrecisionObjective {
287 fn score(&self, c: &(f64, f64), _ctx: &ObjectiveContext) -> f64 {
288 c.0
289 }
290 fn precision(&self, c: &(f64, f64), _ctx: &ObjectiveContext) -> f64 {
291 c.1
292 }
293 }
294
295 let candidates = vec![(10.0f64, 0.1f64), (3.0f64, 1.0f64)];
296 let sel = PrecisionObjective
297 .select(&candidates, &ObjectiveContext::new())
298 .unwrap();
299 assert_eq!(sel.item.0, 3.0);
301 assert_eq!(sel.precision, 1.0);
302 }
303
304 #[test]
305 fn selection_stores_precision_from_winning_candidate() {
306 struct HalfPrecision;
307 impl Objective<i32> for HalfPrecision {
308 fn score(&self, n: &i32, _ctx: &ObjectiveContext) -> f64 {
309 *n as f64
310 }
311 fn precision(&self, _n: &i32, _ctx: &ObjectiveContext) -> f64 {
312 0.5
313 }
314 }
315 let candidates = vec![1, 2, 3];
316 let sel = HalfPrecision
317 .select(&candidates, &ObjectiveContext::new())
318 .unwrap();
319 assert_eq!(sel.precision, 0.5);
320 }
321
322 #[test]
323 fn non_finite_precision_treated_as_one() {
324 struct NanPrecision;
326 impl Objective<i32> for NanPrecision {
327 fn score(&self, n: &i32, _ctx: &ObjectiveContext) -> f64 {
328 *n as f64
329 }
330 fn precision(&self, _n: &i32, _ctx: &ObjectiveContext) -> f64 {
331 f64::NAN
332 }
333 }
334 let candidates = vec![1, 5, 3];
335 let sel = NanPrecision
336 .select(&candidates, &ObjectiveContext::new())
337 .unwrap();
338 assert_eq!(*sel.item, 5);
340 }
341}