ecclesia
Version:
Framework for political and electoral simulations
1 lines • 36.2 kB
Source Map (JSON)
{"version":3,"sources":["../../../src/election/attribution/orderingFactory.ts","../../../src/election/attribution/base.ts","../../../src/election/attribution/transform.ts","../../../src/election/attribution/proportionalBase.ts","../../../src/election/attribution/majorityFactory.ts","../../../src/election/attribution/scoreFactory.ts","../../../src/election/ballots.ts","../../../src/election/attribution/proportionalFactory.ts","../../../src/election/attribution/randomFactory.ts","../../../src/utils.ts"],"sourcesContent":["import { enumerate, max, min } from \"@gouvernathor/python\";\nimport { Counter, DefaultMap } from \"@gouvernathor/python/collections\";\nimport { Order } from \"../ballots\";\nimport { type Attribution, AttributionFailure, type HasNSeats } from \"../attribution\";\n\n/**\n * Creates an attribution method in which the party with the least votes is eliminated,\n * and its votes are redistributed to the other parties according to the voters' preferences.\n * Repeats until a party reaches a majority of the remaining votes, winning all the seats.\n *\n * The ballots are not required to rank all the candidates.\n */\nexport function instantRunoff<Party>(\n { nSeats }: {\n nSeats: number\n }\n): Attribution<Party, Order<Party>> & HasNSeats {\n const attrib = (votes: Order<Party>, rest = {}): Counter<Party> => {\n const blacklisted = new Set<Party>();\n\n const nParties = new Set(votes.flat()).size;\n for (let pn = 0; pn < nParties; pn++) {\n const firstPlaces = new Counter<Party>();\n for (const ballot of votes) {\n for (const party of ballot) {\n if (!blacklisted.has(party)) {\n firstPlaces.increment(party);\n break;\n }\n }\n }\n\n const total = firstPlaces.total;\n for (const [party, score] of firstPlaces) {\n if (score / total > .5) {\n return new Counter([[party, nSeats]]);\n }\n }\n blacklisted.add(min(firstPlaces.keys(), p => firstPlaces.get(p)!));\n }\n throw new Error(\"Should not happen\");\n };\n attrib.nSeats = nSeats;\n return attrib;\n}\n\n/**\n * Creates an attribution method in which each party receives points according\n * to the position it occupies on each ballot, and the party with the most points wins all the seats.\n *\n * Uses the Modified Borda Count, in which the least-ranked candidate gets 1 point,\n * and unranked candidates get 0 points.\n * So, the ballots are not required to rank all the candidates.\n */\nexport function bordaCount<Party>(\n { nSeats }: {\n nSeats: number\n }\n): Attribution<Party, Order<Party>> & HasNSeats {\n const attrib = (votes: Order<Party>, rest = {}): Counter<Party> => {\n const scores = new Counter<Party>();\n for (const ballot of votes) {\n for (const [i, party] of enumerate(ballot.slice().reverse(), 1)) {\n scores.increment(party, i);\n }\n }\n return new Counter([[max(scores.keys(), p => scores.get(p)!), nSeats]]);\n };\n attrib.nSeats = nSeats;\n return attrib;\n}\n\n/**\n * Creates an attribution method in which each party is matched against each other party,\n * and the party winning each of its matchups wins all the seats.\n * If no party wins against all others, the attribution fails.\n *\n * Doesn't support candidates with equal ranks, due to the Order type format.\n * This implementation also doesn't support incomplete ballots.\n *\n * @param contingency An optional contingency attribution method to use in case of a standoff.\n * If not provided (or null), the attribution will fail with a condorcet.Standoff error,\n * which is a subclass of AttributionFailure.\n */\nexport function condorcet<Party>(\n { nSeats, contingency = null }: {\n nSeats: number,\n contingency?: Attribution<Party, Order<Party>> | null,\n }\n): Attribution<Party, Order<Party>> & HasNSeats {\n const attrib = (votes: Order<Party>, rest = {}): Counter<Party> => {\n const counts = new DefaultMap<Party, Counter<Party>>(() => new Counter());\n const majority= votes.length / 2;\n\n for (const ballot of votes) {\n for (const [i, party1] of enumerate(ballot)) {\n for (const party2 of ballot.slice(i + 1)) {\n counts.get(party1)!.increment(party2);\n }\n }\n }\n\n const win = new Set(counts.keys());\n for (const [party, partyCounter] of counts) {\n for (const value of partyCounter.pos.values()) {\n if (value > majority) {\n win.delete(party);\n break;\n }\n }\n }\n\n if (win.size !== 1) {\n if (win.size !== 0) {\n throw new Error(\"Bad attribution\");\n }\n if (contingency === null) {\n throw new condorcet.Standoff(\"No Condorcet winner\");\n }\n return contingency(votes, rest);\n }\n const [winner] = win;\n return new Counter([[winner, nSeats]]);\n };\n attrib.nSeats = nSeats;\n return attrib;\n}\ncondorcet.Standoff = class CondorcetStandoff extends AttributionFailure {};\n","import { type Counter } from \"@gouvernathor/python/collections\";\r\nimport { type Ballots } from \"../ballots\";\r\n\r\n/**\r\n * To be thrown when an attribution fails to attribute seats,\r\n * but only in a case where it's an expected limitation of the attribution method.\r\n * The typical example being the Condorcet standoff, or a majority threshold,\r\n * but a tie could be another example (though they are usually not checked in provided implementations).\r\n *\r\n * Other errors may and will be raised by the attribution methods for other reasons,\r\n * such as invalid input data, division by zero...\r\n */\r\nexport class AttributionFailure extends Error { }\r\n\r\n/**\r\n * A function to manage how the results from the ballots\r\n * translate into an allocation of seats.\r\n *\r\n * If the attribution method is expected, by design, to fail under expected conditions,\r\n * such as a majority threshold not being reached,\r\n * this method should raise an AttributionFailure error.\r\n *\r\n * @param votes The ballots.\r\n * @param rest This parameter, ignored in most cases,\r\n * forces the attributions taking more required or optional parameters\r\n * to take them as an options object.\r\n * Attribution transforms (functions that take an attribution method and return another one)\r\n * should generally let that parameter pass through\r\n * so as to avoid breaking features in the wrapped attributions.\r\n * Attributions should avoid taking other parameters, for the same reason.\r\n *\r\n * @returns The attribution of seats to the parties based upon the votes.\r\n * It is a Counter mapping each party to the number of seats it won.\r\n * The return value's total() should be equal to the nSeats attributes - if any.\r\n */\r\nexport interface Attribution<Party, B extends Ballots<Party>> {\r\n (votes: B, rest?: Record<string, any>): Counter<Party>;\r\n}\r\n// TODO: for all attributions where it can make sense to start with some apportionned seats,\r\n// add an option to pass some.\r\n\r\n/**\r\n * Most attributions should generally implement this interface,\r\n * which reflects that the number of seats they allocate is always the same.\r\n *\r\n * However, some attributions may yield variable numbers of seats,\r\n * for instance the pre-2024 federal German system.\r\n */\r\nexport interface HasNSeats {\r\n readonly nSeats: number;\r\n}\r\n","import { Counter } from \"@gouvernathor/python/collections\";\nimport { type Simple } from \"../ballots\";\nimport { type Attribution, AttributionFailure } from \"../attribution\";\n\n/**\n * Transforms a standard proportional attribution method into one that requires a certain threshold\n * (percentage of the total votes) to be reached by the parties in order to receive seats.\n * Mostly useful for proportional attribution methods -\n * but works for any simple-ballot-based attribution method.\n *\n * Replaces the Proportional class implementation.\n *\n * If the attribution implements a certain type\n * that extends Attribution<Party, Simple<Party>>\n * without adding any new method or property,\n * such as Proportional,\n * and if the contingency implements the same type, null, or undefined / not passed,\n * then it can be considered that the returned value implements that type too.\n *\n * @param threshold The threshold, between 0 and 1\n * @param contingency The fallback attribution in case the threshold is not reached by any party.\n * It takes an Attribution, or undefined (the default), or null.\n * Null in this case has the specific behavior that if no party reaches the threshold,\n * an AttributionFailure error will be raised.\n * If undefined (or nothing is passed), the contingency will default to running\n * the same attribution method without the threshold.\n * @param attribution The votes passed to this attribution method are guaranteed to be above the threshold,\n * except if the contingency is undefined and no party reached the threshold\n * (in that case, all parties will be passed).\n */\nexport function addThresholdToSimpleAttribution<Party>(\n { threshold, attribution, contingency = attribution }: {\n threshold: number,\n attribution: Attribution<Party, Simple<Party>>,\n contingency?: Attribution<Party, Simple<Party>> | null,\n }\n): Attribution<Party, Simple<Party>> {\n const attrib = (votes: Simple<Party>, rest = {}): Counter<Party> => {\n if (threshold > 0) {\n const original_votes = votes;\n const votes_threshold = threshold * votes.total;\n votes = new Counter<Party>([...votes.entries()].filter(([_, v]) => v >= votes_threshold));\n if (votes.size === 0) {\n if (contingency === null) {\n throw new AttributionFailure(\"No party reached the threshold\");\n }\n return contingency(original_votes, rest);\n }\n }\n return attribution(votes, rest);\n };\n return attrib;\n}\n","import { Counter } from \"@gouvernathor/python/collections\";\nimport { type Simple } from \"../ballots\";\nimport { type Attribution, type HasNSeats } from \"../attribution\";\nimport { defaultMetric, type DisproportionMetric } from \"./metrics\";\n\n/**\n * An attribution method that allocates seats proportionally\n * to the number of votes received by each party.\n */\nexport interface Proportional<Party> extends Attribution<Party, Simple<Party>> {};\n\n// TODO: doesn't it always implement HasNSeats ?\n/**\n * An specific kind of proportional attribution method,\n * based on a rank-index function, and which\n */\nexport interface RankIndexMethod<Party> extends Proportional<Party> {};\n\n/**\n * A function that should be pure : it should not take into account\n * any value other than the arguments passed to it.\n *\n * @param t The fraction of votes received by a candidate\n * @param a The number of seats already allocated to that candidate\n * @returns A value that should be increasing as t rises, and decreasing as a rises.\n * The higher the return value, the more likely the candidate is to receive another seat.\n */\nexport interface RankIndexFunction {\n (t: number, a: number): number;\n};\n\n/**\n * A function creating a fixed-seats, proportional, rank-index attribution method\n * from a rank-index function.\n *\n * The implementation is optimized so as to call rankIndexFunction as few times as possible.\n *\n * Replaces the RankIndexMethod class implementation.\n */\nexport function proportionalFromRankIndexFunction<Party>(\n { nSeats, rankIndexFunction }: {\n nSeats: number,\n rankIndexFunction: RankIndexFunction,\n }\n): RankIndexMethod<Party> & HasNSeats {\n const attrib = (votes: Simple<Party>, rest = {}): Counter<Party> => {\n const allVotes = votes.total;\n const fractions = new Map([...votes.entries()].map(([party, v]) => [party, v / allVotes]));\n\n const rankIndexValues = new Map([...fractions.entries()].map(([party, f]) => [party, rankIndexFunction(f, 0)]));\n\n // the parties, sorted by increasing rankIndex value\n const parties = [...votes.keys()].sort((a, b) => rankIndexValues.get(a)! - rankIndexValues.get(b)!);\n\n const seats = new Counter<Party>();\n\n s: for (let sn = 0; sn < nSeats; sn++) {\n // take the most deserving party\n const winner = parties.pop()!;\n // give it a seat\n seats.increment(winner);\n // update the rankIndex value of the party\n rankIndexValues.set(winner, rankIndexFunction(fractions.get(winner)!, seats.get(winner)!));\n\n // insert it in its (new) place in the sorted list of parties\n for (let pn = 0; pn < parties.length; pn++) {\n if (rankIndexValues.get(parties[pn])! >= rankIndexValues.get(winner)!) {\n parties.splice(pn, 0, winner);\n continue s;\n }\n }\n parties.push(winner);\n }\n\n return seats;\n };\n attrib.nSeats = nSeats;\n return attrib;\n}\n\n/**\n * Creates a rank-index (proportional) attribution method in which\n * the total number of seats is NOT fixed.\n * Instead, it takes a metric of disproportionality\n * between the entitlements (the votes) and the numbers of seats,\n * and a range of valid number of seats,\n * and the attribution method will return the allocation of seats\n * that minimizes the metric, while respecting the range of valid number of seats.\n *\n * The attribution will only return a 0-seats attribution when the maxNSeats is 0,\n * otherwise, a minNSeats value of 0 will be treated as 1.\n *\n * The metric has a reasonable default.\n *\n * The implementation is still optimized so as to call rankIndexFunction as few times as possible.\n *\n * @param minNSeats The minimum number of seats to be allocated, inclusive.\n * @param maxNSeats The maximum number of seats to be allocated, inclusive.\n */\nexport function boundedRankIndexMethod<Party>(\n { minNSeats, maxNSeats, rankIndexFunction, metric = defaultMetric }: {\n minNSeats: number,\n maxNSeats: number,\n rankIndexFunction: RankIndexFunction,\n metric?: DisproportionMetric<Party>,\n }\n): RankIndexMethod<Party> {\n const attrib = (votes: Simple<Party>, rest = {}): Counter<Party> => {\n const allVotes = votes.total;\n const fractions = new Map([...votes.entries()].map(([party, v]) => [party, v / allVotes]));\n\n const rankIndexValues = new Map([...fractions.entries()].map(([party, f]) => [party, rankIndexFunction(f, 0)]));\n\n // the parties, sorted by increasing rankIndex value\n const parties = [...votes.keys()].sort((a, b) => rankIndexValues.get(a)! - rankIndexValues.get(b)!);\n\n const seats = new Counter<Party>();\n\n let bestSeats = seats.pos;\n // technically, most metrics give 0 for a 0-seats attribution\n // but we have to patch that out otherwise the 0-seats attribution will always be returned\n let bestSeatsMetric = Infinity;\n\n s: for (let sn = 1; sn <= maxNSeats; sn++) {\n // take the most deserving party\n const winner = parties.pop()!;\n // give it a seat\n seats.increment(winner);\n\n if (sn >= minNSeats) {\n // compute the metric of the new attribution\n const newMetric = metric({ votes, seats });\n if (newMetric < bestSeatsMetric) { // when above the min, favor fewer seats\n bestSeats = seats.pos;\n bestSeatsMetric = newMetric;\n }\n }\n\n // update the rankIndex value of the party\n rankIndexValues.set(winner, rankIndexFunction(fractions.get(winner)!, seats.get(winner)!));\n\n // insert it in its (new) place in the sorted list of parties\n for (let pn = 0; pn < parties.length; pn++) {\n if (rankIndexValues.get(parties[pn])! >= rankIndexValues.get(winner)!) {\n parties.splice(pn, 0, winner);\n continue s;\n }\n }\n parties.push(winner);\n }\n\n return bestSeats;\n }\n attrib.minNSeats = minNSeats;\n attrib.maxNSeats = maxNSeats;\n return attrib;\n}\n\nexport interface DivisorMethod<Party> extends RankIndexMethod<Party> {}\n\n/**\n * A function that should be pure.\n * @param k The number of seats already allocated to a party\n * @returns A value that should be increasing as k rises\n */\nexport interface DivisorFunction {\n (k: number): number;\n}\n\nexport function stationaryDivisorFunction(r: number): DivisorFunction {\n return (k: number) => k + r;\n}\n\nexport function rankIndexFunctionFromDivisorFunction(\n divisorFunction: DivisorFunction\n): RankIndexFunction {\n return (t, a) => t / divisorFunction(a);\n}\n\n/**\n * A function creating a divisor method -\n * one kind of rank-index attribution, itself a kind of proportional attribution.\n */\nexport function proportionalFromDivisorFunction<Party>(\n { nSeats, divisorFunction }: {\n nSeats: number,\n divisorFunction: DivisorFunction,\n }\n): DivisorMethod<Party> & HasNSeats {\n return proportionalFromRankIndexFunction({\n nSeats,\n rankIndexFunction: rankIndexFunctionFromDivisorFunction(divisorFunction),\n });\n}\n","import { max } from \"@gouvernathor/python\";\nimport { Counter } from \"@gouvernathor/python/collections\";\nimport { type Simple } from \"../ballots\";\nimport { type Attribution, AttributionFailure, type HasNSeats } from \"../attribution\";\n\n/**\n * Creates an attribution method in which\n * the party with the most votes wins all the seats.\n *\n * Will throw an AttributionFailure error if no party wins any vote.\n */\nexport function plurality<Party>(\n { nSeats }: {\n nSeats: number,\n }\n): Attribution<Party, Simple<Party>> & HasNSeats {\n const attrib = (votes: Simple<Party>, rest = {}): Counter<Party> => {\n const win = max(votes.keys(), p => votes.get(p)!);\n\n if (votes.get(win)! > 0) {\n return new Counter([[win, nSeats]]);\n }\n throw new AttributionFailure(\"No party won any vote\");\n };\n attrib.nSeats = nSeats;\n return attrib;\n}\n\n/**\n * Creates an attribution method in which a candidate needs to reach\n * a certain percentage of the votes in order to win all the seats.\n *\n * If not party reaches the threshold, the contingency attribution method is called,\n * or if no contingency is provided, an AttributionFailure error is thrown.\n */\nexport function superMajority<Party>(\n { nSeats, threshold, contingency = null }: {\n nSeats: number,\n threshold: number,\n contingency?: Attribution<Party, Simple<Party>> | null,\n }\n): Attribution<Party, Simple<Party>> & HasNSeats {\n const attrib = (votes: Simple<Party>, rest = {}): Counter<Party> => {\n const win = max(votes.keys(), p => votes.get(p)!);\n\n if ((votes.get(win)! / votes.total) > threshold) {\n return new Counter([[win, nSeats]]);\n }\n if (contingency === null) {\n throw new AttributionFailure(\"No party reached the threshold\");\n }\n return contingency(votes, rest);\n };\n attrib.nSeats = nSeats;\n return attrib;\n}\n","import { enumerate, max } from \"@gouvernathor/python\";\nimport { Counter, DefaultMap } from \"@gouvernathor/python/collections\";\nimport { fmean, median } from \"@gouvernathor/python/statistics\";\nimport { Scores } from \"../ballots\";\nimport { type Attribution, type HasNSeats } from \"../attribution\";\n\n/**\n * Creates an attribution method in which all the seats go to the candidate with the highest average score.\n *\n * The ballots are not required to grade all the candidates.\n */\nexport function averageScore<Party>(\n { nSeats }: {\n nSeats: number\n }\n): Attribution<Party, Scores<Party>> & HasNSeats {\n const attrib = (votes: Scores<Party>, rest = {}): Counter<Party> => {\n const counts = new DefaultMap<Party, number[]>(() => []);\n for (const [party, grades] of votes) {\n for (const [grade, qty] of enumerate(grades)) {\n counts.get(party).push(...Array(qty).fill(grade));\n }\n }\n\n return new Counter([[max(counts.keys(), party => fmean(counts.get(party)!)), nSeats]]);\n }\n attrib.nSeats = nSeats;\n return attrib;\n}\n\n/**\n * Creates an attribution method in which all the seats go to the candidate with the highest median score.\n *\n * If there is a tie, the contingency method is called on the candidates that are tied.\n * The default contingency is to take the maximum average score.\n *\n * The ballots are not required to grade all the candidates.\n */\nexport function medianScore<Party>(\n { nSeats, contingency }: {\n nSeats: number,\n contingency?: Attribution<Party, Scores<Party>>,\n }\n): Attribution<Party, Scores<Party>> & HasNSeats {\n if (contingency === undefined) {\n contingency = averageScore({ nSeats });\n }\n\n const attrib = (votes: Scores<Party>, rest = {}): Counter<Party> => {\n const counts = new DefaultMap<Party, number[]>(() => []);\n for (const [party, grades] of votes) {\n for (const [grade, qty] of enumerate(grades)) {\n counts.get(party).push(...Array(qty).fill(grade));\n }\n }\n\n const medians = new Map([...counts.entries()]\n .map(([party, partigrades]) => [party, median(partigrades)]));\n\n const winScore = Math.max(...medians.values());\n const [winner, ...winners] = [...medians.keys()].filter(p => medians.get(p) === winScore);\n\n if (winners.length === 0) { // no tie\n return new Counter([[winner, nSeats]]);\n }\n\n winners.unshift(winner);\n const trimmedResults = Scores.fromEntries(winners.map(party => [party, counts.get(party)!]));\n return contingency(trimmedResults, rest);\n };\n attrib.nSeats = nSeats;\n return attrib;\n}\n","import { ReadonlyCounter } from \"@gouvernathor/python/collections\";\n\n/**\n * A counter, mapping each party to its number of ballots.\n *\n * [[PS, 5], [LR: 7]] -> 5 ballots for PS, 7 for LR.\n */\nexport interface Simple<Party> extends ReadonlyCounter<Party> { }\n\n/**\n * A list of ballots, each ballot ordering parties by decreasing preference.\n *\n * [[LR, PS, LFI], [LFI, PS,], ] -> one voter prefers LR then PS then LFI,\n * another prefers LFI then PS and didn't rank LR.\n *\n * Math.max(result.map(ballot => ballot.length)) <= number of candidates-1\n * == if the voting method requires a full ranking\n *\n * There can be no tie between candidates within a ballot.\n * Note that not ranking all the candidates is permitted by this type,\n * although some attribution methods may not support it.\n */\nexport interface Order<Party> extends ReadonlyArray<ReadonlyArray<Party>> { }\n\n/**\n * A mapping from each party to a list of number of ballots, one for each grade.\n *\n * [[PS, [0, 2, 5, 9, 1]], ] -> PS received the worst grade 0 times, the best grade 1 time and so on.\n *\n * result.get(p).length is constant, equal to the number of grades of the voting method.\n *\n * If the voter must grade all the candidates, then sum(result.get(p)) is constant\n * and equal to the number of voters.\n *\n * Any party not mapped will be assumed to have\n * an array of zeros (of length ngrades).\n */\nexport interface Scores<Party> extends ReadonlyMap<Party, ReadonlyArray<number>> {\n readonly ngrades: number;\n get(key: Party): ReadonlyArray<number>;\n}\n\nexport namespace Scores {\n function get<Party>(this: Scores<Party>, key: Party): ReadonlyArray<number> {\n const value = this.get(key);\n if (value === undefined) {\n return Array(this.ngrades).fill(0);\n }\n return value!;\n }\n\n export function fromEntries<Party>(\n elements: readonly [Party, readonly number[]][],\n ): Scores<Party> {\n if (elements.length === 0) {\n throw new Error(\"Use the fromGrades method to create an empty Scores instance\");\n }\n const ths = new Map(elements) as Partial<Scores<Party>> & { ngrades?: number };\n ths.ngrades = elements[0][1].length;\n ths.get = get.bind(ths as any);\n return ths as Scores<Party>;\n }\n\n export function fromGrades<Party>(ngrades: number): Scores<Party> {\n const ths = new Map() as Partial<Scores<Party>> & { ngrades?: number };\n ths.ngrades = ngrades;\n ths.get = get.bind(ths as any);\n return ths as Scores<Party>;\n }\n}\n\n\nexport type Ballots<Party> = Simple<Party> | Order<Party> | Scores<Party>;\n","import { divmod } from \"@gouvernathor/python\";\nimport { Counter } from \"@gouvernathor/python/collections\";\nimport { type Simple } from \"../ballots\";\nimport { type Attribution, type HasNSeats } from \"../attribution\";\nimport { addThresholdToSimpleAttribution } from \"../attribution/transform\";\nimport { type DivisorFunction, type DivisorMethod, type Proportional, proportionalFromDivisorFunction, proportionalFromRankIndexFunction, rankIndexFunctionFromDivisorFunction, type RankIndexMethod, stationaryDivisorFunction } from \"./proportionalBase\";\n\nconst divisor1 = stationaryDivisorFunction(1);\nexport function jefferson<Party>(\n { nSeats }: {\n nSeats: number,\n }\n): DivisorMethod<Party> & HasNSeats {\n return proportionalFromDivisorFunction<Party>({\n nSeats,\n divisorFunction: divisor1,\n });\n}\nexport const dHondt = jefferson;\n\nconst divisorPoint5: DivisorFunction = k => 2 * k + 1; // int math is better than k + .5\nexport function webster<Party>(\n { nSeats }: {\n nSeats: number,\n }\n): DivisorMethod<Party> & HasNSeats {\n return proportionalFromDivisorFunction<Party>({\n nSeats,\n divisorFunction: divisorPoint5,\n });\n}\nexport const sainteLague = webster;\n\nexport function hamilton<Party>(\n { nSeats }: {\n nSeats: number,\n }\n): Proportional<Party> & HasNSeats {\n const attrib = (votes: Simple<Party>, rest = {}): Counter<Party> => {\n const seats = new Counter<Party>();\n const remainders = new Map<Party, number>();\n const sumVotes = votes.total;\n\n for (const [party, scores] of votes) {\n const [i, r] = divmod(scores * nSeats, sumVotes);\n seats.set(party, i);\n remainders.set(party, r);\n }\n\n seats.update([...remainders.keys()]\n .sort((a, b) => remainders.get(b)! - remainders.get(a)!)\n .slice(0, nSeats - seats.total));\n return seats;\n };\n attrib.nSeats = nSeats;\n return attrib;\n}\nexport const hareLargestRemainders = hamilton;\n\nconst huntingtonHillBaseRankIndexFunction = rankIndexFunctionFromDivisorFunction(k => Math.sqrt(k * (k + 1)));\nconst huntingtonHillRankIndexFunction = (t: number, a: number) => {\n if (a <= 0) {\n return Infinity;\n }\n return huntingtonHillBaseRankIndexFunction(t, a);\n};\n/**\n * This attribution method required some creativity and tweaks,\n * since the standard divisor won't work without an initial seats value,\n * causing a division by 0.\n * As a result, a threshold is required in this case.\n *\n * In a situation where the candidates are already limited by other means,\n * (for instance when distributing representatives between a fixed number of states)\n * to be less or equal to the number of seats, then a threhold of 0 can be passed.\n * In that case, the method will allocate one seat to each candidate\n * until all candidates have a seat,\n * or (but that would be a bug) all seats are allocated.\n *\n * The order in which the first seats are allocated is an implementation detail.\n */\nexport function huntingtonHill<Party>(\n { nSeats, threshold }: {\n nSeats: number,\n threshold: 0,\n }\n): DivisorMethod<Party> & HasNSeats;\nexport function huntingtonHill<Party>(\n { nSeats, threshold, contingency }: {\n nSeats: number,\n threshold: number,\n contingency?: DivisorMethod<Party> | null,\n }\n): DivisorMethod<Party> & HasNSeats;\nexport function huntingtonHill<Party>(\n { nSeats, threshold, contingency }: {\n nSeats: number,\n threshold: number,\n contingency?: RankIndexMethod<Party> | null,\n }\n): RankIndexMethod<Party> & HasNSeats;\nexport function huntingtonHill<Party>(\n { nSeats, threshold, contingency }: {\n nSeats: number,\n threshold: number,\n contingency?: Attribution<Party, Simple<Party>> | null,\n }\n): Attribution<Party, Simple<Party>> & HasNSeats;\nexport function huntingtonHill<Party>(\n { nSeats, threshold, contingency = null }: {\n nSeats: number,\n threshold: number,\n contingency?: Attribution<Party, Simple<Party>> | null,\n }\n) {\n const attrib = addThresholdToSimpleAttribution({\n threshold,\n contingency,\n attribution: proportionalFromRankIndexFunction({\n nSeats,\n rankIndexFunction: huntingtonHillRankIndexFunction,\n }),\n }) as Attribution<Party, Simple<Party>> & { nSeats?: number };\n attrib.nSeats = nSeats;\n return attrib;\n}\n\n/**\n * Creates a highest-averages proportional attribution method.\n *\n * The exact method used is an implementation detail and may change between versions.\n */\nexport const highestAverages = webster;\n\n/**\n * Creates a largest-remainders proportional attribution method.\n *\n * Implementation detail: the quota used is the Hare/Hamilton quota.\n */\nexport const largestRemainders = hamilton;\n","import { Counter } from \"@gouvernathor/python/collections\";\nimport { createRandomObj, type RandomObjParam } from \"../../utils\";\nimport { type Simple } from \"../ballots\";\nimport { type Attribution, type HasNSeats } from \"../attribution\";\n\n/**\n * Randomized attribution.\n * Everyone votes, then one ballot is selected at random.\n * (One ballot per seat to fill, and assuming there's\n * enough ballots that the picking is with replacement.)\n *\n * The randomization is based on the given parameters. If a RNG object\n * is passed, it is used without reseeding across all calls of the attribution.\n * If a seed is passed, the random object is reseeded\n * at each call of the attribution.\n */\nexport function randomize<Party>(\n { nSeats, ...randomParam }: {\n nSeats: number,\n } & RandomObjParam\n): Attribution<Party, Simple<Party>> & HasNSeats {\n const attrib = (votes: Simple<Party>, rest = {}): Counter<Party> => {\n const randomObj = createRandomObj(randomParam);\n return new Counter(randomObj.choices([...votes.keys()], { weights: [...votes.values()], k: nSeats }));\n };\n attrib.nSeats = nSeats;\n return attrib;\n}\n","import RNG from \"@gouvernathor/rng\";\n\nexport type RandomObjParam = {randomObj: RNG} | {randomSeed?: number|string};\n\nexport function createRandomObj(param?: RandomObjParam): RNG;\nexport function createRandomObj({ randomObj, randomSeed }:\n { randomObj?: RNG, randomSeed?: number | string } = {},\n): RNG {\n if (randomObj === undefined) {\n randomObj = new RNG(randomSeed);\n }\n return randomObj;\n}\n\n\n// https://stackoverflow.com/a/71700658\n\n/**\n * Mutable tuple of a single element type and a given length.\n *\n * Warning: due to limitations in TypeScript, when N is unknown/generic,\n * the typing system does not recognize that the type extends array.\n * Using methods such as map will require a (double) cast first to any then to T[].\n */\nexport type Tuple<\n T,\n N extends number,\n R extends T[] = [],\n> = R['length'] extends N ? R : Tuple<T, N, [T, ...R]>;\n\n/**\n * Immutable version of Tuple.\n *\n * Warning: due to limitations in TypeScript, when N is unknown/generic,\n * the typing system does not recognize that the type extends readonly array.\n * Using methods such as map will require a (double) cast first to any then to readonly T[].\n */\nexport type ReadonlyTuple<T, N extends number> = Readonly<Tuple<T, N>>;\n"],"mappings":"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","names":["import_python","import_collections","import_collections","import_collections","import_python","import_collections","Scores","import_python","import_collections","import_collections"]}