/* OpenTally: Open-source election vote counting * Copyright © 2021 Lee Yingtong Li (RunasSudo) * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ #![allow(mutable_borrow_reservation_conflict)] use crate::numbers::Number; use crate::election::{Candidate, CandidateState, CountCard, CountState, Parcel, Vote}; use std::collections::HashMap; use std::ops::Sub; struct NextPreferencesResult<'a, N> { candidates: HashMap<&'a Candidate, NextPreferencesEntry<'a, N>>, exhausted: NextPreferencesEntry<'a, N>, total_ballots: N, } struct NextPreferencesEntry<'a, N> { //count_card: Option<&'a CountCard<'a, N>>, votes: Vec>, num_ballots: N, num_votes: N, } fn next_preferences<'a, N: Number>(state: &CountState<'a, N>, votes: Vec>) -> NextPreferencesResult<'a, N> { let mut result = NextPreferencesResult { candidates: HashMap::new(), exhausted: NextPreferencesEntry { votes: Vec::new(), num_ballots: N::new(), num_votes: N::new(), }, total_ballots: N::new(), }; for mut vote in votes.into_iter() { result.total_ballots += &vote.ballot.orig_value; let mut next_candidate = None; for (i, preference) in vote.ballot.preferences.iter().enumerate().skip(vote.up_to_pref) { let candidate = &state.election.candidates[*preference]; let count_card = state.candidates.get(candidate).unwrap(); if let CandidateState::HOPEFUL | CandidateState::GUARDED = count_card.state { next_candidate = Some(candidate); vote.up_to_pref = i + 1; break; } } // Have to structure like this to satisfy Rust's borrow checker if let Some(candidate) = next_candidate { if result.candidates.contains_key(candidate) { let entry = result.candidates.get_mut(candidate).unwrap(); entry.num_ballots += &vote.ballot.orig_value; entry.num_votes += &vote.value; entry.votes.push(vote); } else { let entry = NextPreferencesEntry { num_ballots: vote.ballot.orig_value.clone(), num_votes: vote.value.clone(), votes: vec![vote], }; result.candidates.insert(candidate, entry); } } else { result.exhausted.num_ballots += &vote.ballot.orig_value; result.exhausted.num_votes += &vote.value; result.exhausted.votes.push(vote); } } return result; } pub fn distribute_first_preferences(state: &mut CountState) { let mut votes = Vec::new(); for ballot in state.election.ballots.iter() { let vote = Vote { ballot: ballot, value: ballot.orig_value.clone(), up_to_pref: 0, }; votes.push(vote); } let result = next_preferences(state, votes); // Transfer candidate votes for (candidate, entry) in result.candidates.into_iter() { let parcel = entry.votes as Parcel; let count_card = state.candidates.get_mut(candidate).unwrap(); count_card.parcels.push(parcel); count_card.transfer(&entry.num_votes); } // Transfer exhausted votes let parcel = result.exhausted.votes as Parcel; state.exhausted.parcels.push(parcel); state.exhausted.transfer(&result.exhausted.num_votes); } pub fn calculate_quota(state: &mut CountState) { // Calculate the total vote state.quota = N::zero(); for count_card in state.candidates.values() { state.quota += &count_card.votes; } // TODO: Different quotas state.quota /= N::from(state.election.seats + 1); // TODO: Different rounding rules state.quota += N::one(); state.quota.floor_mut(); } fn meets_quota(quota: &N, count_card: &CountCard) -> bool { // TODO: Different quota rules return count_card.votes >= *quota; } pub fn elect_meeting_quota(state: &mut CountState) { // Can't use filter(...) magic because of conflict with borrow checker let mut cands_meeting_quota = Vec::new(); for (candidate, count_card) in state.candidates.iter_mut() { if count_card.state == CandidateState::HOPEFUL && meets_quota(&state.quota, count_card) { cands_meeting_quota.push((candidate, count_card)); } } if cands_meeting_quota.len() > 0 { // Sort by votes cands_meeting_quota.sort_unstable_by(|a, b| a.1.votes.partial_cmp(&b.1.votes).unwrap()); // Declare elected in descending order of votes for (_, count_card) in cands_meeting_quota.into_iter().rev() { // TODO: Log count_card.state = CandidateState::ELECTED; state.num_elected += 1; count_card.order_elected = state.num_elected as isize; } } } pub fn distribute_surpluses(state: &mut CountState) -> bool where for<'r> &'r N: Sub<&'r N, Output=N> { // As above regarding filter(...) let mut has_surplus = Vec::new(); for (candidate, count_card) in state.candidates.iter() { if count_card.votes > state.quota { has_surplus.push((candidate, count_card)); } } if has_surplus.len() > 0 { // TODO: Different sorting orders has_surplus.sort_unstable_by(|a, b| a.1.order_elected.partial_cmp(&b.1.order_elected).unwrap()); // Distribute top candidate's surplus // TODO: Handle ties let elected_candidate = has_surplus.first_mut().unwrap().0; distribute_surplus(state, elected_candidate); return true; } return false; } fn distribute_surplus(state: &mut CountState, elected_candidate: &Candidate) where for<'r> &'r N: Sub<&'r N, Output=N> { let count_card = state.candidates.get(elected_candidate).unwrap(); let surplus = &count_card.votes - &state.quota; // Inclusive Gregory // TODO: Other methods let votes = state.candidates.get(elected_candidate).unwrap().parcels.concat(); // Count next preferences let result = next_preferences(state, votes); // Transfer candidate votes // Unweighted inclusive Gregory // TODO: Other methods let transfer_value = surplus.clone() / &result.total_ballots; for (candidate, entry) in result.candidates.into_iter() { let mut parcel = entry.votes as Parcel; // Reweight votes for vote in parcel.iter_mut() { vote.value = vote.ballot.orig_value.clone() * &transfer_value; } let count_card = state.candidates.get_mut(candidate).unwrap(); count_card.parcels.push(parcel); let mut total_transferred = entry.num_ballots * &surplus / &result.total_ballots; // Round transfers // TODO: Make configurable total_transferred.floor_mut(); count_card.transfer(&total_transferred); } // Transfer exhausted votes let parcel = result.exhausted.votes as Parcel; state.exhausted.parcels.push(parcel); state.exhausted.transfer(&result.exhausted.num_votes); // Finalise candidate votes let count_card = state.candidates.get_mut(elected_candidate).unwrap(); count_card.transfers = -surplus; count_card.votes.assign(&state.quota); } pub fn bulk_elect(state: &mut CountState) -> bool { if state.election.candidates.len() - state.num_excluded <= state.election.seats { // Bulk elect all remaining candidates let mut hopefuls = Vec::new(); for (candidate, count_card) in state.candidates.iter_mut() { if count_card.state == CandidateState::HOPEFUL { hopefuls.push((candidate, count_card)); } } // TODO: Handle ties hopefuls.sort_unstable_by(|a, b| a.1.votes.partial_cmp(&b.1.votes).unwrap()); for (_, count_card) in hopefuls.into_iter() { count_card.state = CandidateState::ELECTED; state.num_elected += 1; count_card.order_elected = state.num_elected as isize; } return true; } return false; } pub fn exclude_hopefuls(state: &mut CountState) -> bool { let mut hopefuls = Vec::new(); for (candidate, count_card) in state.candidates.iter() { if count_card.state == CandidateState::HOPEFUL { hopefuls.push((candidate, count_card)); } } // Sort by votes // TODO: Handle ties hopefuls.sort_unstable_by(|a, b| a.1.votes.partial_cmp(&b.1.votes).unwrap()); // Exclude lowest ranked candidate let excluded_candidate = hopefuls.first().unwrap().0; exclude_candidate(state, excluded_candidate); return true; } pub fn continue_exclusion(state: &mut CountState) -> bool { let mut excluded_with_votes = Vec::new(); for (candidate, count_card) in state.candidates.iter() { if count_card.state == CandidateState::EXCLUDED && !count_card.votes.is_zero() { excluded_with_votes.push((candidate, count_card)); } } if excluded_with_votes.len() > 0 { excluded_with_votes.sort_unstable_by(|a, b| a.1.order_elected.partial_cmp(&b.1.order_elected).unwrap()); let excluded_candidate = excluded_with_votes.first().unwrap().0; exclude_candidate(state, excluded_candidate); return true; } return false; } fn exclude_candidate(state: &mut CountState, excluded_candidate: &Candidate) { let count_card = state.candidates.get_mut(excluded_candidate).unwrap(); count_card.state = CandidateState::EXCLUDED; state.num_excluded += 1; count_card.order_elected = -(state.num_excluded as isize); // Exclude in one round // TODO: Exclude by parcel let votes = state.candidates.get(excluded_candidate).unwrap().parcels.concat(); // Count next preferences let result = next_preferences(state, votes); // Transfer candidate votes for (candidate, entry) in result.candidates.into_iter() { let parcel = entry.votes as Parcel; let count_card = state.candidates.get_mut(candidate).unwrap(); count_card.parcels.push(parcel); count_card.transfer(&entry.num_votes); } // Transfer exhausted votes let parcel = result.exhausted.votes as Parcel; state.exhausted.parcels.push(parcel); state.exhausted.transfer(&result.exhausted.num_votes); // Finalise candidate votes let count_card = state.candidates.get_mut(excluded_candidate).unwrap(); count_card.transfers = -count_card.votes.clone(); count_card.votes = N::new(); } pub fn finished_before_stage(state: &CountState) -> bool { if state.num_elected >= state.election.seats { return true; } return false; }