use hashbrown::HashMap;
use num_traits::{Num, NumCast};
use petgraph::Graph;
use super::condorcet::CondorcetTally;
use super::errors::TallyError;
use super::plurality::PluralityTally;
use super::result::CountedCandidates;
use super::result::RankedWinners;
use super::Numeric;
use std::hash::Hash;
use std::ops::AddAssign;
pub enum Variant {
Winning,
Margin,
Ratio,
}
pub type DefaultSchulzeTally<T> = SchulzeTally<T, u64>;
pub struct SchulzeTally<T, C = u64>
where
T: Eq + Clone + Hash, C: Copy + PartialOrd + AddAssign + Num + NumCast, {
variant: Variant,
condorcet: CondorcetTally<T, C>,
}
impl<T, C> SchulzeTally<T, C>
where
T: Eq + Clone + Hash, C: Copy + PartialOrd + AddAssign + Num + NumCast, {
pub fn new(num_winners: u32, variant: Variant) -> Self {
Self::check_types(&variant);
SchulzeTally {
variant: variant,
condorcet: CondorcetTally::new(num_winners),
}
}
pub fn with_candidates(num_winners: u32, variant: Variant, candidates: Vec<T>) -> Self {
Self::check_types(&variant);
SchulzeTally {
variant: variant,
condorcet: CondorcetTally::with_candidates(num_winners, candidates),
}
}
pub fn unchecked(mut self) -> Self {
self.condorcet = self.condorcet.unchecked();
self
}
pub fn add_candidate(&mut self, candidate: T) {
self.condorcet.add_candidate(candidate);
}
pub fn add_candidates(&mut self, candidates: Vec<T>) {
self.condorcet.add_candidates(candidates);
}
pub fn add(&mut self, selection: &[T]) -> Result<(), TallyError> {
self.condorcet.add(selection)
}
pub fn add_weighted(&mut self, selection: &[T], weight: C) -> Result<(), TallyError> {
self.condorcet.add_weighted(selection, weight)
}
pub fn ranked_add(&mut self, vote: &[(T, u32)]) -> Result<(), TallyError> {
self.condorcet.ranked_add(vote)
}
pub fn ranked_add_weighted(&mut self, vote: &[(T, u32)], weight: C) -> Result<(), TallyError> {
self.condorcet.ranked_add_weighted(vote, weight)
}
pub fn candidates(&self) -> Vec<T> {
self.condorcet.candidates()
}
pub fn totals(&self) -> Vec<((T, T), C)> {
self.condorcet.totals()
}
pub fn strongest_paths(&self) -> Vec<((T, T), C)> {
let zero = C::zero();
let mut p = HashMap::<(usize, usize), C>::new();
for i in self.condorcet.candidates.values() {
for j in self.condorcet.candidates.values() {
if i != j {
let dij = self.condorcet.running_total.get(&(*i, *j)).unwrap_or(&zero);
let dji = self.condorcet.running_total.get(&(*j, *i)).unwrap_or(&zero);
if dij > dji {
let strength = match self.variant {
Variant::Winning => *dij,
Variant::Margin => *dij - *dji,
Variant::Ratio => {
if dji != &zero {
*dij / *dji
} else {
C::max_value()
}
}
};
p.insert((*i, *j), strength);
} else {
p.insert((*i, *j), zero);
}
}
}
}
for i in self.condorcet.candidates.values() {
for j in self.condorcet.candidates.values() {
if i != j {
for k in self.condorcet.candidates.values() {
if i != k && j != k {
let pji = p.get(&(*j, *i)).unwrap_or(&zero);
let pik = p.get(&(*i, *k)).unwrap_or(&zero);
let pjk = p.get(&(*j, *k)).unwrap_or(&zero);
let min = if pji < pik { pji } else { pik };
let max = if pjk > min { *pjk } else { *min };
p.insert((*j, *k), max);
}
}
}
}
}
let mut strongest = Vec::<((T, T), C)>::with_capacity(self.condorcet.running_total.len());
let mut candidates = HashMap::<usize, T>::with_capacity(self.condorcet.candidates.len());
for (candidate, i) in self.condorcet.candidates.iter() {
candidates.insert(*i, candidate.clone());
}
for ((candidate1, candidate2), strength) in p.iter() {
let candidate1 = candidates.get(candidate1).unwrap().clone();
let candidate2 = candidates.get(candidate2).unwrap().clone();
strongest.push(((candidate1, candidate2), *strength));
}
strongest
}
pub(crate) fn get_counted(&self) -> CountedCandidates<T, C> {
let mut strongest = self.strongest_paths();
let mut strongest_hash = HashMap::<(T, T), C>::with_capacity(strongest.len());
for ((candidate_1, candidate_2), strength) in strongest.drain(..) {
strongest_hash.insert((candidate_1, candidate_2), strength);
}
let mut running_total = PluralityTally::with_capacity(self.condorcet.num_winners, self.condorcet.candidates.len());
let zero = C::zero();
for ((candidate_1, candidate_2), strength_1) in strongest_hash.iter() {
let strength_2 = strongest_hash.get(&(candidate_2.clone(), candidate_1.clone())).unwrap_or(&zero);
if strength_1 >= strength_2 {
running_total.add_ref(candidate_1);
} else {
running_total.add_weighted_ref(candidate_1, C::zero());
}
}
running_total.get_counted()
}
pub fn ranked(&self) -> Vec<(T, u32)> {
self.get_counted().into_ranked(0).into_vec()
}
pub fn winners(&self) -> RankedWinners<T> {
self.get_counted().into_ranked(self.condorcet.num_winners)
}
pub fn build_graph(&self) -> Graph<T, (C, C)> {
self.condorcet.build_graph()
}
fn check_types(variant: &Variant) {
if let Variant::Ratio = variant {
if !C::fraction() || C::max_value() == C::zero() {
panic!("tallystick::schulze: Variant::Ratio must be used with a type that is bounded and fractional.");
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::util;
use std::io::Cursor;
#[test]
fn schulze_basic() -> Result<(), TallyError> {
let mut tally = DefaultSchulzeTally::<&str>::new(1, Variant::Winning);
tally.add_candidates(vec!["Notorious RBG", "Judge Judy", "Judge Dredd", "Abe Vigoda"]);
tally.add(&vec!["Notorious RBG", "Judge Judy"])?;
tally.add(&vec!["Judge Dredd"])?;
tally.add(&vec!["Abe Vigoda", "Notorious RBG"])?;
tally.add(&vec!["Notorious RBG", "Judge Dredd"])?;
assert_eq!(tally.winners().into_unranked()[0], "Notorious RBG");
Ok(())
}
#[test]
fn schulze_wikipedia() -> Result<(), TallyError> {
let mut tally = DefaultSchulzeTally::with_candidates(1, Variant::Winning, vec!["A", "B", "C", "D", "E"]);
tally.add_weighted(&vec!["A", "C", "B", "E", "D"], 5)?;
tally.add_weighted(&vec!["A", "D", "E", "C", "B"], 5)?;
tally.add_weighted(&vec!["B", "E", "D", "A", "C"], 8)?;
tally.add_weighted(&vec!["C", "A", "B", "E", "D"], 3)?;
tally.add_weighted(&vec!["C", "A", "E", "B", "D"], 7)?;
tally.add_weighted(&vec!["C", "B", "A", "D", "E"], 2)?;
tally.add_weighted(&vec!["D", "C", "E", "B", "A"], 7)?;
tally.add_weighted(&vec!["E", "B", "A", "D", "C"], 8)?;
let totals = tally.totals();
for (pairwise, total) in totals.iter() {
match pairwise {
("A", "B") => assert_eq!(*total, 20),
("A", "C") => assert_eq!(*total, 26),
("A", "D") => assert_eq!(*total, 30),
("A", "E") => assert_eq!(*total, 22),
("B", "A") => assert_eq!(*total, 25),
("B", "C") => assert_eq!(*total, 16),
("B", "D") => assert_eq!(*total, 33),
("B", "E") => assert_eq!(*total, 18),
("C", "A") => assert_eq!(*total, 19),
("C", "B") => assert_eq!(*total, 29),
("C", "D") => assert_eq!(*total, 17),
("C", "E") => assert_eq!(*total, 24),
("D", "A") => assert_eq!(*total, 15),
("D", "B") => assert_eq!(*total, 12),
("D", "C") => assert_eq!(*total, 28),
("D", "E") => assert_eq!(*total, 14),
("E", "A") => assert_eq!(*total, 23),
("E", "B") => assert_eq!(*total, 27),
("E", "C") => assert_eq!(*total, 21),
("E", "D") => assert_eq!(*total, 31),
_ => panic!("Invalid schulze total pairwise"),
}
}
let strongest = tally.strongest_paths();
for (pairwise, strength) in strongest.iter() {
match pairwise {
("A", "B") => assert_eq!(*strength, 28),
("A", "C") => assert_eq!(*strength, 28),
("A", "D") => assert_eq!(*strength, 30),
("A", "E") => assert_eq!(*strength, 24),
("B", "A") => assert_eq!(*strength, 25),
("B", "C") => assert_eq!(*strength, 28),
("B", "D") => assert_eq!(*strength, 33),
("B", "E") => assert_eq!(*strength, 24),
("C", "A") => assert_eq!(*strength, 25),
("C", "B") => assert_eq!(*strength, 29),
("C", "D") => assert_eq!(*strength, 29),
("C", "E") => assert_eq!(*strength, 24),
("D", "A") => assert_eq!(*strength, 25),
("D", "B") => assert_eq!(*strength, 28),
("D", "C") => assert_eq!(*strength, 28),
("D", "E") => assert_eq!(*strength, 24),
("E", "A") => assert_eq!(*strength, 25),
("E", "B") => assert_eq!(*strength, 28),
("E", "C") => assert_eq!(*strength, 28),
("E", "D") => assert_eq!(*strength, 31),
_ => panic!("Invalid schulze strength pairwise"),
}
}
let ranked = tally.ranked();
assert_eq!(ranked, vec![("E", 0), ("A", 1), ("C", 2), ("B", 3), ("D", 4)]);
Ok(())
}
#[test]
fn schulze_favorite_betrayal() -> Result<(), TallyError> {
let mut tally = DefaultSchulzeTally::with_candidates(1, Variant::Winning, vec!["A", "B", "C"]);
tally.add_weighted(&vec!["B", "C", "A"], 9)?;
tally.add_weighted(&vec!["C", "A", "B"], 6)?;
tally.add_weighted(&vec!["A", "B", "C"], 5)?;
assert_eq!(tally.winners().into_unranked()[0], "B");
let mut tally = DefaultSchulzeTally::with_candidates(1, Variant::Winning, vec!["A", "B", "C"]);
tally.add_weighted(&vec!["B", "C", "A"], 9)?;
tally.add_weighted(&vec!["A", "C", "B"], 6)?;
tally.add_weighted(&vec!["A", "B", "C"], 5)?;
assert_eq!(tally.winners().into_unranked()[0], "A");
let mut tally = DefaultSchulzeTally::with_candidates(1, Variant::Margin, vec!["A", "B", "C"]);
tally.add_weighted(&vec!["B", "C", "A"], 9)?;
tally.add_weighted(&vec!["A", "C", "B"], 6)?;
tally.add_weighted(&vec!["A", "B", "C"], 5)?;
assert_eq!(tally.winners().into_unranked()[0], "A");
let mut tally = DefaultSchulzeTally::with_candidates(1, Variant::Winning, vec!["A", "B", "C"]);
tally.add_weighted(&vec!["B", "C", "A"], 9)?;
tally.ranked_add_weighted(&vec![("A", 0), ("C", 0), ("B", 1)], 6)?;
tally.add_weighted(&vec!["A", "B", "C"], 5)?;
assert_eq!(tally.winners().into_unranked()[0], "A");
let mut tally = DefaultSchulzeTally::with_candidates(1, Variant::Margin, vec!["A", "B", "C"]);
tally.add_weighted(&vec!["B", "C", "A"], 9)?;
tally.ranked_add_weighted(&vec![("A", 0), ("C", 0), ("B", 1)], 6)?;
tally.add_weighted(&vec!["A", "B", "C"], 5)?;
assert_eq!(tally.winners().into_unranked()[0], "B");
Ok(())
}
#[test]
fn schulze_example_1() -> Result<(), TallyError> {
let candidates = vec!["A".to_string(), "B".to_string(), "C".to_string(), "D".to_string()];
let votes_raw = "
A > C > D > B * 8
B > A > D > C * 2
C > D > B > A * 4
D > B > A > C * 4
D > C > B > A * 3
";
let votes = Cursor::new(votes_raw);
let mut votes = util::read_votes(votes).unwrap();
let mut tally = DefaultSchulzeTally::with_candidates(1, Variant::Winning, candidates.clone());
for (vote, weight) in votes.drain(..) {
let vote = vote.into_ranked();
tally.ranked_add_weighted(&vote, weight)?;
}
assert_eq!(tally.winners().into_unranked()[0], "D".to_string());
Ok(())
}
#[test]
fn schulze_example_2() -> Result<(), TallyError> {
let candidates = vec!["A".to_string(), "B".to_string(), "C".to_string(), "D".to_string()];
let votes_raw = "
A > C > D > B * 3
B > A > C > D * 9
C > D > A > B * 8
D > A > B > C * 5
D > B > C > A * 5
";
let votes = Cursor::new(votes_raw);
let mut votes = util::read_votes(votes).unwrap();
let mut tally = DefaultSchulzeTally::with_candidates(1, Variant::Winning, candidates.clone());
for (vote, weight) in votes.drain(..) {
let vote = vote.into_ranked();
tally.ranked_add_weighted(&vote, weight)?;
}
assert_eq!(tally.winners().into_unranked()[0], "C".to_string());
Ok(())
}
#[test]
fn schulze_example_3() -> Result<(), TallyError> {
let candidates = vec!["A".to_string(), "B".to_string(), "C".to_string(), "D".to_string(), "E".to_string()];
let votes_raw = "
A > C > B > E > D * 5
A > D > E > C > B * 5
B > E > D > A > C * 8
C > A > B > E > D * 3
C > A > E > B > D * 7
C > B > A > D > E * 2
D > C > E > B > A * 7
E > B > A > D > C * 8
";
let votes = Cursor::new(votes_raw);
let mut votes = util::read_votes(votes).unwrap();
let mut tally = DefaultSchulzeTally::with_candidates(1, Variant::Winning, candidates.clone());
for (vote, weight) in votes.drain(..) {
let vote = vote.into_ranked();
tally.ranked_add_weighted(&vote, weight)?;
}
assert_eq!(tally.winners().into_unranked()[0], "E".to_string());
Ok(())
}
#[test]
fn schulze_example_4() -> Result<(), TallyError> {
let candidates = vec!["A".to_string(), "B".to_string(), "C".to_string(), "D".to_string()];
let votes_raw = "
A > B > C > D * 3
C > B > D > A * 2
D > A > B > C * 2
D > B > C > A * 2
";
let votes = Cursor::new(votes_raw);
let mut votes = util::read_votes(votes).unwrap();
let mut tally = DefaultSchulzeTally::with_candidates(1, Variant::Winning, candidates.clone());
for (vote, weight) in votes.drain(..) {
let vote = vote.into_ranked();
tally.ranked_add_weighted(&vote, weight)?;
}
let winners = tally.winners().into_unranked();
assert!(winners.contains(&"B".to_owned()) && winners.contains(&"D".to_owned()));
assert!(winners.len() == 2);
Ok(())
}
#[test]
fn schulze_example_5() -> Result<(), TallyError> {
let mut tally = DefaultSchulzeTally::with_candidates(1, Variant::Winning, vec!["a", "b", "c", "d"]);
tally.add_weighted(&vec!["a", "b", "c", "d"], 12)?;
tally.add_weighted(&vec!["a", "d", "b", "c"], 6)?;
tally.add_weighted(&vec!["b", "c", "d", "a"], 9)?;
tally.add_weighted(&vec!["c", "d", "a", "b"], 15)?;
tally.add_weighted(&vec!["d", "b", "a", "c"], 21)?;
let ranked = tally.ranked();
assert!(ranked == vec![("d", 0), ("a", 1), ("b", 1), ("c", 2)] || ranked == vec![("d", 0), ("b", 1), ("a", 1), ("c", 2)]);
Ok(())
}
#[test]
fn schulze_example_6() -> Result<(), TallyError> {
let candidates = vec!["A".to_string(), "B".to_string(), "C".to_string(), "D".to_string()];
let votes_raw = "
A > C > D * 6
B > A > D
C > B > D * 3
D > B > A * 3
D > C > B * 2
";
let votes = Cursor::new(votes_raw);
let mut votes = util::read_votes(votes).unwrap();
let mut tally = DefaultSchulzeTally::with_candidates(1, Variant::Winning, candidates.clone());
for (vote, weight) in votes.drain(..) {
let vote = vote.into_ranked();
tally.ranked_add_weighted(&vote, weight)?;
}
let winners = tally.winners().into_unranked();
assert!(winners.contains(&"A".to_owned()) && winners.contains(&"D".to_owned()));
assert!(winners.len() == 2);
Ok(())
}
#[test]
fn schulze_example_7() -> Result<(), TallyError> {
let candidates = vec![
"A".to_string(),
"B".to_string(),
"C".to_string(),
"D".to_string(),
"E".to_string(),
"F".to_string(),
];
let votes_raw = "
A > D > E > B > C > F * 3
B > F > E > C > D > A * 3
C > A > B > F > D > E * 4
D > B > C > E > F > A * 1
D > E > F > A > B > C * 4
E > C > B > D > F > A * 2
F > A > C > D > B > E * 2
";
let votes = Cursor::new(votes_raw);
let mut votes = util::read_votes(votes).unwrap();
let mut tally = DefaultSchulzeTally::with_candidates(1, Variant::Winning, candidates.clone());
for (vote, weight) in votes.drain(..) {
let vote = vote.into_ranked();
tally.ranked_add_weighted(&vote, weight)?;
}
assert_eq!(tally.winners().into_unranked()[0], "A".to_string());
tally.add_weighted(
&vec![
"A".to_string(),
"E".to_string(),
"F".to_string(),
"C".to_string(),
"B".to_string(),
"D".to_string(),
],
2,
)?;
assert_eq!(tally.winners().into_unranked()[0], "D".to_string());
Ok(())
}
#[test]
fn schulze_example_10() -> Result<(), TallyError> {
let candidates = vec!["A".to_string(), "B".to_string(), "C".to_string(), "D".to_string()];
let votes_raw = "
A > B > C > D * 6
A = B * 8
A = C * 8
A = C > D * 18
A = C = D * 8
B * 40
C > B > D * 4
C > D > A * 9
C = D * 8
D > A > B * 14
D > B > C * 11
D > C > A * 4";
let votes = Cursor::new(votes_raw);
let votes = util::read_votes(votes).unwrap();
let mut tally = DefaultSchulzeTally::with_candidates(1, Variant::Margin, candidates.clone());
for (vote, weight) in votes.iter() {
match vote {
util::ParsedVote::Ranked(v) => tally.ranked_add_weighted(v, *weight)?,
util::ParsedVote::Unranked(v) => tally.add_weighted(v, *weight)?,
}
}
assert_eq!(tally.winners().into_unranked()[0], "A".to_string());
let mut tally = DefaultSchulzeTally::with_candidates(1, Variant::Winning, candidates.clone());
for (vote, weight) in votes.iter() {
match vote {
util::ParsedVote::Ranked(v) => tally.ranked_add_weighted(v, *weight)?,
util::ParsedVote::Unranked(v) => tally.add_weighted(v, *weight)?,
}
}
assert_eq!(tally.winners().into_unranked()[0], "D".to_string());
let votes = Cursor::new(votes_raw);
let votes = util::read_votes(votes).unwrap(); let mut tally = SchulzeTally::<_, f64>::with_candidates(1, Variant::Ratio, candidates.clone());
for (vote, weight) in votes.iter() {
match vote {
util::ParsedVote::Ranked(v) => tally.ranked_add_weighted(v, *weight)?,
util::ParsedVote::Unranked(v) => tally.add_weighted(v, *weight)?,
}
}
assert_eq!(tally.winners().into_unranked()[0], "B".to_string());
Ok(())
}
}