diff --git a/docs/options.md b/docs/options.md
index 631403f..3965293 100644
--- a/docs/options.md
+++ b/docs/options.md
@@ -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`).
diff --git a/html/index.html b/html/index.html
index ad2246a..8a3783d 100644
--- a/html/index.html
+++ b/html/index.html
@@ -115,6 +115,7 @@
Backwards then random
Random
+ Prompt
diff --git a/html/index.js b/html/index.js
index 3a38a89..d332016 100644
--- a/html/index.js
+++ b/html/index.js
@@ -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
+ }
});
}
diff --git a/html/worker.js b/html/worker.js
index b0e2d76..ef23dae 100644
--- a/html/worker.js
+++ b/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;
diff --git a/pyRCV2/ties.py b/pyRCV2/ties.py
index c6670bb..cdfdcc5 100644
--- a/pyRCV2/ties.py
+++ b/pyRCV2/ties.py
@@ -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)