use super::check_duplicate;
use super::plurality::PluralityTally;
use super::result::CountedCandidates;
use super::result::RankedCandidate;
use super::result::RankedWinners;
use super::Numeric;
use super::TallyError;
use hashbrown::HashMap;
use hashbrown::HashSet;
use num_traits::Num;
use num_traits::NumCast;
use std::hash::Hash;
use std::ops::AddAssign;
const C_FROM_PANIC: &str = "Cannot convert integer to C, this is likely caused by a bug in the ToPrimitive impl for the count type.";
pub enum Variant<C> {
Borda,
ClassicBorda,
Dowdall,
ModifiedClassicBorda,
Custom(Box<dyn Fn(usize, usize, usize) -> C>),
}
impl<C: Numeric + Num + NumCast> Variant<C> {
pub fn points(&self, candidate_position: usize, num_candidates: usize, num_marked: usize) -> C {
match self {
Variant::Borda => C::from(num_candidates - candidate_position - 1).expect(C_FROM_PANIC),
Variant::ClassicBorda => C::from(num_candidates - candidate_position).expect(C_FROM_PANIC),
Variant::Dowdall => {
if !C::fraction() {
panic!(
"tallystick::borda::Variant::Dowdall cannot be used with an integer count type. Please use a float or a rational."
)
}
C::one() / C::from(candidate_position + 1).expect(C_FROM_PANIC)
}
Variant::ModifiedClassicBorda => C::from(num_marked - candidate_position).expect(C_FROM_PANIC),
Variant::Custom(boxed_func) => boxed_func(candidate_position, num_candidates, num_marked),
}
}
}
pub type DefaultBordaTally<T> = BordaTally<T, u64>;
pub struct BordaTally<T, C = u64>
where
T: Eq + Clone + Hash, C: Copy + PartialOrd + AddAssign + Num + NumCast, {
running_total: HashMap<Vec<T>, C>,
candidates: HashSet<T>,
num_winners: usize,
variant: Variant<C>,
}
impl<T, C> BordaTally<T, C>
where
T: Eq + Clone + Hash, C: Copy + PartialOrd + AddAssign + Num + NumCast, {
pub fn new(num_winners: usize, variant: Variant<C>) -> Self {
BordaTally {
running_total: HashMap::new(),
candidates: HashSet::new(),
num_winners: num_winners,
variant: variant,
}
}
pub fn with_capacity(num_winners: usize, variant: Variant<C>, expected_candidates: usize) -> Self {
BordaTally {
running_total: HashMap::with_capacity(expected_candidates),
candidates: HashSet::with_capacity(expected_candidates),
num_winners: num_winners,
variant: variant,
}
}
pub fn add(&mut self, vote: Vec<T>) -> Result<(), TallyError> {
self.add_weighted(vote, C::one())
}
pub fn add_ref(&mut self, vote: &[T]) -> Result<(), TallyError> {
self.add_weighted_ref(vote, C::one())
}
pub fn add_weighted(&mut self, vote: Vec<T>, weight: C) -> Result<(), TallyError> {
check_duplicate(&vote)?;
for candidate in vote.iter() {
if !self.candidates.contains(candidate) {
self.candidates.insert(candidate.clone());
}
}
let entry = self.running_total.entry(vote);
*entry.or_insert(C::zero()) += weight;
Ok(())
}
pub fn add_weighted_ref(&mut self, vote: &[T], weight: C) -> Result<(), TallyError> {
check_duplicate(vote)?;
for candidate in vote.iter() {
if !self.candidates.contains(candidate) {
self.candidates.insert(candidate.clone());
}
}
let entry = self.running_total.entry(vote.to_vec());
*entry.or_insert(C::zero()) += weight;
Ok(())
}
pub fn winners(&self) -> RankedWinners<T> {
let mut counted = CountedCandidates::new();
for (candidate, votecount) in self.totals().iter() {
counted.push(candidate.clone(), *votecount);
}
counted.into_ranked(self.num_winners)
}
pub fn ranked(&self) -> Vec<RankedCandidate<T>> {
let mut counted = CountedCandidates::new();
for (candidate, votecount) in self.totals().iter() {
counted.push(candidate.clone(), *votecount);
}
counted.into_ranked(0).into_vec()
}
pub fn totals(&self) -> Vec<(T, C)> {
let mut plurality = PluralityTally::with_capacity(self.num_winners, self.candidates.len());
for (selection, votecount) in self.running_total.iter() {
let num_marked = selection.len();
for (position, candidate) in selection.iter().enumerate() {
let points: C = self.variant.points(position, self.candidates.len(), num_marked);
plurality.add_weighted_ref(candidate, *votecount * points);
}
}
plurality.totals()
}
pub fn candidates(&self) -> Vec<T> {
self.candidates.iter().cloned().collect()
}
}
#[allow(dead_code)]
pub type DefaultNansonTally<T> = NansonTally<T, u64>;
#[allow(dead_code)]
pub struct NansonTally<T, C = u64>
where
T: Eq + Clone + Hash, C: Copy + PartialOrd + AddAssign + Num + NumCast, {
borda: BordaTally<T, C>,
}
#[allow(dead_code)]
pub type DefaultBaldwinTally<T> = BaldwinTally<T, u64>;
#[allow(dead_code)]
pub struct BaldwinTally<T, C = u64>
where
T: Eq + Clone + Hash, C: Copy + PartialOrd + AddAssign + Num + NumCast, {
borda: BordaTally<T, C>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn borda_test() -> Result<(), TallyError> {
let mut borda_tally = DefaultBordaTally::new(1, Variant::Borda);
borda_tally.add_weighted(vec!["Andrew", "Catherine", "Brian", "David"], 51)?;
borda_tally.add_weighted(vec!["Catherine", "Brian", "David", "Andrew"], 5)?;
borda_tally.add_weighted(vec!["Brian", "Catherine", "David", "Andrew"], 23)?;
borda_tally.add_weighted(vec!["David", "Catherine", "Brian", "Andrew"], 21)?;
assert!(borda_tally.totals() == vec![("Catherine", 205), ("Andrew", 153), ("Brian", 151), ("David", 91)]);
assert!(borda_tally.winners().into_unranked() == vec!["Catherine"]);
let mut classic_tally = DefaultBordaTally::new(1, Variant::ClassicBorda);
classic_tally.add_weighted(vec!["Andrew", "Catherine", "Brian", "David"], 51)?;
classic_tally.add_weighted(vec!["Catherine", "Brian", "David", "Andrew"], 5)?;
classic_tally.add_weighted(vec!["Brian", "Catherine", "David", "Andrew"], 23)?;
classic_tally.add_weighted(vec!["David", "Catherine", "Brian", "Andrew"], 21)?;
assert!(classic_tally.totals() == vec![("Catherine", 305), ("Andrew", 253), ("Brian", 251), ("David", 191)]);
assert!(classic_tally.winners().into_unranked() == vec!["Catherine"]);
let mut dowdall_tally = BordaTally::<&str, f64>::new(1, Variant::Dowdall);
dowdall_tally.add_weighted(vec!["Andrew", "Catherine", "Brian", "David"], 51.0)?;
dowdall_tally.add_weighted(vec!["Catherine", "Brian", "David", "Andrew"], 5.0)?;
dowdall_tally.add_weighted(vec!["Brian", "Catherine", "David", "Andrew"], 23.0)?;
dowdall_tally.add_weighted(vec!["David", "Catherine", "Brian", "Andrew"], 21.0)?;
let totals = dowdall_tally.totals();
assert_eq!(totals[0], ("Andrew", 63.25));
assert_eq!(totals[1], ("Catherine", 52.5));
assert_eq!(totals[2], ("Brian", 49.5));
assert_eq!(totals[3].0, "David"); assert!(dowdall_tally.winners().into_unranked() == vec!["Andrew"]);
let mut tally = DefaultBordaTally::new(1, Variant::Borda);
tally.add_weighted(vec!["Memphis", "Nashville", "Chattanooga", "Knoxville"], 42)?;
tally.add_weighted(vec!["Nashville", "Chattanooga", "Knoxville", "Memphis"], 26)?;
tally.add_weighted(vec!["Chattanooga", "Knoxville", "Nashville", "Memphis"], 15)?;
tally.add_weighted(vec!["Knoxville", "Chattanooga", "Nashville", "Memphis"], 17)?;
assert!(tally.totals() == vec![("Nashville", 194), ("Chattanooga", 173), ("Memphis", 126), ("Knoxville", 107)]);
assert!(tally.winners().into_unranked() == vec!["Nashville"]);
let mut tally = DefaultBordaTally::new(1, Variant::ModifiedClassicBorda);
tally.add(vec!["Alice", "Bob", "Carlos"])?;
tally.add(vec!["Alice", "Bob"])?;
tally.add(vec!["Bob", "Carlos"])?;
let totals = tally.totals();
assert!(totals == vec![("Alice", 5), ("Bob", 5), ("Carlos", 2)] || totals == vec![("Bob", 5), ("Alice", 5), ("Carlos", 2)]);
let ranked = tally.ranked();
assert!(ranked == vec![("Alice", 0), ("Bob", 0), ("Carlos", 1)] || ranked == vec![("Bob", 0), ("Alice", 0), ("Carlos", 1)]);
assert!(tally.candidates().len() == 3);
let boxed_func = Box::new(|_candidate_position, _num_candidates, _num_marked| 1);
let mut tally = DefaultBordaTally::new(1, Variant::Custom(boxed_func));
tally.add(vec!["Alice", "Bob", "Carlos"])?;
tally.add(vec!["Alice", "Bob"])?;
tally.add(vec!["Bob", "Carlos"])?;
let totals = tally.totals();
assert!(totals == vec![("Bob", 3), ("Alice", 2), ("Carlos", 2)] || totals == vec![("Bob", 3), ("Carlos", 2), ("Alice", 2)]);
let ranked = tally.ranked();
assert!(ranked == vec![("Bob", 0), ("Alice", 1), ("Carlos", 1)] || ranked == vec![("Bob", 0), ("Carlos", 1), ("Alice", 1)]);
assert!(tally.candidates().len() == 3);
let vote_1 = vec!["Alice", "Bob", "Carlos"];
let vote_2 = vec!["Alice", "Bob"];
let mut tally = DefaultBordaTally::with_capacity(1, Variant::Borda, 3);
tally.add_ref(&vote_1)?;
tally.add_ref(&vote_2)?;
assert!(tally.totals() == vec![("Alice", 4), ("Bob", 2), ("Carlos", 0)]);
assert!(tally.ranked() == vec![("Alice", 0), ("Bob", 1), ("Carlos", 2)]);
assert!(tally.candidates().len() == 3);
Ok(())
}
#[test]
#[should_panic]
fn borda_panic_test() {
let _points: u64 = Variant::Dowdall.points(0, 4, 4);
}
}