Implement papers+votes report
This commit is contained in:
parent
c9faa2ef01
commit
8a3361f20d
@ -166,6 +166,19 @@ OpenTally applies constraints using the Grey–Fitzgerald method. Whenever a can
|
|||||||
|
|
||||||
Multiple constraints are supported using the method described by Hill ([*Voting Matters* 1998;(9):2–4](http://www.votingmatters.org.uk/ISSUE9/P1.HTM)) and Otten ([*Voting Matters* 2001;(13):4–7](http://www.votingmatters.org.uk/ISSUE13/P3.HTM)).
|
Multiple constraints are supported using the method described by Hill ([*Voting Matters* 1998;(9):2–4](http://www.votingmatters.org.uk/ISSUE9/P1.HTM)) and Otten ([*Voting Matters* 2001;(13):4–7](http://www.votingmatters.org.uk/ISSUE13/P3.HTM)).
|
||||||
|
|
||||||
|
## Report options
|
||||||
|
|
||||||
|
### Report style
|
||||||
|
|
||||||
|
* *Votes only*: The result sheet displays the number of votes held by each candidate at each stage of the count.
|
||||||
|
* *Ballots and votes*: The result sheet displays the number of votes *and ballot papers* held by each candidate at each stage of the count.
|
||||||
|
|
||||||
|
This functionality is not available on the command line.
|
||||||
|
|
||||||
|
### Display up to [n] d.p. (--pp-decimals)
|
||||||
|
|
||||||
|
This option allows you to specify to how many decimal places votes will be reported in the results report. It does not affect the internal precision of calculations.
|
||||||
|
|
||||||
## Numeric representation
|
## Numeric representation
|
||||||
|
|
||||||
### Numbers (-n/--numbers), Decimal places (--decimals)
|
### Numbers (-n/--numbers), Decimal places (--decimals)
|
||||||
@ -177,10 +190,6 @@ This dropdown allows you to select how numbers (vote totals, etc.) are represent
|
|||||||
* *Rational* (default): Numbers are represented exactly as fractions, resulting in the elimination of rounding error, but increasing computational complexity when the number of surplus transfers is very large.
|
* *Rational* (default): Numbers are represented exactly as fractions, resulting in the elimination of rounding error, but increasing computational complexity when the number of surplus transfers is very large.
|
||||||
* *Float (64-bit)*: Numbers are represented as native 64-bit floating-point numbers. This is fast, but not recommended as unexpectedly large rounding errors may be introduced in some circumstances.
|
* *Float (64-bit)*: Numbers are represented as native 64-bit floating-point numbers. This is fast, but not recommended as unexpectedly large rounding errors may be introduced in some circumstances.
|
||||||
|
|
||||||
### Display up to [n] d.p. (--pp-decimals)
|
|
||||||
|
|
||||||
This option allows you to specify to how many decimal places votes will be reported in the results report. It does not affect the internal precision of calculations.
|
|
||||||
|
|
||||||
### Normalise ballots (--normalise-ballots)
|
### Normalise ballots (--normalise-ballots)
|
||||||
|
|
||||||
In the BLT file format, each set of preferences can have a specified weight – this is typically used to indicate multiple voters who had the same preferences.
|
In the BLT file format, each set of preferences can have a specified weight – this is typically used to indicate multiple voters who had the same preferences.
|
||||||
|
@ -182,6 +182,27 @@
|
|||||||
<div>
|
<div>
|
||||||
<input type="file" id="conFile">
|
<input type="file" id="conFile">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="subheading">
|
||||||
|
Report options:
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<label style="margin-right:1em;">
|
||||||
|
Report style:
|
||||||
|
<select id="selReport">
|
||||||
|
<option value="votes" selected>Votes only</option>
|
||||||
|
<option value="ballots_votes">Ballots and votes</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label style="display:none;">
|
||||||
|
<input type="checkbox" id="chkReportTranspose" disabled>
|
||||||
|
Transpose transfers/<wbr>totals
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<label class="col-12">
|
||||||
|
Display up to
|
||||||
|
<input type="number" id="txtPPDP" value="2" min="0" style="width: 3em;">
|
||||||
|
d.p.
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6 cols-12" style="align-self: start;">
|
<div class="col-6 cols-12" style="align-self: start;">
|
||||||
<div class="col-12 subheading">
|
<div class="col-12 subheading">
|
||||||
@ -203,11 +224,6 @@
|
|||||||
<input type="number" id="txtDP" value="5" min="0" style="width: 3em;">
|
<input type="number" id="txtDP" value="5" min="0" style="width: 3em;">
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<label class="col-12">
|
|
||||||
Display up to
|
|
||||||
<input type="number" id="txtPPDP" value="2" min="0" style="width: 3em;">
|
|
||||||
d.p.
|
|
||||||
</label>
|
|
||||||
<label class="col-12">
|
<label class="col-12">
|
||||||
<input type="checkbox" id="chkNormaliseBallots">
|
<input type="checkbox" id="chkNormaliseBallots">
|
||||||
Normalise ballots
|
Normalise ballots
|
||||||
|
@ -58,16 +58,19 @@ worker.onmessage = function(evt) {
|
|||||||
document.getElementById('resultLogs1').innerHTML = evt.data.content;
|
document.getElementById('resultLogs1').innerHTML = evt.data.content;
|
||||||
|
|
||||||
} else if (evt.data.type === 'updateResultsTable') {
|
} else if (evt.data.type === 'updateResultsTable') {
|
||||||
for (let i = 0; i < evt.data.result.length; i++) {
|
for (let row = 0; row < evt.data.result.length; row++) {
|
||||||
if (evt.data.result[i]) {
|
if (evt.data.result[row]) {
|
||||||
tblResult.rows[i].insertAdjacentHTML('beforeend', evt.data.result[i]);
|
tblResult.rows[row].insertAdjacentHTML('beforeend', evt.data.result[row]);
|
||||||
|
|
||||||
// Update candidate status
|
// Update candidate status
|
||||||
if (i >= 3 && i % 2 == 1) {
|
if (
|
||||||
if (tblResult.rows[i].lastElementChild.classList.contains('elected')) {
|
(document.getElementById('selReport').value == 'votes' && row >= 3 && row % 2 == 1) ||
|
||||||
tblResult.rows[i].cells[0].classList.add('elected');
|
(document.getElementById('selReport').value == 'ballots_votes' && row >= 4 && row % 2 == 0)
|
||||||
|
) {
|
||||||
|
if (tblResult.rows[row].lastElementChild.classList.contains('elected')) {
|
||||||
|
tblResult.rows[row].cells[0].classList.add('elected');
|
||||||
} else {
|
} else {
|
||||||
tblResult.rows[i].cells[0].classList.remove('elected');
|
tblResult.rows[row].cells[0].classList.remove('elected');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -170,14 +173,18 @@ async function clickCount() {
|
|||||||
// Dispatch to worker
|
// Dispatch to worker
|
||||||
worker.postMessage({
|
worker.postMessage({
|
||||||
'type': 'countElection',
|
'type': 'countElection',
|
||||||
|
// Data
|
||||||
'bltData': bltData,
|
'bltData': bltData,
|
||||||
'conData': conData,
|
'conData': conData,
|
||||||
'optsStr': optsStr,
|
|
||||||
'bltPath': bltPath,
|
'bltPath': bltPath,
|
||||||
'conPath': conPath,
|
'conPath': conPath,
|
||||||
|
// Options
|
||||||
|
'optsStr': optsStr,
|
||||||
'numbers': document.getElementById('selNumbers').value,
|
'numbers': document.getElementById('selNumbers').value,
|
||||||
'decimals': document.getElementById('txtDP').value,
|
'decimals': document.getElementById('txtDP').value,
|
||||||
'normaliseBallots': document.getElementById('chkNormaliseBallots').checked,
|
'normaliseBallots': document.getElementById('chkNormaliseBallots').checked,
|
||||||
|
'reportStyle': document.getElementById('selReport').value,
|
||||||
|
'reportTranspose': document.getElementById('chkReportTranspose').checked,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,6 +54,10 @@ li.highlight {
|
|||||||
.menudiv .subheading {
|
.menudiv .subheading {
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
.menudiv > div > .subheading:first-child {
|
||||||
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pill-grey {
|
.pill-grey {
|
||||||
@ -101,7 +105,7 @@ td.count sup {
|
|||||||
font-size: 0.6rem;
|
font-size: 0.6rem;
|
||||||
top: 0;
|
top: 0;
|
||||||
}
|
}
|
||||||
tr.stage-no td, tr.stage-kind td, tr.stage-comment td {
|
tr.stage-no td, tr.stage-kind td, tr.stage-comment td, tr.hint-papers-votes td {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
tr.stage-kind td {
|
tr.stage-kind td {
|
||||||
@ -110,6 +114,10 @@ tr.stage-kind td {
|
|||||||
color: #1b2839;
|
color: #1b2839;
|
||||||
background-color: #f0f5fb;
|
background-color: #f0f5fb;
|
||||||
}
|
}
|
||||||
|
tr.hint-papers-votes td {
|
||||||
|
font-size: 0.75em;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
td.excluded {
|
td.excluded {
|
||||||
background-color: #fde2e2;
|
background-color: #fde2e2;
|
||||||
}
|
}
|
||||||
@ -119,7 +127,7 @@ td.elected {
|
|||||||
tr.info td {
|
tr.info td {
|
||||||
background-color: #f0f5fb;
|
background-color: #f0f5fb;
|
||||||
}
|
}
|
||||||
tr.stage-no td:not(:empty), tr.transfers td {
|
tr.stage-no td:not(:empty), tr.hint-papers-votes td:not(:empty), tr.transfers td {
|
||||||
border-top: 1px solid #76858c;
|
border-top: 1px solid #76858c;
|
||||||
}
|
}
|
||||||
tr.info:last-child td, .bb {
|
tr.info:last-child td, .bb {
|
||||||
@ -134,6 +142,7 @@ tr.info:last-child td, .bb {
|
|||||||
|
|
||||||
tr.stage-no td:nth-child(even):not([rowspan]),
|
tr.stage-no td:nth-child(even):not([rowspan]),
|
||||||
tr.stage-comment td:nth-child(odd),
|
tr.stage-comment td:nth-child(odd),
|
||||||
|
tr.hint-papers-votes td:nth-child(even),
|
||||||
tr.candidate.transfers td:nth-child(even):not(.elected):not(.excluded),
|
tr.candidate.transfers td:nth-child(even):not(.elected):not(.excluded),
|
||||||
tr.candidate.votes td:nth-child(odd):not(.elected):not(.excluded) {
|
tr.candidate.votes td:nth-child(odd):not(.elected):not(.excluded) {
|
||||||
background-color: #f9f9f9;
|
background-color: #f9f9f9;
|
||||||
|
@ -17,6 +17,7 @@ async function initWasm() {
|
|||||||
}
|
}
|
||||||
initWasm();
|
initWasm();
|
||||||
|
|
||||||
|
var reportStyle;
|
||||||
var numbers, election, opts, state, stageNum;
|
var numbers, election, opts, state, stageNum;
|
||||||
|
|
||||||
onmessage = function(evt) {
|
onmessage = function(evt) {
|
||||||
@ -38,6 +39,8 @@ onmessage = function(evt) {
|
|||||||
throw 'Unknown --numbers';
|
throw 'Unknown --numbers';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reportStyle = evt.data.reportStyle;
|
||||||
|
|
||||||
// Init election
|
// Init election
|
||||||
election = wasm['election_from_blt_' + numbers](evt.data.bltData);
|
election = wasm['election_from_blt_' + numbers](evt.data.bltData);
|
||||||
|
|
||||||
@ -60,13 +63,13 @@ onmessage = function(evt) {
|
|||||||
postMessage({'type': 'describeCount', 'content': wasm['describe_count_' + numbers](evt.data.bltPath, election, opts)});
|
postMessage({'type': 'describeCount', 'content': wasm['describe_count_' + numbers](evt.data.bltPath, election, opts)});
|
||||||
|
|
||||||
// Init results table
|
// Init results table
|
||||||
postMessage({'type': 'initResultsTable', 'content': wasm['init_results_table_' + numbers](election, opts)});
|
postMessage({'type': 'initResultsTable', 'content': wasm['init_results_table_' + numbers](election, opts, reportStyle)});
|
||||||
|
|
||||||
// Step election
|
// Step election
|
||||||
state = wasm['CountState' + numbers].new(election);
|
state = wasm['CountState' + numbers].new(election);
|
||||||
wasm['count_init_' + numbers](state, opts);
|
wasm['count_init_' + numbers](state, opts);
|
||||||
|
|
||||||
postMessage({'type': 'updateResultsTable', 'result': wasm['update_results_table_' + numbers](1, state, opts)});
|
postMessage({'type': 'updateResultsTable', 'result': wasm['update_results_table_' + numbers](1, state, opts, reportStyle)});
|
||||||
postMessage({'type': 'updateStageComments', 'comment': wasm['update_stage_comments_' + numbers](state)});
|
postMessage({'type': 'updateStageComments', 'comment': wasm['update_stage_comments_' + numbers](state)});
|
||||||
|
|
||||||
stageNum = 2;
|
stageNum = 2;
|
||||||
@ -104,11 +107,11 @@ function resumeCount() {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
postMessage({'type': 'updateResultsTable', 'result': wasm['update_results_table_' + numbers](stageNum, state, opts)});
|
postMessage({'type': 'updateResultsTable', 'result': wasm['update_results_table_' + numbers](stageNum, state, opts, reportStyle)});
|
||||||
postMessage({'type': 'updateStageComments', 'comment': wasm['update_stage_comments_' + numbers](state)});
|
postMessage({'type': 'updateStageComments', 'comment': wasm['update_stage_comments_' + numbers](state)});
|
||||||
}
|
}
|
||||||
|
|
||||||
postMessage({'type': 'updateResultsTable', 'result': wasm['finalise_results_table_' + numbers](state)});
|
postMessage({'type': 'updateResultsTable', 'result': wasm['finalise_results_table_' + numbers](state, reportStyle)});
|
||||||
postMessage({'type': 'finalResultSummary', 'summary': wasm['final_result_summary_' + numbers](state, opts)});
|
postMessage({'type': 'finalResultSummary', 'summary': wasm['final_result_summary_' + numbers](state, opts)});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -304,6 +304,9 @@ pub struct CountCard<'a, N> {
|
|||||||
/// Votes of the candidate at the end of this stage
|
/// Votes of the candidate at the end of this stage
|
||||||
pub votes: N,
|
pub votes: N,
|
||||||
|
|
||||||
|
/// Net ballots transferred to this candidate in this stage
|
||||||
|
pub ballot_transfers: N,
|
||||||
|
|
||||||
/// Parcels of ballots assigned to this candidate
|
/// Parcels of ballots assigned to this candidate
|
||||||
pub parcels: Vec<Parcel<'a, N>>,
|
pub parcels: Vec<Parcel<'a, N>>,
|
||||||
|
|
||||||
@ -320,6 +323,7 @@ impl<'a, N: Number> CountCard<'a, N> {
|
|||||||
finalised: false,
|
finalised: false,
|
||||||
transfers: N::new(),
|
transfers: N::new(),
|
||||||
votes: N::new(),
|
votes: N::new(),
|
||||||
|
ballot_transfers: N::new(),
|
||||||
parcels: Vec::new(),
|
parcels: Vec::new(),
|
||||||
keep_value: None,
|
keep_value: None,
|
||||||
};
|
};
|
||||||
@ -333,8 +337,8 @@ impl<'a, N: Number> CountCard<'a, N> {
|
|||||||
|
|
||||||
/// Set [transfers](CountCard::transfers) to 0
|
/// Set [transfers](CountCard::transfers) to 0
|
||||||
pub fn step(&mut self) {
|
pub fn step(&mut self) {
|
||||||
//self.orig_votes = self.votes.clone();
|
|
||||||
self.transfers = N::new();
|
self.transfers = N::new();
|
||||||
|
self.ballot_transfers = N::new();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Concatenate all parcels into a single parcel, leaving [parcels](CountCard::parcels) empty
|
/// Concatenate all parcels into a single parcel, leaving [parcels](CountCard::parcels) empty
|
||||||
@ -345,6 +349,11 @@ impl<'a, N: Number> CountCard<'a, N> {
|
|||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return the number of ballots across all parcels
|
||||||
|
pub fn num_ballots(&self) -> N {
|
||||||
|
return self.parcels.iter().fold(N::new(), |acc, p| acc + p.num_ballots());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parcel of [Vote]s during a count
|
/// Parcel of [Vote]s during a count
|
||||||
|
@ -46,6 +46,7 @@ pub fn distribute_first_preferences<N: Number>(state: &mut CountState<N>) {
|
|||||||
let count_card = state.candidates.get_mut(candidate).unwrap();
|
let count_card = state.candidates.get_mut(candidate).unwrap();
|
||||||
count_card.parcels.push(parcel);
|
count_card.parcels.push(parcel);
|
||||||
count_card.transfer(&entry.num_ballots);
|
count_card.transfer(&entry.num_ballots);
|
||||||
|
count_card.ballot_transfers += entry.num_ballots;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Transfer exhausted votes
|
// Transfer exhausted votes
|
||||||
@ -56,6 +57,7 @@ pub fn distribute_first_preferences<N: Number>(state: &mut CountState<N>) {
|
|||||||
};
|
};
|
||||||
state.exhausted.parcels.push(parcel);
|
state.exhausted.parcels.push(parcel);
|
||||||
state.exhausted.transfer(&result.exhausted.num_ballots);
|
state.exhausted.transfer(&result.exhausted.num_ballots);
|
||||||
|
state.exhausted.ballot_transfers += result.exhausted.num_ballots;
|
||||||
|
|
||||||
state.kind = None;
|
state.kind = None;
|
||||||
state.title = "First preferences".to_string();
|
state.title = "First preferences".to_string();
|
||||||
@ -251,7 +253,9 @@ where
|
|||||||
state.title = String::from(&elected_candidate.name);
|
state.title = String::from(&elected_candidate.name);
|
||||||
state.logger.log_literal(format!("Surplus of {} distributed.", elected_candidate.name));
|
state.logger.log_literal(format!("Surplus of {} distributed.", elected_candidate.name));
|
||||||
|
|
||||||
let count_card = &state.candidates[elected_candidate];
|
let count_card = state.candidates.get_mut(elected_candidate).unwrap();
|
||||||
|
count_card.ballot_transfers = -count_card.num_ballots();
|
||||||
|
|
||||||
let surplus = &count_card.votes - state.quota.as_ref().unwrap();
|
let surplus = &count_card.votes - state.quota.as_ref().unwrap();
|
||||||
|
|
||||||
// Determine which votes to examine
|
// Determine which votes to examine
|
||||||
@ -361,13 +365,14 @@ where
|
|||||||
candidate_transfers.insert(candidate, transfers_orig + transfers_add);
|
candidate_transfers.insert(candidate, transfers_orig + transfers_add);
|
||||||
|
|
||||||
// Transfer candidate votes
|
// Transfer candidate votes
|
||||||
let new_parcel = Parcel {
|
let parcel = Parcel {
|
||||||
votes: entry.votes,
|
votes: entry.votes,
|
||||||
value_fraction: reweight_value_fraction(&value_fraction, &surplus, is_weighted, &surplus_fraction, &surplus_denom, opts.round_surplus_fractions),
|
value_fraction: reweight_value_fraction(&value_fraction, &surplus, is_weighted, &surplus_fraction, &surplus_denom, opts.round_surplus_fractions),
|
||||||
source_order: state.num_elected + state.num_excluded,
|
source_order: state.num_elected + state.num_excluded,
|
||||||
};
|
};
|
||||||
let count_card = state.candidates.get_mut(candidate).unwrap();
|
let count_card = state.candidates.get_mut(candidate).unwrap();
|
||||||
count_card.parcels.push(new_parcel);
|
count_card.ballot_transfers += parcel.num_ballots();
|
||||||
|
count_card.parcels.push(parcel);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Record exhausted votes
|
// Record exhausted votes
|
||||||
@ -387,12 +392,14 @@ where
|
|||||||
value_fraction: value_fraction, // TODO: Reweight exhausted votes
|
value_fraction: value_fraction, // TODO: Reweight exhausted votes
|
||||||
source_order: state.num_elected + state.num_excluded,
|
source_order: state.num_elected + state.num_excluded,
|
||||||
};
|
};
|
||||||
|
state.exhausted.ballot_transfers += parcel.num_ballots();
|
||||||
state.exhausted.parcels.push(parcel);
|
state.exhausted.parcels.push(parcel);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut checksum = N::new();
|
let mut checksum = N::new();
|
||||||
|
|
||||||
// Credit transferred votes
|
// Credit transferred votes
|
||||||
|
// ballot_transfers updated above
|
||||||
for (candidate, mut votes) in candidate_transfers {
|
for (candidate, mut votes) in candidate_transfers {
|
||||||
if let Some(dps) = opts.round_votes {
|
if let Some(dps) = opts.round_votes {
|
||||||
votes.floor_mut(dps);
|
votes.floor_mut(dps);
|
||||||
@ -453,6 +460,7 @@ where
|
|||||||
// Exclude in one round
|
// Exclude in one round
|
||||||
for excluded_candidate in excluded_candidates.iter() {
|
for excluded_candidate in excluded_candidates.iter() {
|
||||||
let count_card = state.candidates.get_mut(excluded_candidate).unwrap();
|
let count_card = state.candidates.get_mut(excluded_candidate).unwrap();
|
||||||
|
count_card.ballot_transfers = -count_card.num_ballots();
|
||||||
count_card.finalised = true;
|
count_card.finalised = true;
|
||||||
|
|
||||||
parcels.append(&mut count_card.parcels);
|
parcels.append(&mut count_card.parcels);
|
||||||
@ -493,6 +501,8 @@ where
|
|||||||
|
|
||||||
for mut parcel in cc_parcels {
|
for mut parcel in cc_parcels {
|
||||||
if parcel.value_fraction == max_value {
|
if parcel.value_fraction == max_value {
|
||||||
|
count_card.ballot_transfers -= parcel.num_ballots();
|
||||||
|
|
||||||
let votes_transferred = parcel.num_votes();
|
let votes_transferred = parcel.num_votes();
|
||||||
votes.append(&mut parcel.votes);
|
votes.append(&mut parcel.votes);
|
||||||
|
|
||||||
@ -546,6 +556,8 @@ where
|
|||||||
|
|
||||||
for parcel in cc_parcels {
|
for parcel in cc_parcels {
|
||||||
if parcel.source_order == min_order {
|
if parcel.source_order == min_order {
|
||||||
|
count_card.ballot_transfers -= parcel.num_ballots();
|
||||||
|
|
||||||
let votes_transferred = parcel.num_votes();
|
let votes_transferred = parcel.num_votes();
|
||||||
parcels.push(parcel);
|
parcels.push(parcel);
|
||||||
|
|
||||||
@ -581,6 +593,8 @@ where
|
|||||||
parcels.push(count_card.parcels.remove(0));
|
parcels.push(count_card.parcels.remove(0));
|
||||||
votes_remain = !count_card.parcels.is_empty();
|
votes_remain = !count_card.parcels.is_empty();
|
||||||
|
|
||||||
|
count_card.ballot_transfers -= parcels.first().unwrap().num_ballots();
|
||||||
|
|
||||||
// Update votes
|
// Update votes
|
||||||
let votes_transferred = parcels.first().unwrap().num_votes();
|
let votes_transferred = parcels.first().unwrap().num_votes();
|
||||||
checksum -= &votes_transferred;
|
checksum -= &votes_transferred;
|
||||||
@ -621,6 +635,7 @@ where
|
|||||||
candidate_transfers.insert(candidate, transfers_orig + &entry.num_ballots * &parcel.value_fraction);
|
candidate_transfers.insert(candidate, transfers_orig + &entry.num_ballots * &parcel.value_fraction);
|
||||||
|
|
||||||
let count_card = state.candidates.get_mut(candidate).unwrap();
|
let count_card = state.candidates.get_mut(candidate).unwrap();
|
||||||
|
count_card.ballot_transfers += parcel.num_ballots();
|
||||||
count_card.parcels.push(parcel);
|
count_card.parcels.push(parcel);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -632,8 +647,8 @@ where
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Record transfers
|
// Record transfers
|
||||||
|
state.exhausted.ballot_transfers += parcel.num_ballots();
|
||||||
exhausted_transfers += &result.exhausted.num_ballots * &parcel.value_fraction;
|
exhausted_transfers += &result.exhausted.num_ballots * &parcel.value_fraction;
|
||||||
|
|
||||||
state.exhausted.parcels.push(parcel);
|
state.exhausted.parcels.push(parcel);
|
||||||
|
|
||||||
// TODO: Detailed transfers logs
|
// TODO: Detailed transfers logs
|
||||||
@ -656,6 +671,7 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Credit transferred votes
|
// Credit transferred votes
|
||||||
|
// ballot_transfers updated above
|
||||||
for (candidate, mut votes) in candidate_transfers {
|
for (candidate, mut votes) in candidate_transfers {
|
||||||
if let Some(dps) = opts.round_votes {
|
if let Some(dps) = opts.round_votes {
|
||||||
votes.floor_mut(dps);
|
votes.floor_mut(dps);
|
||||||
|
206
src/stv/wasm.rs
206
src/stv/wasm.rs
@ -118,8 +118,8 @@ macro_rules! impl_type {
|
|||||||
/// Wrapper for [init_results_table]
|
/// Wrapper for [init_results_table]
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
pub fn [<init_results_table_$type>](election: &[<Election$type>], opts: &STVOptions) -> String {
|
pub fn [<init_results_table_$type>](election: &[<Election$type>], opts: &STVOptions, report_style: &str) -> String {
|
||||||
return init_results_table(&election.0, &opts.0);
|
return init_results_table(&election.0, &opts.0, report_style);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Wrapper for [describe_count]
|
/// Wrapper for [describe_count]
|
||||||
@ -132,8 +132,8 @@ macro_rules! impl_type {
|
|||||||
/// Wrapper for [update_results_table]
|
/// Wrapper for [update_results_table]
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
pub fn [<update_results_table_$type>](stage_num: usize, state: &[<CountState$type>], opts: &STVOptions) -> Array {
|
pub fn [<update_results_table_$type>](stage_num: usize, state: &[<CountState$type>], opts: &STVOptions, report_style: &str) -> Array {
|
||||||
return update_results_table(stage_num, &state.0, &opts.0);
|
return update_results_table(stage_num, &state.0, &opts.0, report_style);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Wrapper for [update_stage_comments]
|
/// Wrapper for [update_stage_comments]
|
||||||
@ -146,8 +146,8 @@ macro_rules! impl_type {
|
|||||||
/// Wrapper for [finalise_results_table]
|
/// Wrapper for [finalise_results_table]
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
pub fn [<finalise_results_table_$type>](state: &[<CountState$type>]) -> Array {
|
pub fn [<finalise_results_table_$type>](state: &[<CountState$type>], report_style: &str) -> Array {
|
||||||
return finalise_results_table(&state.0);
|
return finalise_results_table(&state.0, report_style);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Wrapper for [final_result_summary]
|
/// Wrapper for [final_result_summary]
|
||||||
@ -321,8 +321,14 @@ fn should_show_vre(opts: &stv::STVOptions) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Generate the first column of the HTML results table
|
/// Generate the first column of the HTML results table
|
||||||
fn init_results_table<N: Number>(election: &Election<N>, opts: &stv::STVOptions) -> String {
|
fn init_results_table<N: Number>(election: &Election<N>, opts: &stv::STVOptions, report_style: &str) -> String {
|
||||||
let mut result = String::from(r#"<tr class="stage-no"><td rowspan="3"></td></tr><tr class="stage-kind"></tr><tr class="stage-comment"></tr>"#);
|
let mut result = String::from(r#"<tr class="stage-no"><td rowspan="3"></td></tr><tr class="stage-kind"></tr><tr class="stage-comment"></tr>"#);
|
||||||
|
match report_style {
|
||||||
|
"ballots_votes" => {
|
||||||
|
result.push_str(&r#"<tr class="hint-papers-votes"><td></td></tr>"#);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
for candidate in election.candidates.iter() {
|
for candidate in election.candidates.iter() {
|
||||||
result.push_str(&format!(r#"<tr class="candidate transfers"><td rowspan="2">{}</td></tr><tr class="candidate votes"></tr>"#, candidate.name));
|
result.push_str(&format!(r#"<tr class="candidate transfers"><td rowspan="2">{}</td></tr><tr class="candidate votes"></tr>"#, candidate.name));
|
||||||
}
|
}
|
||||||
@ -334,7 +340,7 @@ fn init_results_table<N: Number>(election: &Election<N>, opts: &stv::STVOptions)
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Generate subsequent columns of the HTML results table
|
/// Generate subsequent columns of the HTML results table
|
||||||
fn update_results_table<N: Number>(stage_num: usize, state: &CountState<N>, opts: &stv::STVOptions) -> Array {
|
fn update_results_table<N: Number>(stage_num: usize, state: &CountState<N>, opts: &stv::STVOptions, report_style: &str) -> Array {
|
||||||
let result = Array::new();
|
let result = Array::new();
|
||||||
|
|
||||||
// Insert borders to left of new exclusions in Wright STV
|
// Insert borders to left of new exclusions in Wright STV
|
||||||
@ -345,55 +351,152 @@ fn update_results_table<N: Number>(stage_num: usize, state: &CountState<N>, opts
|
|||||||
tdclasses2 = r#"blw "#;
|
tdclasses2 = r#"blw "#;
|
||||||
}
|
}
|
||||||
|
|
||||||
result.push(&format!(r##"<td{0}><a href="#stage{1}">{1}</a></td>"##, tdclasses1, stage_num).into());
|
// Header rows
|
||||||
result.push(&format!(r#"<td{}>{}</td>"#, tdclasses1, state.kind.unwrap_or("")).into());
|
match report_style {
|
||||||
result.push(&format!(r#"<td{}>{}</td>"#, tdclasses1, state.title).into());
|
"votes" => {
|
||||||
|
result.push(&format!(r##"<td{0}><a href="#stage{1}">{1}</a></td>"##, tdclasses1, stage_num).into());
|
||||||
|
result.push(&format!(r#"<td{}>{}</td>"#, tdclasses1, state.kind.unwrap_or("")).into());
|
||||||
|
result.push(&format!(r#"<td{}>{}</td>"#, tdclasses1, state.title).into());
|
||||||
|
}
|
||||||
|
"ballots_votes" => {
|
||||||
|
result.push(&format!(r##"<td{0} colspan="2"><a href="#stage{1}">{1}</a></td>"##, tdclasses1, stage_num).into());
|
||||||
|
result.push(&format!(r#"<td{} colspan="2">{}</td>"#, tdclasses1, state.kind.unwrap_or("")).into());
|
||||||
|
result.push(&format!(r#"<td{} colspan="2">{}</td>"#, tdclasses1, state.title).into());
|
||||||
|
result.push(&format!(r#"<td{}>Ballots</td><td>Votes</td>"#, tdclasses1).into());
|
||||||
|
}
|
||||||
|
_ => unreachable!("Invalid report_style")
|
||||||
|
}
|
||||||
|
|
||||||
for candidate in state.election.candidates.iter() {
|
for candidate in state.election.candidates.iter() {
|
||||||
let count_card = &state.candidates[candidate];
|
let count_card = &state.candidates[candidate];
|
||||||
match count_card.state {
|
|
||||||
CandidateState::Hopeful | CandidateState::Guarded => {
|
match report_style {
|
||||||
result.push(&format!(r#"<td class="{}count">{}</td>"#, tdclasses2, pp(&count_card.transfers, opts.pp_decimals)).into());
|
"votes" => {
|
||||||
result.push(&format!(r#"<td class="{}count">{}</td>"#, tdclasses2, pp(&count_card.votes, opts.pp_decimals)).into());
|
match count_card.state {
|
||||||
}
|
CandidateState::Hopeful | CandidateState::Guarded => {
|
||||||
CandidateState::Elected => {
|
result.push(&format!(r#"<td class="{}count">{}</td>"#, tdclasses2, pp(&count_card.transfers, opts.pp_decimals)).into());
|
||||||
result.push(&format!(r#"<td class="{}count elected">{}</td>"#, tdclasses2, pp(&count_card.transfers, opts.pp_decimals)).into());
|
result.push(&format!(r#"<td class="{}count">{}</td>"#, tdclasses2, pp(&count_card.votes, opts.pp_decimals)).into());
|
||||||
result.push(&format!(r#"<td class="{}count elected">{}</td>"#, tdclasses2, pp(&count_card.votes, opts.pp_decimals)).into());
|
}
|
||||||
}
|
CandidateState::Elected => {
|
||||||
CandidateState::Doomed => {
|
result.push(&format!(r#"<td class="{}count elected">{}</td>"#, tdclasses2, pp(&count_card.transfers, opts.pp_decimals)).into());
|
||||||
result.push(&format!(r#"<td class="{}count excluded">{}</td>"#, tdclasses2, pp(&count_card.transfers, opts.pp_decimals)).into());
|
result.push(&format!(r#"<td class="{}count elected">{}</td>"#, tdclasses2, pp(&count_card.votes, opts.pp_decimals)).into());
|
||||||
result.push(&format!(r#"<td class="{}count excluded">{}</td>"#, tdclasses2, pp(&count_card.votes, opts.pp_decimals)).into());
|
}
|
||||||
}
|
CandidateState::Doomed => {
|
||||||
CandidateState::Withdrawn => {
|
result.push(&format!(r#"<td class="{}count excluded">{}</td>"#, tdclasses2, pp(&count_card.transfers, opts.pp_decimals)).into());
|
||||||
result.push(&format!(r#"<td class="{}count excluded"></td>"#, tdclasses2).into());
|
result.push(&format!(r#"<td class="{}count excluded">{}</td>"#, tdclasses2, pp(&count_card.votes, opts.pp_decimals)).into());
|
||||||
result.push(&format!(r#"<td class="{}count excluded">WD</td>"#, tdclasses2).into());
|
}
|
||||||
}
|
CandidateState::Withdrawn => {
|
||||||
CandidateState::Excluded => {
|
result.push(&format!(r#"<td class="{}count excluded"></td>"#, tdclasses2).into());
|
||||||
result.push(&format!(r#"<td class="{}count excluded">{}</td>"#, tdclasses2, pp(&count_card.transfers, opts.pp_decimals)).into());
|
result.push(&format!(r#"<td class="{}count excluded">WD</td>"#, tdclasses2).into());
|
||||||
if count_card.votes.is_zero() && count_card.parcels.iter().all(|p| p.votes.is_empty()) {
|
}
|
||||||
result.push(&format!(r#"<td class="{}count excluded">Ex</td>"#, tdclasses2).into());
|
CandidateState::Excluded => {
|
||||||
} else {
|
result.push(&format!(r#"<td class="{}count excluded">{}</td>"#, tdclasses2, pp(&count_card.transfers, opts.pp_decimals)).into());
|
||||||
result.push(&format!(r#"<td class="{}count excluded">{}</td>"#, tdclasses2, pp(&count_card.votes, opts.pp_decimals)).into());
|
if count_card.votes.is_zero() && count_card.parcels.iter().all(|p| p.votes.is_empty()) {
|
||||||
|
result.push(&format!(r#"<td class="{}count excluded">Ex</td>"#, tdclasses2).into());
|
||||||
|
} else {
|
||||||
|
result.push(&format!(r#"<td class="{}count excluded">{}</td>"#, tdclasses2, pp(&count_card.votes, opts.pp_decimals)).into());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"ballots_votes" => {
|
||||||
|
match count_card.state {
|
||||||
|
CandidateState::Hopeful | CandidateState::Guarded => {
|
||||||
|
result.push(&format!(r#"<td class="{}count">{}</td><td class="count">{}</td>"#, tdclasses2, pp(&count_card.ballot_transfers, 0), pp(&count_card.transfers, opts.pp_decimals)).into());
|
||||||
|
result.push(&format!(r#"<td class="{}count">{}</td><td class="count">{}</td>"#, tdclasses2, pp(&count_card.num_ballots(), 0), pp(&count_card.votes, opts.pp_decimals)).into());
|
||||||
|
}
|
||||||
|
CandidateState::Elected => {
|
||||||
|
result.push(&format!(r#"<td class="{}count elected">{}</td><td class="count elected">{}</td>"#, tdclasses2, pp(&count_card.ballot_transfers, 0), pp(&count_card.transfers, opts.pp_decimals)).into());
|
||||||
|
result.push(&format!(r#"<td class="{}count elected">{}</td><td class="count elected">{}</td>"#, tdclasses2, pp(&count_card.num_ballots(), 0), pp(&count_card.votes, opts.pp_decimals)).into());
|
||||||
|
}
|
||||||
|
CandidateState::Doomed => {
|
||||||
|
result.push(&format!(r#"<td class="{}count excluded">{}</td><td class="count excluded">{}</td>"#, tdclasses2, pp(&count_card.ballot_transfers, 0), pp(&count_card.transfers, opts.pp_decimals)).into());
|
||||||
|
result.push(&format!(r#"<td class="{}count excluded">{}</td><td class="count excluded">{}</td>"#, tdclasses2, pp(&count_card.num_ballots(), 0), pp(&count_card.votes, opts.pp_decimals)).into());
|
||||||
|
}
|
||||||
|
CandidateState::Withdrawn => {
|
||||||
|
result.push(&format!(r#"<td class="{}count excluded"></td><td class="count excluded"></td>"#, tdclasses2).into());
|
||||||
|
result.push(&format!(r#"<td class="{}count excluded"></td><td class="count excluded">WD</td>"#, tdclasses2).into());
|
||||||
|
}
|
||||||
|
CandidateState::Excluded => {
|
||||||
|
result.push(&format!(r#"<td class="{}count excluded">{}</td><td class="count excluded">{}</td>"#, tdclasses2, pp(&count_card.ballot_transfers, 0), pp(&count_card.transfers, opts.pp_decimals)).into());
|
||||||
|
if count_card.votes.is_zero() && count_card.parcels.iter().all(|p| p.votes.is_empty()) {
|
||||||
|
result.push(&format!(r#"<td class="{}count excluded"></td><td class="count excluded">Ex</td>"#, tdclasses2).into());
|
||||||
|
} else {
|
||||||
|
result.push(&format!(r#"<td class="{}count excluded">{}</td><td class="count excluded">{}</td>"#, tdclasses2, pp(&count_card.num_ballots(), 0), pp(&count_card.votes, opts.pp_decimals)).into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => unreachable!("Invalid report_style")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
result.push(&format!(r#"<td class="{}count">{}</td>"#, tdclasses2, pp(&state.exhausted.transfers, opts.pp_decimals)).into());
|
|
||||||
result.push(&format!(r#"<td class="{}count">{}</td>"#, tdclasses2, pp(&state.exhausted.votes, opts.pp_decimals)).into());
|
match report_style {
|
||||||
result.push(&format!(r#"<td class="{}count">{}</td>"#, tdclasses2, pp(&state.loss_fraction.transfers, opts.pp_decimals)).into());
|
"votes" => {
|
||||||
result.push(&format!(r#"<td class="{}count">{}</td>"#, tdclasses2, pp(&state.loss_fraction.votes, opts.pp_decimals)).into());
|
result.push(&format!(r#"<td class="{}count">{}</td>"#, tdclasses2, pp(&state.exhausted.transfers, opts.pp_decimals)).into());
|
||||||
|
result.push(&format!(r#"<td class="{}count">{}</td>"#, tdclasses2, pp(&state.exhausted.votes, opts.pp_decimals)).into());
|
||||||
|
result.push(&format!(r#"<td class="{}count">{}</td>"#, tdclasses2, pp(&state.loss_fraction.transfers, opts.pp_decimals)).into());
|
||||||
|
result.push(&format!(r#"<td class="{}count">{}</td>"#, tdclasses2, pp(&state.loss_fraction.votes, opts.pp_decimals)).into());
|
||||||
|
}
|
||||||
|
"ballots_votes" => {
|
||||||
|
result.push(&format!(r#"<td class="{}count">{}</td><td class="count">{}</td>"#, tdclasses2, pp(&state.exhausted.ballot_transfers, 0), pp(&state.exhausted.transfers, opts.pp_decimals)).into());
|
||||||
|
result.push(&format!(r#"<td class="{}count">{}</td><td class="count">{}</td>"#, tdclasses2, pp(&state.exhausted.num_ballots(), 0), pp(&state.exhausted.votes, opts.pp_decimals)).into());
|
||||||
|
result.push(&format!(r#"<td class="{}count"></td><td class="count">{}</td>"#, tdclasses2, pp(&state.loss_fraction.transfers, opts.pp_decimals)).into());
|
||||||
|
result.push(&format!(r#"<td class="{}count"></td><td class="count">{}</td>"#, tdclasses2, pp(&state.loss_fraction.votes, opts.pp_decimals)).into());
|
||||||
|
}
|
||||||
|
_ => unreachable!("Invalid report_style")
|
||||||
|
}
|
||||||
|
|
||||||
// Calculate total votes
|
// Calculate total votes
|
||||||
let mut total_vote = state.candidates.values().fold(N::zero(), |acc, cc| { acc + &cc.votes });
|
let mut total_vote = state.candidates.values().fold(N::zero(), |acc, cc| { acc + &cc.votes });
|
||||||
total_vote += &state.exhausted.votes;
|
total_vote += &state.exhausted.votes;
|
||||||
total_vote += &state.loss_fraction.votes;
|
total_vote += &state.loss_fraction.votes;
|
||||||
result.push(&format!(r#"<td class="{}count">{}</td>"#, tdclasses2, pp(&total_vote, opts.pp_decimals)).into());
|
|
||||||
|
|
||||||
result.push(&format!(r#"<td class="{}count">{}</td>"#, tdclasses2, pp(state.quota.as_ref().unwrap(), opts.pp_decimals)).into());
|
match report_style {
|
||||||
|
"votes" => {
|
||||||
|
result.push(&format!(r#"<td class="{}count">{}</td>"#, tdclasses2, pp(&total_vote, opts.pp_decimals)).into());
|
||||||
|
}
|
||||||
|
"ballots_votes" => {
|
||||||
|
// Calculate total ballots
|
||||||
|
let mut total_ballots = state.candidates.values().fold(N::zero(), |acc, cc| { acc + cc.num_ballots() });
|
||||||
|
total_ballots += state.exhausted.num_ballots();
|
||||||
|
|
||||||
|
result.push(&format!(r#"<td class="{}count">{}</td><td class="count">{}</td>"#, tdclasses2, pp(&total_ballots, 0), pp(&total_vote, opts.pp_decimals)).into());
|
||||||
|
}
|
||||||
|
_ => unreachable!("Invalid report_style")
|
||||||
|
}
|
||||||
|
|
||||||
|
match report_style {
|
||||||
|
"votes" => {
|
||||||
|
result.push(&format!(r#"<td class="{}count">{}</td>"#, tdclasses2, pp(state.quota.as_ref().unwrap(), opts.pp_decimals)).into());
|
||||||
|
}
|
||||||
|
"ballots_votes" => {
|
||||||
|
result.push(&format!(r#"<td class="{}count"></td><td class="count">{}</td>"#, tdclasses2, pp(state.quota.as_ref().unwrap(), opts.pp_decimals)).into());
|
||||||
|
}
|
||||||
|
_ => unreachable!("Invalid report_style")
|
||||||
|
}
|
||||||
|
|
||||||
if should_show_vre(opts) {
|
if should_show_vre(opts) {
|
||||||
if let Some(vre) = &state.vote_required_election {
|
if let Some(vre) = &state.vote_required_election {
|
||||||
result.push(&format!(r#"<td class="{}count">{}</td>"#, tdclasses2, pp(vre, opts.pp_decimals)).into());
|
match report_style {
|
||||||
|
"votes" => {
|
||||||
|
result.push(&format!(r#"<td class="{}count">{}</td>"#, tdclasses2, pp(vre, opts.pp_decimals)).into());
|
||||||
|
}
|
||||||
|
"ballots_votes" => {
|
||||||
|
result.push(&format!(r#"<td class="{}count"></td><td class="count">{}</td>"#, tdclasses2, pp(vre, opts.pp_decimals)).into());
|
||||||
|
}
|
||||||
|
_ => unreachable!("Invalid report_style")
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
result.push(&format!(r#"<td class="{}count"></td>"#, tdclasses2).into());
|
match report_style {
|
||||||
|
"votes" => {
|
||||||
|
result.push(&format!(r#"<td class="{}count"></td>"#, tdclasses2).into());
|
||||||
|
}
|
||||||
|
"ballots_votes" => {
|
||||||
|
result.push(&format!(r#"<td class="{}count"></td><td class="count"></td>"#, tdclasses2).into());
|
||||||
|
}
|
||||||
|
_ => unreachable!("Invalid report_style")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -406,13 +509,24 @@ fn update_stage_comments<N: Number>(state: &CountState<N>) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Generate the final column of the HTML results table
|
/// Generate the final column of the HTML results table
|
||||||
fn finalise_results_table<N: Number>(state: &CountState<N>) -> Array {
|
fn finalise_results_table<N: Number>(state: &CountState<N>, report_style: &str) -> Array {
|
||||||
let result = Array::new();
|
let result = Array::new();
|
||||||
|
|
||||||
// Header rows
|
// Header rows
|
||||||
result.push(&r#"<td rowspan="3"></td>"#.into());
|
match report_style {
|
||||||
result.push(&"".into());
|
"votes" => {
|
||||||
result.push(&"".into());
|
result.push(&r#"<td rowspan="3"></td>"#.into());
|
||||||
|
result.push(&"".into());
|
||||||
|
result.push(&"".into());
|
||||||
|
}
|
||||||
|
"ballots_votes" => {
|
||||||
|
result.push(&r#"<td rowspan="4"></td>"#.into());
|
||||||
|
result.push(&"".into());
|
||||||
|
result.push(&"".into());
|
||||||
|
result.push(&"".into());
|
||||||
|
}
|
||||||
|
_ => unreachable!("Invalid report_style")
|
||||||
|
}
|
||||||
|
|
||||||
// Candidate states
|
// Candidate states
|
||||||
for candidate in state.election.candidates.iter() {
|
for candidate in state.election.candidates.iter() {
|
||||||
|
Loading…
Reference in New Issue
Block a user