Skip to content

Commit

Permalink
Merge pull request #440 from webkom/normal-elections
Browse files Browse the repository at this point in the history
Backend changes to support normal elections
  • Loading branch information
SmithPeder committed Oct 5, 2021
2 parents 427472a + 4f2acbf commit a715ca1
Show file tree
Hide file tree
Showing 46 changed files with 971 additions and 144 deletions.
2 changes: 1 addition & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ node_modules
*.map
dist
public
app/stv/stv.js
app/algorithms/*.js
55 changes: 55 additions & 0 deletions app/algorithms/normal.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

108 changes: 108 additions & 0 deletions app/algorithms/normal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import isEmpty = require('lodash/isEmpty');
import { Status, Vote, Alternative, ElectionResult, Count } from './types';

// This is a TypeScript file in a JavaScript project so it must be complied
// If you make changes to this file it must be recomplied using `tsc` in
// order for the changes to be reflected in the rest of the program.
//
// app/models/election .elect() is the only file that uses this function
// and importes it from normal.js, which is the compiled result of this file.
//
//

/**
* @param votes - All votes for the election
* @param useStrict - Sets the threshold to 67%
*
* @return The amount votes needed to win
*/
const winningThreshold = (votes: Vote[], useStrict: boolean): number => {
if (useStrict) {
return Math.floor((2 * votes.length) / 3) + 1;
}
return Math.floor(votes.length / 2 + 1);
};

/**
* Will calculate the election result using Single Transferable Vote
* @param votes - All votes for the election
* @param useStrict - This election will require a qualified majority. Default false
*
* @return The full election result
*/
const calculateWinnerUsingNormal = (
inputVotes: any,
inputAlternatives: any,
seats = 1,
useStrict = false
): ElectionResult => {
// Stringify and clean the votes
const votes: Vote[] = inputVotes.map((vote: any) => ({
_id: String(vote._id),
priorities: vote.priorities.map((vote: any) => ({
_id: String(vote._id),
description: vote.description,
election: String(vote._id),
})),
hash: vote.hash,
}));

// Stringify and clean the alternatives
let alternatives: Alternative[] = inputAlternatives.map(
(alternative: any) => ({
_id: String(alternative._id),
description: alternative.description,
election: String(alternative._id),
})
);

// Reduce votes to the distinct counts for each alternative
const count: Count = votes.reduce(
(reduced: Count, vote: Vote) => {
if (isEmpty(vote.priorities)) {
reduced['blank'] += 1;
} else {
reduced[vote.priorities[0].description] =
(reduced[vote.priorities[0].description] || 0) + 1;
}

return reduced;
},
{ blank: 0 }
);

// Calculate threshold and see if an alternative can be
const thr = winningThreshold(votes, useStrict);

// Winner key
const maxKey: string = Object.keys(count).reduce((a, b) =>
count[a] > count[b] ? a : b
);

// Check if we can call the vote Resolved based on
const status = count[maxKey] >= thr ? Status.resolved : Status.unresolved;

// Create winner alternative from maxKey
const winner =
count[maxKey] >= thr
? {
...alternatives.find((a) => a.description === maxKey),
count: count[maxKey],
}
: undefined;

return {
result: {
status,
winners: winner ? [winner] : undefined,
},
thr,
seats,
voteCount: inputVotes.length,
blankVoteCount: count['blank'],
useStrict,
log: count,
};
};

export default calculateWinnerUsingNormal;
13 changes: 5 additions & 8 deletions app/stv/stv.js → app/algorithms/stv.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

58 changes: 11 additions & 47 deletions app/stv/stv.ts → app/algorithms/stv.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import cloneDeep = require('lodash/cloneDeep');
import { Status, Vote, Alternative, ElectionResult, Count } from './types';

// This is a TypeScript file in a JavaScript project so it must be complied
// If you make changes to this file it must be recomplied using `tsc` in
Expand All @@ -7,33 +8,6 @@ import cloneDeep = require('lodash/cloneDeep');
// app/models/election .elect() is the only file that uses this function
// and importes it from stv.js, which is the compiled result of this file.

type STV = {
result: STVResult;
log: STVEvent[];
thr: number;
seats: number;
voteCount: number;
blankVoteCount: number;
useStrict: boolean;
};

type Alternative = {
_id: string;
description: string;
election: string;
};

type STVCounts = {
[key: string]: number;
};

type Vote = {
_id: string;
priorities: Alternative[];
hash: string;
weight: number;
};

enum Action {
iteration = 'ITERATION',
win = 'WIN',
Expand All @@ -42,7 +16,7 @@ enum Action {
tie = 'TIE',
}

type STVEvent = {
export type STVEvent = {
action: Action;
iteration?: number;
winners?: Alternative[];
Expand All @@ -66,36 +40,24 @@ interface STVEventWin extends STVEvent {
alternative: Alternative;
voteCount: number;
}

interface STVEventEliminate extends STVEvent {
action: Action.eliminate;
alternative: Alternative;
minScore: number;
}

interface STVEventTie extends STVEvent {
action: Action.tie;
description: string;
}

interface STVEventMulti extends STVEvent {
action: Action.multi_tie_eliminations;
alternatives: Alternative[];
minScore: number;
}

enum Status {
resolved = 'RESOLVED',
unresolved = 'UNRESOLVED',
}

type STVResult =
| {
status: Status;
winners: Alternative[];
}
| {
status: Status;
winners: Alternative[];
};

/**
* The Droop qouta https://en.wikipedia.org/wiki/Droop_quota
* @param votes - All votes for the election
Expand Down Expand Up @@ -127,12 +89,12 @@ const EPSILON = 0.000001;
*
* @return The full election, including result, log and threshold value
*/
exports.calculateWinnerUsingSTV = (
const calculateWinnerUsingSTV = (
inputVotes: any,
inputAlternatives: any,
seats = 1,
useStrict = false
): STV => {
): ElectionResult => {
// Hold the log for the entire election
const log: STVEvent[] = [];

Expand Down Expand Up @@ -177,8 +139,8 @@ exports.calculateWinnerUsingSTV = (
votes = votes.filter((vote: Vote) => vote.priorities.length > 0);

// Dict with the counts for each candidate
const counts: STVCounts = alternatives.reduce(
(counts: STVCounts, alternative: Alternative) => ({
const counts: Count = alternatives.reduce(
(counts: Count, alternative: Alternative) => ({
...counts,
[alternative.description]: 0,
}),
Expand Down Expand Up @@ -431,3 +393,5 @@ const handleFloatsInOutput = (obj: Object) => {
Object.entries(obj).forEach(([k, v]) => (newObj[k] = Number(v.toFixed(4))));
return newObj;
};

export default calculateWinnerUsingSTV;
9 changes: 9 additions & 0 deletions app/algorithms/types.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit a715ca1

Please sign in to comment.