Implement prompting for ties
This commit is contained in:
parent
4e06dfe068
commit
7ad142f19d
@ -82,7 +82,7 @@ This dropdown allows you to select how ties (in surplus transfer or exclusion) a
|
|||||||
|
|
||||||
* Backwards: Ties are broken according to which tied candidate had the most/fewest votes at the end of the last stage where one tied candidate had more/fewer votes than the others, if such a stage exists.
|
* Backwards: Ties are broken according to which tied candidate had the most/fewest votes at the end of the last stage where one tied candidate had more/fewer votes than the others, if such a stage exists.
|
||||||
* Random: Ties are broken at random (see *Random seed*).
|
* Random: Ties are broken at random (see *Random seed*).
|
||||||
* Prompt: The user is prompted to break the tie. This option is not available in the web application.
|
* Prompt: The user is prompted to break the tie.
|
||||||
|
|
||||||
Multiple tie breaking methods can be specified. If the first method cannot resolve the tie, the next is tried, and so on. In the web application, 2 options are available (‘Backwards then random’ and ‘Random’). On the Python command line, the `--ties` option can be specified multiple times (e.g. `--ties backwards --ties random`).
|
Multiple tie breaking methods can be specified. If the first method cannot resolve the tie, the next is tried, and so on. In the web application, 2 options are available (‘Backwards then random’ and ‘Random’). On the Python command line, the `--ties` option can be specified multiple times (e.g. `--ties backwards --ties random`).
|
||||||
|
|
||||||
|
@ -115,6 +115,7 @@
|
|||||||
<select id="selTies">
|
<select id="selTies">
|
||||||
<option value="backwards_random" selected>Backwards then random</option>
|
<option value="backwards_random" selected>Backwards then random</option>
|
||||||
<option value="random">Random</option>
|
<option value="random">Random</option>
|
||||||
|
<option value="prompt">Prompt</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
|
@ -98,6 +98,13 @@ async function clickCount() {
|
|||||||
let election, elComment, elExhausted1, elExhausted2, elLTF1, elLTF2, elTotal, elQuota;
|
let election, elComment, elExhausted1, elExhausted2, elLTF1, elLTF2, elTotal, elQuota;
|
||||||
|
|
||||||
worker.onmessage = function(evt) {
|
worker.onmessage = function(evt) {
|
||||||
|
if (evt.data.type === 'require_input') {
|
||||||
|
let input = window.prompt(evt.data.message);
|
||||||
|
if (input !== null) {
|
||||||
|
worker.postMessage({'type': 'require_input', 'input': input});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (evt.data.type === 'init') {
|
if (evt.data.type === 'init') {
|
||||||
election = evt.data.election;
|
election = evt.data.election;
|
||||||
|
|
||||||
@ -285,20 +292,23 @@ async function clickCount() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
worker.postMessage({
|
worker.postMessage({
|
||||||
'numbers': document.getElementById('selNumbers').value,
|
'type': 'init',
|
||||||
'fixedDPs': parseInt(document.getElementById('txtDP').value),
|
'data': {
|
||||||
'transfers': document.getElementById('selTransfers').value,
|
'numbers': document.getElementById('selNumbers').value,
|
||||||
'options': {
|
'fixedDPs': parseInt(document.getElementById('txtDP').value),
|
||||||
'quota_criterion': document.getElementById('selQuotaCriterion').value,
|
'transfers': document.getElementById('selTransfers').value,
|
||||||
'quota': document.getElementById('selQuota').value,
|
'options': {
|
||||||
'prog_quota': document.getElementById('chkProgQuota').checked,
|
'quota_criterion': document.getElementById('selQuotaCriterion').value,
|
||||||
'bulk_elect': document.getElementById('chkBulkElection').checked,
|
'quota': document.getElementById('selQuota').value,
|
||||||
'bulk_exclude': document.getElementById('chkBulkExclusion').checked,
|
'prog_quota': document.getElementById('chkProgQuota').checked,
|
||||||
'surplus_order': document.getElementById('selSurplus').value,
|
'bulk_elect': document.getElementById('chkBulkElection').checked,
|
||||||
'ties': document.getElementById('selTies').value
|
'bulk_exclude': document.getElementById('chkBulkExclusion').checked,
|
||||||
},
|
'surplus_order': document.getElementById('selSurplus').value,
|
||||||
'seed': document.getElementById('txtSeed').value,
|
'ties': document.getElementById('selTies').value
|
||||||
'data': text
|
},
|
||||||
|
'seed': document.getElementById('txtSeed').value,
|
||||||
|
'data': text
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
150
html/worker.js
150
html/worker.js
@ -18,72 +18,96 @@
|
|||||||
|
|
||||||
importScripts('vendor/BigInt_BigRat-a5f89e2.min.js', 'https://cdn.jsdelivr.net/npm/big.js@6.0.0/big.min.js', 'https://cdn.jsdelivr.net/npm/sjcl@1.0.8/sjcl.min.js', 'bundle.js');
|
importScripts('vendor/BigInt_BigRat-a5f89e2.min.js', 'https://cdn.jsdelivr.net/npm/big.js@6.0.0/big.min.js', 'https://cdn.jsdelivr.net/npm/sjcl@1.0.8/sjcl.min.js', 'bundle.js');
|
||||||
|
|
||||||
|
let result, counter, ppDP, tiesPrompt;
|
||||||
|
|
||||||
onmessage = function(evt) {
|
onmessage = function(evt) {
|
||||||
// Set settings
|
if (evt.data.type === 'init') {
|
||||||
if (evt.data.numbers === 'native') {
|
// Set settings
|
||||||
py.pyRCV2.numbers.set_numclass(py.pyRCV2.numbers.Native);
|
if (evt.data.data.numbers === 'native') {
|
||||||
|
py.pyRCV2.numbers.set_numclass(py.pyRCV2.numbers.Native);
|
||||||
|
}
|
||||||
|
if (evt.data.data.numbers === 'int') {
|
||||||
|
py.pyRCV2.numbers.set_numclass(py.pyRCV2.numbers.NativeInt);
|
||||||
|
}
|
||||||
|
if (evt.data.data.numbers === 'rational') {
|
||||||
|
py.pyRCV2.numbers.set_numclass(py.pyRCV2.numbers.Rational);
|
||||||
|
}
|
||||||
|
if (evt.data.data.numbers === 'fixed') {
|
||||||
|
py.pyRCV2.numbers.set_numclass(py.pyRCV2.numbers.Fixed);
|
||||||
|
py.pyRCV2.numbers.set_dps(evt.data.data.fixedDPs);
|
||||||
|
}
|
||||||
|
|
||||||
|
ppDP = evt.data.data.fixedDPs > 2 ? 2 : evt.data.data.fixedDPs;
|
||||||
|
|
||||||
|
let election = py.pyRCV2.blt.readBLT(evt.data.data.data);
|
||||||
|
postMessage({'type': 'init', 'election': {
|
||||||
|
'candidates': election.candidates.map(c => c.py_name)
|
||||||
|
}});
|
||||||
|
|
||||||
|
// Create counter
|
||||||
|
if (evt.data.data.transfers === 'uig') {
|
||||||
|
counter = py.pyRCV2.method.base_stv.UIGSTVCounter(election, evt.data.data.options);
|
||||||
|
} else if (evt.data.data.transfers === 'parcelled_eg') {
|
||||||
|
counter = py.pyRCV2.method.parcels.ParcelledEGSTVCounter(election, evt.data.data.options);
|
||||||
|
} else if (evt.data.data.transfers === 'wright') {
|
||||||
|
counter = py.pyRCV2.method.wright.WrightSTVCounter(election, evt.data.data.options);
|
||||||
|
} else {
|
||||||
|
counter = py.pyRCV2.method.base_stv.WIGSTVCounter(election, evt.data.data.options);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (evt.data.data.options['ties'] === 'backwards_random') {
|
||||||
|
counter.options['ties'] = [py.pyRCV2.ties.TiesBackwards(counter), py.pyRCV2.ties.TiesRandom(evt.data.data.seed)];
|
||||||
|
} else if (evt.data.data.options['ties'] === 'random') {
|
||||||
|
counter.options['ties'] = [py.pyRCV2.ties.TiesRandom(evt.data.data.seed)];
|
||||||
|
} else if (evt.data.data.options['ties'] === 'prompt') {
|
||||||
|
tiesPrompt = py.pyRCV2.ties.TiesPrompt();
|
||||||
|
counter.options['ties'] = [tiesPrompt];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset
|
||||||
|
result = counter.reset();
|
||||||
|
|
||||||
|
postMessage({'type': 'result', 'result': {
|
||||||
|
'comment': result.comment,
|
||||||
|
'candidates': result.candidates.py_items().map(([c, cc]) => [c.py_name, {
|
||||||
|
'transfers': cc.transfers.pp(ppDP),
|
||||||
|
'votes': cc.votes.pp(ppDP),
|
||||||
|
'state': cc.state
|
||||||
|
}]),
|
||||||
|
'exhausted': {
|
||||||
|
'transfers': result.exhausted.transfers.pp(ppDP),
|
||||||
|
'votes': result.exhausted.votes.pp(ppDP)
|
||||||
|
},
|
||||||
|
'loss_fraction': {
|
||||||
|
'transfers': result.loss_fraction.transfers.pp(ppDP),
|
||||||
|
'votes': result.loss_fraction.votes.pp(ppDP)
|
||||||
|
},
|
||||||
|
'total': result.total.pp(ppDP),
|
||||||
|
'quota': result.quota.pp(ppDP)
|
||||||
|
}});
|
||||||
|
|
||||||
|
stepElection();
|
||||||
|
} else if (evt.data.type === 'require_input') {
|
||||||
|
// Data received from the user
|
||||||
|
tiesPrompt.buffer = evt.data.input;
|
||||||
|
stepElection();
|
||||||
}
|
}
|
||||||
if (evt.data.numbers === 'int') {
|
}
|
||||||
py.pyRCV2.numbers.set_numclass(py.pyRCV2.numbers.NativeInt);
|
|
||||||
}
|
function stepElection() {
|
||||||
if (evt.data.numbers === 'rational') {
|
|
||||||
py.pyRCV2.numbers.set_numclass(py.pyRCV2.numbers.Rational);
|
|
||||||
}
|
|
||||||
if (evt.data.numbers === 'fixed') {
|
|
||||||
py.pyRCV2.numbers.set_numclass(py.pyRCV2.numbers.Fixed);
|
|
||||||
py.pyRCV2.numbers.set_dps(evt.data.fixedDPs);
|
|
||||||
}
|
|
||||||
|
|
||||||
let ppDP = evt.data.fixedDPs > 2 ? 2 : evt.data.fixedDPs;
|
|
||||||
|
|
||||||
let election = py.pyRCV2.blt.readBLT(evt.data.data);
|
|
||||||
postMessage({'type': 'init', 'election': {
|
|
||||||
'candidates': election.candidates.map(c => c.py_name)
|
|
||||||
}});
|
|
||||||
|
|
||||||
// Create counter
|
|
||||||
let counter;
|
|
||||||
if (evt.data.transfers === 'uig') {
|
|
||||||
counter = py.pyRCV2.method.base_stv.UIGSTVCounter(election, evt.data.options);
|
|
||||||
} else if (evt.data.transfers === 'parcelled_eg') {
|
|
||||||
counter = py.pyRCV2.method.parcels.ParcelledEGSTVCounter(election, evt.data.options);
|
|
||||||
} else if (evt.data.transfers === 'wright') {
|
|
||||||
counter = py.pyRCV2.method.wright.WrightSTVCounter(election, evt.data.options);
|
|
||||||
} else {
|
|
||||||
counter = py.pyRCV2.method.base_stv.WIGSTVCounter(election, evt.data.options);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (evt.data.options['ties'] === 'backwards_random') {
|
|
||||||
counter.options['ties'] = [py.pyRCV2.ties.TiesBackwards(counter), py.pyRCV2.ties.TiesRandom(evt.data.seed)];
|
|
||||||
} else if (evt.data.options['ties'] === 'random') {
|
|
||||||
counter.options['ties'] = [py.pyRCV2.ties.TiesRandom(evt.data.seed)];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset
|
|
||||||
let result = counter.reset();
|
|
||||||
|
|
||||||
postMessage({'type': 'result', 'result': {
|
|
||||||
'comment': result.comment,
|
|
||||||
'candidates': result.candidates.py_items().map(([c, cc]) => [c.py_name, {
|
|
||||||
'transfers': cc.transfers.pp(ppDP),
|
|
||||||
'votes': cc.votes.pp(ppDP),
|
|
||||||
'state': cc.state
|
|
||||||
}]),
|
|
||||||
'exhausted': {
|
|
||||||
'transfers': result.exhausted.transfers.pp(ppDP),
|
|
||||||
'votes': result.exhausted.votes.pp(ppDP)
|
|
||||||
},
|
|
||||||
'loss_fraction': {
|
|
||||||
'transfers': result.loss_fraction.transfers.pp(ppDP),
|
|
||||||
'votes': result.loss_fraction.votes.pp(ppDP)
|
|
||||||
},
|
|
||||||
'total': result.total.pp(ppDP),
|
|
||||||
'quota': result.quota.pp(ppDP)
|
|
||||||
}});
|
|
||||||
|
|
||||||
// Step election
|
|
||||||
while (true) {
|
while (true) {
|
||||||
result = counter.step();
|
try {
|
||||||
|
result = counter.step();
|
||||||
|
} catch (ex) {
|
||||||
|
if (py.isinstance(ex, py.pyRCV2.ties.RequireInput)) {
|
||||||
|
// Signals we require input to break a tie
|
||||||
|
postMessage({'type': 'require_input', 'message': ex.message});
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (py.isinstance(result, py.pyRCV2.model.CountCompleted)) {
|
if (py.isinstance(result, py.pyRCV2.model.CountCompleted)) {
|
||||||
postMessage({'type': 'done'});
|
postMessage({'type': 'done'});
|
||||||
break;
|
break;
|
||||||
|
@ -22,9 +22,17 @@ __pragma__('noskip')
|
|||||||
|
|
||||||
from pyRCV2.random import SHARandom
|
from pyRCV2.random import SHARandom
|
||||||
|
|
||||||
|
class RequireInput(Exception):
|
||||||
|
# Exceptions for control flow? In my code? It's more likely thank you think!
|
||||||
|
def __init__(self, message):
|
||||||
|
self.message = message
|
||||||
|
|
||||||
class TiesPrompt:
|
class TiesPrompt:
|
||||||
"""Prompt the user to break ties"""
|
"""Prompt the user to break ties"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.buffer = None
|
||||||
|
|
||||||
def choose_lowest(self, l):
|
def choose_lowest(self, l):
|
||||||
if is_py:
|
if is_py:
|
||||||
print('Multiple tied candidates:')
|
print('Multiple tied candidates:')
|
||||||
@ -44,23 +52,25 @@ class TiesPrompt:
|
|||||||
|
|
||||||
return l[i - 1]
|
return l[i - 1]
|
||||||
else:
|
else:
|
||||||
# UNUSED IN JS - Cannot call window.prompt from Web Worker
|
if self.buffer is not None:
|
||||||
# TODO: Work this out
|
|
||||||
message = ''
|
|
||||||
for i, x in enumerate(l):
|
|
||||||
message += (i + 1) + '. ' + x[0].name
|
|
||||||
message += 'Which candidate to select?'
|
|
||||||
|
|
||||||
while True:
|
|
||||||
choice = window.prompt(message)
|
|
||||||
try:
|
try:
|
||||||
choice = int(choice)
|
choice = int(self.buffer)
|
||||||
if choice >= 1 and choice < len(l) + 1:
|
if choice >= 1 and choice < len(l) + 1:
|
||||||
break
|
self.buffer = None
|
||||||
|
return l[choice - 1]
|
||||||
except ValueError:
|
except ValueError:
|
||||||
|
self.buffer = None
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return l[i - 1]
|
self.buffer = None
|
||||||
|
|
||||||
|
# Require prompting
|
||||||
|
message = 'Multiple tied candidates:\n'
|
||||||
|
for i, x in enumerate(l):
|
||||||
|
message += (i + 1) + '. ' + x[0].name + '\n'
|
||||||
|
message += 'Which candidate to select?'
|
||||||
|
|
||||||
|
raise RequireInput(message)
|
||||||
|
|
||||||
def choose_highest(self, l):
|
def choose_highest(self, l):
|
||||||
return self.choose_lowest(l)
|
return self.choose_lowest(l)
|
||||||
|
Reference in New Issue
Block a user