diff --git a/pyRCV2/method/STVCCounter.py b/pyRCV2/method/STVCCounter.py index 6ebf713..8d3f167 100644 --- a/pyRCV2/method/STVCCounter.py +++ b/pyRCV2/method/STVCCounter.py @@ -29,6 +29,9 @@ class STVCCounter: def reset(self): self.candidates = SafeDict([(c, CountCard()) for c in self.election.candidates]) self.exhausted = CountCard() + self.loss_fraction = CountCard() + + self.total_orig = sum((b.value for b in self.election.ballots), Num('0')) # Withdraw candidates for candidate in self.election.withdrawn: @@ -57,7 +60,8 @@ class STVCCounter: 'First preferences', self.candidates, self.exhausted, - self.total + self.exhausted.votes, + self.loss_fraction, + self.total + self.exhausted.votes + self.loss_fraction.votes, self.quota ) __pragma__('noopov') @@ -67,6 +71,7 @@ class STVCCounter: for candidate, count_card in self.candidates.items(): count_card.step() self.exhausted.step() + 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: @@ -84,7 +89,8 @@ class STVCCounter: 'Bulk election', self.candidates, self.exhausted, - self.total + self.exhausted.votes, + self.loss_fraction, + self.total + self.exhausted.votes + self.loss_fraction.votes, self.quota ) __pragma__('noopov') @@ -132,7 +138,8 @@ class STVCCounter: 'Surplus of ' + candidate_surplus.name, self.candidates, self.exhausted, - self.total + self.exhausted.votes, + self.loss_fraction, + self.total + self.exhausted.votes + self.loss_fraction.votes, self.quota ) __pragma__('noopov') @@ -171,7 +178,8 @@ class STVCCounter: 'Exclusion of ' + candidate_excluded.name, self.candidates, self.exhausted, - self.total + self.exhausted.votes, + self.loss_fraction, + self.total + self.exhausted.votes + self.loss_fraction.votes, self.quota ) __pragma__('noopov') @@ -180,6 +188,7 @@ class STVCCounter: # Compute 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) __pragma__('noopov') diff --git a/pyRCV2/model.py b/pyRCV2/model.py index b8776f4..4fbb22d 100644 --- a/pyRCV2/model.py +++ b/pyRCV2/model.py @@ -77,11 +77,12 @@ class CountCompleted: pass class CountStepResult: - def __init__(self, comment, candidates, exhausted, total, quota): + def __init__(self, comment, candidates, exhausted, loss_fraction, total, quota): self.comment = comment self.candidates = candidates # SafeDict: Candidate -> CountCard self.exhausted = exhausted # CountCard + self.loss_fraction = loss_fraction # CountCard self.total = total self.quota = quota diff --git a/pyRCV2/numbers/__init__.py b/pyRCV2/numbers/__init__.py index e9a8daf..df655b4 100644 --- a/pyRCV2/numbers/__init__.py +++ b/pyRCV2/numbers/__init__.py @@ -23,11 +23,13 @@ __pragma__('noskip') if is_py: __pragma__('skip') from pyRCV2.numbers.fixed_py import Fixed, set_dps + from pyRCV2.numbers.int_py import NativeInt from pyRCV2.numbers.native_py import Native from pyRCV2.numbers.rational_py import Rational __pragma__('noskip') else: from pyRCV2.numbers.fixed_js import Fixed, set_dps + from pyRCV2.numbers.int_js import NativeInt from pyRCV2.numbers.native_js import Native from pyRCV2.numbers.rational_js import Rational diff --git a/pyRCV2/numbers/fixed_py.py b/pyRCV2/numbers/fixed_py.py index 52530de..1b2f5fd 100644 --- a/pyRCV2/numbers/fixed_py.py +++ b/pyRCV2/numbers/fixed_py.py @@ -35,13 +35,13 @@ class Fixed: return format(self.impl, '.{}f'.format(dp)) def __add__(self, other): - return Fixed((self.impl + other.impl).quantize(_quantize_exp)) + return Fixed(self.impl + other.impl) def __sub__(self, other): - return Fixed((self.impl - other.impl).quantize(_quantize_exp)) + return Fixed(self.impl - other.impl) def __mul__(self, other): - return Fixed((self.impl * other.impl).quantize(_quantize_exp)) + return Fixed(self.impl * other.impl) def __div__(self, other): - return Fixed((self.impl / other.impl).quantize(_quantize_exp)) + return Fixed(self.impl / other.impl) def __gt__(self, other): return self.impl > other.impl diff --git a/pyRCV2/numbers/int_js.py b/pyRCV2/numbers/int_js.py new file mode 100644 index 0000000..6769278 --- /dev/null +++ b/pyRCV2/numbers/int_js.py @@ -0,0 +1,45 @@ +# pyRCV2: Preferential vote counting +# Copyright © 2020 Lee Yingtong Li (RunasSudo) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +class NativeInt: + """ + Wrapper for JS integers + """ + + def __init__(self, val): + self.impl = Math.floor(parseFloat(val)) + + def pp(self, dp): + """Pretty print to specified number of decimal places""" + return self.impl.toFixed(0) + + def __add__(self, other): + return NativeInt(self.impl + other.impl) + def __sub__(self, other): + return NativeInt(self.impl - other.impl) + def __mul__(self, other): + return NativeInt(self.impl * other.impl) + def __div__(self, other): + return NativeInt(self.impl / other.impl) + + def __gt__(self, other): + return self.impl > other.impl + def __ge__(self, other): + return self.impl >= other.impl + def __lt__(self, other): + return self.impl < other.impl + def __le__(self, other): + return self.impl <= other.impl diff --git a/pyRCV2/numbers/int_py.py b/pyRCV2/numbers/int_py.py new file mode 100644 index 0000000..40d7c99 --- /dev/null +++ b/pyRCV2/numbers/int_py.py @@ -0,0 +1,45 @@ +# pyRCV2: Preferential vote counting +# Copyright © 2020 Lee Yingtong Li (RunasSudo) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +class NativeInt: + """ + Wrapper for Python int + """ + + def __init__(self, val): + self.impl = int(val) + + def pp(self, dp): + """Pretty print to specified number of decimal places""" + return str(self.impl) + + def __add__(self, other): + return NativeInt(self.impl + other.impl) + def __sub__(self, other): + return NativeInt(self.impl - other.impl) + def __mul__(self, other): + return NativeInt(self.impl * other.impl) + def __div__(self, other): + return NativeInt(self.impl / other.impl) + + def __gt__(self, other): + return self.impl > other.impl + def __ge__(self, other): + return self.impl >= other.impl + def __lt__(self, other): + return self.impl < other.impl + def __le__(self, other): + return self.impl <= other.impl diff --git a/test.html b/test.html index 66f77c0..e1190e8 100644 --- a/test.html +++ b/test.html @@ -33,7 +33,7 @@ - +
@@ -54,7 +54,7 @@ // Step election let worker = new Worker('worker.js'); - let election, elComment, elExhausted1, elExhausted2, elQuota; + let election, elComment, elExhausted1, elExhausted2, elLTF1, elLTF2, elTotal, elQuota; worker.onmessage = function(evt) { if (evt.data.type == 'init') { @@ -98,12 +98,26 @@ tblResults.appendChild(elExhausted1); tblResults.appendChild(elExhausted2); + // Loss to fraction row + elLTF1 = document.createElement('tr'); + elLTF1.classList.add('info'); + elLTF2 = document.createElement('tr'); + elLTF2.classList.add('info'); + + elTd = document.createElement('td'); + elTd.setAttribute('rowspan', '2'); + elTd.style.borderTop = '1px solid black'; + elTd.innerText = 'Loss to fraction'; + elLTF1.appendChild(elTd); + + tblResults.appendChild(elLTF1); + tblResults.appendChild(elLTF2); + // Total row elTotal = document.createElement('tr'); elTotal.classList.add('info'); elTd = document.createElement('td'); elTd.style.borderTop = '1px solid black'; - elTd.style.borderBottom = '1px solid black'; elTd.innerText = 'Total'; elTotal.appendChild(elTd); tblResults.appendChild(elTotal); @@ -177,11 +191,24 @@ elTd.innerText = result.exhausted.votes; elExhausted2.appendChild(elTd); + // Display loss to fraction + elTd = document.createElement('td'); + elTd.classList.add('count'); + elTd.style.borderTop = '1px solid black'; + if (result.loss_fraction.transfers != '0.00') { + elTd.innerText = result.loss_fraction.transfers; + } + elLTF1.appendChild(elTd); + + elTd = document.createElement('td'); + elTd.classList.add('count'); + elTd.innerText = result.loss_fraction.votes; + elLTF2.appendChild(elTd); + // Display total elTd = document.createElement('td'); elTd.classList.add('count'); elTd.style.borderTop = '1px solid black'; - elTd.style.borderBottom = '1px solid black'; elTd.innerText = result.total; elTotal.appendChild(elTd); diff --git a/worker.js b/worker.js index 0f4f602..15c452d 100644 --- a/worker.js +++ b/worker.js @@ -8,6 +8,9 @@ onmessage = async function(evt) { if (evt.data.numbers == 'native') { py.pyRCV2.numbers.set_numclass(py.pyRCV2.numbers.Native); } + if (evt.data.numbers == 'int') { + py.pyRCV2.numbers.set_numclass(py.pyRCV2.numbers.NativeInt); + } if (evt.data.numbers == 'rational') { py.pyRCV2.numbers.set_numclass(py.pyRCV2.numbers.Rational); } @@ -37,6 +40,10 @@ onmessage = async function(evt) { 'transfers': result.exhausted.transfers.pp(2), 'votes': result.exhausted.votes.pp(2) }, + 'loss_fraction': { + 'transfers': result.loss_fraction.transfers.pp(2), + 'votes': result.loss_fraction.votes.pp(2) + }, 'total': result.total.pp(2), 'quota': result.quota.pp(2) }}); @@ -60,6 +67,10 @@ onmessage = async function(evt) { 'transfers': result.exhausted.transfers.pp(2), 'votes': result.exhausted.votes.pp(2) }, + 'loss_fraction': { + 'transfers': result.loss_fraction.transfers.pp(2), + 'votes': result.loss_fraction.votes.pp(2) + }, 'total': result.total.pp(2), 'quota': result.quota.pp(2) }});