From 1e2f74e60bf7d23a581650242c43a5457cb840a2 Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Fri, 15 Jan 2021 16:00:22 +1100 Subject: [PATCH] Fix bug where withdrawn candidates would interfere with bulk exclusion, etc. logic --- pyRCV2/method/base_stv.py | 8 +++++--- tests/test_aec.py | 1 + tests/test_combinations.py | 5 ++++- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/pyRCV2/method/base_stv.py b/pyRCV2/method/base_stv.py index c80012c..4ff1c6e 100644 --- a/pyRCV2/method/base_stv.py +++ b/pyRCV2/method/base_stv.py @@ -66,12 +66,14 @@ class BaseSTVCounter: self.step_results = [] self.num_elected = 0 self.num_excluded = 0 + self.num_withdrawn = 0 # Withdraw candidates for candidate in self.election.withdrawn: __pragma__('opov') self.candidates[candidate].state = CandidateState.WITHDRAWN __pragma__('noopov') + self.num_withdrawn += 1 def reset(self): """ @@ -177,7 +179,7 @@ class BaseSTVCounter: # Are there just enough candidates to fill all the seats? if self.options['bulk_elect']: # 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: + if len(self.election.candidates) - self.num_withdrawn - 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 candidates_elected = [(c, cc) for c, cc in self.candidates.items() if cc.state == CandidateState.HOPEFUL] if len(candidates_elected) == 1: @@ -295,7 +297,7 @@ 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 len(self.election.candidates) - self.num_excluded <= self.election.seats: + if len(self.election.candidates) - self.num_withdrawn - 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) @@ -367,7 +369,7 @@ class BaseSTVCounter: Returns List[Tuple[Candidate, CountCard]] """ - remaining_candidates = len(self.election.candidates) - self.num_excluded + remaining_candidates = len(self.election.candidates) - self.num_withdrawn - 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') diff --git a/tests/test_aec.py b/tests/test_aec.py index 72ea757..029ff45 100644 --- a/tests/test_aec.py +++ b/tests/test_aec.py @@ -47,6 +47,7 @@ def test_aec_tas19(): counter = UIGSTVCounter(election, { 'surplus_order': 'order', 'exclusion': 'by_value', + 'bulk_exclude': True, 'round_quota': 0, 'round_votes': 0, }) diff --git a/tests/test_combinations.py b/tests/test_combinations.py index 17c429f..b938285 100644 --- a/tests/test_combinations.py +++ b/tests/test_combinations.py @@ -59,6 +59,9 @@ def maketst(numbers, counter_cls, options, options_description): return t_py, t_js test_prsa1_scottish_py, test_prsa1_scottish_js = maketst('Fixed', 'pyRCV2.method.gregory.WIGSTVCounter', {'round_quota': 0}, '--round-quota 0') -test_prsa1_senate_py, test_prsa1_senate_js = maketst('Fixed', 'pyRCV2.method.gregory.UIGSTVCounter', {'surplus_order': 'order', 'exclusion': 'by_value', 'round_quota': 3, 'round_votes': 3, 'round_tvs': 3, 'round_weights': 3}, '--method uig --round-quota 3 --round-votes 3 --round-tvs 3 --round-weights 3 --surplus-order order --exclusion by_value') +test_prsa1_senate_py, test_prsa1_senate_js = maketst('Fixed', 'pyRCV2.method.gregory.UIGSTVCounter', {'surplus_order': 'order', 'exclusion': 'by_value', 'bulk_exclude': True, 'round_quota': 0, 'round_votes': 0}, '--method uig --bulk-exclude --round-quota 0 --round-votes 0 --surplus-order order --exclusion by_value') test_prsa1_meek_py, test_prsa1_meek_js = maketst('Fixed', 'pyRCV2.method.meek.MeekSTVCounter', {'quota_criterion': 'gt', 'quota': 'droop_exact', 'quota_mode': 'progressive'}, '--method meek --quota droop_exact --quota-criterion gt --quota-mode progressive') test_prsa1_wright_py, test_prsa1_wright_js = maketst('Fixed', 'pyRCV2.method.gregory.WIGSTVCounter', {'exclusion': 'wright', 'round_quota': 0, 'bulk_exclude': True}, '--bulk-exclude --round-quota 0 --exclusion wright') + +# Regression test: Withdrawn candidates interfering with bulk exclusion +test_meekm_senate_py, test_meekm_senate_js = maketst('Fixed', 'pyRCV2.method.gregory.UIGSTVCounter', {'surplus_order': 'order', 'exclusion': 'by_value', 'bulk_exclude': True, 'round_quota': 0, 'round_votes': 0}, '--method uig --bulk-exclude --round-quota 0 --round-votes 0 --surplus-order order --exclusion by_value')