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

Benjamin Mako Hill || Want to submit a patch?