1 # election library -- a ruby library for elections
2 # copyright © 2005 MIT Media Lab and Benjamin Mako Hill
8 # This program is free software; you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation; either version 2 of the License, or
11 # (at your option) any later version.
13 # This program is distributed in the hope that it will be useful, but
14 # WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16 # General Public License for more details.
18 # You should have received a copy of the GNU General Public License
19 # along with this program; if not, write to the Free Software
20 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
23 #################################################################
24 ## ==== condorcet.rb ====
26 ## This file contains Condorcet election methods. Currently this
27 ## includes a pure condorcet and a CloneproofSSD implementation
28 ## modeled after the Python-based Debian project election code and
29 ## that gives the same results in several tested corner cases.
30 #################################################################
32 ##################################################################
33 ## CondorcetVote Classes and SubClasses
35 ## The CondorcetVote class is subclassed by the PureCondorcetVote and
36 ## the CloneproofSSDVote classes but should not be used directly.
38 class CondorcetVote < ElectionVote
40 def initialize(votes=nil)
41 unless defined?(@candidates)
42 @candidates = Array.new
43 votes.each do |vote_row|
44 vote_row = vote_row.flatten if vote_row.class == Array
45 vote_row.each do |vote|
46 @candidates << vote unless @candidates.include?(vote)
53 def tally_vote(vote=nil)
55 vote.each_with_index do |winners, index|
56 if vote.flatten.length < @candidates.length
57 implied_losers = @candidates.select { |c| not vote.flatten.include?(c) }
58 vote.push(implied_losers)
60 if vote.length - 1 == index
63 losers = vote.flatten.last( vote.flatten.length - index - 1)
66 losers.each do |place|
67 place = [place] unless place.class == Array
70 winners = [winners] unless winners.class == Array
71 next if winners.include?(loser)
72 winners.each do |winner|
73 @votes[winner] = Hash.new unless @votes.has_key?(winner)
74 @votes[loser] = Hash.new unless @votes.has_key?(loser)
76 if @votes[winner].has_key?(loser)
77 @votes[winner][loser] += 1
79 @votes[winner][loser] = 1
82 # make sure we have a comparable object
83 @votes[loser][winner] = 0 unless @votes[loser].has_key?( winner )
92 def verify_vote(vote=nil)
93 vote.instance_of?( Array ) and
98 class PureCondorcetVote < CondorcetVote
100 PureCondorcetResult.new(self)
104 class CloneproofSSDVote < CondorcetVote
106 CloneproofSSDResult.new(self)
111 ##################################################################
112 ## CondorcetResult Classes and SubClasses
114 ## The CondorcetResult class is subclassed by the PureCondorcetResult
115 ## and the CloneproofSSDResult classes but should not be used
118 class CondorcetResult < ElectionResult
121 def initialize(voteobj=nil)
122 unless voteobj and voteobj.kind_of?( CondorcetVote )
123 raise ArgumentError, "You must pass a CondorcetVote array.", caller
126 @matrix = voteobj.votes
129 def victories_and_ties
133 candidates = @matrix.keys.sort
135 candidates.each do |candidate|
136 candidates.each do |challenger|
137 next if candidate == challenger
138 diff = @matrix[candidate][challenger] - @matrix[challenger][candidate]
140 victors << [candidate, challenger, diff]
141 elsif diff == 0 && ties.include?([challenger, candidate]) == false
142 ties << [candidate, challenger]
147 victors.each do |list|
148 if victories.has_key?(list[0])
149 victories[list[0]][list[1]] = list[2]
151 victories[list[0]] = Hash.new
152 victories[list[0]][list[1]] = list[2]
156 return victories, ties
159 def ranked_candidates
160 if not defined?(@ranked_candidates)
161 @ranked_candidates = build_ranked_candidates()
168 def defeats(candidates=nil, votes=nil)
169 candidates ||= @election.candidates || []
170 # we're assumign that if there are candidates, there must be at
171 # least one vote for them
172 votes ||= @election.votes
175 candidates.each do |candidate|
176 candidates.each do |challenger|
177 next if candidate == challenger
178 if votes[candidate][challenger] > votes[challenger][candidate]
179 defeats << [ candidate, challenger ]
187 def build_ranked_candidates
188 # build a lis of ranked candidates by dropping the winner and
191 ranked_candidates = []
194 candidates = self.election.candidates
196 until candidates.empty?
197 ranked_candidates << resultobj.winners
199 new_voteobj = resultobj.election.dup
200 candidates = new_voteobj.candidates
201 new_voteobj.candidates.delete_if {|x| resultobj.winners.include?(x)}
202 resultobj = new_voteobj.result
210 class PureCondorcetResult < CondorcetResult
211 def initialize(voteobj=nil)
219 votes = @election.votes
220 candidates = @election.candidates
222 unless votes.length > 0 and candidates.length > 0
227 candidates.each do |candidate|
228 victors[candidate] = Array.new
231 self.defeats.each do |pair|
232 winner, loser = *pair
233 victors[winner] << loser
238 winners = @election.candidates.find_all do |candidate|
239 victors[candidate].length == @election.candidates.length - victory_margin
241 if winners.length > 0
251 class CloneproofSSDResult < CondorcetResult
252 def initialize(voteobj=nil)
254 @winners = self.cpssd()
260 votes = @election.votes
261 candidates = @election.candidates.dup
263 def in_schwartz_set?(candidate, candidates, transitive_defeats)
264 candidates.each do |challenger|
265 next if candidate == challenger
267 if transitive_defeats.include?( [ challenger, candidate ] ) and
268 not transitive_defeats.include?( [ candidate, challenger ] )
277 # see the array with the standard defeats
278 transitive_defeats = self.defeats(candidates, votes)
279 defeats_hash = Hash.new
280 transitive_defeats.each { |td| defeats_hash[td] = 1 }
282 candidates = [candidates] unless candidates.class == Array
283 candidates.each do |cand1|
284 candidates.each do |cand2|
285 unless cand1 == cand2
286 candidates.each do |cand3|
287 if not cand2 == cand3 and
288 not cand1 == cand3 and
289 defeats_hash[[cand2, cand1]] and
290 defeats_hash[[cand1, cand3]] and
291 not defeats_hash[[cand2, cand3]]
292 transitive_defeats << [cand2, cand3]
293 defeats_hash[[cand2, cand3]] = 1
300 schwartz_set = Array.new
301 candidates.each do |candidate|
302 if in_schwartz_set?(candidate, candidates, transitive_defeats)
303 schwartz_set << candidate
307 candidates = schwartz_set
309 # look through the schwartz set now for defeats
310 defeats = self.defeats(candidates, votes)
312 # it's a tie or there's only one option
313 break if defeats.length == 0
315 def is_weaker_defeat?( pair1, pair2 )
316 votes = @election.votes
317 if votes[pair1[0]][pair1[1]] > votes[pair2[0]][pair2[1]]
319 elsif votes[pair1[0]][pair1[1]] == votes[pair2[0]][pair2[1]] and
320 votes[pair1[1]][pair1[0]] > votes[pair2[1]][pair2[0]]
327 defeats.sort! do |pair1, pair2|
328 if is_weaker_defeat?( pair1, pair2 )
330 elsif is_weaker_defeat?( pair2, pair1 )
337 votes[defeats[0][0]][defeats[0][1]] = 0
338 votes[defeats[0][1]][defeats[0][0]] = 0