votes
    Preparing search index...

    votes

    votes

    version npm bundle size language downloads last commit license CI 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'] ]

    Round-based methods (InstantRunoff, Baldwin, Coombs, Nanson, TwoRoundRunoff, BottomTwoRunoff) accept a tieBreakers array. When multiple candidates tie for last place in a round, tiebreakers are applied in order until one candidate can be singled out.

    import {
    InstantRunoff,
    RandomCandidates,
    Borda,
    Copeland,
    tb,
    rngGenerator,
    } from 'votes'

    // Simple: pass constructors directly
    new InstantRunoff({ candidates, ballots, tieBreakers: [Borda, Copeland] })

    // tb() lets you pass extra constructor options
    const rng = rngGenerator('seed')
    new InstantRunoff({
    candidates,
    ballots,
    tieBreakers: [
    Copeland, // first try Copeland
    tb(Borda, { full: true }), // run Borda on ALL candidates, not just the tied subset
    tb(Borda, { stable: true }), // recurse into sub-ties until stable
    tb(RandomCandidates, { rng }), // last resort: random with seeded RNG
    ],
    })
    Option Default Description
    full false Run the tiebreaker on all candidates instead of the tied subset. Useful when a method's relative ranking is more meaningful globally.
    stable false After one pass, recurse into any remaining sub-ties until no further progress can be made.
    Any other prop Passed through to the method constructor (e.g. rng for random methods).

    Method type is auto-detected: matrix methods (Copeland, Schulze, …) receive a pairwise matrix; ballot methods receive the filtered ballots; candidates-only methods (RandomCandidates) receive just the candidate list.

    computeRounds() returns detailed per-round results:

    const rounds = new InstantRunoff({
    candidates,
    ballots,
    tieBreakers: [Borda],
    }).computeRounds()

    rounds[0].roundResult
    // {
    // scores: { a: 5, b: 3, c: 3 },
    // qualified: ['a'],
    // eliminated: ['c'], // resolved by tiebreaker
    // tieBreakSteps: [{
    // tbIndex: 0, // first tiebreaker
    // tbName: 'Borda',
    // input: ['b', 'c'], // tied candidates
    // ranking: [['b'], ['c']], // tiebreaker's verdict
    // resolved: ['b'], // promoted out of tie
    // remaining: ['c'], // eliminated
    // }]
    // }

    Election chains pre-built ranker instances. The first provides the primary ranking; subsequent rankers refine any remaining ties.

    import {
    Election,
    InstantRunoff,
    Schulze,
    matrixFromBallots,
    tb,
    Copeland,
    } from 'votes'

    const election = new Election({
    rankers: [
    new InstantRunoff({ candidates, ballots, tieBreakers: [tb(Copeland)] }),
    new Schulze(matrixFromBallots(ballots, candidates)),
    ],
    })

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

    Each StepResult records rankerName, before, after, and optionally rounds / scores from that step.

    Method Class Input
    Absolute majority AbsoluteMajority ballots
    Approval voting Approbation ballots
    Baldwin method Baldwin ballots
    Borda count Borda ballots
    Bottom-two-runoff BottomTwoRunoff ballots
    Coombs' method Coombs ballots
    Copeland's method Copeland matrix
    First-past-the-post FirstPastThePost ballots
    Instant-runoff (IRV) InstantRunoff ballots
    Kemeny–Young ⚠️ Kemeny matrix
    Majority judgment MajorityJudgment ballots (6 grades)
    Maximal lotteries MaximalLotteries matrix
    Minimax Condorcet Minimax matrix
    Minimax-TD MinimaxTD matrix
    Nanson method Nanson ballots
    Random candidate RandomCandidates
    Random dictator RandomDictator ballots
    Randomized Condorcet RandomizedCondorcet matrix
    Ranked pairs RankedPairs matrix
    Schulze method Schulze matrix
    Smith's method Smith matrix
    Two-round runoff TwoRoundRunoff ballots

    ⚠️ Kemeny runs in O(n!) time — impractical beyond ~8 candidates.

    Matrix-input methods take the output of matrixFromBallots(ballots, candidates).

    BottomTwoRunoff always prepends tb(FirstPastThePost) to tieBreakers — that FPTP step is the head-to-head runoff mechanism, not a fallback. It will appear as the first entry in tieBreakSteps. User-supplied tieBreakers fire after it only if the head-to-head itself ties.

    Every method also exposes deTie(), which recursively resolves ties by re-running the same method on each tied subset:

    new Borda({ candidates, ballots }).deTie()
    // same as ranking() but each tied tier is re-ranked with Borda on that subset
    import { matrixFromBallots, rngGenerator } from 'votes'

    // Build a pairwise matrix from ballots
    const matrix = matrixFromBallots(ballots, candidates)

    // Seeded RNG for reproducible random methods
    const rng = rngGenerator('my-seed')
    new RandomCandidates({ candidates, rng })

    Parse and serialize the Condorcet Election Format (.blt-style text 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.