diff --git a/html/index.html b/html/index.html
index f01d77c..590c187 100644
--- a/html/index.html
+++ b/html/index.html
@@ -158,13 +158,15 @@
-
-
-
+
+
Count optimisations:
diff --git a/html/index.js b/html/index.js
index 3aabe75..87f14c6 100644
--- a/html/index.js
+++ b/html/index.js
@@ -116,6 +116,7 @@ async function clickCount() {
'filePath': filePath,
'numbers': document.getElementById('selNumbers').value,
'decimals': document.getElementById('txtDP').value,
+ 'normaliseBallots': document.getElementById('chkNormaliseBallots').checked,
});
}
@@ -301,6 +302,7 @@ function changePreset() {
document.getElementById('chkDeferSurpluses').checked = false;
document.getElementById('selNumbers').value = 'rational';
document.getElementById('txtPPDP').value = '2';
+ document.getElementById('chkNormaliseBallots').checked = false;
document.getElementById('chkRoundQuota').checked = false;
document.getElementById('chkRoundVotes').checked = false;
document.getElementById('chkRoundTVs').checked = false;
@@ -321,6 +323,7 @@ function changePreset() {
document.getElementById('selNumbers').value = 'fixed';
document.getElementById('txtDP').value = '5';
document.getElementById('txtPPDP').value = '5';
+ document.getElementById('chkNormaliseBallots').checked = true;
document.getElementById('chkRoundQuota').checked = true;
document.getElementById('txtRoundQuota').value = '0';
document.getElementById('chkRoundVotes').checked = false;
@@ -343,6 +346,7 @@ function changePreset() {
document.getElementById('selNumbers').value = 'fixed';
document.getElementById('txtDP').value = '5';
document.getElementById('txtPPDP').value = '0';
+ document.getElementById('chkNormaliseBallots').checked = false;
document.getElementById('chkRoundQuota').checked = true;
document.getElementById('txtRoundQuota').value = '0';
document.getElementById('chkRoundVotes').checked = true;
@@ -365,6 +369,7 @@ function changePreset() {
document.getElementById('selNumbers').value = 'fixed';
document.getElementById('txtDP').value = '5';
document.getElementById('txtPPDP').value = '3';
+ document.getElementById('chkNormaliseBallots').checked = false;
document.getElementById('chkRoundQuota').checked = true;
document.getElementById('txtRoundQuota').value = '3';
document.getElementById('chkRoundVotes').checked = true;
@@ -389,6 +394,7 @@ function changePreset() {
document.getElementById('selNumbers').value = 'fixed';
document.getElementById('txtDP').value = '5';
document.getElementById('txtPPDP').value = '2';
+ document.getElementById('chkNormaliseBallots').checked = false;
document.getElementById('chkRoundQuota').checked = true;
document.getElementById('txtRoundQuota').value = '2';
document.getElementById('chkRoundVotes').checked = true;
diff --git a/html/worker.js b/html/worker.js
index d25b23d..f766283 100644
--- a/html/worker.js
+++ b/html/worker.js
@@ -25,6 +25,10 @@ onmessage = function(evt) {
// Init election
let election = wasm['election_from_blt_' + numbers](evt.data.electionData);
+ if (evt.data.normaliseBallots) {
+ wasm['election_normalise_ballots_' + numbers](election);
+ }
+
// Init STV options
let opts = wasm.STVOptions.new.apply(null, evt.data.optsStr);
diff --git a/src/election.rs b/src/election.rs
index 4a06dd9..a238ca3 100644
--- a/src/election.rs
+++ b/src/election.rs
@@ -87,6 +87,23 @@ impl Election {
return election;
}
+
+ pub fn normalise_ballots(&mut self) {
+ let mut normalised_ballots = Vec::new();
+ for ballot in self.ballots.iter() {
+ let mut n = N::new();
+ let one = N::one();
+ while n < ballot.orig_value {
+ let new_ballot = Ballot {
+ orig_value: N::one(),
+ preferences: ballot.preferences.clone(),
+ };
+ normalised_ballots.push(new_ballot);
+ n += &one;
+ }
+ }
+ self.ballots = normalised_ballots;
+ }
}
#[derive(PartialEq, Eq, Hash)]
diff --git a/src/main.rs b/src/main.rs
index 24b4ebb..b9df557 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -59,6 +59,10 @@ struct STV {
#[clap(help_heading=Some("NUMBERS"), long, default_value="5", value_name="dps")]
decimals: usize,
+ /// Convert ballots with value >1 to multiple ballots of value 1
+ #[clap(help_heading=Some("NUMBERS"), long)]
+ normalise_ballots: bool,
+
// -----------------------
// -- Rounding settings --
@@ -164,7 +168,7 @@ fn main() {
}
}
-fn count_election(election: Election, cmd_opts: STV)
+fn count_election(mut election: Election, cmd_opts: STV)
where
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
for<'r> &'r N: ops::Div<&'r N, Output=N>,
@@ -189,6 +193,11 @@ where
cmd_opts.pp_decimals,
);
+ // Normalise ballots if requested
+ if cmd_opts.normalise_ballots {
+ election.normalise_ballots();
+ }
+
// Initialise count state
let mut state = CountState::new(&election);
diff --git a/src/stv/wasm.rs b/src/stv/wasm.rs
index 475187f..2002e12 100644
--- a/src/stv/wasm.rs
+++ b/src/stv/wasm.rs
@@ -47,6 +47,12 @@ macro_rules! impl_type {
return [](election);
}
+ #[wasm_bindgen]
+ #[allow(non_snake_case)]
+ pub fn [](election: &mut []) {
+ election.0.normalise_ballots();
+ }
+
#[wasm_bindgen]
#[allow(non_snake_case)]
pub fn [](state: &mut [], opts: &stv::STVOptions) {
diff --git a/tests/scotland.rs b/tests/scotland.rs
index 7af4aa9..0ebec2a 100644
--- a/tests/scotland.rs
+++ b/tests/scotland.rs
@@ -69,7 +69,10 @@ fn scotland_linn07_fixed5() {
let file_reader = io::BufReader::new(file);
let lines = file_reader.lines();
- let election: Election = Election::from_blt(lines.map(|r| r.expect("IO Error").to_string()).into_iter());
+ let mut election: Election = Election::from_blt(lines.map(|r| r.expect("IO Error").to_string()).into_iter());
+
+ // !!! FOR SCOTTISH STV !!!
+ election.normalise_ballots();
// Initialise count state
let mut state = CountState::new(&election);
@@ -87,7 +90,7 @@ fn scotland_linn07_fixed5() {
.get_text().unwrap()
.to_string();
- assert!(approx_eq(&(&state.exhausted.votes + &state.loss_fraction.votes), &parse_str(nt_votes)));
+ assert!((&state.exhausted.votes + &state.loss_fraction.votes) == parse_str(nt_votes));
for (candidate, cand_xml) in state.election.candidates.iter().zip(candidates.iter()) {
let count_card = state.candidates.get(candidate).unwrap();
@@ -98,7 +101,7 @@ fn scotland_linn07_fixed5() {
.get_text().unwrap()
.to_string();
let cand_votes = parse_str(cand_votes);
- assert!(approx_eq(&count_card.votes, &cand_votes), "Failed to validate votes for candidate {}. Expected {:}, got {:}", candidate.name, cand_votes, count_card.votes);
+ assert!(count_card.votes == cand_votes, "Failed to validate votes for candidate {}. Expected {:}, got {:}", candidate.name, cand_votes, count_card.votes);
// Validate candidate states
let cand_state = get_cand_stage(cand_xml, i)
@@ -137,17 +140,3 @@ fn parse_str(s: String) -> Fixed {
let f: f64 = s.parse().expect("Syntax Error");
return opentally::numbers::From::from(f);
}
-
-fn approx_eq(a: &Fixed, b: &Fixed) -> bool {
- // Some tolerance required in equality comparisons, as eSTV incorrectly computes transfers
- // by value, instead of all at once, resulting in increased rounding error
- let eq_tol: Fixed = opentally::numbers::From::from(0.0001);
-
- if a - b > eq_tol {
- return false;
- }
- if b - a > eq_tol {
- return false;
- }
- return true;
-}