diff --git a/pyRCV2/method/STVCCounter.py b/pyRCV2/method/base_stv.py
similarity index 74%
rename from pyRCV2/method/STVCCounter.py
rename to pyRCV2/method/base_stv.py
index 8d3f167..2623bab 100644
--- a/pyRCV2/method/STVCCounter.py
+++ b/pyRCV2/method/base_stv.py
@@ -20,11 +20,25 @@ from pyRCV2.model import CandidateState, CountCard, CountCompleted, CountStepRes
from pyRCV2.numbers import Num
from pyRCV2.safedict import SafeDict
-class STVCCounter:
- """Count an STV election using pyRCV STV-C rules"""
+class STVException(Exception):
+ pass
+
+class BaseSTVCounter:
+ """Basic STV counter for various different variations"""
- def __init__(self, election):
+ def __init__(self, election, options=None):
self.election = election
+
+ # Default options
+ self.options = {
+ 'prog_quota': False, # Progressively reducing quota?
+ 'quota': 'droop', # 'droop', 'droop_exact', 'hare' or 'hare_exact'
+ 'quota_criterion': 'geq', # 'geq' or 'gt'
+ 'surplus_order': 'size', # 'size' or 'order'
+ }
+
+ if options is not None:
+ self.options.update(options)
def reset(self):
self.candidates = SafeDict([(c, CountCard()) for c in self.election.candidates])
@@ -33,6 +47,8 @@ class STVCCounter:
self.total_orig = sum((b.value for b in self.election.ballots), Num('0'))
+ self.num_elected = 0
+
# Withdraw candidates
for candidate in self.election.withdrawn:
__pragma__('opov')
@@ -52,6 +68,7 @@ class STVCCounter:
self.exhausted.ballots.append((ballot, ballot.value))
__pragma__('noopov')
+ self.quota = None
self.compute_quota()
self.elect_meeting_quota()
@@ -74,15 +91,19 @@ class STVCCounter:
self.loss_fraction.step()
# Have sufficient candidates been elected?
- if sum(1 for c, cc in self.candidates.items() if cc.state == CandidateState.PROVISIONALLY_ELECTED or cc.state == CandidateState.ELECTED) >= self.election.seats:
+ if self.num_elected >= self.election.seats:
return CountCompleted()
# Are there just enough candidates to fill all the seats
- if sum(1 for c, cc in self.candidates.items() if cc.state == CandidateState.PROVISIONALLY_ELECTED or cc.state == CandidateState.ELECTED or cc.state == CandidateState.HOPEFUL) <= self.election.seats:
+ if self.num_elected + sum(1 for c, cc in self.candidates.items() if cc.state == CandidateState.HOPEFUL) <= self.election.seats:
+ # TODO: Sort by size
+
# 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
__pragma__('opov')
return CountStepResult(
@@ -101,8 +122,14 @@ class STVCCounter:
__pragma__('noopov')
if len(has_surplus) > 0:
- # Distribute surpluses in order of size
- has_surplus.sort(lambda x: x[1].votes, True)
+ # Distribute surpluses in specified order
+ if self.options['surplus_order'] == 'size':
+ has_surplus.sort(lambda x: x[1].votes, True)
+ elif self.options['surplus_order'] == 'order':
+ has_surplus.sort(lambda x: x[1].order_elected)
+ else:
+ raise STVException('Invalid surplus order option')
+
candidate_surplus, count_card = has_surplus[0]
count_card.state = CandidateState.ELECTED
@@ -185,20 +212,43 @@ class STVCCounter:
__pragma__('noopov')
def compute_quota(self):
- # Compute quota
+ """Recount total votes and (if applicable) recalculate the quota"""
__pragma__('opov')
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.quota = self.total / Num(self.election.seats + 1)
+
+ if self.quota is None or self.options['prog_quota']:
+ if self.options['quota'] == 'droop':
+ self.quota = (self.total / Num(self.election.seats + 1)).__floor__() + Num('1')
+ elif self.options['quota'] == 'droop_exact':
+ self.quota = self.total / Num(self.election.seats + 1)
+ elif self.options['quota'] == 'hare':
+ self.quota = (self.total / Num(self.election.seats)).__floor__() + Num('1')
+ elif self.options['quota'] == 'hare_exact':
+ self.quota = self.total / Num(self.election.seats)
+ else:
+ raise STVException('Invalid quota option')
__pragma__('noopov')
+ def meets_quota(self, count_card):
+ if self.options['quota_criterion'] == 'geq':
+ __pragma__('opov')
+ return count_card.votes >= self.quota
+ __pragma__('noopov')
+ elif self.options['quota_criterion'] == 'gt':
+ __pragma__('opov')
+ return count_card.votes > self.quota
+ __pragma__('noopov')
+ else:
+ raise STVException('Invalid quota criterion')
+
def elect_meeting_quota(self):
# Does a candidate meet the quota?
- __pragma__('opov')
- meets_quota = [(c, cc) for c, cc in self.candidates.items() if cc.state == CandidateState.HOPEFUL and cc.votes > self.quota]
- __pragma__('noopov')
+ meets_quota = [(c, cc) for c, cc in self.candidates.items() if cc.state == CandidateState.HOPEFUL and self.meets_quota(cc)]
if len(meets_quota) > 0:
# Declare elected any candidate who meets the quota
for candidate, count_card in meets_quota:
count_card.state = CandidateState.PROVISIONALLY_ELECTED
+ count_card.order_elected = self.num_elected
+ self.num_elected += 1
diff --git a/pyRCV2/model.py b/pyRCV2/model.py
index 4fbb22d..04ebcb9 100644
--- a/pyRCV2/model.py
+++ b/pyRCV2/model.py
@@ -61,6 +61,7 @@ class CountCard:
self.transfers = Num('0')
self.ballots = []
self.state = CandidateState.HOPEFUL
+ self.order_elected = None
@property
def votes(self):
diff --git a/pyRCV2/numbers/fixed_js.py b/pyRCV2/numbers/fixed_js.py
index 77e11ca..6485797 100644
--- a/pyRCV2/numbers/fixed_js.py
+++ b/pyRCV2/numbers/fixed_js.py
@@ -48,3 +48,6 @@ class Fixed:
return self.impl.lt(other.impl)
def __le__(self, other):
return self.impl.lte(other.impl)
+
+ def __floor__(self):
+ return Fixed(Math.floor(self.impl))
diff --git a/pyRCV2/numbers/fixed_py.py b/pyRCV2/numbers/fixed_py.py
index 1b2f5fd..7a595d5 100644
--- a/pyRCV2/numbers/fixed_py.py
+++ b/pyRCV2/numbers/fixed_py.py
@@ -15,6 +15,7 @@
# along with this program. If not, see .
from decimal import Decimal
+import math
_quantize_exp = 6
@@ -51,3 +52,6 @@ class Fixed:
return self.impl < other.impl
def __le__(self, other):
return self.impl <= other.impl
+
+ def __floor__(self):
+ return Fixed(math.floor(self.impl))
diff --git a/pyRCV2/numbers/int_js.py b/pyRCV2/numbers/int_js.py
index 6769278..928dae3 100644
--- a/pyRCV2/numbers/int_js.py
+++ b/pyRCV2/numbers/int_js.py
@@ -43,3 +43,6 @@ class NativeInt:
return self.impl < other.impl
def __le__(self, other):
return self.impl <= other.impl
+
+ def __floor__(self):
+ return NativeInt(Math.floor(self.impl))
diff --git a/pyRCV2/numbers/int_py.py b/pyRCV2/numbers/int_py.py
index 40d7c99..41c7b34 100644
--- a/pyRCV2/numbers/int_py.py
+++ b/pyRCV2/numbers/int_py.py
@@ -14,6 +14,8 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
+import math
+
class NativeInt:
"""
Wrapper for Python int
@@ -43,3 +45,6 @@ class NativeInt:
return self.impl < other.impl
def __le__(self, other):
return self.impl <= other.impl
+
+ def __floor__(self):
+ return NativeInt(math.floor(self.impl))
diff --git a/pyRCV2/numbers/native_js.py b/pyRCV2/numbers/native_js.py
index 4ebd0d3..b23afef 100644
--- a/pyRCV2/numbers/native_js.py
+++ b/pyRCV2/numbers/native_js.py
@@ -43,3 +43,6 @@ class Native:
return self.impl < other.impl
def __le__(self, other):
return self.impl <= other.impl
+
+ def __floor__(self):
+ return Native(Math.floor(self.impl))
diff --git a/pyRCV2/numbers/native_py.py b/pyRCV2/numbers/native_py.py
index 8af6f4f..702e9bf 100644
--- a/pyRCV2/numbers/native_py.py
+++ b/pyRCV2/numbers/native_py.py
@@ -14,6 +14,8 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
+import math
+
class Native:
"""
Wrapper for Python float (naive floating-point arithmetic)
@@ -43,3 +45,6 @@ class Native:
return self.impl < other.impl
def __le__(self, other):
return self.impl <= other.impl
+
+ def __floor__(self):
+ return Native(math.floor(self.impl))
diff --git a/pyRCV2/numbers/rational_js.py b/pyRCV2/numbers/rational_js.py
index 9109cf2..b7db956 100644
--- a/pyRCV2/numbers/rational_js.py
+++ b/pyRCV2/numbers/rational_js.py
@@ -43,3 +43,6 @@ class Rational:
return self.impl.lesser(other.impl)
def __le__(self, other):
return self.impl.lesserOrEquals(other.impl)
+
+ def __floor__(self):
+ return Rational(self.impl.floor())
diff --git a/pyRCV2/numbers/rational_py.py b/pyRCV2/numbers/rational_py.py
index c161e85..6d4a56e 100644
--- a/pyRCV2/numbers/rational_py.py
+++ b/pyRCV2/numbers/rational_py.py
@@ -15,6 +15,7 @@
# along with this program. If not, see .
from fractions import Fraction
+import math
class Rational:
"""
@@ -45,3 +46,6 @@ class Rational:
return self.impl < other.impl
def __le__(self, other):
return self.impl <= other.impl
+
+ def __floor__(self):
+ return Rational(math.floor(self.impl))
diff --git a/pyRCV2/transcrypt.py b/pyRCV2/transcrypt.py
index c0228e0..20795f0 100644
--- a/pyRCV2/transcrypt.py
+++ b/pyRCV2/transcrypt.py
@@ -16,7 +16,7 @@
import pyRCV2.blt
import pyRCV2.model
-import pyRCV2.method, pyRCV2.method.STVCCounter
+import pyRCV2.method, pyRCV2.method.base_stv
import pyRCV2.numbers
__pragma__('js', '{}', 'export {pyRCV2, isinstance, repr, str};')
diff --git a/test.html b/test.html
index 54d7a6e..b417c59 100644
--- a/test.html
+++ b/test.html
@@ -167,7 +167,6 @@
} else if (countCard.state === py.pyRCV2.model.CandidateState.ELECTED || countCard.state === py.pyRCV2.model.CandidateState.PROVISIONALLY_ELECTED) {
elTd.classList.add('elected');
elTd.innerText = countCard.votes;
- elTd.style.fontWeight = 'bold';
elTr1.querySelector('td:first-child').classList.add('elected');
} else if (countCard.state === py.pyRCV2.model.CandidateState.EXCLUDED) {
diff --git a/worker.js b/worker.js
index 02d1b84..9f919f8 100644
--- a/worker.js
+++ b/worker.js
@@ -24,7 +24,7 @@ onmessage = async function(evt) {
}});
// Create counter
- let counter = py.pyRCV2.method.STVCCounter.STVCCounter(election);
+ let counter = py.pyRCV2.method.base_stv.BaseSTVCounter(election);
// Reset
let result = counter.reset();