Code refactor

This commit is contained in:
RunasSudo 2020-10-18 21:24:12 +11:00
parent e0c56c5b1d
commit 631fa432c6
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A

View File

@ -42,7 +42,6 @@ class BaseSTVCounter:
if options is not None: if options is not None:
self.options.update(options) self.options.update(options)
def reset(self):
self.candidates = SafeDict([(c, CountCard()) for c in self.election.candidates]) self.candidates = SafeDict([(c, CountCard()) for c in self.election.candidates])
self.exhausted = CountCard() self.exhausted = CountCard()
self.loss_fraction = CountCard() self.loss_fraction = CountCard()
@ -57,13 +56,56 @@ class BaseSTVCounter:
self.candidates[candidate].state = CandidateState.WITHDRAWN self.candidates[candidate].state = CandidateState.WITHDRAWN
__pragma__('noopov') __pragma__('noopov')
def reset(self):
"""
Public function:
Reset the count and perform the first step
Subclasses must override this function
"""
raise NotImplementedError('Method not implemented')
def step(self): def step(self):
"""
Public function:
Perform one step of the STV count
"""
# Step count cards # Step count cards
self.step_count_cards()
# Check if done
result = self.check_if_done()
if result:
return result
# Distribute surpluses
result = self.distribute_surpluses()
if result:
return result
# Insufficient winners and no surpluses to distribute
# Exclude the lowest ranked hopeful
result = self.exclude_candidate()
if result:
return result
raise STVException('Unable to complete step')
def step_count_cards(self):
"""
Reset the count cards for the beginning of a new step
"""
for candidate, count_card in self.candidates.items(): for candidate, count_card in self.candidates.items():
count_card.step() count_card.step()
self.exhausted.step() self.exhausted.step()
self.loss_fraction.step() self.loss_fraction.step()
def check_if_done(self):
"""
Check if the count can be completed
"""
# Have sufficient candidates been elected? # Have sufficient candidates been elected?
if self.num_elected >= self.election.seats: if self.num_elected >= self.election.seats:
return CountCompleted() return CountCompleted()
@ -90,6 +132,11 @@ class BaseSTVCounter:
) )
__pragma__('noopov') __pragma__('noopov')
def distribute_surpluses(self):
"""
Distribute surpluses, if any
"""
# Do surpluses need to be distributed? # Do surpluses need to be distributed?
__pragma__('opov') __pragma__('opov')
has_surplus = [(c, cc) for c, cc in self.candidates.items() if cc.state == CandidateState.PROVISIONALLY_ELECTED and cc.votes > self.quota] has_surplus = [(c, cc) for c, cc in self.candidates.items() if cc.state == CandidateState.PROVISIONALLY_ELECTED and cc.votes > self.quota]
@ -112,13 +159,13 @@ class BaseSTVCounter:
__pragma__('noopov') __pragma__('noopov')
# Transfer surplus # Transfer surplus
self.transfer_surplus(candidate_surplus, count_card, surplus) self.do_surplus(candidate_surplus, count_card, surplus)
__pragma__('opov') __pragma__('opov')
count_card.transfers -= surplus count_card.transfers -= surplus
__pragma__('noopov') __pragma__('noopov')
# Declare any candidates meeting the quota as a result of surpluses # Declare elected any candidates meeting the quota as a result of surpluses
self.compute_quota() self.compute_quota()
self.elect_meeting_quota() self.elect_meeting_quota()
@ -133,8 +180,18 @@ class BaseSTVCounter:
) )
__pragma__('noopov') __pragma__('noopov')
# Insufficient winners and no surpluses to distribute def do_surplus(self, candidate_surplus, count_card, surplus):
# Exclude the lowest ranked hopeful """
Transfer the surplus of the given candidate
Subclasses must override this function
"""
raise NotImplementedError('Method not implemented')
def exclude_candidate(self):
"""
Exclude the lowest ranked hopeful
"""
hopefuls = [(c, cc) for c, cc in self.candidates.items() if cc.state == CandidateState.HOPEFUL] hopefuls = [(c, cc) for c, cc in self.candidates.items() if cc.state == CandidateState.HOPEFUL]
hopefuls.sort(lambda x: x[1].votes) hopefuls.sort(lambda x: x[1].votes)
@ -143,7 +200,7 @@ class BaseSTVCounter:
count_card.state = CandidateState.EXCLUDED count_card.state = CandidateState.EXCLUDED
# Exclude this candidate # Exclude this candidate
self.exclude_candidate(candidate_excluded, count_card) self.do_exclusion(candidate_excluded, count_card)
__pragma__('opov') __pragma__('opov')
count_card.transfers -= count_card.votes count_card.transfers -= count_card.votes
@ -164,8 +221,18 @@ class BaseSTVCounter:
) )
__pragma__('noopov') __pragma__('noopov')
def do_exclusion(self, candidate_excluded, count_card):
"""
Exclude the given candidate and transfer the votes
Subclasses must override this function
"""
raise NotImplementedError('Method not implemented')
def compute_quota(self): def compute_quota(self):
"""Recount total votes and (if applicable) recalculate the quota""" """
Recount total votes and (if applicable) recalculate the quota
"""
__pragma__('opov') __pragma__('opov')
self.total = sum((cc.votes for c, cc in self.candidates.items()), Num('0')) self.total = sum((cc.votes for c, cc in self.candidates.items()), Num('0'))
self.loss_fraction.transfers += (self.total_orig - self.total - self.exhausted.votes) - self.loss_fraction.votes self.loss_fraction.transfers += (self.total_orig - self.total - self.exhausted.votes) - self.loss_fraction.votes
@ -184,6 +251,10 @@ class BaseSTVCounter:
__pragma__('noopov') __pragma__('noopov')
def meets_quota(self, count_card): def meets_quota(self, count_card):
"""
Determine if the given candidate meets the quota
"""
if self.options['quota_criterion'] == 'geq': if self.options['quota_criterion'] == 'geq':
__pragma__('opov') __pragma__('opov')
return count_card.votes >= self.quota return count_card.votes >= self.quota
@ -196,6 +267,10 @@ class BaseSTVCounter:
raise STVException('Invalid quota criterion') raise STVException('Invalid quota criterion')
def elect_meeting_quota(self): def elect_meeting_quota(self):
"""
Elect all candidates meeting the quota
"""
# Does a candidate meet the quota? # Does a candidate meet the quota?
meets_quota = [(c, cc) for c, cc in self.candidates.items() if cc.state == CandidateState.HOPEFUL and self.meets_quota(cc)] meets_quota = [(c, cc) for c, cc in self.candidates.items() if cc.state == CandidateState.HOPEFUL and self.meets_quota(cc)]
@ -212,8 +287,6 @@ class BaseWIGSTVCounter(BaseSTVCounter):
""" """
def reset(self): def reset(self):
BaseSTVCounter.reset(self)
# Distribute first preferences # Distribute first preferences
for ballot in self.election.ballots: for ballot in self.election.ballots:
__pragma__('opov') __pragma__('opov')
@ -242,7 +315,7 @@ class BaseWIGSTVCounter(BaseSTVCounter):
) )
__pragma__('noopov') __pragma__('noopov')
def transfer_surplus(self, candidate_surplus, count_card, surplus): def do_surplus(self, candidate_surplus, count_card, surplus):
for ballot, ballot_value in count_card.ballots: for ballot, ballot_value in count_card.ballots:
__pragma__('opov') __pragma__('opov')
new_value = (ballot_value * surplus) / count_card.votes # Multiply first to avoid rounding errors new_value = (ballot_value * surplus) / count_card.votes # Multiply first to avoid rounding errors
@ -257,7 +330,7 @@ class BaseWIGSTVCounter(BaseSTVCounter):
self.exhausted.ballots.append((ballot, new_value)) self.exhausted.ballots.append((ballot, new_value))
__pragma__('noopov') __pragma__('noopov')
def exclude_candidate(self, candidate_excluded, count_card): def do_exclusion(self, candidate_excluded, count_card):
for ballot, ballot_value in count_card.ballots: for ballot, ballot_value in count_card.ballots:
__pragma__('opov') __pragma__('opov')
candidate = next((c for c in ballot.preferences if self.candidates[c].state == CandidateState.HOPEFUL), None) candidate = next((c for c in ballot.preferences if self.candidates[c].state == CandidateState.HOPEFUL), None)
@ -276,8 +349,6 @@ class BaseUIGSTVCounter(BaseSTVCounter):
""" """
def reset(self): def reset(self):
BaseSTVCounter.reset(self)
# Distribute first preferences # Distribute first preferences
for ballot in self.election.ballots: for ballot in self.election.ballots:
__pragma__('opov') __pragma__('opov')
@ -306,7 +377,7 @@ class BaseUIGSTVCounter(BaseSTVCounter):
) )
__pragma__('noopov') __pragma__('noopov')
def transfer_surplus(self, candidate_surplus, count_card, surplus): def do_surplus(self, candidate_surplus, count_card, surplus):
# FIXME: Is it okay to use native int's here? # FIXME: Is it okay to use native int's here?
next_preferences = SafeDict([(c, []) for c, cc in self.candidates.items()]) next_preferences = SafeDict([(c, []) for c, cc in self.candidates.items()])
next_exhausted = [] next_exhausted = []
@ -352,7 +423,7 @@ class BaseUIGSTVCounter(BaseSTVCounter):
__pragma__('noopov') __pragma__('noopov')
self.exhausted.ballots.append((ballot, new_value)) self.exhausted.ballots.append((ballot, new_value))
def exclude_candidate(self, candidate_excluded, count_card): def do_exclusion(self, candidate_excluded, count_card):
next_preferences = SafeDict([(c, Rational('0')) for c, cc in self.candidates.items()]) next_preferences = SafeDict([(c, Rational('0')) for c, cc in self.candidates.items()])
next_exhausted = Rational('0') next_exhausted = Rational('0')
total_votes = Rational('0') total_votes = Rational('0')