ecclesia
Version:
Framework for political and electoral simulations
108 lines • 4.36 kB
JavaScript
import { enumerate, max, min } from "@gouvernathor/python";
import { DefaultMap, NumberCounter } from "@gouvernathor/python/collections";
import { AttributionFailure } from "../attribution";
/**
* Creates an attribution method in which the party with the least votes is eliminated,
* and its votes are redistributed to the other parties according to the voters' preferences.
* Repeats until a party reaches a majority of the remaining votes, winning all the seats.
*
* The ballots are not required to rank all the candidates.
*/
export function instantRunoff({ nSeats }) {
const attrib = (votes, _rest = {}) => {
const blacklisted = new Set();
const nParties = new Set(votes.flat()).size;
for (let pn = 0; pn < nParties; pn++) {
const firstPlaces = NumberCounter.fromEntries();
for (const ballot of votes) {
for (const party of ballot) {
if (!blacklisted.has(party)) {
firstPlaces.increment(party);
break;
}
}
}
const total = firstPlaces.total;
for (const [party, score] of firstPlaces) {
if (score / total > .5) {
return NumberCounter.fromEntries([[party, nSeats]]);
}
}
blacklisted.add(min(firstPlaces.keys(), p => firstPlaces.get(p)));
}
throw new Error("Should not happen");
};
attrib.nSeats = nSeats;
return attrib;
}
/**
* Creates an attribution method in which each party receives points according
* to the position it occupies on each ballot, and the party with the most points wins all the seats.
*
* Uses the Modified Borda Count, in which the least-ranked candidate gets 1 point,
* and unranked candidates get 0 points.
* So, the ballots are not required to rank all the candidates.
*/
export function bordaCount({ nSeats }) {
const attrib = (votes, _rest = {}) => {
const scores = NumberCounter.fromEntries();
for (const ballot of votes) {
for (const [i, party] of enumerate(ballot.slice().reverse(), 1)) {
scores.increment(party, i);
}
}
return NumberCounter.fromEntries([[max(scores.keys(), p => scores.get(p)), nSeats]]);
};
attrib.nSeats = nSeats;
return attrib;
}
/**
* Creates an attribution method in which each party is matched against each other party,
* and the party winning each of its matchups wins all the seats.
* If no party wins against all others, the attribution fails.
*
* Doesn't support candidates with equal ranks, due to the Order type format.
* This implementation also doesn't support incomplete ballots.
*
* @param contingency An optional contingency attribution method to use in case of a standoff.
* If not provided (or null), the attribution will fail with a condorcet.Standoff error,
* which is a subclass of AttributionFailure.
*/
export function condorcet({ nSeats, contingency = null }) {
const attrib = (votes, rest = {}) => {
const counts = new DefaultMap(() => NumberCounter.fromEntries());
const majority = votes.length / 2;
for (const ballot of votes) {
for (const [i, party1] of enumerate(ballot)) {
for (const party2 of ballot.slice(i + 1)) {
counts.get(party1).increment(party2);
}
}
}
const win = new Set(counts.keys());
for (const [party, partyCounter] of counts) {
for (const value of partyCounter.pos.values()) {
if (value > majority) {
win.delete(party);
break;
}
}
}
if (win.size !== 1) {
if (win.size !== 0) {
throw new Error("Bad attribution");
}
if (contingency === null) {
throw new condorcet.Standoff("No Condorcet winner");
}
return contingency(votes, rest);
}
const [winner] = win;
return NumberCounter.fromEntries([[winner, nSeats]]);
};
attrib.nSeats = nSeats;
return attrib;
}
condorcet.Standoff = class CondorcetStandoff extends AttributionFailure {
};
//# sourceMappingURL=orderingFactory.js.map