Skip to content

Commit

Permalink
Merge pull request #27 from lzear/ranked-pairs
Browse files Browse the repository at this point in the history
Ranked pairs
  • Loading branch information
lzear authored Nov 14, 2020
2 parents 706810a + 85b9114 commit ac5a15f
Show file tree
Hide file tree
Showing 25 changed files with 912 additions and 774 deletions.
2 changes: 2 additions & 0 deletions .codacy.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
exclude_paths:
- '**/*.test.ts'
2 changes: 1 addition & 1 deletion codecov.yml
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
coverage:
range: "0...100"
range: 0..100
8 changes: 7 additions & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ module.exports = {
testEnvironment: 'node',
testRegex: '(/__tests__/.*|\\.(test|spec))\\.(ts|tsx|js)$',
moduleFileExtensions: ['ts', 'tsx', 'js'],
coveragePathIgnorePatterns: ['/node_modules/', '/test/', '/src/axioms/'],
coveragePathIgnorePatterns: [
'/node_modules/',
'/test/',
'/src/axioms/',
'/dist/',
],
coverageThreshold: {
global: {
branches: 90,
Expand All @@ -16,4 +21,5 @@ module.exports = {
},
collectCoverageFrom: ['src/**/*.{js,ts}'],
coverageReporters: ['text', 'text-summary', 'html', 'lcov'],
testPathIgnorePatterns: ['/node_modules/', '/dist/'],
}
37 changes: 19 additions & 18 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@
"commit": "git-cz",
"semantic-release": "semantic-release",
"precommit": "lint-staged",
"travis-deploy-once": "travis-deploy-once"
"travis-deploy-once": "travis-deploy-once",
"updatedeps": "npx npm-check-updates -u"
},
"lint-staged": {
"{src,test}/**/*.ts": [
Expand All @@ -51,35 +52,35 @@
},
"devDependencies": {
"@commitlint/cli": "^11.0.0",
"@rollup/plugin-commonjs": "^15.1.0",
"@rollup/plugin-commonjs": "^16.0.0",
"@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^9.0.0",
"@types/jest": "^26.0.14",
"@types/lodash": "^4.14.162",
"@types/node": "^14.11.10",
"@typescript-eslint/eslint-plugin": "^4.5.0",
"@typescript-eslint/parser": "^4.5.0",
"commitizen": "^4.2.1",
"@rollup/plugin-node-resolve": "^10.0.0",
"@types/jest": "^26.0.15",
"@types/lodash": "^4.14.165",
"@types/node": "^14.14.7",
"@typescript-eslint/eslint-plugin": "^4.7.0",
"@typescript-eslint/parser": "^4.7.0",
"commitizen": "^4.2.2",
"dotenv": "^8.2.0",
"eslint": "^7.11.0",
"eslint-config-prettier": "^6.13.0",
"eslint": "^7.13.0",
"eslint-config-prettier": "^6.15.0",
"eslint-plugin-prettier": "^3.1.4",
"husky": "^4.3.0",
"jest": "^26.5.3",
"lint-staged": "^10.4.2",
"jest": "^26.6.3",
"lint-staged": "^10.5.1",
"prettier": "^2.1.2",
"rimraf": "^3.0.2",
"rollup": "^2.32.0",
"rollup": "^2.33.1",
"rollup-plugin-sizes": "^1.0.3",
"rollup-plugin-sourcemaps": "^0.6.3",
"rollup-plugin-typescript2": "^0.28.0",
"semantic-release": "^17.2.1",
"rollup-plugin-typescript2": "^0.29.0",
"semantic-release": "^17.2.2",
"shelljs": "^0.8.4",
"travis-deploy-once": "^5.0.11",
"ts-jest": "^26.4.1",
"ts-jest": "^26.4.4",
"ts-node": "^9.0.0",
"typedoc": "^0.19.2",
"typescript": "^4.0.3"
"typescript": "^4.0.5"
},
"dependencies": {
"lodash": "^4.17.20"
Expand Down
1 change: 1 addition & 0 deletions src/descriptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const descriptions: { [type in VotingSystem]: string } = {
'Create a preference ranking that minimizes the amount of pairwise preferences contradiction the voters opinion',
[VotingSystem.Minimax]:
'Minimax selects the winner as the candidate whose greatest pairwise defeat is smaller',
[VotingSystem.RankedPairs]: 'https://en.wikipedia.org/wiki/Ranked_pairs',
[VotingSystem.Schulze]: 'https://en.wikipedia.org/wiki/Schulze_method',
[VotingSystem.TwoRoundRunoff]:
'Majority vote followed by another majority vote amongst the 2 best ranked candidates',
Expand Down
2 changes: 1 addition & 1 deletion src/methods/approbation/approbation.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import approbation from '.'
import { approbation } from '.'

it('skips empty votes', () => {
expect(
Expand Down
4 changes: 1 addition & 3 deletions src/methods/approbation/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
Ballot,
} from '../../types'

const approbation: SystemUsingRankings = {
export const approbation: SystemUsingRankings = {
type: VotingSystem.Approbation,
computeFromBallots(ballots: Ballot[], candidates: string[]): ScoreObject {
const result: ScoreObject = _.zipObject(
Expand All @@ -24,5 +24,3 @@ const approbation: SystemUsingRankings = {
return result
},
}

export default approbation
4 changes: 1 addition & 3 deletions src/methods/borda/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
Ballot,
} from '../../types'

const borda: SystemUsingRankings = {
export const borda: SystemUsingRankings = {
type: VotingSystem.Borda,
computeFromBallots(ballots: Ballot[], candidates: string[]): ScoreObject {
const result: ScoreObject = _.zipObject(
Expand All @@ -26,5 +26,3 @@ const borda: SystemUsingRankings = {
return result
},
}

export default borda
4 changes: 1 addition & 3 deletions src/methods/copeland/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
/**
* https://en.wikipedia.org/wiki/Copeland%27s_method
*/
const copeland: SystemUsingMatrix = {
export const copeland: SystemUsingMatrix = {
type: VotingSystem.Copeland,
computeFromMatrix(matrix: Matrix): ScoreObject {
const n = matrix.candidates.length
Expand All @@ -37,5 +37,3 @@ const copeland: SystemUsingMatrix = {
return _.zipObject(matrix.candidates, scores2)
},
}

export default copeland
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
@@ -1,4 +1,4 @@
import firstPastThePost from '.'
import { firstPastThePost } from '.'

it('skips empty votes', () => {
expect(
Expand Down
4 changes: 1 addition & 3 deletions src/methods/first-past-the-post/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
Ballot,
} from '../../types'

const firstPastThePost: SystemUsingRankings = {
export const firstPastThePost: SystemUsingRankings = {
type: VotingSystem.FirstPastThePost,
computeFromBallots(ballots: Ballot[], candidates: string[]): ScoreObject {
const result: ScoreObject = _.zipObject(
Expand All @@ -25,5 +25,3 @@ const firstPastThePost: SystemUsingRankings = {
return result
},
}

export default firstPastThePost
8 changes: 3 additions & 5 deletions src/methods/instant-runoff/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import {
VotingSystem,
Ballot,
} from '../../types'
import firstPastThePost from '../first-past-the-post'
import { firstPastThePost } from '../first-past-the-post'
import { normalizeBallots } from '../../utils'

const irunoff: SystemUsingRankings = {
export const instantRunoff: SystemUsingRankings = {
type: VotingSystem.InstantRunoff,
computeFromBallots(ballots: Ballot[], candidates: string[]): ScoreObject {
const round1: ScoreObject = firstPastThePost.computeFromBallots(
Expand All @@ -19,12 +19,10 @@ const irunoff: SystemUsingRankings = {
const candidates2 = candidates.filter((c) => round1[c] > minScore)
return {
...round1,
...irunoff.computeFromBallots(
...instantRunoff.computeFromBallots(
normalizeBallots(ballots, candidates2),
candidates2,
),
}
},
}

export default irunoff
4 changes: 1 addition & 3 deletions src/methods/kemeny/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ const nextPermutation = (arr: number[]) => {
return array
}

const kemeny: SystemUsingMatrix = {
export const kemeny: SystemUsingMatrix = {
type: VotingSystem.Kemeny,
computeFromMatrix(matrix: Matrix): ScoreObject {
let bestP = _.range(matrix.candidates.length)
Expand All @@ -71,5 +71,3 @@ const kemeny: SystemUsingMatrix = {
)
},
}

export default kemeny
3 changes: 1 addition & 2 deletions src/methods/minimax/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
/**
* https://en.wikipedia.org/wiki/Schulze_method
*/
const minimax: SystemUsingMatrix = {
export const minimax: SystemUsingMatrix = {
type: VotingSystem.Minimax,
computeFromMatrix(matrix: Matrix): ScoreObject {
const s: ScoreObject = {}
Expand All @@ -20,4 +20,3 @@ const minimax: SystemUsingMatrix = {
return s
},
}
export default minimax
65 changes: 65 additions & 0 deletions src/methods/ranked-pairs/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import * as _ from 'lodash'
import {
SystemUsingMatrix,
VotingSystem,
Matrix,
ScoreObject,
} from '../../types'

type Edge = { from: number; to: number; value: number }

export const rankedPairs: SystemUsingMatrix = {
type: VotingSystem.RankedPairs,
computeFromMatrix(matrix: Matrix): ScoreObject {
const allEdges: Edge[] = matrix.array
.flatMap(
(row, from) =>
row
.map((value, to) =>
value > 0 && to !== from ? { from, to, value } : null,
)
.filter(Boolean) as Edge[],
)
.sort((a, b) => b.value - a.value)
const acyclicGraph = allEdges.reduce((graph, edgeToAdd) => {
const active: number[] = []
const visited: number[] = []
let cur: number | undefined = edgeToAdd.to
while (cur !== undefined) {
visited.push(cur)
for (const { to } of graph.filter(({ from }) => from === cur)) {
if (to === edgeToAdd.from) return graph
if (!visited.includes(to) && !active.includes(to)) active.push(to)
}
cur = active.pop()
}
return [...graph, edgeToAdd]
}, [] as Edge[])

const winnersIdx = _.range(matrix.candidates.length).filter(
(candidate, key) => !acyclicGraph.some(({ to }) => to === key),
)

if (winnersIdx.length === matrix.candidates.length)
return _.zipObject(
matrix.candidates,
Array(matrix.candidates.length).fill(1),
)
const nextRound = {
array: matrix.array
.filter((c, k) => !winnersIdx.includes(k))
.map((row) => row.filter((c, k) => !winnersIdx.includes(k))),
candidates: matrix.candidates.filter((c, k) => !winnersIdx.includes(k)),
}

const nextResults = rankedPairs.computeFromMatrix(nextRound)
const maxScore = Math.max(...Object.values(nextResults))
return winnersIdx.reduce(
(scoreObject, winnerIdx) => ({
...scoreObject,
[matrix.candidates[winnerIdx]]: maxScore + 1,
}),
nextResults,
)
},
}
65 changes: 65 additions & 0 deletions src/methods/ranked-pairs/rankedPairs.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { rankedPairs } from './index'

const example1 = [
[0, -2, 8],
[2, 0, -4],
[-8, 4, 0],
]

const example2 = [
[0, -4, -4, -4, -4],
[4, 0, -2, 8, 6],
[4, 2, 0, -4, 2],
[4, -8, 4, 0, 6],
[4, -6, -2, -6, 0],
]

const example3 = [
[0, -5, 7, 15, -1],
[5, 0, -13, 21, -9],
[-7, 13, 0, -11, 3],
[-15, -21, 11, 0, -22],
[1, 9, -3, 22, 0],
]

const candidates = ['a', 'b', 'c', 'd', 'e']

describe('ranked pairs', () => {
it('works with "simple" example', () => {
expect(
rankedPairs.computeFromMatrix({
array: example1,
candidates: ['a', 'b', 'c'],
}),
).toEqual({
a: 3,
b: 1,
c: 2,
})
})
it('works with "complexer" example', () => {
expect(
rankedPairs.computeFromMatrix({ array: example2, candidates }),
).toEqual({
a: 1,
b: 5,
c: 3,
d: 4,
e: 2,
})
})
it('works with "example3" example', () => {
expect(
rankedPairs.computeFromMatrix({
array: example3,
candidates: ['a', 'b', 'c', 'd', 'e'],
}),
).toEqual({
a: 5,
b: 2,
c: 4,
d: 1,
e: 3,
})
})
})
4 changes: 1 addition & 3 deletions src/methods/schulze/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
/**
* https://en.wikipedia.org/wiki/Schulze_method
*/
const schulze: SystemUsingMatrix = {
export const schulze: SystemUsingMatrix = {
type: VotingSystem.Schulze,
computeFromMatrix(matrix: Matrix): ScoreObject {
const n = matrix.candidates.length
Expand Down Expand Up @@ -43,5 +43,3 @@ const schulze: SystemUsingMatrix = {
return s
},
}

export default schulze
6 changes: 2 additions & 4 deletions src/methods/two-round-runoff/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import {
Ballot,
} from '../../types'
import { normalizeBallots } from '../../utils'
import firstPastThePost from '../first-past-the-post'
import { firstPastThePost } from '../first-past-the-post'

const twoRoundRunoff: SystemUsingRankings = {
export const twoRoundRunoff: SystemUsingRankings = {
type: VotingSystem.TwoRoundRunoff,
computeFromBallots(ballots: Ballot[], candidates: string[]): ScoreObject {
const round1: ScoreObject = firstPastThePost.computeFromBallots(
Expand All @@ -25,5 +25,3 @@ const twoRoundRunoff: SystemUsingRankings = {
}
},
}

export default twoRoundRunoff
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import twoRoundRunoff from '.'
import { twoRoundRunoff } from '.'

it('skips empty votes', () => {
expect(
Expand Down
Loading

0 comments on commit ac5a15f

Please sign in to comment.