use std::cmp::Ordering;
use std::collections::BTreeMap;
use super::{AgendaRanking, Ballot, PlacedProposal, SeatRanking};
use crate::ids::PostId;
impl SeatRanking {
pub fn render(&self) -> String {
let list: Vec<String> =
self.ranking.iter().map(|n| format!("P{n}")).collect();
format!(
"Ranking: {} — this sitting can hear {}.",
list.join(", "),
self.sitting_capacity
)
}
}
impl AgendaRanking {
pub fn docket(&self) -> impl Iterator<Item = &PlacedProposal> {
self.order.iter().take(self.cut as usize)
}
pub fn aggregate(
candidates: &BTreeMap<u32, PostId>,
ballots: Vec<Ballot>,
) -> Self {
let k = candidates.len() as u32;
#[derive(Default)]
struct Tally {
borda: u32,
seats: u32,
position_sum: u32,
}
let mut tallies: BTreeMap<u32, Tally> = BTreeMap::new();
for ballot in &ballots {
for (i, number) in ballot.ranking.iter().enumerate() {
if !candidates.contains_key(number) {
continue;
}
let position = i as u32 + 1;
let t = tallies.entry(*number).or_default();
t.borda += k.saturating_sub(position) + 1;
t.seats += 1;
t.position_sum += position;
}
}
let compare = |a: &(u32, &Tally), b: &(u32, &Tally)| -> Ordering {
b.1.borda
.cmp(&a.1.borda)
.then(b.1.seats.cmp(&a.1.seats))
.then(
(a.1.position_sum * b.1.seats)
.cmp(&(b.1.position_sum * a.1.seats)),
)
};
let mut ranked: Vec<(u32, &Tally)> =
tallies.iter().map(|(n, t)| (*n, t)).collect();
ranked.sort_by(|a, b| compare(a, b).then(a.0.cmp(&b.0)));
let order: Vec<PlacedProposal> = ranked
.iter()
.enumerate()
.map(|(i, (number, t))| PlacedProposal {
number: *number,
post_id: candidates[number],
borda: t.borda,
seats: t.seats,
position_sum: t.position_sum,
tied_with_next: ranked.get(i + 1).is_some_and(|next| {
compare(&ranked[i], next) == Ordering::Equal
}),
})
.collect();
let mut capacities: Vec<u32> =
ballots.iter().map(|b| b.sitting_capacity).collect();
capacities.sort_unstable();
let cut = capacities
.get(capacities.len().saturating_sub(1) / 2)
.copied()
.unwrap_or(0);
let contenders: Vec<u32> = order.iter().map(|p| p.number).collect();
let condorcet_winner = condorcet_winner(&contenders, &ballots);
Self {
rankable: k,
ballots,
order,
cut,
condorcet_winner,
}
}
}
fn condorcet_winner(contenders: &[u32], ballots: &[Ballot]) -> Option<u32> {
let position =
|ballot: &Ballot, n: u32| ballot.ranking.iter().position(|&x| x == n);
let prefers = |a: u32, b: u32| -> u32 {
ballots
.iter()
.filter(|ballot| match (position(ballot, a), position(ballot, b)) {
(Some(pa), Some(pb)) => pa < pb,
(Some(_), None) => true,
(None, Some(_)) | (None, None) => false,
})
.count() as u32
};
contenders.iter().copied().find(|&a| {
contenders
.iter()
.filter(|&&b| b != a)
.all(|&b| prefers(a, b) > prefers(b, a))
})
}
#[cfg(test)]
mod tests;