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