mod cand_pair;
mod forced_dag;
use self::PairScore::*;
use crate::common::{ElectionError, PairScore};
use crate::outcome::GenericOutcome;
use crate::prng::Prng;
use crate::tally::{Tally, VoteMatrix};
use cand_pair::CandPair;
use forced_dag::ForcedDag;
pub struct RankedPairs<'a> {
tally: &'a VoteMatrix,
pair_score: PairScore,
seed: u32,
}
impl<'a> RankedPairs<'a> {
pub fn new(tally: &'a VoteMatrix) -> Self {
Self {
tally,
pair_score: Winning,
seed: 21,
}
}
pub fn pair_score(&mut self, pair_score: PairScore) -> &mut Self {
self.pair_score = pair_score;
self
}
pub fn seed(&mut self, seed: u32) -> &mut Self {
self.seed = seed;
self
}
pub fn run(&self) -> Result<GenericOutcome<'a>, ElectionError> {
if self.tally.get_candidates() < 1 {
return Err(ElectionError::NotEnoughCandidates);
}
let mut rnd = Prng::new(self.seed);
let mut pairs: Vec<CandPair> = self
.tally
.pairs()
.filter_map(|((runner, opponent), (win, opposition))| {
let score: u64 = match (win > opposition, self.pair_score) {
(true, PairScore::Margin) => win - opposition,
(true, PairScore::Winning) => win,
(_, PairScore::Opposition) => win,
_ => 0,
};
if win > opposition {
Some(CandPair::new(runner, opponent, score, opposition))
} else {
None
}
})
.collect();
pairs.sort_unstable();
let mut dag = ForcedDag::new(self.tally.get_candidates());
while !pairs.is_empty() {
let mut group: Vec<CandPair> = Vec::new();
while group.is_empty() || pairs.ends_with(&group[0..1]) {
group.push(pairs.pop().unwrap());
}
if group.len() == 1 {
for x in &group {
dag.add_edge(x.runner, x.opponent);
}
} else {
group.retain(|x| dag.try_edge(x.runner, x.opponent));
rnd.push();
rnd.shuffle(&mut group);
#[allow(clippy::unnecessary_fold)]
if group
.iter()
.fold(true, |flag, x| flag && dag.add_edge(x.runner, x.opponent))
{
rnd.pop().unwrap();
}
}
}
if let Ok(winner) = dag.get_winner() {
Ok(GenericOutcome::new(
Some(winner),
None,
self.tally.get_candidates(),
rnd.sealed(),
self.tally.get_meta(),
))
} else {
Err(ElectionError::DegeneratedElectionGraph)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Outcome;
#[test]
fn ranked_pairs_basic() {
let x = VoteMatrix::with_vector_bycol(
&vec![58, 58, 58, 42, 32, 32, 42, 68, 17, 42, 68, 83],
);
assert_eq!(
RankedPairs::new(&x)
.pair_score(PairScore::Margin)
.run()
.unwrap()
.winner()
.unwrap(),
1
);
}
#[test]
fn ranked_pairs_schulze_wiki() {
let x = VoteMatrix::with_vector_bycol(&vec![
25, 0, 0, 23, 0, 29, 0, 27, 26, 0, 28, 0, 30, 33, 0, 31, 0, 0, 24, 0,
]);
assert_eq!(
RankedPairs::new(&x)
.pair_score(PairScore::Winning)
.run()
.unwrap()
.winner()
.unwrap(),
0
);
}
#[test]
fn ranked_pairs_no_wins() {
let x = VoteMatrix::with_vector_bycol(&vec![3, 3, 3, 3, 3, 3]);
let ans = RankedPairs::new(&x).pair_score(PairScore::Margin).run();
match ans.unwrap_err() {
ElectionError::DegeneratedElectionGraph => (),
_ => unreachable!(),
};
}
#[test]
fn ranked_pairs_co2() {
let x = VoteMatrix::with_vector_bycol(&vec![0, 0, 1, 1, 0, 0]);
let ans = RankedPairs::new(&x).pair_score(PairScore::Margin).run();
match ans.unwrap_err() {
ElectionError::DegeneratedElectionGraph => (),
_ => unreachable!(),
};
}
#[test]
fn ranked_pairs_determinism() {
let x = VoteMatrix::with_vector_bycol(&vec![0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1]);
assert!(RankedPairs::new(&x).run().unwrap().deterministic());
let x = VoteMatrix::with_vector_bycol(&vec![0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1]);
assert!(!RankedPairs::new(&x).run().unwrap().deterministic());
let x = VoteMatrix::with_vector_bycol(&vec![0, 0, 1, 1, 0, 0, 0, 1, 2, 0, 0, 1]);
assert!(RankedPairs::new(&x).run().unwrap().deterministic());
let x = VoteMatrix::with_vector_bycol(&vec![0, 1, 0, 2, 0, 0, 0, 0, 2, 0, 1, 0]);
assert!(!RankedPairs::new(&x).run().unwrap().deterministic());
let x = VoteMatrix::with_vector_bycol(&vec![
0, 1, 0, 0, 2, 0, 1, 0, 0, 2, 0, 1, 1, 0, 2, 0, 0, 1, 0, 2,
]);
assert!(RankedPairs::new(&x).run().unwrap().deterministic());
}
#[test]
fn no_cands() {
use crate::ElectionError;
let x: VoteMatrix = VoteMatrix::with_vector_bycol(&vec![]);
match RankedPairs::new(&x).run().unwrap_err() {
ElectionError::NotEnoughCandidates => (),
_ => unreachable!(),
}
}
}