Skip to content

Commit

Permalink
Merge pull request #40 from lzear/next
Browse files Browse the repository at this point in the history
Next
  • Loading branch information
lzear authored Jan 30, 2021
2 parents 6f67815 + 9e19a03 commit 59c8901
Show file tree
Hide file tree
Showing 34 changed files with 581 additions and 294 deletions.
29 changes: 20 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@

[![version](https://img.shields.io/npm/v/votes)](https://www.npmjs.com/package/votes)
![bundle size](https://img.shields.io/bundlephobia/min/votes)
[![Known Vulnerabilities](https://snyk.io/test/github/lzear/votes/badge.svg?targetFile=package.json)](https://snyk.io/test/github/lzear/votes?targetFile=package.json)
![downloads](https://img.shields.io/npm/dm/votes)
![Semantic release](https://github.com/lzear/votes/workflows/Semantic%20release/badge.svg)
[![Build Status](https://travis-ci.com/lzear/votes.svg?branch=master)](https://travis-ci.com/lzear/votes)
[![Codacy Badge](https://app.codacy.com/project/badge/Coverage/d2378c63d95f41efb79072176f015976)](https://www.codacy.com/gh/lzear/votes/dashboard?utm_source=github.com&utm_medium=referral&utm_content=lzear/votes&utm_campaign=Badge_Coverage)
[![Codacy Badge](https://api.codacy.com/project/badge/Grade/08af655918d741d1bffca7ec12ba72be)](https://app.codacy.com/gh/lzear/votes?utm_source=github.com&utm_medium=referral&utm_content=lzear/votes&utm_campaign=Badge_Grade_Settings)
[![CodeClimate Coverage](https://api.codeclimate.com/v1/badges/0a98aa30f16e04bc3eac/test_coverage)](https://codeclimate.com/github/lzear/votes/test_coverage)
[![CodeClimate Maintainability](https://api.codeclimate.com/v1/badges/0a98aa30f16e04bc3eac/maintainability)](https://codeclimate.com/github/lzear/votes/maintainability)
[![CodeFactor](https://www.codefactor.io/repository/github/lzear/votes/badge)](https://www.codefactor.io/repository/github/lzear/votes)
![codecov](https://codecov.io/gh/lzear/votes/branch/master/graph/badge.svg?token=Fd9Jk4FeBY)
![last commit](https://img.shields.io/github/last-commit/lzear/votes)
![language](https://img.shields.io/github/languages/top/lzear/votes)
Expand Down Expand Up @@ -49,11 +51,10 @@ See
[Comparison of electoral systems (Wikipedia)](https://en.wikipedia.org/wiki/Comparison_of_electoral_systems)
for more information.

**⚠️Maximal lotteries & Randomized Condorcet⚠️** (Errors included):
Returns probabilities for each candidate that should be used for a lottery
between the Candidates. If a candidate is the Condorcet winner, its
probability will be 1. Despite being non-deterministic,
those methods are the most fair.
**⚠️Maximal lotteries & Randomized Condorcet⚠️** (Errors included): Returns
probabilities for each candidate that should be used for a lottery between the
Candidates. If a candidate is the Condorcet winner, its probability will be 1.
Despite being non-deterministic, those methods are the fairest.

**Ranked pairs**: Using the duel results as edges, build an acyclic graph
starting by the strongest score differences. The roots of the graph are the
Expand All @@ -78,15 +79,25 @@ candidates.
**Approval voting**: Each voter can select (“approve”) any number of candidates.
The winner is the most-approved candidate.

**Borda's count**: For each voter, every candidate is given a number of points
**Borda count**: For each voter, every candidate is given a number of points
which equals the number of candidates ranked lower in the voter's preference.

**Instant runoff**: Considering only the top choice of each voter, the candidate
**Nanson method**: Iterative Borda count in which, each round, candidates
scoring the average score or less are eliminated.

**Baldwin method**: Iterative Borda count in which, each round, candidates
scoring the lowest score are eliminated.

**Instant-runoff**: Considering only the top choice of each voter, the candidate
with the fewest votes is eliminated. The election repeats until there is a
winner. This voting system is very similar to single transferable vote method.

**Two-round system**: If no candidate receives 50% of the votes in the first
round, then a second round of voting is held with only the top two candidates.
**Coombs' method**: Each round, the candidate with the most last rank is
eliminated. The election repeats until there is a winner.

**Contingent vote** (immediate **Two-round system**): If no candidate receives
50% of the votes in the first round, then a second round of voting is held with
only the top two candidates.

**Plurality**: Simple voting method where only the preferred candidate of each
voter gets 1 point. AKA first-past-the-post.
Expand Down
4 changes: 2 additions & 2 deletions codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ coverage:
status:
project:
default:
target: 90% # the required coverage value
threshold: 1% # the leniency in hitting the target
target: 90% # the required coverage value
threshold: 1% # the leniency in hitting the target
patch:
default:
target: 80%
36 changes: 17 additions & 19 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,20 @@
"module": "dist/votes.es5.js",
"types": "dist/votes.d.ts",
"files": [
"dist"
"dist",
"src"
],
"author": "lzear",
"repository": {
"type": "git",
"url": "https://github.com/lzear/votes.git"
},
"license": "MIT",
"engines": {
"node": ">=6.0.0"
},
"scripts": {
"eslint": "eslint --ext .js --ext .ts .",
"fix:prettier": "prettier --write \"**/*.*\"",
"prebuild": "rimraf dist",
"build": "rollup -c rollup.config.js && typedoc",
"build": "rollup -c rollup.config.js && npx typedoc src/votes.ts",
"build:watch": "tsc --module commonjs && rollup -c rollup.config.js -w",
"test": "jest --coverage",
"test:watch": "jest --coverage --watch",
Expand Down Expand Up @@ -55,32 +53,32 @@
"@commitlint/cli": "^11.0.0",
"@rollup/plugin-commonjs": "^17.0.0",
"@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^11.0.1",
"@types/jest": "^26.0.19",
"@types/lodash": "^4.14.165",
"@types/node": "^14.14.14",
"@typescript-eslint/eslint-plugin": "^4.11.0",
"@typescript-eslint/parser": "^4.11.0",
"commitizen": "^4.2.2",
"@rollup/plugin-node-resolve": "^11.1.0",
"@types/jest": "^26.0.20",
"@types/lodash": "^4.14.168",
"@types/node": "^14.14.22",
"@typescript-eslint/eslint-plugin": "^4.14.0",
"@typescript-eslint/parser": "^4.14.0",
"commitizen": "^4.2.3",
"dotenv": "^8.2.0",
"eslint": "^7.16.0",
"eslint-config-prettier": "^7.1.0",
"eslint-plugin-prettier": "^3.3.0",
"husky": "^4.3.6",
"eslint": "^7.18.0",
"eslint-config-prettier": "^7.2.0",
"eslint-plugin-prettier": "^3.3.1",
"husky": "^4.3.8",
"jest": "^26.6.3",
"lint-staged": "^10.5.3",
"prettier": "^2.2.1",
"rimraf": "^3.0.2",
"rollup": "^2.35.1",
"rollup": "^2.37.0",
"rollup-plugin-sizes": "^1.0.3",
"rollup-plugin-sourcemaps": "^0.6.3",
"rollup-plugin-typescript2": "^0.29.0",
"semantic-release": "^17.3.0",
"semantic-release": "^17.3.4",
"shelljs": "^0.8.4",
"travis-deploy-once": "^5.0.11",
"ts-jest": "^26.4.4",
"ts-node": "^9.1.1",
"typedoc": "^0.19.2",
"typedoc": "^0.20.16",
"typescript": "^4.1.3"
},
"dependencies": {
Expand Down
2 changes: 1 addition & 1 deletion src/methods/approbation/approbation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ import { approbation } from '.'
it('skips empty votes', () => {
expect(
approbation.computeFromBallots([{ weight: 1, ranking: [] }], ['a']),
).toMatchObject({})
).toStrictEqual({ a: 0 })
})
2 changes: 1 addition & 1 deletion src/methods/approbation/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
VotingSystem,
Ballot,
} from '../../types'
import { iterateFirstChoices } from '../first-past-the-post'
import { iterateFirstChoices } from '../first-past-the-post/iterateFirstChoices'

export const approbation: SystemUsingRankings = {
type: VotingSystem.Approbation,
Expand Down
27 changes: 27 additions & 0 deletions src/methods/baldwin/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import difference from 'lodash/difference'
import {
SystemUsingRankings,
ScoreObject,
VotingSystem,
Ballot,
} from '../../types'
import { borda } from '../borda'
import { scoresToRanking } from '../../utils/scores'

export const baldwin: SystemUsingRankings = {
type: VotingSystem.Baldwin,
computeFromBallots(ballots: Ballot[], candidates: string[]): ScoreObject {
const score: ScoreObject = {}
let remainingCandidates = candidates
let points = 0
while (remainingCandidates.length > 0) {
const bordaScores = borda.computeFromBallots(ballots, remainingCandidates)
const ranking = scoresToRanking(bordaScores)
const losers = ranking[ranking.length - 1]
for (const loser of losers) score[loser] = points
remainingCandidates = difference(remainingCandidates, losers)
points++
}
return score
},
}
3 changes: 2 additions & 1 deletion src/methods/borda/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ import {
Ballot,
} from '../../types'
import { scoresZero } from '../../utils/scoresZero'
import { normalizeBallots } from '../../utils/normalize'

export const borda: SystemUsingRankings = {
type: VotingSystem.Borda,
computeFromBallots(ballots: Ballot[], candidates: string[]): ScoreObject {
const result: ScoreObject = scoresZero(candidates)
ballots.forEach((ballot) => {
normalizeBallots(ballots, candidates).forEach((ballot) => {
let voteValue = candidates.length - 1
ballot.ranking.forEach((candidatesAtRank) => {
const value = voteValue - (candidatesAtRank.length - 1) / 2
Expand Down
33 changes: 33 additions & 0 deletions src/methods/coombs/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import difference from 'lodash/difference'
import {
SystemUsingRankings,
ScoreObject,
VotingSystem,
Ballot,
} from '../../types'
import { firstPastThePost } from '../first-past-the-post'
import { scoresToRanking } from '../../utils/scores'

export const coombs: SystemUsingRankings = {
type: VotingSystem.Coombs,
computeFromBallots(ballots: Ballot[], candidates: string[]): ScoreObject {
const score: ScoreObject = {}
const reversedBallots = ballots.map((ballot) => ({
ranking: [...ballot.ranking].reverse(),
weight: ballot.weight,
}))
let remainingCandidates = candidates
let points = 0
while (remainingCandidates.length > 0) {
const fptpScore = firstPastThePost.computeFromBallots(
reversedBallots,
remainingCandidates,
)
const ranking = scoresToRanking(fptpScore)
for (const winner of ranking[0]) score[winner] = points
remainingCandidates = difference(remainingCandidates, ranking[0])
points++
}
return score
},
}
2 changes: 1 addition & 1 deletion src/methods/first-past-the-post/fptp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ import { firstPastThePost } from '.'
it('skips empty votes', () => {
expect(
firstPastThePost.computeFromBallots([{ weight: 1, ranking: [] }], ['a']),
).toMatchObject({})
).toStrictEqual({ a: 0 })
})
20 changes: 1 addition & 19 deletions src/methods/first-past-the-post/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,7 @@ import {
VotingSystem,
Ballot,
} from '../../types'
import { scoresZero } from '../../utils/scoresZero'

export const iterateFirstChoices = (
ballots: Ballot[],
candidates: string[],
compute: (rank: string[]) => number,
): ScoreObject => {
const result: ScoreObject = scoresZero(candidates)
ballots.forEach((ballot) => {
if (ballot.ranking.length) {
const votes = ballot.ranking[0].filter((c) => candidates.includes(c))
votes.forEach(
(candidate, idx, rank) =>
(result[candidate] += compute(rank) * ballot.weight),
)
}
})
return result
}
import { iterateFirstChoices } from './iterateFirstChoices'

export const firstPastThePost: SystemUsingRankings = {
type: VotingSystem.FirstPastThePost,
Expand Down
21 changes: 21 additions & 0 deletions src/methods/first-past-the-post/iterateFirstChoices.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Ballot, ScoreObject } from '../../types'
import { normalizeBallots } from '../../utils/normalize'
import { scoresZero } from '../../utils/scoresZero'

export const iterateFirstChoices = (
ballots: Ballot[],
candidates: string[],
compute: (rank: string[]) => number,
): ScoreObject => {
const result: ScoreObject = scoresZero(candidates)
normalizeBallots(ballots, candidates).forEach((ballot) => {
if (ballot.ranking.length) {
const votes = ballot.ranking[0].filter((c) => candidates.includes(c))
votes.forEach(
(candidate, idx, rank) =>
(result[candidate] += compute(rank) * ballot.weight),
)
}
})
return result
}
6 changes: 6 additions & 0 deletions src/methods/index.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,32 @@
import { VotingSystem } from '../types'
import { approbation } from './approbation'
import { baldwin } from './baldwin'
import { borda } from './borda'
import { coombs } from './coombs'
import { copeland } from './copeland'
import { firstPastThePost } from './first-past-the-post'
import { instantRunoff } from './instant-runoff'
import { kemeny } from './kemeny'
import { maximalLotteries } from './maximal-lotteries'
import { minimax } from './minimax'
import { nanson } from './nanson'
import { rankedPairs } from './ranked-pairs'
import { randomizedCondorcet } from './randomized-condorcet'
import { schulze } from './schulze'
import { twoRoundRunoff } from './two-round-runoff'

export const methods = {
[VotingSystem.Approbation]: approbation,
[VotingSystem.Baldwin]: baldwin,
[VotingSystem.Borda]: borda,
[VotingSystem.Coombs]: coombs,
[VotingSystem.Copeland]: copeland,
[VotingSystem.FirstPastThePost]: firstPastThePost,
[VotingSystem.InstantRunoff]: instantRunoff,
[VotingSystem.Kemeny]: kemeny,
[VotingSystem.MaximalLotteries]: maximalLotteries,
[VotingSystem.Minimax]: minimax,
[VotingSystem.Nanson]: nanson,
[VotingSystem.RankedPairs]: rankedPairs,
[VotingSystem.RandomizedCondorcet]: randomizedCondorcet,
[VotingSystem.Schulze]: schulze,
Expand Down
2 changes: 1 addition & 1 deletion src/methods/instant-runoff/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
Ballot,
} from '../../types'
import { firstPastThePost } from '../first-past-the-post'
import { normalizeBallots } from '../../utils'
import { normalizeBallots } from '../../utils/normalize'

export const instantRunoff: SystemUsingRankings = {
type: VotingSystem.InstantRunoff,
Expand Down
2 changes: 1 addition & 1 deletion src/methods/maximal-lotteries/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
Matrix,
ScoreObject,
} from '../../types'
import { makeAntisymetric } from '../../utils'
import { makeAntisymetric } from '../../utils/makeMatrix'
import { findCondorcet } from '../../utils/condorcet'

export const computeLottery = (
Expand Down
2 changes: 1 addition & 1 deletion src/methods/maximal-lotteries/maximal-lotteries.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { performPivots, simplexTableau } from '../../simplex'
import { matrixString } from '../../simplex/utils'
import { matrixString } from '../../test/testUtils'
import { maximalLotteries } from './index'

const example1 = [
Expand Down
33 changes: 33 additions & 0 deletions src/methods/nanson/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import difference from 'lodash/difference'
import sum from 'lodash/sum'
import {
SystemUsingRankings,
ScoreObject,
VotingSystem,
Ballot,
} from '../../types'
import { borda } from '../borda'

export const nanson: SystemUsingRankings = {
type: VotingSystem.Nanson,
computeFromBallots(ballots: Ballot[], candidates: string[]): ScoreObject {
const score: ScoreObject = {}
let remainingCandidates = candidates
let points = 0
while (remainingCandidates.length > 0) {
const bordaScores = borda.computeFromBallots(ballots, remainingCandidates)
const scores = Object.values(bordaScores)
const avg = sum(scores) / scores.length
const losers = remainingCandidates.filter((c) => bordaScores[c] <= avg)
let maxPoints = points + 1
for (const loser of losers) {
const p = points + bordaScores[loser] + 1
score[loser] = p
if (p > maxPoints) maxPoints = p
}
remainingCandidates = difference(remainingCandidates, losers)
points = maxPoints
}
return score
},
}
2 changes: 1 addition & 1 deletion src/methods/randomized-condorcet/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
Matrix,
ScoreObject,
} from '../../types'
import { makeAntisymetric } from '../../utils'
import { makeAntisymetric } from '../../utils/makeMatrix'
import { computeLottery } from '../maximal-lotteries'

export const randomizedCondorcet: SystemUsingMatrix = {
Expand Down
Loading

0 comments on commit 59c8901

Please sign in to comment.