diff --git a/Cargo.toml b/Cargo.toml index 98f1296..4171317 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "trie_rcv" -version = "1.0.1" +version = "1.1.0" edition = "2021" description = "Ranked Choice Voting implementation using Tries in Rust" license = "Apache-2.0" @@ -10,3 +10,4 @@ repository = "https://github.com/milselarch/trie_rcv" [dependencies] itertools = "0.12.1" +petgraph = "0.6.4" diff --git a/README.md b/README.md index c2ca905..f0804ae 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,11 @@ # trie_rcv [https://crates.io/crates/trie_rcv](https://crates.io/crates/trie_rcv) Ranked Choice Voting (RCV) implementation using Tries in Rust. + RCV differs from normal first past the post voting in that voters are allowed to rank candidates from most to least preferable. To determine the winner of an RCV election, the -least votes for the least popular candidate(s) are transferred to their next choice until -some candidate reaches a majority. +votes for the least popular candidate each round is transferred to the next candidate in the +respective ranked votes until some candidate achieves an effective majority. Example usage: ```rust @@ -13,6 +14,19 @@ use trie_rcv::RankedChoiceVoteTrie; use trie_rcv::vote::RankedVote; fn main() { + let mut rcv = RankedChoiceVoteTrie::new(); + // remove all candidates with the lowest number of votes each round + rcv.set_elimination_strategy(EliminationStrategies::EliminateAll); + rcv.insert_vote(RankedVote::from_vector(&vec![1, 2, 3, 4]).unwrap()); + rcv.insert_vote(RankedVote::from_vector(&vec![1, 2, 3]).unwrap()); + rcv.insert_vote(RankedVote::from_vector(&vec![3]).unwrap()); + rcv.insert_vote(RankedVote::from_vector(&vec![3, 2, 4]).unwrap()); + rcv.insert_vote(RankedVote::from_vector(&vec![4, 1]).unwrap()); + let winner = rcv.determine_winner(); + println!("WINNER = {:?}", winner); + assert_eq!(winner, Some(1)); + + // alternatively: let votes = RankedVote::from_vectors(&vec![ vec![1, 2, 3, 4], vec![1, 2, 3], @@ -21,18 +35,25 @@ fn main() { vec![4, 1] ]).unwrap(); - let rcv = RankedChoiceVoteTrie::new(); - let winner = rcv.run_election(votes); - println!("WINNER = {:?}", winner); - assert_eq!(winner, Some(1)); + let winner2 = rcv.run_election(votes); + println!("WINNER = {:?}", winner2); + assert_eq!(winner2, Some(1)); } ``` -This implementation also supports votes containing `withhold` and `abstain` votes, -where the `withhold` vote allows the voter to declare for none of the candidates, and -the abstain vote allows the voter to voluntarily remove himself from the poll -(this is useful for improving the chances that the rest of the votes are able -to conclude with a winning candidate) +This implementation also supports ranked votes ending +with `SpecialVotes::WITHHOLD` and `SpecialVotes::ABSTAIN` values: +1. `SpecialVotes::WITHHOLD` +Allows the voter to explicitly declare for none of the candidates. +Qualitatively this allows a voter to declare a vote of no confidence. +Serializes to `-1` via `SpecialVotes::WITHHOLD.to_int()` +2. `SpecialVotes::ABSTAIN` +Allows the voter to explicitly declare for none of the candidates while also +voluntarily removing himself from the poll. +Qualitatively this allows a voter to indicate +that he wants one of the candidates to win but isn't able to decide for himself and +would thus want to delegate the decision to the rest of the electorate. +Serializes to `-2` via `SpecialVotes::ABSTAIN.to_int()` ```rust use trie_rcv; @@ -40,23 +61,68 @@ use trie_rcv::RankedChoiceVoteTrie; use trie_rcv::vote::{RankedVote, SpecialVotes}; fn main() { - let votes = RankedVote::from_vectors(&vec![ + let rcv = RankedChoiceVoteTrie::new(); + + let votes_round1 = RankedVote::from_vectors(&vec![ vec![1, SpecialVotes::WITHHOLD.to_int()], vec![2, 1], vec![3, 2], vec![3] ]).unwrap(); - let rcv = RankedChoiceVoteTrie::new(); - let winner = rcv.run_election(votes); + let winner_round1 = rcv.run_election(votes_round1); println!("WINNER = {:?}", winner); assert_eq!( - winner, None, concat![ + winner_round1, None, concat![ "Candidate 1's vote should not count after round 1, ", "no one should have majority" ]); + + let votes_round2 = RankedVote::from_vectors(&vec![ + vec![1, SpecialVotes::ABSTAIN.to_int()], + vec![2, 1], + vec![3, 2], + vec![3] + ]).unwrap(); + + let winner_round2 = rcv.run_election(votes_round2); + println!("WINNER = {:?}", winner_round2); + assert_eq!( + winner_round2, Some(3), concat![ + "First vote is ignored in round 2, candidate 3 wins" + ]); } ``` +### Elimination Strategies +Technically the RCV algorithm specification doesn't state what to do in the situation that +there are multiple candidates who all have the same, lowest number of votes in some round during +RCV. The `elimination_strategy` setting handles how to deal with this edge case: + +1. `EliminationStrategies::ElimnateAll` +Removes all candidates with the lowest number of votes each round. +2. `EliminationStrategies::DowdallScoring` (default) +Among multiple candidates with the lowest number of votes each round, +sort the candidates by their dowdall score and eliminate the candidate(s) +with the lowest [Dowdall score](https://rdrr.io/cran/votesys/man/dowdall_method.html). +The Dowdall score for each candidate is calculated by +the sum of the inverse of the ranking (starting from 1) for each ranked vote. +If a ranked vote does not contain a candidate, then it does not count +towards the dowdall score) +3. `EliminationStrategies::RankedPairs` +Among multiple candidates with the lowest number of votes each round, attempt +to construct a directed acyclic graph establishing a pecking order between +these candidates via [ranked-pair](https://en.wikipedia.org/wiki/Ranked_pairs) +comparisons, whereby candidate A is said to be better than candidate B +if there are more votes that rank A higher than B +than vice versa. + 1. Note that if a ranked vote ranks A explicitly but not B, then that is + counted as ranking A higher than B as well + 2. In the event that a directed acyclic preference graph cannot be established + (such as when there are cyclic preferences between candidates), then the elimination + behavior will default to eliminating all candidates with the same, + lowest number of votes each round i.e. it will fall back to the + behavior of `EliminationStrategies::ElimnateAll` + ## Build instructions Build crate using `cargo build`, run integration tests with `cargo test` diff --git a/src/lib.rs b/src/lib.rs index bdc665c..7e2654f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,9 +1,20 @@ use std::cmp::min; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; +use petgraph::graph::{DiGraph, NodeIndex}; +use itertools::{iproduct, Itertools}; +use std::collections::VecDeque; +use petgraph::Direction; +use petgraph::prelude::EdgeRef; + pub use vote::*; pub mod vote; +#[derive(PartialEq)] +pub enum PairPreferences { + PreferredOver, Inconclusive, PreferredAgainst +} + #[derive(Default)] struct TrieNode { children: HashMap, @@ -36,7 +47,8 @@ impl TrieNode { pub struct RankedChoiceVoteTrie { root: TrieNode, dowdall_score_map: HashMap, - elimination_strategy: EliminationStrategies + elimination_strategy: EliminationStrategies, + unique_candidates: HashSet } struct VoteTransfer<'a> { @@ -51,9 +63,106 @@ struct VoteTransferChanges<'a> { vote_transfers: Vec> } -#[derive(Clone)] +// strategies for how to eliminate candidates each round +#[derive(Clone, PartialEq)] pub enum EliminationStrategies { - EliminateAll, DowdallScoring + // removes all candidates with the lowest number of votes each round + EliminateAll, + // eliminate the candidate(s) with both the lowest number of votes + // followed by the lowest dowdall score + DowdallScoring, + // eliminate the candidate(s) with both the lowest number of votes + // and who lose against other candidates with the same number of votes + // in a head-to-head comparison + RankedPairs +} + +fn is_graph_acyclic(graph: &DiGraph) -> bool { + /* + checks if there doesn't exist any path of directed edges + from some edge in the graph back to itself + */ + if graph.node_count() == 0 { return true } + let nodes: Vec = graph.node_indices().collect(); + let mut all_explored_nodes = HashSet::::new(); + + fn dfs_find_cycle( + node: &NodeIndex, path: &mut Vec, + explored: &mut HashSet::, graph: &DiGraph + ) -> bool { + // use DFS to see if a cycle can be created from paths starting from node + explored.insert(*node); + + // get neighbors of node where there is an + // outgoing edge from node to neighbor + let directed_neighbors: Vec = graph + .edges_directed(*node, petgraph::Direction::Outgoing) + .map(|edge| { edge.target()} ) + .collect(); + + for neighbor in directed_neighbors { + if path.contains(&neighbor) { return true } + path.push(neighbor); + let has_cycle = dfs_find_cycle(&neighbor, path, explored, graph); + path.pop(); + + if has_cycle { return true } + } + + return false; + } + + for node in nodes { + if all_explored_nodes.contains(&node) { continue } + let mut dfs_explored_nodes = HashSet::::new(); + let has_cycle = dfs_find_cycle( + &node, &mut Vec::::new(), &mut dfs_explored_nodes, graph + ); + + if has_cycle { return false } + all_explored_nodes.extend(dfs_explored_nodes.iter().collect_vec()); + } + + return true; +} + +fn is_graph_weakly_connected(graph: &DiGraph) -> bool { + /* + checks if there is a path from every node to every other + node when all the edges are converted from directed to undirected + */ + if graph.node_count() == 0 { return true } + let mut queue = VecDeque::::new(); + let mut explored_nodes = HashSet::::new(); + let nodes: Vec = graph.node_indices().collect(); + let start_node = nodes[0]; + queue.push_back(start_node); + + let get_undirected_neighbors = |node: NodeIndex| { + let mut neighbors = Vec::::new(); + neighbors.extend(graph.neighbors_directed(node, Direction::Incoming)); + neighbors.extend(graph.neighbors_directed(node, Direction::Outgoing)); + return neighbors; + }; + + // do a DFS search to see if all nodes are reachable from start_node + loop { + let node = match queue.pop_back() { + None => { break; } + Some(node) => node + }; + + if explored_nodes.contains(&node) { continue } + explored_nodes.insert(node); + + let neighbors: Vec = get_undirected_neighbors(node); + // println!("DFS {:?}", (node, &neighbors)); + for neighbor in neighbors { + queue.push_back(neighbor) + } + } + + return explored_nodes.len() == graph.node_count() } impl RankedChoiceVoteTrie { @@ -62,6 +171,7 @@ impl RankedChoiceVoteTrie { root: TrieNode::new(), dowdall_score_map: Default::default(), elimination_strategy: EliminationStrategies::DowdallScoring, + unique_candidates: Default::default(), } } @@ -78,12 +188,14 @@ impl RankedChoiceVoteTrie { pub fn insert_vote<'a>(&mut self, vote: RankedVote) { let mut current = &mut self.root; let vote_items = vote.iter().enumerate(); + current.num_votes += 1; for (ranking, vote_value) in vote_items { // println!("ITEM {}", ranking); match vote_value { VoteValues::SpecialVote(_) => {} VoteValues::Candidate(candidate) => { + self.unique_candidates.insert(candidate); let score = *self.dowdall_score_map .entry(candidate).or_insert(0f32); let new_score = score + 1.0 / (ranking + 1) as f32; @@ -141,11 +253,121 @@ impl RankedChoiceVoteTrie { return transfer_changes; } - fn get_or_create_nodes<'a>( - &'a self, candidate: &u16, - frontier_nodes: &'a mut HashMap> - ) -> &mut Vec<&TrieNode> { - frontier_nodes.entry(*candidate).or_insert(Vec::new()) + fn find_ranked_pairs_weakest( + &self, candidates: Vec, + ranked_pairs_map: &HashMap<(u16, u16), u64> + ) -> Vec { + // println!("\n----------------"); + // println!("PRE_RANK_FILTER {:?}", candidates); + // println!("PAIRS_MAP {:?}", ranked_pairs_map); + let mut graph = DiGraph::::new(); + let mut node_map = HashMap::::new(); + + /* + Determines whether candidate1 is preferred over candidate2 overall, + or vice versa, or there is no net preference between the two. + Also returns the net number of votes along said overall preference + */ + let get_preference = | + candidate1: u16, candidate2: u16 + | -> (PairPreferences, u64) { + let preferred_over_votes = + ranked_pairs_map.get(&(candidate1, candidate2)) + .unwrap_or(&0); + let preferred_against_votes = + ranked_pairs_map.get(&(candidate2, candidate1)) + .unwrap_or(&0); + + /* + println!("C_PAIR {:?}", ( + (candidate1, candidate2), + (preferred_over_votes, preferred_against_votes) + )); + */ + + return if preferred_over_votes > preferred_against_votes { + let strength = preferred_over_votes - preferred_against_votes; + (PairPreferences::PreferredOver, strength) + } else if preferred_over_votes == preferred_against_votes { + (PairPreferences::Inconclusive, 0) + } else { + let strength = preferred_against_votes - preferred_over_votes; + (PairPreferences::PreferredAgainst, strength) + } + }; + + fn get_or_create_node ( + graph: &mut DiGraph, + node_map: &mut HashMap, + candidate: u16 + ) -> NodeIndex { + // println!("NODE_MAP_PRE {:?}", (candidate, &node_map, &graph)); + let node = match node_map.get(&candidate) { + Some(node) => { *node } + None => { + let node = graph.add_node(candidate); + node_map.insert(candidate, node); + node + } + }; + + // println!("NODE_MAP_POST {:?}", (candidate, &node_map, &graph)); + return node; + } + + // initialize all the nodes in the graph + for candidate in &candidates { + get_or_create_node(&mut graph, &mut node_map, *candidate); + } + + // construct preference strength graph between candidates + for (candidate1, candidate2) in iproduct!(&candidates, &candidates) { + if candidate1 == candidate2 { continue } + let (preference, strength) = + get_preference(*candidate1, *candidate2); + + match preference { + PairPreferences::PreferredAgainst => { continue } + PairPreferences::Inconclusive => { continue } + PairPreferences::PreferredOver => {} + } + + assert!(preference == PairPreferences::PreferredOver); + let node1_idx = + get_or_create_node(&mut graph, &mut node_map, *candidate1); + let node2_idx = + get_or_create_node(&mut graph, &mut node_map, *candidate2); + if !graph.contains_edge(node1_idx, node2_idx) { + // println!("ADD_EDGE {:?}", (node1_idx, node2_idx)); + graph.add_edge(node1_idx, node2_idx, strength); + } + } + + // println!("GRAPH {:?}", graph); + // unable to establish pecking order among candidates + if !(is_graph_acyclic(&graph) && is_graph_weakly_connected(&graph)) { + /* + println!("POST_RANK_FILTER {:?}", ( + &candidates, is_graph_acyclic(&graph), + is_graph_weakly_connected(&graph)) + ); + */ + return candidates.clone(); + } + + let has_no_outgoing_edges = |&node: &NodeIndex| -> bool { + graph.neighbors_directed(node, Direction::Outgoing).count() == 0 + }; + let weakest_nodes: Vec = graph + .node_indices() + .filter(has_no_outgoing_edges) + .collect(); + + let weakest_candidates = weakest_nodes + .iter().map(|&index| graph[index]).collect(); + // println!("POST_NODES {:?}", weakest_nodes); + // println!("POST_RANK_FILTER {:?}", weakest_candidates); + return weakest_candidates; } fn find_dowdall_weakest(&self, candidates: Vec) -> Vec { @@ -177,12 +399,76 @@ impl RankedChoiceVoteTrie { let mut rcv = RankedChoiceVoteTrie { root: Default::default(), dowdall_score_map: Default::default(), - elimination_strategy: self.elimination_strategy.clone() + elimination_strategy: self.elimination_strategy.clone(), + unique_candidates: Default::default() }; rcv.insert_votes(votes); return rcv.determine_winner(); } + fn build_ranked_pairs_map( + &self, node: &TrieNode, search_path: &mut Vec, + ranked_pairs_map: &mut HashMap<(u16, u16), u64>, + unique_candidates: &HashSet + ) { + let kv_pairs_vec: Vec<(Box<&VoteValues>, Box<&TrieNode>)> = + node.children.iter().map(|(vote_value, node)| { + (Box::new(vote_value), Box::new(node)) + }).collect(); + + // number of votes that terminate at node + let mut terminating_votes: u64 = node.num_votes; + // println!("NODE_VOTES {:?}", node.num_votes); + + for (boxed_vote_value, boxed_child) in kv_pairs_vec { + let vote_value = *boxed_vote_value; + let child = *boxed_child; + + // println!("CHILD_VOTES {:?}", child.num_votes); + assert!(terminating_votes >= child.num_votes); + terminating_votes -= child.num_votes; + + let candidate = match vote_value { + VoteValues::SpecialVote(_) => { continue } + VoteValues::Candidate(candidate) => { candidate } + }; + + for preferable_candidate in search_path.iter() { + let ranked_pair = (*preferable_candidate, *candidate); + let pairwise_votes = + ranked_pairs_map.entry(ranked_pair).or_insert(0); + *pairwise_votes += child.num_votes; + } + + search_path.push(*candidate); + self.build_ranked_pairs_map( + child, search_path, ranked_pairs_map, unique_candidates + ); + search_path.pop(); + }; + + if terminating_votes > 0 { + // println!("UNIQUE {:?}", unique_candidates); + // println!("TERMINATE {:?}", (&search_path, terminating_votes)); + // candidates who weren't explicitly listed in current vote path + let search_path: &Vec = search_path.as_ref(); + let mut unspecified_candidates = unique_candidates.clone(); + for candidate in search_path { + unspecified_candidates.remove(candidate); + } + + for preferable_candidate in search_path { + for candidate in &unspecified_candidates { + let ranked_pair = (*preferable_candidate, *candidate); + let pairwise_votes = + ranked_pairs_map.entry(ranked_pair).or_insert(0); + // println!("INSERT {:?}", (ranked_pair, terminating_votes)); + *pairwise_votes += terminating_votes; + } + } + } + } + pub fn determine_winner<'a>(&self) -> Option { // println!("RUN_ELECTION_START"); let mut candidate_vote_counts: HashMap = HashMap::new(); @@ -197,6 +483,7 @@ impl RankedChoiceVoteTrie { let kv_pairs_vec: Vec<(&VoteValues, &TrieNode)> = self.root.children.iter().collect(); + for (vote_value, node) in kv_pairs_vec { match vote_value { VoteValues::SpecialVote(SpecialVotes::ABSTAIN) => {} @@ -212,6 +499,14 @@ impl RankedChoiceVoteTrie { }; } + let mut ranked_pairs_map: HashMap<(u16, u16), u64> = HashMap::new(); + if self.elimination_strategy == EliminationStrategies::RankedPairs { + self.build_ranked_pairs_map( + &self.root, &mut Vec::new(), &mut ranked_pairs_map, + &self.unique_candidates + ); + } + while candidate_vote_counts.len() > 0 { // println!("COUNTS {:?}", candidate_vote_counts); let mut min_candidate_votes: u64 = u64::MAX; @@ -242,7 +537,14 @@ impl RankedChoiceVoteTrie { match self.elimination_strategy { EliminationStrategies::EliminateAll => {}, EliminationStrategies::DowdallScoring => { - weakest_candidates = self.find_dowdall_weakest(weakest_candidates); + weakest_candidates = self.find_dowdall_weakest( + weakest_candidates + ); + }, + EliminationStrategies::RankedPairs => { + weakest_candidates = self.find_ranked_pairs_weakest( + weakest_candidates, &ranked_pairs_map + ); } } diff --git a/src/vote.rs b/src/vote.rs index 3b96231..d3a50e2 100644 --- a/src/vote.rs +++ b/src/vote.rs @@ -109,45 +109,62 @@ impl RankedVote { return Ok(votes); } - pub fn from_vector(raw_rankings: &Vec) -> Result { + pub fn from_candidates( + candidates: &Vec + ) -> Result { + return Self::from_vector( + &candidates.iter().map(|x| *x as i32).collect() + ) + } + + pub fn from_vector( + raw_ranked_vote: &Vec + ) -> Result { // println!("INSERT {:?}", raw_rankings); - let mut rankings: Vec = Vec::new(); - let mut special_vote: Option = None; + let mut candidates: Vec = Vec::new(); + let mut special_vote_value: Option = None; let mut unique_values = HashSet::new(); - let length = raw_rankings.len(); + let length = raw_ranked_vote.len(); let last_index = length - 1; - for (k, raw_ranking) in raw_rankings.iter().enumerate() { + for (k, raw_ranked_vote_value) in raw_ranked_vote.iter().enumerate() { let is_last_index = k == last_index; - if unique_values.contains(raw_ranking) { + if unique_values.contains(raw_ranked_vote_value) { return Err(VoteErrors::DuplicateVotes); } else { - unique_values.insert(*raw_ranking); + unique_values.insert(*raw_ranked_vote_value); } - if raw_ranking.is_negative() { + if raw_ranked_vote_value.is_negative() { if !is_last_index { return Err(VoteErrors::NonFinalSpecialVote); } assert!(is_last_index); - let cast_result = SpecialVotes::from_int(*raw_ranking); + let cast_result = + SpecialVotes::from_int(*raw_ranked_vote_value); match cast_result { Err(cast_error) => { return Err(cast_error); }, - Ok(cast_value) => { special_vote = Some(cast_value) } + Ok(cast_value) => { + special_vote_value = Some(cast_value) + } } } else { - assert!(raw_ranking.is_positive()); - let cast_result = u16::try_from(*raw_ranking); + assert!(raw_ranked_vote_value.is_positive()); + let cast_result = u16::try_from(*raw_ranked_vote_value); match cast_result { - Err(_) => { return Err(VoteErrors::InvalidCastToSpecialVote); }, - Ok(choice) => { rankings.push(choice) } + Ok(candidate) => { candidates.push(candidate) } + Err(_) => { + return Err(VoteErrors::InvalidCastToSpecialVote); + }, } } } // println!("INSERT_END {:?}", raw_rankings); - return Ok(RankedVote { rankings, special_vote }) + return Ok(RankedVote { + rankings: candidates, special_vote: special_vote_value + }) } pub fn to_vector(&self) -> Vec { diff --git a/tests/integration_test.rs b/tests/integration_test.rs index e412d39..2688580 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -24,6 +24,24 @@ fn test_basic_scenario() { ); } +#[test] +fn test_vote_insert() { + let mut rcv = RankedChoiceVoteTrie::new(); + rcv.set_elimination_strategy(EliminationStrategies::EliminateAll); + + rcv.insert_vote(RankedVote::from_vector(&vec![1, 2, 3, 4]).unwrap()); + rcv.insert_vote(RankedVote::from_vector(&vec![1, 2, 3]).unwrap()); + rcv.insert_vote(RankedVote::from_vector(&vec![3]).unwrap()); + rcv.insert_vote(RankedVote::from_vector(&vec![3, 2, 4]).unwrap()); + rcv.insert_vote(RankedVote::from_vector(&vec![4, 1]).unwrap()); + let winner = rcv.determine_winner(); + println!("WINNER = {:?}", winner); + assert_eq!( + winner, Some(1), + "Vote 4 > 1 should go to 1, leading to Candidate 1 winning" + ); +} + #[test] fn test_simple_majority() { let votes = RankedVote::from_vectors(&vec![ @@ -54,7 +72,7 @@ fn test_tie_scenario() { } #[test] -fn test_zero_vote_end() { +fn test_withold_vote_end() { let votes = RankedVote::from_vectors(&vec![ vec![1, WITHOLD_VOTE_VAL], vec![2, 1], @@ -72,34 +90,36 @@ fn test_zero_vote_end() { ]); } -#[test] -fn test_zero_nil_votes_only() { +fn test_abstain_vote_end() { let votes = RankedVote::from_vectors(&vec![ - vec![WITHOLD_VOTE_VAL], - vec![WITHOLD_VOTE_VAL], - vec![WITHOLD_VOTE_VAL], - vec![ABSTAIN_VOTE_VAL] + vec![1, ABSTAIN_VOTE_VAL], + vec![2, 1], + vec![3, 2], + vec![3] ]).unwrap(); let rcv = RankedChoiceVoteTrie::new(); let winner = rcv.run_election(votes); println!("WINNER = {:?}", winner); - assert_eq!(winner, None); + assert_eq!( + winner, Some(3), concat![ + "First vote is ignored in round 2, candidate 3 wins" + ]); } #[test] -fn test_null_vote_end() { +fn test_withhold_votes_only() { let votes = RankedVote::from_vectors(&vec![ - vec![1, ABSTAIN_VOTE_VAL], - vec![2, 1], - vec![3, 2], - vec![3] + vec![WITHOLD_VOTE_VAL], + vec![WITHOLD_VOTE_VAL], + vec![WITHOLD_VOTE_VAL], + vec![ABSTAIN_VOTE_VAL] ]).unwrap(); let rcv = RankedChoiceVoteTrie::new(); let winner = rcv.run_election(votes); println!("WINNER = {:?}", winner); - assert_eq!(winner, Some(3)); + assert_eq!(winner, None); } #[test] @@ -133,4 +153,21 @@ fn test_all_elimination() { let winner = rcv.run_election(votes); println!("WINNER = {:?}", winner); assert_eq!(winner, Some(1)); +} + +#[test] +fn test_ranked_pairs_elimination() { + let votes = RankedVote::from_vectors(&vec![ + vec![1, 6, 15], + vec![1, 2, 6, 15, 5, 4, 7, 3, 11], + vec![6, 15, 1, 11, 10, 16, 17, 8, 2, 3, 5, 7], + vec![9, 8, 6, 11, 13, 3, 1], + vec![13, 14, 16, 6, 3, 4, 5, 2, 1, 8, 9] + ]).unwrap(); + + let mut rcv = RankedChoiceVoteTrie::new(); + rcv.set_elimination_strategy(EliminationStrategies::RankedPairs); + let winner = rcv.run_election(votes); + println!("WINNER = {:?}", winner); + assert_eq!(winner, Some(6)); } \ No newline at end of file