Skip to main content

khive_score/
comparator.rs

1//! Deterministic ranking with tie-breaking.
2
3use crate::DeterministicScore;
4use std::cmp::Ordering;
5
6/// Scored item implementing max-heap `Ord`: higher score wins, lower ID breaks ties.
7#[derive(Clone, Debug, Eq, PartialEq)]
8pub struct Ranked<T: Ord> {
9    score: DeterministicScore,
10    id: T,
11}
12
13impl<T: Ord> Ranked<T> {
14    /// Construct a `Ranked` item from a score and a tie-breaking ID.
15    #[inline]
16    pub fn new(score: DeterministicScore, id: T) -> Self {
17        Self { score, id }
18    }
19
20    /// Return the score component.
21    #[inline]
22    pub fn score(&self) -> DeterministicScore {
23        self.score
24    }
25
26    /// Return a reference to the tie-breaking ID.
27    #[inline]
28    pub fn id(&self) -> &T {
29        &self.id
30    }
31
32    /// Consume `self` and return the ID.
33    #[inline]
34    pub fn into_id(self) -> T {
35        self.id
36    }
37
38    /// Consume `self` and return `(score, id)`.
39    #[inline]
40    pub fn into_parts(self) -> (DeterministicScore, T) {
41        (self.score, self.id)
42    }
43}
44
45impl<T: Ord> Ord for Ranked<T> {
46    #[inline]
47    fn cmp(&self, other: &Self) -> Ordering {
48        self.score
49            .cmp(&other.score)
50            .then_with(|| other.id.cmp(&self.id))
51    }
52}
53
54impl<T: Ord> PartialOrd for Ranked<T> {
55    #[inline]
56    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
57        Some(self.cmp(other))
58    }
59}
60
61/// Compare scores descending, lower ID wins ties.
62#[inline(always)]
63pub fn cmp_desc_then_id<T: Ord>(
64    a_score: DeterministicScore,
65    a_id: &T,
66    b_score: DeterministicScore,
67    b_id: &T,
68) -> Ordering {
69    b_score.cmp(&a_score).then_with(|| a_id.cmp(b_id))
70}
71
72/// Compare scores ascending, lower ID wins ties.
73#[inline(always)]
74pub fn cmp_asc_then_id<T: Ord>(
75    a_score: DeterministicScore,
76    a_id: &T,
77    b_score: DeterministicScore,
78    b_id: &T,
79) -> Ordering {
80    a_score.cmp(&b_score).then_with(|| a_id.cmp(b_id))
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86    use std::collections::BinaryHeap;
87
88    #[test]
89    fn ranked_heap_determinism() {
90        let mut heap: BinaryHeap<Ranked<u64>> = BinaryHeap::new();
91        heap.push(Ranked::new(DeterministicScore::from_f64(0.95), 3));
92        heap.push(Ranked::new(DeterministicScore::from_f64(0.95), 1));
93        heap.push(Ranked::new(DeterministicScore::from_f64(0.95), 2));
94        heap.push(Ranked::new(DeterministicScore::from_f64(0.87), 4));
95
96        let results: Vec<_> = std::iter::from_fn(|| heap.pop()).collect();
97        assert_eq!(results[0].id(), &1);
98        assert_eq!(results[1].id(), &2);
99        assert_eq!(results[2].id(), &3);
100        assert_eq!(results[3].id(), &4);
101    }
102
103    #[test]
104    fn cmp_desc() {
105        let mut items = [
106            (DeterministicScore::from_f64(0.9), 2u64),
107            (DeterministicScore::from_f64(0.9), 1u64),
108            (DeterministicScore::from_f64(0.8), 3u64),
109        ];
110        items.sort_by(|(sa, ia), (sb, ib)| cmp_desc_then_id(*sa, ia, *sb, ib));
111        assert_eq!(items[0].1, 1);
112        assert_eq!(items[1].1, 2);
113        assert_eq!(items[2].1, 3);
114    }
115
116    #[test]
117    fn cmp_asc_then_id_tie_lower_id_wins() {
118        let score = DeterministicScore::from_f64(0.5);
119        let mut items = [(score, 3u64), (score, 1u64), (score, 2u64)];
120        items.sort_by(|(sa, ia), (sb, ib)| cmp_asc_then_id(*sa, ia, *sb, ib));
121        assert_eq!(items[0].1, 1);
122        assert_eq!(items[1].1, 2);
123        assert_eq!(items[2].1, 3);
124    }
125
126    #[test]
127    fn ranked_into_parts_returns_score_and_id() {
128        let score = DeterministicScore::from_f64(0.75);
129        let ranked = Ranked::new(score, 42u64);
130        let (s, id) = ranked.into_parts();
131        assert_eq!(s, score);
132        assert_eq!(id, 42u64);
133    }
134
135    // ── Ranked Ord is max-heap adapter, not natural sort order ───────────────
136
137    /// `BinaryHeap<Ranked<_>>` must pop best (highest score) first.
138    #[test]
139    fn ranked_heap_pops_highest_score_first() {
140        use std::collections::BinaryHeap;
141        let mut heap = BinaryHeap::new();
142        heap.push(Ranked::new(DeterministicScore::from_f64(0.3), 3u64));
143        heap.push(Ranked::new(DeterministicScore::from_f64(0.9), 1u64));
144        heap.push(Ranked::new(DeterministicScore::from_f64(0.5), 2u64));
145
146        let first = heap.pop().unwrap();
147        assert_eq!(first.score(), DeterministicScore::from_f64(0.9));
148        assert_eq!(first.id(), &1u64);
149    }
150
151    /// `Vec<Ranked<_>>::sort()` produces ascending order (lowest score first)
152    /// because `Ranked::Ord` is a max-heap adapter.  This test documents that
153    /// behaviour — callers who need descending order MUST use `cmp_desc_then_id`.
154    #[test]
155    fn ranked_vec_sort_is_ascending_not_ranking_order() {
156        let mut items = [
157            Ranked::new(DeterministicScore::from_f64(0.9), 1u64),
158            Ranked::new(DeterministicScore::from_f64(0.3), 3u64),
159            Ranked::new(DeterministicScore::from_f64(0.5), 2u64),
160        ];
161        items.sort();
162        // sort() gives ascending (lowest score first) because Ord is max-heap-adapted.
163        assert_eq!(items[0].score(), DeterministicScore::from_f64(0.3));
164        assert_eq!(items[2].score(), DeterministicScore::from_f64(0.9));
165    }
166
167    /// To get descending (ranking) order, use `cmp_desc_then_id`.
168    #[test]
169    fn cmp_desc_then_id_gives_descending_order() {
170        let mut items: Vec<(DeterministicScore, u64)> = vec![
171            (DeterministicScore::from_f64(0.3), 3),
172            (DeterministicScore::from_f64(0.9), 1),
173            (DeterministicScore::from_f64(0.5), 2),
174        ];
175        items.sort_unstable_by(|(sa, ia), (sb, ib)| cmp_desc_then_id(*sa, ia, *sb, ib));
176        assert_eq!(items[0].1, 1); // 0.9 first
177        assert_eq!(items[1].1, 2); // 0.5 second
178        assert_eq!(items[2].1, 3); // 0.3 last
179    }
180}