1mod canonical;
17mod compare;
18mod has_id;
19mod scored_entry;
20
21pub use canonical::{canonical_f32, canonical_f64};
22pub use compare::{cmp_asc_score_then_id, cmp_desc_score_then_id};
23pub use has_id::HasId;
24pub use scored_entry::ScoredEntry;
25
26pub use khive_score::QuantKey;
28pub use khive_score::{cmp_asc_then_id, cmp_desc_then_id, DeterministicScore, Ranked};
29
30#[cfg(test)]
31use canonical::{CANONICAL_NAN_F32, CANONICAL_NAN_F64};
32
33#[cfg(test)]
34mod tests {
35 use super::*;
36 use std::cmp::Ordering;
37 use uuid::Uuid;
38
39 #[test]
44 fn test_canonical_f64_nan_variants() {
45 let nans = [
46 f64::NAN,
47 -f64::NAN,
48 f64::from_bits(0x7ff0_0000_0000_0001), f64::from_bits(0x7ff8_0000_0000_0001), f64::from_bits(0xfff8_0000_0000_0001), ];
52
53 for nan in nans {
54 let canonical = canonical_f64(nan);
55 assert!(canonical.is_nan(), "Should still be NaN");
56 assert_eq!(
57 canonical.to_bits(),
58 CANONICAL_NAN_F64,
59 "NaN variant {:016x} should canonicalize to {:016x}",
60 nan.to_bits(),
61 CANONICAL_NAN_F64
62 );
63 }
64 }
65
66 #[test]
67 fn test_canonical_f64_zero() {
68 assert!(canonical_f64(-0.0).is_sign_positive());
69 assert_eq!(canonical_f64(-0.0), 0.0);
70 assert_eq!(canonical_f64(-0.0).to_bits(), 0u64);
71 }
72
73 #[test]
74 fn test_canonical_f64_preserves_normal() {
75 let values = [1.0, -1.0, 0.5, f64::MAX, f64::MIN_POSITIVE, f64::EPSILON];
76 for v in values {
77 assert_eq!(canonical_f64(v), v);
78 assert_eq!(canonical_f64(v).to_bits(), v.to_bits());
79 }
80 }
81
82 #[test]
83 fn test_canonical_f32_nan_variants() {
84 let nans = [
85 f32::NAN,
86 -f32::NAN,
87 f32::from_bits(0x7f80_0001), f32::from_bits(0x7fc0_0001), ];
90
91 for nan in nans {
92 let canonical = canonical_f32(nan);
93 assert!(canonical.is_nan());
94 assert_eq!(canonical.to_bits(), CANONICAL_NAN_F32);
95 }
96 }
97
98 #[test]
99 fn test_canonical_f32_zero() {
100 assert!(canonical_f32(-0.0_f32).is_sign_positive());
101 assert_eq!(canonical_f32(-0.0_f32), 0.0_f32);
102 }
103
104 #[test]
105 fn test_canonical_idempotent() {
106 let values = [0.0, -0.0, 1.0, f64::NAN, f64::INFINITY, f64::NEG_INFINITY];
107 for v in values {
108 let once = canonical_f64(v);
109 let twice = canonical_f64(once);
110 assert_eq!(once.to_bits(), twice.to_bits());
111 }
112 }
113
114 #[test]
119 fn test_descending_score_ordering() {
120 let id_a = Uuid::from_u128(1);
121 let id_b = Uuid::from_u128(2);
122
123 assert_eq!(
124 cmp_desc_score_then_id(0.9, id_a, 0.5, id_b),
125 Ordering::Less,
126 "Higher score should come first"
127 );
128 assert_eq!(
129 cmp_desc_score_then_id(0.5, id_a, 0.9, id_b),
130 Ordering::Greater,
131 "Lower score should come second"
132 );
133 }
134
135 #[test]
136 fn test_uuid_tie_breaking() {
137 let id_a = Uuid::from_u128(1);
138 let id_b = Uuid::from_u128(2);
139
140 assert_eq!(
141 cmp_desc_score_then_id(0.5, id_a, 0.5, id_b),
142 Ordering::Less,
143 "Lower UUID should come first on tie"
144 );
145 assert_eq!(
146 cmp_desc_score_then_id(0.5, id_b, 0.5, id_a),
147 Ordering::Greater,
148 "Higher UUID should come second on tie"
149 );
150 }
151
152 #[test]
153 fn test_nan_handling() {
154 let id_a = Uuid::from_u128(1);
155 let id_b = Uuid::from_u128(2);
156
157 assert_eq!(
158 cmp_desc_score_then_id(f64::NAN, id_a, 0.5, id_b),
159 Ordering::Greater,
160 "NaN should sort after normal values in descending"
161 );
162 assert_eq!(
163 cmp_desc_score_then_id(0.5, id_a, f64::NAN, id_b),
164 Ordering::Less,
165 "Normal value should sort before NaN in descending"
166 );
167 assert_eq!(
168 cmp_desc_score_then_id(f64::NAN, id_a, f64::NAN, id_b),
169 Ordering::Less,
170 "Two NaNs should use UUID tie-breaking"
171 );
172 }
173
174 #[test]
175 fn test_sorting_stability() {
176 let entries: Vec<(f64, Uuid)> = (0..100)
177 .map(|i| (0.5, Uuid::from_u128(i as u128)))
178 .collect();
179
180 let mut sorted1 = entries.clone();
181 let mut sorted2 = entries.clone();
182
183 sorted1.sort_by(|a, b| cmp_desc_score_then_id(a.0, a.1, b.0, b.1));
184 sorted2.sort_by(|a, b| cmp_desc_score_then_id(a.0, a.1, b.0, b.1));
185
186 assert_eq!(sorted1, sorted2, "Multiple sorts should produce same order");
187
188 for i in 0..99 {
189 assert!(sorted1[i].1 < sorted1[i + 1].1);
190 }
191 }
192
193 #[test]
194 fn test_ascending_variant() {
195 let id_a = Uuid::from_u128(1);
196 let id_b = Uuid::from_u128(2);
197
198 assert_eq!(
199 cmp_asc_score_then_id(0.3, id_a, 0.5, id_b),
200 Ordering::Less,
201 "Lower score should come first in ascending"
202 );
203 assert_eq!(
204 cmp_asc_score_then_id(0.5, id_a, 0.5, id_b),
205 Ordering::Less,
206 "Equal scores use UUID tie-breaking"
207 );
208 }
209
210 #[derive(Debug, Clone)]
215 #[allow(dead_code)]
216 struct TestCandidate {
217 id: Uuid,
218 value: i32,
219 }
220
221 impl HasId for TestCandidate {
222 fn id(&self) -> Uuid {
223 self.id
224 }
225 }
226
227 #[test]
228 fn test_scored_entry_ord() {
229 let a = TestCandidate {
230 id: Uuid::from_u128(1),
231 value: 10,
232 };
233 let b = TestCandidate {
234 id: Uuid::from_u128(2),
235 value: 20,
236 };
237
238 let entry_a = ScoredEntry::new(&a, 0.9, 0);
239 let entry_b = ScoredEntry::new(&b, 0.5, 1);
240
241 assert!(entry_a > entry_b);
242 }
243
244 #[test]
245 fn test_scored_entry_heap() {
246 use std::collections::BinaryHeap;
247
248 let candidates: Vec<TestCandidate> = (0..10i32)
249 .map(|i| TestCandidate {
250 id: Uuid::from_u128(i as u128),
251 value: i * 10,
252 })
253 .collect();
254
255 let mut heap: BinaryHeap<ScoredEntry<&TestCandidate>> = candidates
256 .iter()
257 .enumerate()
258 .map(|(i, c)| ScoredEntry::new(c, 0.5, i))
259 .collect();
260
261 let mut last_id = Uuid::nil();
262 while let Some(entry) = heap.pop() {
263 if last_id != Uuid::nil() {
264 assert!(entry.id() > last_id, "Should pop in UUID order");
265 }
266 last_id = entry.id();
267 }
268 }
269
270 #[test]
271 fn test_scored_entry_equality() {
272 let a = TestCandidate {
273 id: Uuid::from_u128(1),
274 value: 10,
275 };
276 let b = TestCandidate {
277 id: Uuid::from_u128(1),
278 value: 20,
279 };
280
281 let entry_a = ScoredEntry::new(&a, 0.5, 0);
282 let entry_b = ScoredEntry::new(&b, 0.5, 1);
283
284 assert_eq!(entry_a, entry_b);
285 }
286
287 #[test]
288 fn test_scored_entry_hash() {
289 use std::collections::HashSet;
290
291 let a = TestCandidate {
292 id: Uuid::from_u128(1),
293 value: 10,
294 };
295
296 let entry1 = ScoredEntry::new(&a, 0.5, 0);
297 let entry2 = ScoredEntry::new(&a, 0.5, 1);
298
299 let mut set = HashSet::new();
300 set.insert(entry1);
301 assert!(set.contains(&entry2));
302 }
303
304 #[test]
309 fn test_quant_key_precision() {
310 let a = QuantKey::new(0.123456, 1);
311 let b = QuantKey::new(0.123457, 2);
312 assert_ne!(
313 a.quantized_score(),
314 b.quantized_score(),
315 "1e-6 difference should be distinguishable"
316 );
317 }
318
319 #[test]
320 fn test_quant_key_rounding() {
321 let a = QuantKey::new(0.12345642, 1);
322 let b = QuantKey::new(0.12345647, 2);
323 assert_eq!(
324 a.quantized_score(),
325 b.quantized_score(),
326 "Sub-1e-6 differences should round same"
327 );
328 }
329
330 #[test]
331 fn test_quant_key_nan_maps_to_zero() {
332 let nan = QuantKey::new(f32::NAN, 1);
333 let zero = QuantKey::new(0.0, 1);
334 assert_eq!(
335 nan.quantized_score(),
336 zero.quantized_score(),
337 "NaN maps to 0 in score's QuantKey"
338 );
339 }
340
341 #[test]
342 fn test_quant_key_heap_order() {
343 use std::collections::BinaryHeap;
344
345 let mut heap: BinaryHeap<QuantKey> = BinaryHeap::new();
346 heap.push(QuantKey::new(0.95, 3));
347 heap.push(QuantKey::new(0.95, 1));
348 heap.push(QuantKey::new(0.95, 2));
349 heap.push(QuantKey::new(0.87, 4));
350
351 assert_eq!(heap.pop().unwrap().id_prefix(), 1);
352 assert_eq!(heap.pop().unwrap().id_prefix(), 2);
353 assert_eq!(heap.pop().unwrap().id_prefix(), 3);
354 assert_eq!(heap.pop().unwrap().id_prefix(), 4);
355 }
356
357 #[test]
362 fn test_deterministic_score_roundtrip() {
363 let s = DeterministicScore::from_f64(0.75);
364 assert!((s.to_f64() - 0.75).abs() < 1e-9);
365 }
366
367 #[test]
368 fn test_deterministic_score_nan_maps_to_zero() {
369 let s = DeterministicScore::from_f64(f64::NAN);
370 assert_eq!(s, DeterministicScore::ZERO);
371 }
372
373 #[test]
374 fn test_ranked_heap_with_uuid_ids() {
375 use std::collections::BinaryHeap;
376
377 let mut heap: BinaryHeap<Ranked<Uuid>> = BinaryHeap::new();
378 heap.push(Ranked::new(
379 DeterministicScore::from_f64(0.9),
380 Uuid::from_u128(3),
381 ));
382 heap.push(Ranked::new(
383 DeterministicScore::from_f64(0.9),
384 Uuid::from_u128(1),
385 ));
386 heap.push(Ranked::new(
387 DeterministicScore::from_f64(0.9),
388 Uuid::from_u128(2),
389 ));
390 heap.push(Ranked::new(
391 DeterministicScore::from_f64(0.5),
392 Uuid::from_u128(4),
393 ));
394
395 let first = heap.pop().unwrap();
397 assert_eq!(*first.id(), Uuid::from_u128(1));
398 let second = heap.pop().unwrap();
399 assert_eq!(*second.id(), Uuid::from_u128(2));
400 let third = heap.pop().unwrap();
401 assert_eq!(*third.id(), Uuid::from_u128(3));
402 let fourth = heap.pop().unwrap();
404 assert_eq!(*fourth.id(), Uuid::from_u128(4));
405 }
406}