a number of improvements
[rubyvote] / lib / rubyvote / irv.rb
1 # IRV is a preferential voting system used widely for government elections
2 # in Australia and New Zealand and elsewhere. IRV asks voters to rank
3 # candidates in preference and then holds a series of "runoff" elections
4 # by eliminating the weakest candidate and recomputing the election
5 # results until there exists a candidate who has a majority of the
6 # remaining votes.
7
8 # Example::
9
10 #   require 'irv'
11 #   vote_array = [ ["A", "B"],  ["B", "A"], ["B", "A"] ]
12 #   resultobject = InstantRunoffVote.new(vote_array).result
13 #
14 class InstantRunoffVote < ElectionVote
15
16   def initialize(votes=nil)
17     @candidates = Array.new
18     votes.each do |vote|
19       @candidates = vote.uniq if vote.uniq.length > candidates.length
20     end
21     super(votes)
22     @candidates.each do |candidate|
23       @votes[candidate] = [0, Hash.new] unless @votes.has_key?(candidate)
24     end
25   end
26
27   def result(params={})
28     InstantRunoffResult.new(self, params)
29   end
30   
31   protected
32     def vote_valid?(vote = nil)
33       super && vote.is_a?(Array) && vote == vote.uniq
34     end
35
36     def tally_vote(vote)
37       votecopy = vote.dup
38       candidate = votecopy.shift
39       votes[candidate] = [0, Hash.new] unless votes.has_key?(candidate)
40       votes[candidate][0] += 1
41       if votes[candidate][1].has_key?(votecopy)
42         votes[candidate][1][votecopy] += 1
43       else
44         votes[candidate][1][votecopy] = 1
45       end
46     end
47
48 end
49
50 class InstantRunoffLogicVote < InstantRunoffVote
51   def result(params={})
52     InstantRunoffLogicResult.new(self, params)
53   end
54 end
55
56 class InstantRunoffFirstRoundVote < InstantRunoffVote
57   def result(params={})
58     InstantRunoffFirstRoundResult.new(self, params)
59   end
60 end
61
62 class InstantRunoffAllVote < InstantRunoffVote
63   def result(params={})
64     InstantRunoffAllResult.new(self, params)
65   end
66 end
67
68 class InstantRunoffRandomVote < InstantRunoffVote
69   def result(params={})
70     InstantRunoffRandomResult.new(self, params)
71   end
72 end
73
74 class InstantRunoffResult < ElectionResult
75   attr_reader :ranked_candidates
76
77   def initialize(voteobj=nil, params={})
78     unless voteobj and voteobj.kind_of?( InstantRunoffVote )
79       raise ArgumentError, "You must pass an InstantRunoffVote array.", caller
80     end
81     super(voteobj)
82
83     votes = @election.votes.clone
84     candidates = @election.candidates
85     votes_sum = votes.inject(0) {|n, value| n + value[1][0]}
86     @majority = votes_sum/2 + 1
87     @ranked_candidates = Array.new()
88     ranked_candidates = Array.new()
89     losers = Array.new()
90
91     if params.has_key?('candidate_count')
92       apply_candidate_count(votes, params['candidate_count'])
93     end
94     if params.has_key?('vote_minimum')
95       apply_vote_minimum(votes, params['vote_minimum'])
96     end
97     if params.has_key?('percent_minimum')
98       apply_vote_minimum(votes, votes_sum * params['percent_minimum'])
99     end
100     if params.has_key?('percent_retention')
101       apply_retention(votes, votes_sum * params['percent_retention'])
102     end
103     
104     unless votes.length > 0
105       @winners=[]
106       return
107     end
108
109     begin
110       ranked_candidates = votes.sort do |a, b|
111         b[1][0] <=> a[1][0]
112       end.collect {|i| i[0]}
113       @winners = ranked_candidates.find_all do |i|
114         votes[i][0] >= @majority
115       end
116     end while not self.winner? and next_round(votes, ranked_candidates)
117     @ranked_candidates.unshift(*ranked_candidates)
118   end
119
120 protected
121   def apply_candidate_count(votes, candidate_count)
122     if votes.size > candidate_count
123       losers = votes.sort do |a, b|
124         b[1][0] <=> a[1][0]
125       end.collect {|i| i[0]}.last(votes.size - candidate_count)
126       @ranked_candidates.unshift(losers) unless losers.empty?
127       losers.each { |loser| remove_candidate(votes, loser) }
128     end
129   end
130
131   def apply_vote_minimum(votes, vote_minimum)
132     losers = votes.find_all do |i|
133       i[1][0] < vote_minimum
134     end.collect {|i| i[0]}
135     if losers.length == votes.size
136       votes.clear
137     else
138       @ranked_candidates.unshift(losers) unless losers.empty?
139       losers.each { |loser| remove_candidate(votes, loser) }
140     end
141   end
142
143   def apply_retention(votes, retention)
144     losers = votes.sort do |a, b|
145       b[1][0] <=> a[1][0]
146     end.collect {|i| i[0]}
147     partial_sum = 0
148     while partial_sum < retention
149       partial_sum += votes[losers.shift][0]
150     end
151     @ranked_candidates.unshift(losers) unless losers.empty?
152     losers.each { |loser| remove_candidate(votes, loser) }
153   end
154   
155   def next_round(votes, ranked_candidates)
156     loser = ranked_candidates[-1]
157     if votes.empty? or votes[loser][0] == votes[ranked_candidates[-2]][0]
158       false
159     else
160       @ranked_candidates.unshift(loser)
161       remove_candidate(votes, loser) 
162       true
163     end
164   end
165
166   def remove_candidate(votes, loser)
167     votes.each_pair do |candidate, morevotes|
168       hash = morevotes[1]
169       hash.each_pair do |vote, count|
170         hash.delete(vote)
171         vote.delete(loser)
172         hash[vote] = count
173       end
174     end
175     votes[loser][1].each_pair do |vote, count|
176       candidate = vote.shift
177       votes[candidate][0] += count
178       if votes[candidate][1].has_key?(vote)
179         votes[candidate][1][vote] += count
180       else
181         votes[candidate][1][vote] = count
182       end
183     end
184     votes.delete(loser)
185   end
186 end
187
188 class InstantRunoffLogicResult < InstantRunoffResult
189   def next_round(votes, ranked_candidates)
190     losers = ranked_candidates.find_all do |i|
191       votes[i][0] == votes[ranked_candidates[-1]][0]
192     end
193     if losers.inject(0) {|n, loser| n + votes[loser][0]} >= @majority
194       false
195     else
196       @ranked_candidates.unshift(losers)
197       losers.each do |loser|
198         remove_candidate(votes, loser)
199       end
200       true
201     end
202   end
203 end
204
205 class InstantRunoffFirstRoundResult < InstantRunoffResult
206   def next_round(votes, ranked_candidates)
207     losers = ranked_candidates.find_all do |i|
208       votes[i][0] == votes[ranked_candidates[-1]][0]
209     end
210     loser = losers.sort do |a, b|
211       @election.votes[a][0] <=> @election.votes[b][0]
212     end.last
213     @ranked_candidates.unshift(loser)
214     remove_candidate(votes, loser)
215   end
216 end
217
218 class InstantRunoffAllResult < InstantRunoffResult
219   def next_round(votes, ranked_candidates)
220     losers = ranked_candidates.find_all do |i|
221       votes[i][0] == votes[ranked_candidates[-1]][0]
222     end
223     if losers.length == ranked_candidates.length
224       false
225     else
226       @ranked_candidates.unshift(losers)
227       losers.each do |loser|
228         remove_candidate(votes, loser)
229       end
230       true
231     end
232   end
233 end
234
235 class InstantRunoffRandomResult < InstantRunoffResult
236   def next_round(votes, ranked_candidates)
237     losers = ranked_candidates.find_all do |i|
238       votes[i][0] == votes[ranked_candidates[-1]][0]
239     end
240     loser = losers[rand(losers.length)]
241     @ranked_candidates.unshift(loser)
242     remove_candidate(votes, loser)
243   end
244 end

Benjamin Mako Hill || Want to submit a patch?