Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

lzear/votes

Open more actions menu

Repository files navigation

votes

version npm bundle size language downloads last commit license CI Website

TypeScript library of ranked voting methods, published to NPM.

Install

yarn add votes
# or: npm install votes

Basic usage

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. 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).

Tiebreakers

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
  ],
})

tb(Ctor, opts?) options

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.

Round trace

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
//   }]
// }

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

Election: chaining rankers

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.

Iterated ranking

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({ rankers }).iteratedRanking() // re-runs the whole chain per place

Under the hood it uses restrict(candidates), which every method exposes to re-run itself on a subset of candidates.

Voting systems

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.

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

RankedPairs locks equal-strength pairs simultaneously by default (ties are preserved rather than order-dependent). Pass edgeSorter — e.g. the exported byTotalParticipation — to process them sequentially like canonical Tideman.

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

Utilities

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 })

Condorcet election format

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 })

Documentation

Contributing

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

Releases

Packages

Used by

Contributors

Languages

Morty Proxy This is a proxified and sanitized view of the page, visit original site.