Implement support for equal rankings
This commit is contained in:
parent
b0f869bf02
commit
a24ac3658a
@ -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
|
* 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
|
* different tie breaking rules (backwards, random, manual) with auditable deterministic random number generation
|
||||||
* multiple constraints (e.g. affirmative action rules)
|
* multiple constraints (e.g. affirmative action rules)
|
||||||
|
* equal rankings
|
||||||
|
|
||||||
## Online usage
|
## Online usage
|
||||||
|
|
||||||
|
@ -9,9 +9,9 @@ The file format is as follows:
|
|||||||
-2
|
-2
|
||||||
3 1 3 4 0
|
3 1 3 4 0
|
||||||
4 1 3 2 0
|
4 1 3 2 0
|
||||||
2 4 1 3 0
|
2 4 1=3 0
|
||||||
1 2 0
|
1 2 0
|
||||||
2 2 4 3 1 0
|
2 2=4=3 1 0
|
||||||
1 3 4 2 0
|
1 3 4 2 0
|
||||||
0
|
0
|
||||||
"Adam"
|
"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 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`.
|
The end of the list of ballots must be indicated with a single `0`.
|
||||||
|
|
||||||
|
@ -29,7 +29,7 @@ var tblResult = document.getElementById('result');
|
|||||||
var divLogs2 = document.getElementById('resultLogs2');
|
var divLogs2 = document.getElementById('resultLogs2');
|
||||||
var olStageComments;
|
var olStageComments;
|
||||||
|
|
||||||
var worker = new Worker('worker.js');
|
var worker = new Worker('worker.js?v=GITVERSION');
|
||||||
|
|
||||||
worker.onmessage = function(evt) {
|
worker.onmessage = function(evt) {
|
||||||
if (evt.data.type === 'init') {
|
if (evt.data.type === 'init') {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
importScripts('opentally.js');
|
importScripts('opentally.js?v=GITVERSION');
|
||||||
|
|
||||||
var wasm = wasm_bindgen;
|
var wasm = wasm_bindgen;
|
||||||
var wasmRaw;
|
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
|
const DATA_END = 50 * 1024; // Needs to be increased compared with Asyncify default
|
||||||
|
|
||||||
async function initWasm() {
|
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]);
|
new Int32Array(wasmRaw.memory.buffer, DATA_ADDR).set([DATA_START, DATA_END]);
|
||||||
|
|
||||||
|
@ -24,6 +24,8 @@ use crate::stv::{QuotaMode, STVOptions};
|
|||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
use rkyv::{Archive, Archived, Deserialize, Fallible, Resolver, Serialize, with::{ArchiveWith, DeserializeWith, SerializeWith}};
|
use rkyv::{Archive, Archived, Deserialize, Fallible, Resolver, Serialize, with::{ArchiveWith, DeserializeWith, SerializeWith}};
|
||||||
|
|
||||||
|
use itertools::Itertools;
|
||||||
|
|
||||||
use std::cmp::max;
|
use std::cmp::max;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
@ -414,11 +416,27 @@ impl<N: Number> Ballot<N> {
|
|||||||
|
|
||||||
for preference in self.preferences.iter() {
|
for preference in self.preferences.iter() {
|
||||||
if preference.len() == 1 {
|
if preference.len() == 1 {
|
||||||
|
// Single preference so just add to the end of existing preferences
|
||||||
for minivoter in minivoters.iter_mut() {
|
for minivoter in minivoters.iter_mut() {
|
||||||
minivoter.push(preference.clone());
|
minivoter.push(preference.clone());
|
||||||
}
|
}
|
||||||
} else {
|
} 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -144,13 +144,19 @@ impl<N: Number, I: Iterator<Item=char>> BLTParser<N, I> {
|
|||||||
self.delimiter_not_nl();
|
self.delimiter_not_nl();
|
||||||
|
|
||||||
// Read preferences
|
// Read preferences
|
||||||
let mut preferences = Vec::new();
|
let mut preferences: Vec<Vec<usize>> = Vec::new();
|
||||||
loop {
|
loop {
|
||||||
if self.lookahead() == '0' || self.lookahead() == '\n' {
|
if self.lookahead() == '0' || self.lookahead() == '\n' {
|
||||||
// End of preferences
|
// End of preferences
|
||||||
self.accept();
|
self.accept();
|
||||||
break;
|
break;
|
||||||
|
} else if self.lookahead() == '=' {
|
||||||
|
// Equal preference
|
||||||
|
self.accept();
|
||||||
|
preferences.last_mut().unwrap().push(self.usize()? - 1);
|
||||||
|
self.delimiter_not_nl();
|
||||||
} else {
|
} else {
|
||||||
|
// No equal preference
|
||||||
preferences.push(vec![self.usize()? - 1]);
|
preferences.push(vec![self.usize()? - 1]);
|
||||||
self.delimiter_not_nl();
|
self.delimiter_not_nl();
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,7 @@ use crate::election::{Ballot, Candidate, Election};
|
|||||||
use crate::numbers::Number;
|
use crate::numbers::Number;
|
||||||
|
|
||||||
use csv::ReaderBuilder;
|
use csv::ReaderBuilder;
|
||||||
|
use itertools::Itertools;
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
@ -82,12 +83,21 @@ pub fn parse_reader<R: Read, N: Number>(reader: R) -> Election<N> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Sort by ranking
|
// Sort by ranking
|
||||||
// FIXME: Handle equal rankings and skipped rankings
|
let mut unique_rankings: Vec<usize> = preferences.iter().map(|(r, _)| *r).unique().collect();
|
||||||
preferences.sort_by(|a, b| a.0.cmp(&b.0));
|
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 {
|
ballots.push(Ballot {
|
||||||
orig_value: value,
|
orig_value: value,
|
||||||
preferences: preferences.into_iter().map(|(_, i)| vec![*i]).collect(),
|
preferences: sorted_preferences,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
15
tests/data/equal_ranks.blt
Normal file
15
tests/data/equal_ranks.blt
Normal 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
28
tests/equal_ranks.rs
Normal 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);
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user