1 # election library -- a ruby library for elections
2 # copyright © 2005 MIT Media Lab and Benjamin Mako Hill
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 2 of the License, or
7 # (at your option) any later version.
9 # This program is distributed in the hope that it will be useful, but
10 # WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 # General Public License for more details.
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19 #################################################################
20 ## ==== condorcet.rb ====
22 ## This file contains Condorcet election methods. Currently this
23 ## includes a pure condorcet and a CloneproofSSD implementation
24 ## modeled after the Python-based Debian project election code and
25 ## that gives the same results in several tested corner cases.
26 #################################################################
28 ##################################################################
29 ## CondorcetVote Classes and SubClasses
31 ## The CondorcetVote class is subclassed by the PureCondorcetVote and
32 ## the CloneproofSSDVote classes but should not be used directly.
34 class CondorcetVote < ElectionVote
36 def initialize(votes=nil)
37 unless defined?(@candidates)
38 @candidates = Array.new
39 votes.each do |vote_row|
40 vote_row = vote_row.flatten if vote_row.class == Array
41 vote_row.each do |vote|
42 @candidates << vote unless @candidates.include?(vote)
49 def tally_vote(vote=nil)
51 vote.each_with_index do |winners, index|
52 if vote.flatten.length < @candidates.length
53 implied_losers = @candidates.select { |c| not vote.flatten.include?(c) }
54 vote.push(implied_losers)
56 if vote.length - 1 == index
59 losers = vote.flatten.last( vote.flatten.length - index - 1)
62 losers.each do |place|
63 place = [place] unless place.class == Array
66 winners = [winners] unless winners.class == Array
67 next if winners.include?(loser)
68 winners.each do |winner|
69 @votes[winner] = Hash.new unless @votes.has_key?(winner)
70 @votes[loser] = Hash.new unless @votes.has_key?(loser)
72 if @votes[winner].has_key?(loser)
73 @votes[winner][loser] += 1
75 @votes[winner][loser] = 1
78 # make sure we have a comparable object
79 @votes[loser][winner] = 0 unless @votes[loser].has_key?( winner )
88 def verify_vote(vote=nil)
89 vote.instance_of?( Array ) and
94 class PureCondorcetVote < CondorcetVote
96 PureCondorcetResult.new(self)
100 class CloneproofSSDVote < CondorcetVote
102 CloneproofSSDResult.new(self)
107 ##################################################################
108 ## CondorcetResult Classes and SubClasses
110 ## The CondorcetResult class is subclassed by the PureCondorcetResult
111 ## and the CloneproofSSDResult classes but should not be used
114 class CondorcetResult < ElectionResult
117 def initialize(voteobj=nil)
118 unless voteobj and voteobj.kind_of?( CondorcetVote )
119 raise ArgumentError, "You must pass a CondorcetVote array.", caller
122 @matrix = voteobj.votes
125 def victories_and_ties
129 candidates = @matrix.keys.sort
131 candidates.each do |candidate|
132 candidates.each do |challenger|
133 next if candidate == challenger
134 diff = @matrix[candidate][challenger] - @matrix[challenger][candidate]
136 victors << [candidate, challenger, diff]
137 elsif diff == 0 && ties.include?([challenger, candidate]) == false
138 ties << [candidate, challenger]
143 victors.each do |list|
144 if victories.has_key?(list[0])
145 victories[list[0]][list[1]] = list[2]
147 victories[list[0]] = Hash.new
148 victories[list[0]][list[1]] = list[2]
152 return victories, ties
155 def ranked_candidates
156 if not defined?(@ranked_candidates)
157 @ranked_candidates = build_ranked_candidates()
164 def defeats(candidates=nil, votes=nil)
165 candidates ||= @election.candidates || []
166 # we're assumign that if there are candidates, there must be at
167 # least one vote for them
168 votes ||= @election.votes
171 candidates.each do |candidate|
172 candidates.each do |challenger|
173 next if candidate == challenger
174 if votes[candidate][challenger] > votes[challenger][candidate]
175 defeats << [ candidate, challenger ]
183 def build_ranked_candidates
184 # build a lis of ranked candidates by dropping the winner and
187 ranked_candidates = []
190 candidates = self.election.candidates
192 until candidates.empty?
193 ranked_candidates << resultobj.winners
195 new_voteobj = resultobj.election.dup
196 candidates = new_voteobj.candidates
197 new_voteobj.candidates.delete_if {|x| resultobj.winners.include?(x)}
198 resultobj = new_voteobj.result
206 class PureCondorcetResult < CondorcetResult
207 def initialize(voteobj=nil)
215 votes = @election.votes
216 candidates = @election.candidates
218 unless votes.length > 0 and candidates.length > 0
223 candidates.each do |candidate|
224 victors[candidate] = Array.new
227 self.defeats.each do |pair|
228 winner, loser = *pair
229 victors[winner] << loser
234 winners = @election.candidates.find_all do |candidate|
235 victors[candidate].length == @election.candidates.length - victory_margin
237 if winners.length > 0
247 class CloneproofSSDResult < CondorcetResult
248 def initialize(voteobj=nil)
250 @winners = self.cpssd()
256 votes = @election.votes
257 candidates = @election.candidates.dup
259 def in_schwartz_set?(candidate, candidates, transitive_defeats)
260 candidates.each do |challenger|
261 next if candidate == challenger
263 if transitive_defeats.include?( [ challenger, candidate ] ) and
264 not transitive_defeats.include?( [ candidate, challenger ] )
273 # see the array with the standard defeats
274 transitive_defeats = self.defeats(candidates, votes)
275 defeats_hash = Hash.new
276 transitive_defeats.each { |td| defeats_hash[td] = 1 }
278 candidates = [candidates] unless candidates.class == Array
279 candidates.each do |cand1|
280 candidates.each do |cand2|
281 unless cand1 == cand2
282 candidates.each do |cand3|
283 if not cand2 == cand3 and
284 not cand1 == cand3 and
285 defeats_hash[[cand2, cand1]] and
286 defeats_hash[[cand1, cand3]] and
287 not defeats_hash[[cand2, cand3]]
288 transitive_defeats << [cand2, cand3]
289 defeats_hash[[cand2, cand3]] = 1
296 schwartz_set = Array.new
297 candidates.each do |candidate|
298 if in_schwartz_set?(candidate, candidates, transitive_defeats)
299 schwartz_set << candidate
303 candidates = schwartz_set
305 # look through the schwartz set now for defeats
306 defeats = self.defeats(candidates, votes)
308 # it's a tie or there's only one option
309 break if defeats.length == 0
311 def is_weaker_defeat?( pair1, pair2 )
312 votes = @election.votes
313 if votes[pair1[0]][pair1[1]] > votes[pair2[0]][pair2[1]]
315 elsif votes[pair1[0]][pair1[1]] == votes[pair2[0]][pair2[1]] and
316 votes[pair1[1]][pair1[0]] > votes[pair2[1]][pair2[0]]
323 defeats.sort! do |pair1, pair2|
324 if is_weaker_defeat?( pair1, pair2 )
326 elsif is_weaker_defeat?( pair2, pair1 )
333 votes[defeats[0][0]][defeats[0][1]] = 0
334 votes[defeats[0][1]][defeats[0][0]] = 0