Skip to content

Commit

Permalink
Merge pull request #35 from simberaj/votes-per-seat
Browse files Browse the repository at this point in the history
#8: Votes-per-seat distributor
  • Loading branch information
simberaj authored Nov 7, 2020
2 parents 0b3f042 + f7b0a2b commit 50861b7
Show file tree
Hide file tree
Showing 2 changed files with 68 additions and 0 deletions.
23 changes: 23 additions & 0 deletions tests/evaluate/test_proportional.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@

import sys
import os
import decimal
from fractions import Fraction

sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..'))
Expand Down Expand Up @@ -78,6 +79,7 @@ def test_from_biprop_reg():
slag = votelib.evaluate.proportional.HighestAverages('sainte_lague')
assert slag.evaluate(votes, 20) == {'I': 7, 'II': 5, 'III': 8}


def test_from_biprop_party():
votes = {
'A': 983,
Expand All @@ -86,3 +88,24 @@ def test_from_biprop_party():
}
slag = votelib.evaluate.proportional.HighestAverages('sainte_lague')
assert slag.evaluate(votes, 20) == {'A': 5, 'B': 11, 'C': 4}


def test_votes_per_seat():
votes = {
'A': 983,
'B': 2040,
'C': 782,
}
vps = votelib.evaluate.proportional.VotesPerSeat(100)
assert vps.evaluate(votes) == {'A': 9, 'B': 20, 'C': 7}

def test_votes_per_seat_round_math():
votes = {
'A': 983,
'B': 2040,
'C': 782,
'D': 1050,
}
vps = votelib.evaluate.proportional.VotesPerSeat(100, rounding=decimal.ROUND_HALF_UP)
assert vps.evaluate(votes) == {'A': 10, 'B': 20, 'C': 8, 'D': 11}

45 changes: 45 additions & 0 deletions votelib/evaluate/proportional.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
'''

import bisect
import decimal
from fractions import Fraction
from typing import Dict, Union, Callable
from numbers import Number
Expand Down Expand Up @@ -82,6 +83,50 @@ def evaluate(self,
}


@simple_serialization
class VotesPerSeat:
"""Award seats for each N votes cast for each candidate.
This is an old and simple system that was used e.g. in pre-war Germany
[#wrstag]_. It divides the number of votes a pre-specified constant and
rounds to give the appropriate number of seats.
:param votes_per_seat: Number of votes required for the candidate to
obtain a single seat.
:param rounding: A rounding mode from the *decimal* Python library. The
default option is to round down, which is the most frequent case,
but this evaluator allows to specify a different method as well.
.. [#wrstag] "Reichstag (Weimarer Republik): Wahlsystem", Wikipedia.
https://de.wikipedia.org/wiki/Reichstag_(Weimarer_Republik)#Wahlsystem
"""
def __init__(self,
votes_per_seat: int,
rounding=decimal.ROUND_DOWN
):
self.votes_per_seat = decimal.Decimal(votes_per_seat)
self.rounding = rounding

def evaluate(self,
votes: Dict[Candidate, int],
prev_gains: Dict[Candidate, int] = {},
max_seats: Dict[Candidate, int] = {},
) -> Dict[Candidate, int]:
with decimal.localcontext() as context:
context.rounding = self.rounding
out = {}
for cand, n_votes in votes.items():
entitlement = int(round(
decimal.Decimal(n_votes) / self.votes_per_seat,
0
))
entitlement = min(entitlement, max_seats.get(cand, INF))
entitlement -= prev_gains.get(cand, 0)
if entitlement:
out[cand] = entitlement
return out


@simple_serialization
class QuotaDistributor:
'''Distribute seats proportionally, according to multiples of quota filled.
Expand Down

0 comments on commit 50861b7

Please sign in to comment.