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.
|
||||
* 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`).
|
||||
|
||||
|
@ -115,6 +115,7 @@
|
||||
<select id="selTies">
|
||||
<option value="backwards_random" selected>Backwards then random</option>
|
||||
<option value="random">Random</option>
|
||||
<option value="prompt">Prompt</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
|
@ -98,6 +98,13 @@ async function clickCount() {
|
||||
let election, elComment, elExhausted1, elExhausted2, elLTF1, elLTF2, elTotal, elQuota;
|
||||
|
||||
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') {
|
||||
election = evt.data.election;
|
||||
|
||||
@ -285,20 +292,23 @@ async function clickCount() {
|
||||
}
|
||||
|
||||
worker.postMessage({
|
||||
'numbers': document.getElementById('selNumbers').value,
|
||||
'fixedDPs': parseInt(document.getElementById('txtDP').value),
|
||||
'transfers': document.getElementById('selTransfers').value,
|
||||
'options': {
|
||||
'quota_criterion': document.getElementById('selQuotaCriterion').value,
|
||||
'quota': document.getElementById('selQuota').value,
|
||||
'prog_quota': document.getElementById('chkProgQuota').checked,
|
||||
'bulk_elect': document.getElementById('chkBulkElection').checked,
|
||||
'bulk_exclude': document.getElementById('chkBulkExclusion').checked,
|
||||
'surplus_order': document.getElementById('selSurplus').value,
|
||||
'ties': document.getElementById('selTies').value
|
||||
},
|
||||
'seed': document.getElementById('txtSeed').value,
|
||||
'data': text
|
||||
'type': 'init',
|
||||
'data': {
|
||||
'numbers': document.getElementById('selNumbers').value,
|
||||
'fixedDPs': parseInt(document.getElementById('txtDP').value),
|
||||
'transfers': document.getElementById('selTransfers').value,
|
||||
'options': {
|
||||
'quota_criterion': document.getElementById('selQuotaCriterion').value,
|
||||
'quota': document.getElementById('selQuota').value,
|
||||
'prog_quota': document.getElementById('chkProgQuota').checked,
|
||||
'bulk_elect': document.getElementById('chkBulkElection').checked,
|
||||
'bulk_exclude': document.getElementById('chkBulkExclusion').checked,
|
||||
'surplus_order': document.getElementById('selSurplus').value,
|
||||
'ties': document.getElementById('selTies').value
|
||||
},
|
||||
'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');
|
||||
|
||||
let result, counter, ppDP, tiesPrompt;
|
||||
|
||||
onmessage = function(evt) {
|
||||
// Set settings
|
||||
if (evt.data.numbers === 'native') {
|
||||
py.pyRCV2.numbers.set_numclass(py.pyRCV2.numbers.Native);
|
||||
if (evt.data.type === 'init') {
|
||||
// Set settings
|
||||
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);
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
function stepElection() {
|
||||
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)) {
|
||||
postMessage({'type': 'done'});
|
||||
break;
|
||||
|
@ -22,9 +22,17 @@ __pragma__('noskip')
|
||||
|
||||
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:
|
||||
"""Prompt the user to break ties"""
|
||||
|
||||
def __init__(self):
|
||||
self.buffer = None
|
||||
|
||||
def choose_lowest(self, l):
|
||||
if is_py:
|
||||
print('Multiple tied candidates:')
|
||||
@ -44,23 +52,25 @@ class TiesPrompt:
|
||||
|
||||
return l[i - 1]
|
||||
else:
|
||||
# UNUSED IN JS - Cannot call window.prompt from Web Worker
|
||||
# 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)
|
||||
if self.buffer is not None:
|
||||
try:
|
||||
choice = int(choice)
|
||||
choice = int(self.buffer)
|
||||
if choice >= 1 and choice < len(l) + 1:
|
||||
break
|
||||
self.buffer = None
|
||||
return l[choice - 1]
|
||||
except ValueError:
|
||||
self.buffer = None
|
||||
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):
|
||||
return self.choose_lowest(l)
|
||||
|
Reference in New Issue
Block a user