meekster 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ MzMyNTE4M2Q1YmUwMTRhNDQ4YjE2YzgzZmJkMmUwZWU4ZjYyOGNhZA==
5
+ data.tar.gz: !binary |-
6
+ OWJmYzcxYmJiNjI2Y2QxMDY3Yzk2YjM3YjY4MmEzNzBiNjI3OGQ4Mw==
7
+ !binary "U0hBNTEy":
8
+ metadata.gz: !binary |-
9
+ MDhlODM1MmJhYWNkYzUwMzc0NmI3NmJlOTI1YTJjNDQwZWRjZmVjNTQ1NmQw
10
+ MjMzYmQxZTAwMTZiYjY0ZjBhNjAwOTFjNmU0Y2JhYTg2OTJmMjJiYTA0MTA1
11
+ ZTQ4YjYzNjhlMTc5Mzg5ODRkMTA3YmE5MDY3ZmQwYzk3ZTY1NDI=
12
+ data.tar.gz: !binary |-
13
+ YWJmMTc0N2ZmNTM4MjJlNzFlYWM5ZmFjYzYzNTkxZDllMDdkNTRlYWE5ZDI1
14
+ MjRmZjZiZWEwMzEzNzNjZTZjZDQ1MDA5ODdmYjc2YTU4ZDk1MTBkNzEwZmJi
15
+ NWNmMTdiNTBiMjg2MzM3MGU1YWFiYzU2ZGNjYzAwMTQwMWM3Nzc=
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
@@ -0,0 +1,31 @@
1
+ Meekster
2
+ ========
3
+
4
+ Meekster implements a Meek Single Transferable Vote (STV) voting system, as described in the [Proportional Representation Foundation's] [1] [Reference Meek Rule] [2], in Ruby.
5
+
6
+ This software is a work-in-progress. You probably shouldn't use it for an actual election (yet).
7
+
8
+ Some testing has been done against [Droop] [3], a Python Library that implements STV. Thanks to the authors of Droop for the software and test ballot files.
9
+
10
+ License
11
+ -------
12
+
13
+ Copyright &copy; 2012 Chris Mear <chris@feedmechocolate.com>.
14
+
15
+ This program is free software: you can redistribute it and/or modify
16
+ it under the terms of the GNU General Public License as published by
17
+ the Free Software Foundation, either version 3 of the License, or
18
+ (at your option) any later version.
19
+
20
+ This program is distributed in the hope that it will be useful,
21
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
22
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23
+ GNU General Public License for more details.
24
+
25
+ You should have received a copy of the GNU General Public License
26
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
27
+
28
+
29
+ [1]: http://prfound.org/ "Proportional Representation Foundation"
30
+ [2]: http://prfound.org/resources/reference/reference-meek-rule/ "Reference Meek Rule"
31
+ [3]: http://code.google.com/p/droop/ "Droop"
@@ -0,0 +1,5 @@
1
+ require 'rspec/core/rake_task'
2
+
3
+ RSpec::Core::RakeTask.new(:spec)
4
+
5
+ task :default => :spec
@@ -0,0 +1,9 @@
1
+ module Meekster
2
+ end
3
+
4
+ require 'meekster/ballot'
5
+ require 'meekster/ballot_file'
6
+ require 'meekster/candidate'
7
+ require 'meekster/election'
8
+ require 'meekster/round'
9
+ require 'meekster/version'
@@ -0,0 +1,9 @@
1
+ require File.expand_path('../meekster', File.dirname(__FILE__))
2
+
3
+ class Meekster::Ballot
4
+ attr_accessor :ranking, :weight
5
+
6
+ def initialize(ranking=nil)
7
+ self.ranking = ranking
8
+ end
9
+ end
@@ -0,0 +1,60 @@
1
+ require File.expand_path('ballot', File.dirname(__FILE__))
2
+ require File.expand_path('candidate', File.dirname(__FILE__))
3
+
4
+ class Meekster::BallotFile
5
+ attr_accessor :candidates, :ballots, :seats
6
+
7
+ def initialize(options={})
8
+ if options[:filename]
9
+ @file = File.open(options[:filename], 'r')
10
+ elsif options[:string]
11
+ @file = StringIO.new(options[:string])
12
+ elsif options[:file]
13
+ @file = options[:file]
14
+ end
15
+
16
+ @read = false
17
+ end
18
+
19
+ def read!
20
+ @file.rewind
21
+
22
+ @ballots = []
23
+
24
+ candidates_and_seats = @file.gets
25
+ @candidate_count, @seats = candidates_and_seats.split(' ').map{|n| n.to_i}
26
+
27
+ @candidates = Array.new(@candidate_count){Meekster::Candidate.new}
28
+
29
+ ballot_line = @file.gets
30
+
31
+ until ballot_line.match(/^0/)
32
+ line_atoms = ballot_line.split(' ')
33
+
34
+ count = line_atoms.delete_at(0).to_i
35
+ line_atoms.delete_at(-1)
36
+
37
+ ranking = line_atoms.map{|id| @candidates[id.to_i - 1]}
38
+
39
+ count.times do
40
+ @ballots << Meekster::Ballot.new(ranking)
41
+ end
42
+
43
+ ballot_line = @file.gets
44
+
45
+ end
46
+
47
+ @candidate_count.times do |i|
48
+ candidate_name = @file.gets.strip
49
+ # Remove double-quotes
50
+ candidate_name = candidate_name.match(/\A\"(.*)\"\Z/)[1]
51
+ @candidates[i].name = candidate_name
52
+ end
53
+
54
+ @read = true
55
+ end
56
+
57
+ def read?
58
+ !!@read
59
+ end
60
+ end
@@ -0,0 +1,13 @@
1
+ require 'bigdecimal'
2
+
3
+ class Meekster::Candidate
4
+ STATES = [:hopeful, :withdrawn, :elected, :defeated]
5
+
6
+ attr_accessor :name, :state, :keep_factor, :votes
7
+
8
+ def initialize(name=nil)
9
+ self.name = name
10
+ self.keep_factor = BigDecimal("1")
11
+ self.votes = BigDecimal('0')
12
+ end
13
+ end
@@ -0,0 +1,85 @@
1
+ require 'bigdecimal'
2
+ require File.expand_path('../round', __FILE__)
3
+
4
+ class Meekster::Election
5
+ attr_accessor :ballots, :candidates, :seats
6
+
7
+ def initialize(parameters={})
8
+ if parameters[:ballot_file]
9
+ bf = parameters[:ballot_file]
10
+ bf.read! unless bf.read?
11
+ self.ballots = bf.ballots
12
+ self.candidates = bf.candidates
13
+ self.seats = bf.seats
14
+ end
15
+ if parameters[:ballots]
16
+ self.ballots = parameters[:ballots]
17
+ end
18
+ if parameters[:candidates]
19
+ self.candidates = parameters[:candidates]
20
+ end
21
+ if parameters[:seats]
22
+ self.seats = parameters[:seats]
23
+ end
24
+ end
25
+
26
+
27
+
28
+ def run!
29
+ raise RuntimeError, "ballots not found" unless ballots
30
+ raise RuntimeError, "candidates not found" unless candidates
31
+ raise RuntimeError, "seats not found" unless seats
32
+
33
+ candidates.each do |candidate|
34
+ candidate.state = :hopeful
35
+ end
36
+
37
+ @omega = BigDecimal("0.000001")
38
+
39
+ @rounds = []
40
+
41
+ while true do
42
+
43
+ round = Meekster::Round.new(
44
+ :ballots => ballots,
45
+ :candidates => candidates,
46
+ :seats => seats,
47
+ :omega => @omega
48
+ )
49
+
50
+ round.run!
51
+
52
+ break if round.count_complete?
53
+
54
+ @rounds.push(round)
55
+ end
56
+
57
+ elected_candidates_count = candidates.select{|c| c.state == :elected}.length
58
+ hopeful_candidates = candidates.select{|c| c.state == :hopeful}
59
+ if elected_candidates_count < seats
60
+ hopeful_candidates.each do |hopeful_candidate|
61
+ hopeful_candidate.state = :elected
62
+ end
63
+ else
64
+ hopeful_candidates.each do |hopeful_candidate|
65
+ hopeful_candidate.state = :defeated
66
+ end
67
+ end
68
+
69
+ candidates
70
+ end
71
+
72
+ def results
73
+ output = ""
74
+ elected_candidates = candidates.select{|c| c.state == :elected}.sort{|a, b| a.votes <=> b.votes}
75
+ defeated_candidates = candidates.select{|c| c.state == :defeated}.sort{|a, b| a.name <=> b.name}
76
+
77
+ elected_candidates.each do |ec|
78
+ output << "Elected: #{ec.name} (#{ec.votes.to_f})\n"
79
+ end
80
+ output << "Defeated: "
81
+ output << defeated_candidates.map{|dc| dc.name}.join(', ')
82
+ output << "\n"
83
+ output
84
+ end
85
+ end
@@ -0,0 +1,211 @@
1
+ require 'bigdecimal'
2
+
3
+ class Meekster::Round
4
+ attr_accessor :ballots, :candidates, :seats, :omega, :quota, :surplus
5
+
6
+ def initialize(parameters={})
7
+ self.ballots = parameters[:ballots]
8
+ self.candidates = parameters[:candidates]
9
+ self.seats = parameters[:seats]
10
+ self.omega = parameters[:omega]
11
+
12
+ @candidate_elected_this_round = false
13
+ end
14
+
15
+ def run!
16
+ if count_complete?
17
+ return # actually return something?
18
+ end
19
+
20
+ @previous_surpluses = []
21
+
22
+ while true do
23
+ log "STARTING NEW ITERATION"
24
+
25
+ log "Keep factors:"
26
+ candidates.each do |candidate|
27
+ log " #{candidate.name} | #{candidate.keep_factor.to_f}"
28
+ end
29
+
30
+ reset_votes!
31
+
32
+ distribute_votes!
33
+
34
+ log "Counted votes:"
35
+ candidates.each do |candidate|
36
+ log " #{candidate.name} | #{candidate.votes.to_f}"
37
+ end
38
+
39
+
40
+ update_quota!
41
+ find_winners!
42
+ calculate_total_surplus!
43
+
44
+ if candidate_elected_this_round?
45
+ return
46
+ end
47
+
48
+ if need_to_defeat_low_candidate?
49
+ defeat_low_candidate!
50
+ return
51
+ end
52
+
53
+ update_keep_factors!
54
+ end
55
+ end
56
+
57
+ def count_complete?
58
+ # Are all seats filled?
59
+ elected_candidates_count = candidates.select{|c| c.state == :elected}.length
60
+ if elected_candidates_count >= seats
61
+ log "Count complete: enough elected candidates to fill the seats"
62
+ return true
63
+ end
64
+
65
+ # Is number of elected plus hopeful candidates less than or equal to number of seats?
66
+ hopeful_candidates_count = candidates.select{|c| c.state == :hopeful}.length
67
+ if (elected_candidates_count + hopeful_candidates_count) <= seats
68
+ log "Count complete: elected+hopeful candidates less than or equal to seats"
69
+ return true
70
+ end
71
+
72
+ false
73
+ end
74
+
75
+ def reset_votes!
76
+ candidates.each do |candidate|
77
+ candidate.votes = BigDecimal('0')
78
+ end
79
+ end
80
+
81
+ def distribute_votes!
82
+ ballots.each do |ballot|
83
+ ballot.weight = BigDecimal('1')
84
+
85
+ ballot.ranking.each do |ranked_candidate|
86
+
87
+ weight_times_keep_factor = ballot.weight * ranked_candidate.keep_factor
88
+ weight_times_keep_factor = Meekster::Round.round_up_to_nine_decimal_places(weight_times_keep_factor)
89
+
90
+ ranked_candidate.votes += weight_times_keep_factor
91
+
92
+ ballot.weight -= weight_times_keep_factor
93
+
94
+ break if ballot.weight <= 0
95
+ end
96
+ end
97
+ end
98
+
99
+ def update_quota!
100
+ sum_of_votes = candidates.inject(BigDecimal('0')){|sum, c| sum + c.votes}
101
+ new_quota = sum_of_votes / (seats + 1)
102
+ # TODO truncate to nine decimal places
103
+ new_quota += BigDecimal('0.000000001')
104
+ log "Updating quota: #{new_quota.to_f}"
105
+ self.quota = new_quota
106
+ end
107
+
108
+ def find_winners!
109
+ candidates.select{|c| c.state == :hopeful}.each do |candidate|
110
+ if candidate.votes >= quota
111
+ log "Found winner: #{candidate.name}"
112
+ candidate.state = :elected
113
+ @candidate_elected_this_round = true
114
+ end
115
+ end
116
+ end
117
+
118
+ def calculate_total_surplus!
119
+ elected_candidates = candidates.select{|c| c.state == :elected}
120
+ sum_of_surpluses = elected_candidates.inject(BigDecimal('0')){|memo, c| memo + (c.votes - quota)}
121
+ log "Calculating total surplus. Sum of surpluses: #{sum_of_surpluses.to_f}"
122
+ if sum_of_surpluses < 0
123
+ self.surplus = 0
124
+ @previous_surpluses.push(surplus)
125
+ else
126
+ self.surplus = sum_of_surpluses
127
+ @previous_surpluses.push(surplus)
128
+ end
129
+ end
130
+
131
+ def candidate_elected_this_round?
132
+ !!@candidate_elected_this_round
133
+ end
134
+
135
+ def need_to_defeat_low_candidate?
136
+ if surplus < omega
137
+ log "Need to defeat low candidate."
138
+ return true
139
+ end
140
+
141
+ if @previous_surpluses.length > 1 && (surplus >= @previous_surpluses[-2])
142
+ log "Need to defeat low candidate (surplus greater than previous iteration)."
143
+ return true
144
+ end
145
+
146
+ log "Do not need to defeat low candidate."
147
+ false
148
+ end
149
+
150
+ def defeat_low_candidate!
151
+ log "Defeating lowest candidate:"
152
+ hopeful_candidates = candidates.select{|c| c.state == :hopeful}
153
+ candidate_with_lowest_vote = nil
154
+ hopeful_candidates.each do |hopeful_candidate|
155
+ if candidate_with_lowest_vote.nil? || hopeful_candidate.votes < candidate_with_lowest_vote.votes
156
+ candidate_with_lowest_vote = hopeful_candidate
157
+ end
158
+ end
159
+
160
+ log " Lowest candidate: #{candidate_with_lowest_vote.name}"
161
+
162
+ # Detect ties
163
+
164
+ hopeful_candidates.delete(candidate_with_lowest_vote)
165
+ tied_candidates = hopeful_candidates.select{|c| c.votes <= (candidate_with_lowest_vote.votes + surplus)}
166
+
167
+ if tied_candidates.empty?
168
+ candidate_to_defeat = candidate_with_lowest_vote
169
+ else
170
+ candidates_with_lowest_votes = tied_candidates
171
+ candidates_with_lowest_votes.push(candidate_with_lowest_vote)
172
+ candidate_to_defeat = tiebreaker_select(candidates_with_lowest_votes)
173
+ end
174
+
175
+ candidate_to_defeat.state = :defeated
176
+ candidate_to_defeat.keep_factor = BigDecimal.new('0')
177
+ end
178
+
179
+ def update_keep_factors!
180
+ log "Updating keep factors:"
181
+ elected_candidates = candidates.select{|c| c.state == :elected}
182
+ quota_rounded = Meekster::Round.round_up_to_nine_decimal_places(quota)
183
+ elected_candidates.each do |elected_candidate|
184
+ elected_candidate_votes_rounded = Meekster::Round.round_up_to_nine_decimal_places(elected_candidate.votes)
185
+ new_keep_factor = elected_candidate.keep_factor * quota_rounded
186
+ new_keep_factor = new_keep_factor / elected_candidate_votes_rounded
187
+ log " Candidate #{elected_candidate.name}: #{elected_candidate.keep_factor.to_f} -> #{new_keep_factor.to_f}"
188
+ elected_candidate.keep_factor = new_keep_factor
189
+ end
190
+ end
191
+
192
+ def self.round_up_to_nine_decimal_places(x)
193
+ ((x * BigDecimal('1E9')).ceil)/BigDecimal('1E9')
194
+ end
195
+
196
+ def self.truncate_to_nine_decimal_places(x)
197
+ ((x * BigDecimal('1E9')).floor)/BigDecimal('1E9')
198
+ end
199
+
200
+ def tiebreaker_select(array)
201
+ array[rand(array.length)]
202
+ end
203
+
204
+ # DEBUGGING
205
+
206
+ def log(msg)
207
+ return # Comment out to enable debugging
208
+ puts msg
209
+ gets
210
+ end
211
+ end