votes
    Preparing search index...

    votes

    votes

    version npm bundle size language downloads last commit license CI Codacy grade Codacy coverage Website

    TypeScript library of ranked voting methods, published to NPM.

    yarn add votes
    # or: npm install votes
    import { Borda } from 'votes'

    const borda = new Borda({
    candidates: ['Lion', 'Bear', 'Sheep'],
    ballots: [
    { ranking: [['Lion'], ['Bear'], ['Sheep']], weight: 4 },
    { ranking: [['Sheep'], ['Bear'], ['Lion']], weight: 3 },
    { ranking: [['Bear', 'Sheep'], ['Lion']], weight: 2 },
    ],
    })

    borda.scores()
    // { Lion: 17, Bear: 19, Sheep: 18 }

    borda.ranking()
    // [ ['Bear'], ['Sheep'], ['Lion'] ]

    A ballot ranks candidates in tiers: [['Bear', 'Sheep'], ['Lion']] means Bear and Sheep tied first, Lion last. weight is how many voters cast that ballot (default 1). Candidates a ballot leaves unranked join it as one tied bottom tier by default; pass unrankedLast: false to score only expressed preferences (unranked candidates then earn nothing from that ballot). Blank ballots, ranking none of the candidates still in the race, don't count as cast; pass countBlank: true to count them toward absolute majorities (AbsoluteMajority, Bucklin, Coombs, TwoRoundRunoff, SupplementaryVote) and, with unrankedLast, as worst grades in MajorityJudgment.

    Every method takes this same { candidates, ballots, unrankedLast, countBlank } input. Matrix methods (see the table below) also take a pairwise matrix { candidates, array }, such as matrixFromBallots(ballots, candidates).

    Election chains methods on one input. The first ranks the candidates; each next one re-ranks the tiers still tied. Round-based methods (InstantRunoff and its Condorcet hybrids below, Baldwin, Coombs, Nanson, TwoRoundRunoff, SupplementaryVote, BottomTwoRunoff) also break their round ties (for last place, at two-round runoff's cut, or Nanson's complete tie) with the methods after them, in order. A round tie they leave goes out at once if no order of it changes the outcome; otherwise the count stops, and those who win in some order of breaking the ties tie first (or, past 10,000 orders tried, everyone left). Alone, a round method has no tiebreakers.

    import {
    Election,
    InstantRunoff,
    RandomCandidates,
    rngGenerator,
    Schulze,
    } from 'votes'

    const election = new Election({
    candidates,
    ballots,
    methods: [
    InstantRunoff,
    Schulze, // breaks IRV's round ties, then the ties left in its ranking
    RandomCandidates.with({ rng: rngGenerator('seed') }), // last resort
    ],
    })

    election.ranking() // final ranking after all tie-breaking
    election.result() // { ranking, steps: StepResult[] }

    Each StepResult records name, before, after, and runs: the method's ranking of each tied tier, with its scores or rounds.

    Option Default Description
    mode 'restricted' Which candidates the method ranks a tie among: the tied ones, everyone ('full'), or the tied ones, then each tie that leaves, until none splits ('recursive').
    label Class.label Name in traces (TieBreakStep.name, StepResult.name).
    Any other prop — Passed through to the method constructor (e.g. rng for random methods).

    Among two tied candidates, most methods only count who more voters prefer; 'full' brings in how each fares against everyone: [InstantRunoff, FirstPastThePost.with({ mode: 'full' })] breaks instant runoff's ties by first-round votes.

    rounds() returns detailed per-round results; in an Election, they are in the round method's step:

    const [step] = new Election({
    candidates,
    ballots,
    methods: [InstantRunoff, Borda],
    }).result().steps

    const [round] = step.runs[0].rounds
    // {
    // index: 0,
    // candidates: ['a', 'b', 'c'],
    // scores: { a: 5, b: 3, c: 3 },
    // qualified: ['a', 'b'],
    // eliminated: ['c'], // resolved by tiebreaker
    // tieBreakSteps: [{
    // index: 0, // first tiebreaker
    // name: 'Borda',
    // candidates: ['b', 'c'], // tied candidates
    // ranking: [['b'], ['c']], // tiebreaker's verdict
    // scores: { b: 19, c: 14 }, // when the tiebreaker scores
    // resolved: ['b'], // promoted out of tie
    // remaining: ['c'], // eliminated
    // }]
    // }

    Some methods add method-specific detail as info: Coombs reports whether a round was resolved by majority or elimination (CoombsInfo), Nanson reports the Borda average used as elimination cutoff (NansonInfo).

    ranking() is each method's own full ranking. iteratedRanking() instead fills places by repeated wins: run the method, take the winning tier, re-run the method without the placed candidates, and so on. The two differ whenever a method's full ranking disagrees with how it ranks subsets — e.g. instant runoff orders losers by elimination time.

    const irv = new InstantRunoff({ candidates, ballots })
    irv.ranking() // losers ordered by elimination time
    irv.iteratedRanking() // 2nd place = winner of a re-run without the winner

    new Election({ candidates, ballots, methods }).iteratedRanking() // re-runs the chain per place

    Under the hood it uses restrict(candidates), which every method exposes to re-run itself on a subset of candidates, as if only they ran: a ballot ranking none of them is blank. MajorityJudgment keeps its grades instead, as a grade does not depend on who else runs; CandidateOrder keeps its order.

    Method Class Input
    Absolute majority AbsoluteMajority ballots
    Anti-plurality AntiPlurality ballots
    Approval voting Approval ballots
    Baldwin method Baldwin ballots
    Benham's method Benham ballots
    Black's method Black ballots
    Borda count Borda ballots
    Bottom-two-runoff BottomTwoRunoff ballots
    Bucklin voting Bucklin ballots
    Candidate order CandidateOrder —
    Coombs' method Coombs ballots
    Copeland's method Copeland ballots or matrix
    Dictator Dictator ballots
    First-past-the-post FirstPastThePost ballots
    Instant-runoff (IRV) InstantRunoff ballots
    Kemeny–Young ⚠️ Kemeny ballots or matrix
    Majority judgment MajorityJudgment ballots as grades
    Maximal lotteries MaximalLotteries ballots or matrix
    Minimax Condorcet Minimax ballots or matrix
    Minimax-TD MinimaxTD ballots or matrix
    Nanson method Nanson ballots
    Random candidate RandomCandidates —
    Random dictator RandomDictator ballots
    Randomized Condorcet RandomizedCondorcet ballots or matrix
    Ranked pairs RankedPairs ballots or matrix
    Schulze method Schulze ballots or matrix
    Smith's method Smith ballots or matrix
    Smith//IRV SmithInstantRunoff ballots
    Split Cycle SplitCycle ballots or matrix
    Stable Voting ⚠️ StableVoting ballots or matrix
    Supplementary vote SupplementaryVote ballots
    Tideman's Alternative TidemanAlternative ballots
    Two-round runoff TwoRoundRunoff ballots
    Woodall's method Woodall ballots

    methods maps each VotingSystem id to its class: methods[VotingSystem.Borda] is Borda.

    Approval approves the candidates in each ballot's first tier.

    AbsoluteMajority ranks everyone tied when no candidate has more than half the ballots, for the next method in an Election to settle; its winner() is then undefined.

    Bucklin scores the count that finds a majority, a tier across its cut counting for its share of places within; counts() lists each round's count.

    CandidateOrder ranks candidates in the order given, whatever the order of a tie it breaks, and Dictator as the first ballot, breaking its ties with the next and so on: deterministic last resorts for an Election. RandomDictator is Dictator on ballots drawn in random order, by weight.

    Baldwin and Nanson take Borda scores from the pairwise matrix: the same as Borda's when ballots rank everyone, and a Condorcet winner always wins.

    ⚠️ Kemeny runs in O(n·2ⁿ) time and memory — slow beyond ~20 candidates, and throws beyond maxCandidates (default 20, at most 30).

    ⚠️ StableVoting runs in exponential time at worst — seconds when majority cycles span 20 candidates — and throws beyond maxCandidates (default 20, at most 30). Below its winners, it ranks by running again without them; SplitCycle ranks the undefeated first, then those only they defeat, and so on.

    ⚠️ RandomDictator's probabilities() runs in exponential time on ballots with ties, and throws past maxTies distinct ties explored (default 2000, well under a second); strict ballots explore one. ranking() has no cap.

    MajorityJudgment reads a ballot's tiers as grades, best first (grades, default 6), and breaks ties one vote at a time: give it whole weights. A ballot ranked past the last grade throws, or with clampGrades gives those ranks the worst grade.

    Benham, Woodall, SmithInstantRunoff (Smith//IRV) and TidemanAlternative run instant runoff with a Condorcet check: Benham stops on a Condorcet winner among those left, Woodall on the last member of the Smith set, Smith//IRV puts everyone outside the Smith set out first, and Tideman's Alternative narrows to the Smith set of those left before each count (topSet: 'schwartz' for the Schwartz set). A round that narrows puts everyone else out at once.

    TwoRoundRunoff counts the runoff from the ballots' whole rankings, as the contingent vote does; SupplementaryVote counts only each ballot's first two places (preferences, default 2), so A > B > C counts for neither in a C–D runoff. The two agree when no ballot ranks past preferences.

    BottomTwoRunoff settles each round with a first-choices head-to-head, the first tieBreakSteps entry (named FirstPastThePost); the methods after it in an Election apply only if that ties.

    Random methods expose probabilities(), each candidate's chance to win, instead of scores(), and draw their ranking once per instance.

    Schulze also exposes strengths() — the beatpath strength matrix its scores derive from.

    Minimax also exposes worstDefeats(): each candidate's worst defeat and the opponents inflicting it. It measures a defeat by its margin; pass variant (MinimaxVariant.WinningVotes or PairwiseOpposition) to measure it otherwise, and excludeTies: true to leave tied duels out.

    RankedPairs and Schulze measure a pairwise win by the winner's votes by default; pass strength: 'margins' to subtract the loser's. The two differ only when voters tie both candidates or leave both unranked.

    RankedPairs locks equal-strength pairs simultaneously by default (ties are preserved rather than order-dependent). Pass edgeSorter — e.g. the exported byTotalParticipation — to lock them in its order like canonical Tideman; pairs it cannot tell apart still lock together. lockSteps() traces it: each group of pairs, in order, each locked or skipped.

    matrixFromBallots(ballots, candidates) // array[i][j]: weight ranking i above j
    makeAntisymmetric(matrix) // margins: array[i][j] - array[j][i]
    smithSet({ candidates, ballots }) // smallest set beating everyone outside it
    condorcetWinner({ candidates, ballots }) // who beats everyone else head-to-head, or undefined
    toWeightedBallots([
    [['a'], ['b']],
    [['a'], ['b']],
    ]) // one ballot per ranking
    groupBallots(ballots) // equal ballots merged, heaviest first
    normalizeBallots(ballots, candidates) // unknown and repeated candidates dropped, unranked ones tied last
    scoresToRanking({ a: 2, b: 1 }) // [['a'], ['b']]
    rngGenerator('my-seed') // seeded RNG for random methods' `rng`

    Parse and serialize the Condorcet Election Format (.cvotes files):

    import {
    parseCondorcetElectionFormat,
    stringifyCondorcetElectionFormat,
    } from 'votes'

    const { candidates, ballots } = parseCondorcetElectionFormat(`
    #/Candidates: A; B; C
    A > B > C * 3
    B > C > A * 2
    `)

    const text = stringifyCondorcetElectionFormat({ candidates, ballots })

    Contributions, issues and feature requests are welcome. Feel free to check the issues page.