#4 Implement Relative Placement
Showing
4 changed files
with
114 additions
and
22 deletions
1 | /** | 1 | /** |
2 | * Created by Techniv on 30/11/2016. | 2 | * Created by Techniv on 30/11/2016. |
3 | * @module relative_placement | ||
3 | */ | 4 | */ |
4 | (function (define) { | 5 | (function (define) { |
5 | // TODO define exhaustive module platform | ||
6 | module.exports = define(); | 6 | module.exports = define(); |
7 | })(function(){ | 7 | })(function(){ |
8 | 8 | ||
9 | /** | 9 | /** |
10 | * | 10 | * @name RelativePlacement |
11 | * @constructor | 11 | * @constructor |
12 | * @property {Candidate[]} candidateList | 12 | * @property {Candidate[]} candidateList |
13 | * @property {String[]} votes | 13 | * @property {String[][]} votes |
14 | * @property {Number} numberOfCandidates | 14 | * @property {Number} numberOfCandidates |
15 | * | 15 | * |
16 | */ | 16 | */ |
... | @@ -29,6 +29,7 @@ | ... | @@ -29,6 +29,7 @@ |
29 | * @this RelativePlacement | 29 | * @this RelativePlacement |
30 | */ | 30 | */ |
31 | RelativePlacement.prototype.addCandidate = function addCandidate(candidateName){ | 31 | RelativePlacement.prototype.addCandidate = function addCandidate(candidateName){ |
32 | if(this.votes.length) throw new Error("Adding candidates after votes is not permit."); | ||
32 | if(this.candidateList[candidateName]) throw new Error('"' +candidateName+ '" already exist in candidate list.'); | 33 | if(this.candidateList[candidateName]) throw new Error('"' +candidateName+ '" already exist in candidate list.'); |
33 | this.candidateList[candidateName] = new Candidate(candidateName); | 34 | this.candidateList[candidateName] = new Candidate(candidateName); |
34 | }; | 35 | }; |
... | @@ -74,23 +75,106 @@ | ... | @@ -74,23 +75,106 @@ |
74 | } | 75 | } |
75 | }; | 76 | }; |
76 | RelativePlacement.prototype.addVotes = function () {}; | 77 | RelativePlacement.prototype.addVotes = function () {}; |
77 | RelativePlacement.prototype.getResult = function () {}; | 78 | RelativePlacement.prototype.getResult = function () { |
79 | return relativePlacement(this.votes, this.candidateList); | ||
80 | }; | ||
78 | 81 | ||
79 | /** | 82 | /** |
80 | * | 83 | * |
81 | * @param name | 84 | * @param {String} name |
82 | * @constructor | 85 | * @constructor |
83 | * @property {String} name | 86 | * @property {String} name |
84 | * @property {Number[]} votes | 87 | * @property {Number[]} votes |
85 | * @property {Number[]} placements | 88 | * @property {Number[]} placements |
89 | * @property {Number[]} cumulativePlacement | ||
86 | */ | 90 | */ |
87 | function Candidate(name) { | 91 | function Candidate(name) { |
88 | Object.defineProperties(this, { | 92 | Object.defineProperties(this, { |
89 | name: {value: name, enumerable: true}, | 93 | name: {value: name, enumerable: true}, |
90 | votes: {value:[], enumerable: true}, | 94 | votes: {value:[], enumerable: true}, |
91 | placements: {value:[], enumerable: true, writable: true} | 95 | placements: {value:[], enumerable: true, writable: true}, |
96 | cumulativePlacement: {value:[], enumerable: true, writable: true} | ||
97 | }); | ||
98 | } | ||
99 | |||
100 | /** | ||
101 | * @name relativePlacement | ||
102 | * @memberOf RelativePlacement | ||
103 | * @static | ||
104 | * @param {String[][]} votes | ||
105 | * @param {Object<String,Candidate>} [candidates] | ||
106 | * @returns {String[]} | ||
107 | */ | ||
108 | function relativePlacement(votes, candidates){ | ||
109 | if(votes.length <= 1) return votes[0]; | ||
110 | |||
111 | // Prepare candidate list if not pass | ||
112 | var init = false, majority, result, candidateNames; | ||
113 | if(!candidates) { | ||
114 | candidates = {}; | ||
115 | init = true; | ||
116 | } | ||
117 | candidateNames = votes[0]; | ||
118 | majority = Math.floor(votes.length / 2) + 1; | ||
119 | result = []; | ||
120 | |||
121 | candidateNames.forEach((name) => { | ||
122 | if(init) candidates[name] = new Candidate(name); | ||
123 | candidates[name].placements[candidateNames.length-1] = 0; | ||
124 | candidates[name].placements.fill(0,0,candidateNames.length-1); | ||
125 | candidates[name].cumulativePlacement[candidateNames.length-1] = 0; | ||
126 | candidates[name].cumulativePlacement.fill(0,0,candidateNames.length-1); | ||
92 | }); | 127 | }); |
128 | |||
129 | // Assign votes | ||
130 | votes.forEach((vote,num) => { | ||
131 | vote.forEach((name, place) => { | ||
132 | var candidate = candidates[name]; | ||
133 | candidate.votes[num] = place; | ||
134 | for(let i = place; i < candidate.placements.length; i++){ | ||
135 | candidate.placements[i]++; | ||
136 | candidate.cumulativePlacement[i] += place + 1; | ||
137 | } | ||
138 | }); | ||
139 | }); | ||
140 | |||
141 | // Search placement | ||
142 | var cursor = 0, nextPlace = 0, proposed = []; | ||
143 | while(result.length < candidateNames.length && cursor < candidateNames.length){ | ||
144 | |||
145 | // Search majority | ||
146 | candidateNames.forEach(name => { | ||
147 | if(result.indexOf(name) != -1) return; | ||
148 | var candidate = candidates[name]; | ||
149 | if(candidate.placements[cursor] >= majority) proposed.push(candidate); | ||
150 | }); | ||
151 | |||
152 | if(proposed.length){ | ||
153 | proposed.sort((a,b) => { | ||
154 | var sortCursor = cursor; | ||
155 | while(sortCursor < candidateNames.length){ | ||
156 | if(a.placements[sortCursor] != b.placements[sortCursor]) | ||
157 | return b.placements[sortCursor] - a.placements[sortCursor]; | ||
158 | if(a.cumulativePlacement[sortCursor] != b.cumulativePlacement[sortCursor]) | ||
159 | return a.cumulativePlacement[sortCursor] - b.cumulativePlacement[sortCursor]; | ||
160 | |||
161 | sortCursor++; | ||
162 | } | ||
163 | return 0; | ||
164 | }); | ||
165 | result = result.concat(proposed.map(candidate => candidate.name)); | ||
166 | cursor = result.length; | ||
167 | proposed.splice(0); | ||
168 | continue; | ||
169 | } | ||
170 | |||
171 | cursor++; | ||
172 | } | ||
173 | |||
174 | |||
175 | return result; | ||
93 | } | 176 | } |
94 | 177 | ||
178 | RelativePlacement.relativePlacement = relativePlacement; | ||
95 | return RelativePlacement; | 179 | return RelativePlacement; |
96 | }); | 180 | }); |
... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
... | @@ -33,7 +33,8 @@ describe('RelativePlacement global', () => { | ... | @@ -33,7 +33,8 @@ describe('RelativePlacement global', () => { |
33 | assert.deepEqual({ | 33 | assert.deepEqual({ |
34 | name: 'foo', | 34 | name: 'foo', |
35 | votes:[], | 35 | votes:[], |
36 | placements: [] | 36 | placements: [], |
37 | cumulativePlacement: [] | ||
37 | }, election.candidateList['foo']); | 38 | }, election.candidateList['foo']); |
38 | }); | 39 | }); |
39 | 40 | ||
... | @@ -45,12 +46,14 @@ describe('RelativePlacement global', () => { | ... | @@ -45,12 +46,14 @@ describe('RelativePlacement global', () => { |
45 | assert.deepEqual({ | 46 | assert.deepEqual({ |
46 | name: 'foo', | 47 | name: 'foo', |
47 | votes:[], | 48 | votes:[], |
48 | placements: [] | 49 | placements: [], |
50 | cumulativePlacement: [] | ||
49 | }, election.candidateList['foo']); | 51 | }, election.candidateList['foo']); |
50 | assert.deepEqual({ | 52 | assert.deepEqual({ |
51 | name: 'bar', | 53 | name: 'bar', |
52 | votes:[], | 54 | votes:[], |
53 | placements: [] | 55 | placements: [], |
56 | cumulativePlacement: [] | ||
54 | }, election.candidateList['bar']); | 57 | }, election.candidateList['bar']); |
55 | }); | 58 | }); |
56 | 59 | ... | ... |
... | @@ -2,16 +2,12 @@ | ... | @@ -2,16 +2,12 @@ |
2 | * Created by Techniv on 02/12/2016. | 2 | * Created by Techniv on 02/12/2016. |
3 | */ | 3 | */ |
4 | var assert = require('assert'); | 4 | var assert = require('assert'); |
5 | var RelativePlacement = require('../../relative-placement'); | 5 | /** @type RelativePlacement.relativePlacement */ |
6 | var relativePlacement = require('../../relative-placement').relativePlacement; | ||
6 | /** @var {TestData[]} */ | 7 | /** @var {TestData[]} */ |
7 | var testDataList = require('./results_data.json'); | 8 | var testDataList = require('./results_data.json'); |
8 | describe('RelativePlacement algo', () => { | 9 | describe('RelativePlacement algo', () => { |
9 | 10 | ||
10 | var election; | ||
11 | |||
12 | beforeEach(()=>{ | ||
13 | election = new RelativePlacement(); | ||
14 | }); | ||
15 | 11 | ||
16 | describe('Process result data',() => { | 12 | describe('Process result data',() => { |
17 | 13 | ||
... | @@ -22,14 +18,11 @@ describe('RelativePlacement algo', () => { | ... | @@ -22,14 +18,11 @@ describe('RelativePlacement algo', () => { |
22 | */ | 18 | */ |
23 | (testData,id) => { | 19 | (testData,id) => { |
24 | it('#'+id+': '+(testData.comment||''), ()=>{ | 20 | it('#'+id+': '+(testData.comment||''), ()=>{ |
25 | election.addCandidates(testData.result.concat().sort(()=>Math.random()-Math.random())); | ||
26 | var votes = compileVotes(testData); | 21 | var votes = compileVotes(testData); |
27 | 22 | ||
28 | votes.forEach((vote) => { | 23 | var result = relativePlacement(votes); |
29 | election.addVote(vote); | ||
30 | }); | ||
31 | 24 | ||
32 | assert.deepEqual(election.getResult(), testData.result); | 25 | assert.deepEqual(result, testData.result); |
33 | }); | 26 | }); |
34 | } | 27 | } |
35 | ); | 28 | ); | ... | ... |
... | @@ -9,6 +9,18 @@ | ... | @@ -9,6 +9,18 @@ |
9 | "result": ["A","B","C"] | 9 | "result": ["A","B","C"] |
10 | }, | 10 | }, |
11 | { | 11 | { |
12 | "comment": "boogiebythebay.org study case", | ||
13 | "votes":{ | ||
14 | "Couple 1":[1,1,3,2,3], | ||
15 | "Couple 2":[6,5,4,1,2], | ||
16 | "Couple 3":[2,4,1,5,5], | ||
17 | "Couple 4":[4,2,5,6,6], | ||
18 | "Couple 5":[5,6,2,3,4], | ||
19 | "Couple 6":[3,3,6,4,1] | ||
20 | }, | ||
21 | "result": ["Couple 1","Couple 6","Couple 3","Couple 2","Couple 5","Couple 4"] | ||
22 | }, | ||
23 | { | ||
12 | "comment": "Novice Strictly final", | 24 | "comment": "Novice Strictly final", |
13 | "votes":{ | 25 | "votes":{ |
14 | "320": [1,1,1,1,1], | 26 | "320": [1,1,1,1,1], | ... | ... |
-
Please register or sign in to post a comment