Implement support for equal rankings

This commit is contained in:
RunasSudo 2021-09-04 02:26:30 +10:00
parent b0f869bf02
commit a24ac3658a
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
9 changed files with 89 additions and 11 deletions

View File

@ -24,6 +24,7 @@ OpenTally is highly customisable, including options for:
* calculations using fixed-point arithmetic, guarded fixed-point ([quasi-exact](http://www.votingmatters.org.uk/ISSUE24/I24P2.pdf)) or exact rational numbers
* different tie breaking rules (backwards, random, manual) with auditable deterministic random number generation
* multiple constraints (e.g. affirmative action rules)
* equal rankings
## Online usage

View File

@ -9,9 +9,9 @@ The file format is as follows:
-2
3 1 3 4 0
4 1 3 2 0
2 4 1 3 0
2 4 1=3 0
1 2 0
2 2 4 3 1 0
2 2=4=3 1 0
1 3 4 2 0
0
"Adam"
@ -25,7 +25,7 @@ The first line (`4 2`) indicates that there are 4 candidates for 2 vacancies.
The second line (`-2`), which is optional, indicates that the 2nd candidate (Basil) has withdrawn. Multiple withdrawn candidates may be specified on this line, e.g. `-2 -3 -4`.
The third line (second, if there are no withdrawn candidates) begins the ballot data. `3 1 3 4 0` indicates that there were 3 ballots which voted, in order of preference, for the 1st candidate (Adam), then the 3rd candidate (Charlotte), then the 4th candidate (Donald). A `0` optionally indicates the end of the list of preferences.
The third line (second, if there are no withdrawn candidates) begins the ballot data. `3 1 3 4 0` indicates that there were 3 ballots which voted, in order of preference, for the 1st candidate (Adam), then the 3rd candidate (Charlotte), then the 4th candidate (Donald). An `=` indicates that multiple candidates were ranked at the same preference. A `0` optionally indicates the end of the list of preferences.
The end of the list of ballots must be indicated with a single `0`.

View File

@ -29,7 +29,7 @@ var tblResult = document.getElementById('result');
var divLogs2 = document.getElementById('resultLogs2');
var olStageComments;
var worker = new Worker('worker.js');
var worker = new Worker('worker.js?v=GITVERSION');
worker.onmessage = function(evt) {
if (evt.data.type === 'init') {

View File

@ -1,4 +1,4 @@
importScripts('opentally.js');
importScripts('opentally.js?v=GITVERSION');
var wasm = wasm_bindgen;
var wasmRaw;
@ -9,7 +9,7 @@ const DATA_START = DATA_ADDR + 8;
const DATA_END = 50 * 1024; // Needs to be increased compared with Asyncify default
async function initWasm() {
wasmRaw = await wasm_bindgen('opentally_async.wasm');
wasmRaw = await wasm_bindgen('opentally_async.wasm?v=GITVERSION');
new Int32Array(wasmRaw.memory.buffer, DATA_ADDR).set([DATA_START, DATA_END]);

View File

@ -24,6 +24,8 @@ use crate::stv::{QuotaMode, STVOptions};
#[cfg(not(target_arch = "wasm32"))]
use rkyv::{Archive, Archived, Deserialize, Fallible, Resolver, Serialize, with::{ArchiveWith, DeserializeWith, SerializeWith}};
use itertools::Itertools;
use std::cmp::max;
use std::collections::HashMap;
@ -414,11 +416,27 @@ impl<N: Number> Ballot<N> {
for preference in self.preferences.iter() {
if preference.len() == 1 {
// Single preference so just add to the end of existing preferences
for minivoter in minivoters.iter_mut() {
minivoter.push(preference.clone());
}
} else {
todo!();
// Equal ranking
// Get all possible permutations
let permutations: Vec<Vec<usize>> = preference.iter().copied().permutations(preference.len()).collect();
// Split into new "minivoters" for each possible permutation
let mut new_minivoters = Vec::with_capacity(minivoters.len() * permutations.len());
for permutation in permutations {
for minivoter in minivoters.iter() {
let mut new_minivoter = minivoter.clone();
for p in permutation.iter() {
new_minivoter.push(vec![*p]);
}
new_minivoters.push(new_minivoter);
}
}
minivoters = new_minivoters;
}
}

View File

@ -144,13 +144,19 @@ impl<N: Number, I: Iterator<Item=char>> BLTParser<N, I> {
self.delimiter_not_nl();
// Read preferences
let mut preferences = Vec::new();
let mut preferences: Vec<Vec<usize>> = Vec::new();
loop {
if self.lookahead() == '0' || self.lookahead() == '\n' {
// End of preferences
self.accept();
break;
} else if self.lookahead() == '=' {
// Equal preference
self.accept();
preferences.last_mut().unwrap().push(self.usize()? - 1);
self.delimiter_not_nl();
} else {
// No equal preference
preferences.push(vec![self.usize()? - 1]);
self.delimiter_not_nl();
}

View File

@ -19,6 +19,7 @@ use crate::election::{Ballot, Candidate, Election};
use crate::numbers::Number;
use csv::ReaderBuilder;
use itertools::Itertools;
use std::collections::HashMap;
use std::io::Read;
@ -82,12 +83,21 @@ pub fn parse_reader<R: Read, N: Number>(reader: R) -> Election<N> {
}
// Sort by ranking
// FIXME: Handle equal rankings and skipped rankings
preferences.sort_by(|a, b| a.0.cmp(&b.0));
let mut unique_rankings: Vec<usize> = preferences.iter().map(|(r, _)| *r).unique().collect();
unique_rankings.sort();
let mut sorted_preferences = Vec::with_capacity(preferences.len());
for ranking in unique_rankings {
// Filter for preferences at this ranking
let prefs_this_ranking: Vec<usize> = preferences.iter()
.filter_map(|(r, i)| if *r == ranking { Some(**i) } else { None })
.collect();
sorted_preferences.push(prefs_this_ranking);
}
ballots.push(Ballot {
orig_value: value,
preferences: preferences.into_iter().map(|(_, i)| vec![*i]).collect(),
preferences: sorted_preferences,
});
}

View File

@ -0,0 +1,15 @@
# Comment: Test data featuring equal rankings
# Source: RunasSudo
# Contributor: RunasSudo
7 4
1 1 2=3=4 5 6=7 0
1 5=6=7 0
0
"Candidate 1"
"Candidate 2"
"Candidate 3"
"Candidate 4"
"Candidate 5"
"Candidate 6"
"Candidate 7"
"Test data featuring equal rankings"

28
tests/equal_ranks.rs Normal file
View File

@ -0,0 +1,28 @@
/* 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/>.
*/
use opentally::election::Election;
use opentally::numbers::Rational;
use opentally::parser;
#[test]
fn parse_equal_ranks() {
let mut election: Election<Rational> = parser::blt::parse_path("tests/data/equal_ranks.blt").expect("Parse Error");
election.realise_equal_rankings();
assert_eq!(election.ballots.len(), 18);
}