158 lines
5.0 KiB
JavaScript
158 lines
5.0 KiB
JavaScript
/*
|
|
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 <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
importScripts('vendor/BigInt_BigRat-a5f89e2.min.js', 'vendor/big-6.0.0.min.js', 'vendor/sjcl-1.0.8.min.js', 'bundle.js');
|
|
|
|
let stage, result, counter, ppDPs, tiesPrompt;
|
|
|
|
onmessage = function(evt) {
|
|
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 === '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);
|
|
}
|
|
|
|
ppDPs = evt.data.data.ppDPs;
|
|
|
|
let election = py.pyRCV2.blt.readBLT(evt.data.data.data);
|
|
let total_ballots = py.pyRCV2.numbers.Num(0);
|
|
for (let b of election.ballots) {
|
|
total_ballots.__iadd__(b.value);
|
|
}
|
|
|
|
// Create counter
|
|
if (evt.data.data.transfers === 'uig') {
|
|
counter = py.pyRCV2.method.gregory.UIGSTVCounter(election, evt.data.data.options);
|
|
} else if (evt.data.data.transfers === 'eg') {
|
|
counter = py.pyRCV2.method.gregory.EGSTVCounter(election, evt.data.data.options);
|
|
} else if (evt.data.data.transfers === 'meek') {
|
|
counter = py.pyRCV2.method.meek.MeekSTVCounter(election, evt.data.data.options);
|
|
} else {
|
|
counter = py.pyRCV2.method.gregory.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'] === 'forwards_random') {
|
|
counter.options['ties'] = [py.pyRCV2.ties.TiesForwards(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];
|
|
}
|
|
|
|
postMessage({
|
|
'type': 'init',
|
|
'election': {
|
|
'name': election.py_name,
|
|
'candidates': election.candidates.map(c => c.py_name)
|
|
},
|
|
'total_ballots': total_ballots.pp(0),
|
|
'options': counter.describe_options(),
|
|
});
|
|
|
|
// Reset
|
|
stage = 1;
|
|
try {
|
|
result = counter.reset();
|
|
} catch (ex) {
|
|
handleException(ex);
|
|
return;
|
|
}
|
|
|
|
if (py.isinstance(result, py.pyRCV2.model.CountCompleted)) {
|
|
postMessage({'type': 'done', 'result': resultToJS(result)});
|
|
return;
|
|
} else {
|
|
postMessage({'type': 'result', 'result': resultToJS(result)});
|
|
}
|
|
|
|
stepElection();
|
|
} else if (evt.data.type === 'require_input') {
|
|
// Data received from the user
|
|
tiesPrompt.buffer = evt.data.input;
|
|
stepElection();
|
|
}
|
|
}
|
|
|
|
function stepElection() {
|
|
while (true) {
|
|
try {
|
|
result = counter.step();
|
|
} catch (ex) {
|
|
handleException(ex);
|
|
return;
|
|
}
|
|
|
|
stage += 1;
|
|
|
|
if (py.isinstance(result, py.pyRCV2.model.CountCompleted)) {
|
|
postMessage({'type': 'done', 'result': resultToJS(result)});
|
|
break;
|
|
} else {
|
|
postMessage({'type': 'result', 'result': resultToJS(result)});
|
|
}
|
|
}
|
|
}
|
|
|
|
function handleException(ex) {
|
|
if (py.isinstance(ex, py.pyRCV2.ties.RequireInput)) {
|
|
// Signals we require input to break a tie
|
|
postMessage({'type': 'require_input', 'message': ex.message});
|
|
} else if (py.isinstance(ex, py.pyRCV2.method.base_stv.STVException)) {
|
|
console.error(ex);
|
|
postMessage({'type': 'stv_exception', 'message': ex.message});
|
|
} else {
|
|
console.error(ex);
|
|
throw ex;
|
|
}
|
|
}
|
|
|
|
function resultToJS(result) {
|
|
return {
|
|
'stage': stage,
|
|
'comment': result.comment,
|
|
'logs': result.logs,
|
|
'candidates': result.candidates.py_items().map(([c, cc]) => [c.py_name, {
|
|
'transfers': cc.transfers.pp(ppDPs),
|
|
'votes': cc.votes.pp(ppDPs),
|
|
'state': cc.state,
|
|
'order_elected': cc.order_elected,
|
|
}]),
|
|
'exhausted': {
|
|
'transfers': result.exhausted.transfers.pp(ppDPs),
|
|
'votes': result.exhausted.votes.pp(ppDPs)
|
|
},
|
|
'loss_fraction': {
|
|
'transfers': result.loss_fraction.transfers.pp(ppDPs),
|
|
'votes': result.loss_fraction.votes.pp(ppDPs)
|
|
},
|
|
'total': result.total.pp(ppDPs),
|
|
'quota': result.quota.pp(ppDPs),
|
|
'vote_required_election': result.vote_required_election === null ? null : result.vote_required_election.pp(ppDPs),
|
|
};
|
|
}
|