diff --git a/html/index.js b/html/index.js index 33b697f..4915c8e 100644 --- a/html/index.js +++ b/html/index.js @@ -361,6 +361,32 @@ async function clickCount() { elVRE.appendChild(elTd); } } + + if (evt.data.type === 'done') { + let result = evt.data.result; + + // Display results + + for (let [candidate, countCard] of result.candidates) { + [elTr1, elTr2] = candMap[candidate]; + elTd = document.createElement('td'); + elTd.setAttribute('rowspan', '2'); + if (countCard.state === py.pyRCV2.model.CandidateState.WITHDRAWN) { + elTd.classList.add('excluded'); + elTd.innerHTML = 'Withdrawn'; + } else if (countCard.state === py.pyRCV2.model.CandidateState.EXCLUDED || countCard.state === py.pyRCV2.model.CandidateState.EXCLUDING) { + elTd.classList.add('excluded'); + elTd.innerHTML = 'Excluded ' + (-countCard.order_elected); + } else if (countCard.state === py.pyRCV2.model.CandidateState.ELECTED || countCard.state === py.pyRCV2.model.CandidateState.PROVISIONALLY_ELECTED || countCard.state === py.pyRCV2.model.CandidateState.DISTRIBUTING_SURPLUS) { + elTd.classList.add('elected'); + elTd.innerHTML = 'ELECTED ' + countCard.order_elected; + } + elTd.style.borderTop = '1px solid black'; + elTr1.appendChild(elTd); + } + + elTd.style.borderBottom = '1px solid black'; + } } worker.onerror = function(evt) { diff --git a/html/worker.js b/html/worker.js index 29bcf19..8325265 100644 --- a/html/worker.js +++ b/html/worker.js @@ -65,26 +65,7 @@ onmessage = function(evt) { stage = 1; result = counter.reset(); - postMessage({'type': 'result', 'result': { - 'stage': stage, - 'comment': result.comment, - 'candidates': result.candidates.py_items().map(([c, cc]) => [c.py_name, { - 'transfers': cc.transfers.pp(ppDP), - 'votes': cc.votes.pp(ppDP), - 'state': cc.state - }]), - 'exhausted': { - 'transfers': result.exhausted.transfers.pp(ppDP), - 'votes': result.exhausted.votes.pp(ppDP) - }, - 'loss_fraction': { - 'transfers': result.loss_fraction.transfers.pp(ppDP), - 'votes': result.loss_fraction.votes.pp(ppDP) - }, - 'total': result.total.pp(ppDP), - 'quota': result.quota.pp(ppDP), - 'vote_required_election': result.vote_required_election === null ? null : result.vote_required_election.pp(ppDP), - }}); + postMessage({'type': 'result', 'result': resultToJS(result)}); stepElection(); } else if (evt.data.type === 'require_input') { @@ -115,29 +96,34 @@ function stepElection() { } if (py.isinstance(result, py.pyRCV2.model.CountCompleted)) { - postMessage({'type': 'done'}); + postMessage({'type': 'done', 'result': resultToJS(result)}); break; } else { - postMessage({'type': 'result', 'result': { - 'stage': stage, - 'comment': result.comment, - 'candidates': result.candidates.py_items().map(([c, cc]) => [c.py_name, { - 'transfers': cc.transfers.pp(ppDP), - 'votes': cc.votes.pp(ppDP), - 'state': cc.state - }]), - 'exhausted': { - 'transfers': result.exhausted.transfers.pp(ppDP), - 'votes': result.exhausted.votes.pp(ppDP) - }, - 'loss_fraction': { - 'transfers': result.loss_fraction.transfers.pp(ppDP), - 'votes': result.loss_fraction.votes.pp(ppDP) - }, - 'total': result.total.pp(ppDP), - 'quota': result.quota.pp(ppDP), - 'vote_required_election': result.vote_required_election === null ? null : result.vote_required_election.pp(ppDP), - }}); + postMessage({'type': 'result', 'result': resultToJS(result)}); } } } + +function resultToJS(result) { + return { + 'stage': stage, + 'comment': result.comment, + 'candidates': result.candidates.py_items().map(([c, cc]) => [c.py_name, { + 'transfers': cc.transfers.pp(ppDP), + 'votes': cc.votes.pp(ppDP), + 'state': cc.state, + 'order_elected': cc.order_elected, + }]), + 'exhausted': { + 'transfers': result.exhausted.transfers.pp(ppDP), + 'votes': result.exhausted.votes.pp(ppDP) + }, + 'loss_fraction': { + 'transfers': result.loss_fraction.transfers.pp(ppDP), + 'votes': result.loss_fraction.votes.pp(ppDP) + }, + 'total': result.total.pp(ppDP), + 'quota': result.quota.pp(ppDP), + 'vote_required_election': result.vote_required_election === null ? null : result.vote_required_election.pp(ppDP), + }; +} diff --git a/pyRCV2/cli/stv.py b/pyRCV2/cli/stv.py index 9dd113f..8e7ce61 100644 --- a/pyRCV2/cli/stv.py +++ b/pyRCV2/cli/stv.py @@ -60,9 +60,9 @@ def print_step(args, stage, result): for candidate, count_card in results: state = None if count_card.state == pyRCV2.model.CandidateState.ELECTED or count_card.state == pyRCV2.model.CandidateState.PROVISIONALLY_ELECTED or count_card.state == pyRCV2.model.CandidateState.DISTRIBUTING_SURPLUS: - state = 'ELECTED' + state = 'ELECTED {}'.format(count_card.order_elected) elif count_card.state == pyRCV2.model.CandidateState.EXCLUDED or count_card.state == pyRCV2.model.CandidateState.EXCLUDING: - state = 'Excluded' + state = 'Excluded {}'.format(-count_card.order_elected) elif count_card.state == pyRCV2.model.CandidateState.WITHDRAWN: state = 'Withdrawn' diff --git a/pyRCV2/method/base_stv.py b/pyRCV2/method/base_stv.py index f1647de..3c7372e 100644 --- a/pyRCV2/method/base_stv.py +++ b/pyRCV2/method/base_stv.py @@ -89,6 +89,7 @@ class BaseSTVCounter: self.total_orig = sum((b.value for b in self.election.ballots), Num('0')) self.num_elected = 0 + self.num_excluded = 0 # Withdraw candidates for candidate in self.election.withdrawn: @@ -196,17 +197,28 @@ class BaseSTVCounter: # Have sufficient candidates been elected? if self.num_elected >= self.election.seats: - return CountCompleted() + __pragma__('opov') + return CountCompleted( + 'Count complete', + self.candidates, + self.exhausted, + self.loss_fraction, + self.total + self.exhausted.votes + self.loss_fraction.votes, + self.quota, + self.vote_required_election, + ) + __pragma__('noopov') # Are there just enough candidates to fill all the seats? if self.options['bulk_elect']: - if self.num_elected + sum(1 for c, cc in self.candidates.items() if cc.state == CandidateState.HOPEFUL or cc.state == CandidateState.EXCLUDING) <= self.election.seats: + # Include EXCLUDING to avoid interrupting an exclusion + if len(self.election.candidates) - self.num_excluded + sum(1 for c, cc in self.candidates.items() if cc.state == CandidateState.EXCLUDING) <= self.election.seats: # Declare elected all remaining candidates for candidate, count_card in self.candidates.items(): if count_card.state == CandidateState.HOPEFUL: count_card.state = CandidateState.PROVISIONALLY_ELECTED - count_card.order_elected = self.num_elected self.num_elected += 1 + count_card.order_elected = self.num_elected __pragma__('opov') result = CountStepResult( @@ -338,15 +350,15 @@ class BaseSTVCounter: # If we did not perform bulk election in before_surpluses: Are there just enough candidates to fill all the seats? if not self.options['bulk_elect']: - if self.num_elected + sum(1 for c, cc in self.candidates.items() if cc.state == CandidateState.HOPEFUL) <= self.election.seats: + if len(self.election.candidates) - self.num_excluded <= self.election.seats: # Declare elected one remaining candidate at a time hopefuls = [(c, cc) for c, cc in self.candidates.items() if cc.state == CandidateState.HOPEFUL] hopefuls.sort(key=lambda x: x[1].votes, reverse=True) candidate_elected, count_card = self.choose_highest(hopefuls) count_card.state = CandidateState.PROVISIONALLY_ELECTED - count_card.order_elected = self.num_elected self.num_elected += 1 + count_card.order_elected = self.num_elected __pragma__('opov') result = CountStepResult( @@ -370,7 +382,10 @@ class BaseSTVCounter: candidates_excluded = self.candidates_to_exclude() for candidate, count_card in candidates_excluded: - count_card.state = CandidateState.EXCLUDING + if count_card.state != CandidateState.EXCLUDING: + count_card.state = CandidateState.EXCLUDING + self.num_excluded += 1 + count_card.order_elected = -self.num_excluded # Handle Wright STV if self.options['exclusion'] == 'wright': @@ -433,7 +448,7 @@ class BaseSTVCounter: Returns List[Tuple[Candidate, CountCard]] """ - remaining_candidates = self.num_elected + sum(1 for c, cc in self.candidates.items() if cc.state == CandidateState.HOPEFUL) + remaining_candidates = len(self.election.candidates) - self.num_excluded __pragma__('opov') total_surpluses = sum((cc.votes - self.quota for c, cc in self.candidates.items() if cc.votes > self.quota), Num(0)) __pragma__('noopov') @@ -574,8 +589,8 @@ class BaseSTVCounter: candidate, count_card = x[0], x[1] count_card.state = CandidateState.PROVISIONALLY_ELECTED - count_card.order_elected = self.num_elected self.num_elected += 1 + count_card.order_elected = self.num_elected meets_quota.remove(x) diff --git a/pyRCV2/model.py b/pyRCV2/model.py index b501627..4f303fb 100644 --- a/pyRCV2/model.py +++ b/pyRCV2/model.py @@ -65,7 +65,7 @@ class CountCard: self.orig_votes = Num('0') self.transfers = Num('0') self.state = CandidateState.HOPEFUL - self.order_elected = None + self.order_elected = None # Negative for order of exclusion # self.parcels = List[Parcel] # Parcel = List[Tuple[Ballot, Num]] @@ -94,9 +94,6 @@ class CountCard: result.order_elected = self.order_elected return result -class CountCompleted: - pass - class CountStepResult: def __init__(self, comment, candidates, exhausted, loss_fraction, total, quota, vote_required_election): self.comment = comment @@ -119,3 +116,6 @@ class CountStepResult: __pragma__('noopov') return CountStepResult(self.comment, candidates, self.exhausted.clone(), self.loss_fraction.clone(), self.total, self.quota) + +class CountCompleted(CountStepResult): + pass