Condorcet methods determine all candidates before tallying, in case of
[rubyvote] / lib / rubyvote / condorcet.rb
1 # election library -- a ruby library for elections
2 # copyright © 2005 MIT Media Lab and Benjamin Mako Hill
3
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.
8
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.
13
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
17 # 02110-1301, USA.
18
19 #################################################################
20 ## ==== condorcet.rb ====
21 ##
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 #################################################################
27
28 ##################################################################
29 ## CondorcetVote Classes and SubClasses
30 ##
31 ## The CondorcetVote class is subclassed by the PureCondorcetVote and
32 ## the CloneproofSSDVote classes but should not be used directly.
33
34 class CondorcetVote < ElectionVote
35
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)
43         end
44       end
45     end
46     super(votes)
47   end
48
49   def tally_vote(vote=nil)
50
51     vote.each_with_index do |winner, index|
52       if vote.flatten.length < @candidates.length
53         implied_losers = @candidates.select { |c| not vote.include?(c) }
54         vote.push(implied_losers)
55       end
56       if vote.length - 1 == index
57         losers = []
58       else
59         losers = vote.last( vote.flatten.length - index )
60       end
61
62       losers.each do |place|
63         place = [place] unless place.class == Array
64         place.each do |loser|
65           
66           next if winner == loser
67
68           @votes[winner] = Hash.new unless @votes.has_key?(winner)
69           @votes[loser] = Hash.new unless @votes.has_key?(loser)
70
71           if @votes[winner].has_key?(loser)
72             @votes[winner][loser] += 1
73           else
74             @votes[winner][loser] = 1
75           end
76
77           # make sure we have a comparable object
78           @votes[loser][winner] = 0 unless @votes[loser].has_key?( winner )
79
80           @candidates << loser unless @candidates.include?( loser )
81         end
82       end
83     end
84   end
85
86   def result
87     top_result = resultFactory( self )
88     until @candidates.empty?
89       aResult = resultFactory( self )
90       top_result.full_results << aResult
91       filter_out(aResult)
92     end
93     top_result
94   end
95
96   protected
97   def verify_vote(vote=nil)
98     vote.instance_of?( Array ) and
99       vote == vote.uniq
100   end
101
102 end
103
104 class PureCondorcetVote < CondorcetVote
105   def resultFactory(init)
106     PureCondorcetResult.new(init)
107   end
108 end
109
110 class CloneproofSSDVote < CondorcetVote
111   def resultFactory(init)
112     CloneproofSSDResult.new(init)
113   end
114 end
115
116
117 ##################################################################
118 ## CondorcetResult Classes and SubClasses
119 ##
120 ## The CondorcetResult class is subclassed by the PureCondorcetResult
121 ## and the CloneproofSSDResult classes but should not be used
122 ## directly.
123
124 class CondorcetResult < ElectionResult
125   def initialize(voteobj=nil)
126     unless voteobj and voteobj.kind_of?( CondorcetVote )
127       raise ArgumentError, "You must pass a CondorcetVote array.", caller
128     end
129     super(voteobj)
130   end
131
132   protected
133   def defeats(candidates=nil, votes=nil)
134     candidates = @election.candidates unless candidates
135     votes = @election.votes unless votes
136
137     defeats = Array.new
138     candidates.each do |candidate|
139       candidates.each do |challenger|
140         next if candidate == challenger
141         if votes[candidate][challenger] > votes[challenger][candidate]
142           defeats << [ candidate, challenger ]
143         end
144       end
145     end
146
147     defeats
148   end
149
150 end
151
152 class PureCondorcetResult < CondorcetResult
153   def initialize(voteobj=nil)
154     super(voteobj)
155     self.condorcet()
156   end
157
158   protected
159   def condorcet
160     votes = @election.votes
161     candidates = @election.candidates
162
163     victors = Hash.new
164     candidates.each do |candidate|
165       victors[candidate] = Array.new
166     end
167
168     self.defeats.each do |pair|
169       winner, loser = *pair
170       victors[winner] << loser
171     end
172
173     winners = @election.candidates.find_all do |candidate|
174         victors[candidate].length == @election.candidates.length - 1
175     end
176
177     @winners << winners if winners.length == 1
178   end
179 end
180
181 class CloneproofSSDResult < CondorcetResult
182   def initialize(voteobj=nil)
183     super(voteobj)
184     @winners = self.cpssd()
185   end
186
187   protected
188   def cpssd
189     votes = @election.votes
190     candidates = *@election.candidates
191
192     def in_schwartz_set?(candidate, candidates, transitive_defeats)
193       candidates.each do |challenger|
194         next if candidate == challenger
195
196         if transitive_defeats.include?( [ challenger, candidate ] ) and
197             not transitive_defeats.include?( [ candidate, challenger ] )
198           return false
199         end
200       end
201       return true
202     end
203
204     loop do
205
206       # see the array with the standard defeats
207       transitive_defeats = self.defeats(candidates, votes)
208
209       candidates.each do |cand1|
210         candidates.each do |cand2|
211           candidates.each do |cand3|
212             if transitive_defeats.include?( [ cand2, cand1 ] ) and
213                 transitive_defeats.include?( [ cand1, cand3 ] ) and
214                 not transitive_defeats.include?( [ cand2, cand3 ] ) and
215                 not cand2 == cand3
216               transitive_defeats << [ cand2, cand3 ]
217             end
218           end
219         end
220       end
221
222       schwartz_set = Array.new
223       candidates.each do |candidate|
224         if in_schwartz_set?(candidate, candidates, transitive_defeats)
225           schwartz_set << candidate
226         end
227       end
228
229       candidates = schwartz_set
230
231       # look through the schwartz set now for defeats
232       defeats = self.defeats(candidates, votes)
233       
234       # it's a tie or there's only one option
235       break if defeats.length == 0
236
237       def is_weaker_defeat?( pair1, pair2 )
238         votes = @election.votes
239         if votes[pair1[0]][pair1[1]] > votes[pair2[0]][pair2[1]]
240           return true
241         elsif votes[pair1[0]][pair1[1]] == votes[pair2[0]][pair2[1]] and
242             votes[pair1[1]][pair1[0]] > votes[pair2[1]][pair2[0]]
243           return true
244         else
245           false
246         end
247       end
248       
249       defeats.sort! do |pair1, pair2|
250         if is_weaker_defeat?( pair1, pair2 ) 
251           +1
252         elsif is_weaker_defeat?( pair2, pair1 ) 
253           -1
254         else
255           0
256         end
257       end
258  
259       votes[defeats[0][0]][defeats[0][1]] = 0
260       votes[defeats[0][1]][defeats[0][0]] = 0
261       
262     end
263
264     return candidates
265   end
266
267 end

Benjamin Mako Hill || Want to submit a patch?