1use std::collections::{HashMap, HashSet};
15use std::hash::Hash;
16
17pub type ScoredBallot<T> = Vec<(T, f64)>;
19
20enum Comb {
21 Sum,
22 Mnz,
23}
24
25pub fn combsum_merge<T>(ballots: &[ScoredBallot<T>]) -> Vec<T>
27where
28 T: Clone + Eq + Hash,
29{
30 comb_merge(ballots, Comb::Sum)
31}
32
33pub fn combmnz_merge<T>(ballots: &[ScoredBallot<T>]) -> Vec<T>
35where
36 T: Clone + Eq + Hash,
37{
38 comb_merge(ballots, Comb::Mnz)
39}
40
41fn comb_merge<T>(ballots: &[ScoredBallot<T>], kind: Comb) -> Vec<T>
42where
43 T: Clone + Eq + Hash,
44{
45 if ballots.is_empty() {
46 return Vec::new();
47 }
48 let mut scores: HashMap<T, f64> = HashMap::new();
49 let mut support: HashMap<T, usize> = HashMap::new();
50 let mut first_seen: Vec<T> = Vec::new();
51 for ballot in ballots {
52 for (id, norm) in normalize_list(ballot) {
53 if !scores.contains_key(&id) {
54 first_seen.push(id.clone());
55 }
56 *scores.entry(id.clone()).or_insert(0.0) += norm;
57 *support.entry(id).or_insert(0) += 1;
58 }
59 }
60 if matches!(kind, Comb::Mnz) {
61 for (id, score) in scores.iter_mut() {
62 *score *= *support.get(id).unwrap_or(&0) as f64;
63 }
64 }
65 let mut ranked = first_seen;
66 ranked.sort_by(|a, b| scores[b].total_cmp(&scores[a]));
67 ranked
68}
69
70fn normalize_list<T>(ballot: &ScoredBallot<T>) -> Vec<(T, f64)>
71where
72 T: Clone + Eq + Hash,
73{
74 let mut seen = HashSet::new();
75 let mut items: Vec<(T, f64)> = Vec::new();
76 for (id, raw) in ballot {
77 if !raw.is_finite() || !seen.insert(id.clone()) {
78 continue;
79 }
80 items.push((id.clone(), *raw));
81 }
82 if items.is_empty() {
83 return Vec::new();
84 }
85 let min = items.iter().map(|(_, s)| *s).fold(f64::INFINITY, f64::min);
86 let max = items
87 .iter()
88 .map(|(_, s)| *s)
89 .fold(f64::NEG_INFINITY, f64::max);
90 let span = max - min;
91 if span == 0.0 {
92 return items.into_iter().map(|(id, _)| (id, 1.0)).collect();
93 }
94 items
95 .into_iter()
96 .map(|(id, s)| (id, (s - min) / span))
97 .collect()
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103
104 #[test]
105 fn combsum_two_lists_tie_after_minmax() {
106 let a = vec![("x", 1.0), ("y", 0.2)];
107 let b = vec![("x", 0.1), ("y", 0.9)];
108 let out = combsum_merge(&[a, b]);
109 assert_eq!(out, vec!["x", "y"]);
111 }
112
113 #[test]
114 fn combmnz_lifts_double_hit_over_single_list_champion() {
115 let a = vec![("c", 1.0), ("d", 0.4), ("low", 0.0)];
116 let b = vec![("hi", 1.0), ("d", 0.25), ("lo", 0.0)];
117 let sum = combsum_merge(&[a.clone(), b.clone()]);
119 assert_eq!(sum[0], "c");
120 let mnz = combmnz_merge(&[a, b]);
122 assert_eq!(mnz[0], "d");
123 }
124
125 #[test]
126 fn missing_list_is_zero() {
127 let a = vec![("only", 1.0), ("pad", 0.0)];
128 let b: ScoredBallot<&str> = vec![];
129 let out = combsum_merge(&[a, b]);
130 assert_eq!(out[0], "only");
131 }
132}