o Fix PureCondorcetVote to return all results, not just the winner
[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   attr_accessor :results
37
38   def initialize(votes=nil)
39     unless defined?(@candidates)
40       @candidates = Array.new
41       votes.each do |vote_row|
42         vote_row = vote_row.flatten if vote_row.class == Array
43         vote_row.each do |vote| 
44           @candidates << vote unless @candidates.include?(vote)
45         end
46       end
47     end
48     super(votes)
49     @results = Array.new
50   end
51
52   def tally_vote(vote=nil)
53
54     vote.each_with_index do |winner, index|
55       if vote.flatten.length < @candidates.length
56         implied_losers = @candidates.select { |c| not vote.include?(c) }
57         vote.push(implied_losers)
58       end
59       if vote.length - 1 == index
60         losers = []
61       else
62         losers = vote.flatten.last( vote.flatten.length - index - 1)
63       end
64
65       losers.each do |place|
66         place = [place] unless place.class == Array
67         place.each do |loser|
68           
69
70           @votes[winner] = Hash.new unless @votes.has_key?(winner)
71           @votes[loser] = Hash.new unless @votes.has_key?(loser)
72
73           if @votes[winner].has_key?(loser)
74             @votes[winner][loser] += 1
75           else
76             @votes[winner][loser] = 1
77           end
78
79           # make sure we have a comparable object
80           @votes[loser][winner] = 0 unless @votes[loser].has_key?( winner )
81         end
82       end
83     end
84   end
85
86   def results
87     if @results.size < 2 && (not @candidates.empty?)
88       tabulate
89     end
90     @results
91   end
92
93   def result
94     find_only_winner unless @winner
95     @winner
96   end
97
98   protected
99
100   def verify_vote(vote=nil)
101     vote.instance_of?( Array ) and
102       vote == vote.uniq
103   end
104
105   def tabulate
106     find_only_winner unless @winner
107     until @candidates.empty? 
108       aResult = resultFactory( self )
109       @results << aResult.winners
110       filter_out(aResult)
111     end
112   end
113
114   def find_only_winner
115     @winner = resultFactory( self )
116     @results << @winner.winners
117     filter_out(@winner)
118   end
119
120 end
121
122 class PureCondorcetVote < CondorcetVote
123   def resultFactory(init)
124     PureCondorcetResult.new(init)
125   end
126 end
127
128 class CloneproofSSDVote < CondorcetVote
129   def resultFactory(init)
130     CloneproofSSDResult.new(init)
131   end
132
133 end
134
135
136 ##################################################################
137 ## CondorcetResult Classes and SubClasses
138 ##
139 ## The CondorcetResult class is subclassed by the PureCondorcetResult
140 ## and the CloneproofSSDResult classes but should not be used
141 ## directly.
142
143 class CondorcetResult < ElectionResult
144   def initialize(voteobj=nil)
145     unless voteobj and voteobj.kind_of?( CondorcetVote )
146       raise ArgumentError, "You must pass a CondorcetVote array.", caller
147     end
148     super(voteobj)
149   end
150
151   protected
152
153   def defeats(candidates=nil, votes=nil)
154     candidates = @election.candidates unless candidates
155     votes = @election.votes unless votes
156
157     defeats = Array.new
158     candidates = [candidates] unless candidates.class == Array
159     candidates.each do |candidate|
160       candidates.each do |challenger|
161         next if candidate == challenger
162         if votes[candidate][challenger] > votes[challenger][candidate]
163           defeats << [ candidate, challenger ]
164         end
165       end
166     end
167
168     defeats
169   end
170
171 end
172
173 class PureCondorcetResult < CondorcetResult
174   def initialize(voteobj=nil)
175     super(voteobj)
176     self.condorcet()
177   end
178
179   protected
180
181   def condorcet
182     votes = @election.votes
183     candidates = @election.candidates
184
185     victors = Hash.new
186     candidates.each do |candidate|
187       victors[candidate] = Array.new
188     end
189
190     self.defeats.each do |pair|
191       winner, loser = *pair
192       victors[winner] << loser
193     end
194
195     victory_margin = 1
196     while true
197       winners = @election.candidates.find_all do |candidate|
198         victors[candidate].length == @election.candidates.length - victory_margin
199       end
200       if winners.length > 0
201         @winners = winners
202         return @winners
203       else
204         victory_margin += 1
205       end
206     end
207   end
208 end
209
210 class CloneproofSSDResult < CondorcetResult
211   def initialize(voteobj=nil)
212     super(voteobj)
213     @winners = self.cpssd()
214   end
215
216   protected
217
218   def cpssd
219     votes = @election.votes
220     candidates = *@election.candidates
221
222     def in_schwartz_set?(candidate, candidates, transitive_defeats)
223       candidates.each do |challenger|
224         next if candidate == challenger
225
226         if transitive_defeats.include?( [ challenger, candidate ] ) and
227             not transitive_defeats.include?( [ candidate, challenger ] )
228           return false
229         end
230       end
231       return true
232     end
233
234     loop do
235
236       # see the array with the standard defeats
237       transitive_defeats = self.defeats(candidates, votes)
238       defeats_hash = Hash.new
239       transitive_defeats.each { |td| defeats_hash[td] = 1 }
240
241       candidates = [candidates] unless candidates.class == Array
242       candidates.each do |cand1|
243         candidates.each do |cand2|
244           unless cand1 == cand2
245             candidates.each do |cand3|
246               if not cand2 == cand3 and 
247                   not cand1 == cand3 and 
248                   defeats_hash[[cand2, cand1]] and
249                   defeats_hash[[cand1, cand3]] and
250                   not defeats_hash[[cand2, cand3]] 
251                 transitive_defeats << [cand2, cand3]
252                 defeats_hash[[cand2, cand3]] = 1
253               end
254             end
255           end
256         end
257       end
258
259       schwartz_set = Array.new
260       candidates.each do |candidate|
261         if in_schwartz_set?(candidate, candidates, transitive_defeats)
262           schwartz_set << candidate
263         end
264       end
265
266       candidates = schwartz_set
267
268       # look through the schwartz set now for defeats
269       defeats = self.defeats(candidates, votes)
270       
271       # it's a tie or there's only one option
272       break if defeats.length == 0
273
274       def is_weaker_defeat?( pair1, pair2 )
275         votes = @election.votes
276         if votes[pair1[0]][pair1[1]] > votes[pair2[0]][pair2[1]]
277           return true
278         elsif votes[pair1[0]][pair1[1]] == votes[pair2[0]][pair2[1]] and
279             votes[pair1[1]][pair1[0]] > votes[pair2[1]][pair2[0]]
280           return true
281         else
282           false
283         end
284       end
285       
286       defeats.sort! do |pair1, pair2|
287         if is_weaker_defeat?( pair1, pair2 ) 
288           +1
289         elsif is_weaker_defeat?( pair2, pair1 ) 
290           -1
291         else
292           0
293         end
294       end
295  
296       votes[defeats[0][0]][defeats[0][1]] = 0
297       votes[defeats[0][1]][defeats[0][0]] = 0
298       
299     end
300
301     return candidates
302   end
303
304 end

Benjamin Mako Hill || Want to submit a patch?