use num_rational::BigRational;
use crate::ballot::{Ballot, PairPreference};
use crate::error::Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(transparent)]
pub struct Candidate(pub usize);
impl From<usize> for Candidate {
fn from(index: usize) -> Self {
Candidate(index)
}
}
impl From<Candidate> for usize {
fn from(candidate: Candidate) -> Self {
candidate.0
}
}
impl std::fmt::Display for Candidate {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct Lottery(Vec<BigRational>);
impl Lottery {
pub fn new(probabilities: Vec<BigRational>) -> Self {
Self(probabilities)
}
pub fn len(&self) -> usize {
self.0.len()
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn get(&self, candidate: Candidate) -> Option<&BigRational> {
self.0.get(candidate.0)
}
pub fn probabilities(&self) -> &[BigRational] {
&self.0
}
pub fn into_inner(self) -> Vec<BigRational> {
self.0
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MarginMatrix(Vec<Vec<i64>>);
impl MarginMatrix {
pub fn from_vec(margins: Vec<Vec<i64>>) -> Result<Self, Error> {
let n = margins.len();
for (i, row) in margins.iter().enumerate() {
if row.len() != n {
return Err(Error::InvalidMargins);
}
if row[i] != 0 {
return Err(Error::InvalidMargins);
}
for (j, &entry) in row.iter().enumerate() {
if margins[j][i] != -entry {
return Err(Error::InvalidMargins);
}
}
}
Ok(Self(margins))
}
pub fn n_candidates(&self) -> usize {
self.0.len()
}
pub fn margins(&self) -> &[Vec<i64>] {
&self.0
}
pub fn get(&self, i: Candidate, j: Candidate) -> Option<i64> {
self.0.get(i.0)?.get(j.0).copied()
}
pub fn condorcet_winner(&self) -> Option<Candidate> {
let n = self.n_candidates();
(0..n)
.find(|&i| (0..n).all(|j| i == j || self.0[i][j] > 0))
.map(Candidate)
}
}
#[derive(Debug, Clone)]
pub struct PreferenceProfile<B: Ballot> {
n: usize,
ballots: Vec<B>,
}
impl<B: Ballot> PreferenceProfile<B> {
pub fn try_new(ballots: Vec<B>) -> Result<Self, Error> {
let n = ballots.first().map(Ballot::n_candidates).unwrap_or(0);
if ballots.iter().any(|b| b.n_candidates() != n) {
return Err(Error::InconsistentCandidateCount);
}
Ok(Self { n, ballots })
}
pub fn n_candidates(&self) -> usize {
self.n
}
pub fn ballots(&self) -> &[B] {
&self.ballots
}
pub fn tally_margins(&self) -> MarginMatrix {
let n = self.n;
let mut margins = vec![vec![0i64; n]; n];
for ballot in &self.ballots {
for (i, j) in unordered_pairs(n) {
match ballot.preference(Candidate(i), Candidate(j)) {
PairPreference::Left => {
margins[i][j] += 1;
margins[j][i] -= 1;
}
PairPreference::Right => {
margins[i][j] -= 1;
margins[j][i] += 1;
}
PairPreference::Abstain => {}
}
}
}
MarginMatrix::from_vec(margins).expect("internally constructed margins are valid")
}
}
fn unordered_pairs(n: usize) -> impl Iterator<Item = (usize, usize)> {
(0..n).flat_map(move |i| (i + 1..n).map(move |j| (i, j)))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ballot::{PairPreference, PairwiseBallot};
#[test]
fn test_empty_profile() {
let profile = PreferenceProfile::<PairwiseBallot>::try_new(vec![]).unwrap();
assert_eq!(profile.n_candidates(), 0);
}
#[test]
fn test_inconsistent_candidate_count() {
let b1 = PairwiseBallot::new(3);
let b2 = PairwiseBallot::new(4);
assert!(PreferenceProfile::try_new(vec![b1, b2]).is_err());
}
#[test]
fn test_tally_empty() {
let profile = PreferenceProfile::<PairwiseBallot>::try_new(vec![]).unwrap();
let margins = profile.tally_margins();
assert_eq!(margins.margins(), &Vec::<Vec<i64>>::new());
}
#[test]
fn test_tally_single_ballot() {
let n = 3;
let ballot = PairwiseBallot::from_pairs(
&[
(Candidate(0), Candidate(1), PairPreference::Left),
(Candidate(0), Candidate(2), PairPreference::Left),
(Candidate(1), Candidate(2), PairPreference::Left),
],
n,
)
.unwrap();
let profile = PreferenceProfile::try_new(vec![ballot]).unwrap();
let margins = profile.tally_margins();
assert_eq!(margins.get(Candidate(0), Candidate(1)), Some(1));
assert_eq!(margins.get(Candidate(1), Candidate(0)), Some(-1));
assert_eq!(margins.get(Candidate(0), Candidate(2)), Some(1));
assert_eq!(margins.get(Candidate(2), Candidate(0)), Some(-1));
assert_eq!(margins.get(Candidate(1), Candidate(2)), Some(1));
assert_eq!(margins.get(Candidate(2), Candidate(1)), Some(-1));
}
#[test]
fn test_margin_matrix_validation() {
let valid = vec![vec![0, 1, -1], vec![-1, 0, 1], vec![1, -1, 0]];
assert!(MarginMatrix::from_vec(valid).is_ok());
let bad_diag = vec![vec![1, 0], vec![0, 0]];
assert!(MarginMatrix::from_vec(bad_diag).is_err());
let not_skew = vec![vec![0, 1], vec![1, 0]];
assert!(MarginMatrix::from_vec(not_skew).is_err());
}
#[test]
fn test_condorcet_winner() {
let margins =
MarginMatrix::from_vec(vec![vec![0, 1, 2], vec![-1, 0, 1], vec![-2, -1, 0]]).unwrap();
assert_eq!(margins.condorcet_winner(), Some(Candidate(0)));
}
#[test]
fn test_no_condorcet() {
let margins =
MarginMatrix::from_vec(vec![vec![0, 1, -1], vec![-1, 0, 1], vec![1, -1, 0]]).unwrap();
assert_eq!(margins.condorcet_winner(), None);
}
}