2021-05-28 19:58:40 +10:00
|
|
|
/* 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 <https://www.gnu.org/licenses/>.
|
|
|
|
*/
|
|
|
|
|
|
|
|
#![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<Vote<'a, N>>,
|
|
|
|
num_ballots: N,
|
|
|
|
num_votes: N,
|
|
|
|
}
|
|
|
|
|
|
|
|
fn next_preferences<'a, N: Number>(state: &CountState<'a, N>, votes: Vec<Vote<'a, N>>) -> 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<N: Number>(state: &mut CountState<N>) {
|
2021-05-28 22:37:18 +10:00
|
|
|
let votes = state.election.ballots.iter().map(|b| Vote {
|
|
|
|
ballot: b,
|
|
|
|
value: b.orig_value.clone(),
|
|
|
|
up_to_pref: 0,
|
|
|
|
}).collect();
|
2021-05-28 19:58:40 +10:00
|
|
|
|
|
|
|
let result = next_preferences(state, votes);
|
|
|
|
|
|
|
|
// Transfer candidate votes
|
|
|
|
for (candidate, entry) in result.candidates.into_iter() {
|
|
|
|
let parcel = entry.votes as Parcel<N>;
|
|
|
|
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<N>;
|
|
|
|
state.exhausted.parcels.push(parcel);
|
|
|
|
state.exhausted.transfer(&result.exhausted.num_votes);
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn calculate_quota<N: Number>(state: &mut CountState<N>) {
|
|
|
|
// Calculate the total vote
|
2021-05-28 22:37:18 +10:00
|
|
|
state.quota = state.candidates.values().fold(N::zero(), |acc, cc| { acc + &cc.votes });
|
2021-05-28 19:58:40 +10:00
|
|
|
|
|
|
|
// 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<N: Number>(quota: &N, count_card: &CountCard<N>) -> bool {
|
|
|
|
// TODO: Different quota rules
|
|
|
|
return count_card.votes >= *quota;
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn elect_meeting_quota<N: Number>(state: &mut CountState<N>) {
|
2021-05-28 22:37:18 +10:00
|
|
|
let quota = &state.quota; // Have to do this or else the borrow checker gets confused
|
|
|
|
let mut cands_meeting_quota: Vec<(&&Candidate, &mut CountCard<N>)> = state.candidates.iter_mut()
|
|
|
|
.filter(|(_, cc)| cc.state == CandidateState::HOPEFUL && meets_quota(quota, cc))
|
|
|
|
.collect();
|
2021-05-28 19:58:40 +10:00
|
|
|
|
|
|
|
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<N: Number>(state: &mut CountState<N>) -> bool where for<'r> &'r N: Sub<&'r N, Output=N> {
|
2021-05-28 22:37:18 +10:00
|
|
|
let mut has_surplus: Vec<(&&Candidate, &CountCard<N>)> = state.candidates.iter()
|
|
|
|
.filter(|(_, cc)| cc.votes > state.quota)
|
|
|
|
.collect();
|
2021-05-28 19:58:40 +10:00
|
|
|
|
|
|
|
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<N: Number>(state: &mut CountState<N>, 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<N>;
|
|
|
|
|
|
|
|
// 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<N>;
|
|
|
|
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<N: Number>(state: &mut CountState<N>) -> bool {
|
|
|
|
if state.election.candidates.len() - state.num_excluded <= state.election.seats {
|
|
|
|
// Bulk elect all remaining candidates
|
2021-05-28 22:37:18 +10:00
|
|
|
let mut hopefuls: Vec<(&&Candidate, &mut CountCard<N>)> = state.candidates.iter_mut()
|
|
|
|
.filter(|(_, cc)| cc.state == CandidateState::HOPEFUL)
|
|
|
|
.collect();
|
|
|
|
|
2021-05-28 19:58:40 +10:00
|
|
|
// 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<N: Number>(state: &mut CountState<N>) -> bool {
|
2021-05-28 22:37:18 +10:00
|
|
|
let mut hopefuls: Vec<(&&Candidate, &CountCard<N>)> = state.candidates.iter()
|
|
|
|
.filter(|(_, cc)| cc.state == CandidateState::HOPEFUL)
|
|
|
|
.collect();
|
2021-05-28 19:58:40 +10:00
|
|
|
|
|
|
|
// 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<N: Number>(state: &mut CountState<N>) -> bool {
|
2021-05-28 22:37:18 +10:00
|
|
|
let mut excluded_with_votes: Vec<(&&Candidate, &CountCard<N>)> = state.candidates.iter()
|
|
|
|
.filter(|(_, cc)| cc.state == CandidateState::EXCLUDED && !cc.votes.is_zero())
|
|
|
|
.collect();
|
2021-05-28 19:58:40 +10:00
|
|
|
|
|
|
|
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<N: Number>(state: &mut CountState<N>, 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<N>;
|
|
|
|
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<N>;
|
|
|
|
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<N: Number>(state: &CountState<N>) -> bool {
|
|
|
|
if state.num_elected >= state.election.seats {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|