Class: Meekster::Round

Inherits:
Object
  • Object
show all
Defined in:
lib/meekster/round.rb

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(parameters = {}) ⇒ Round

Returns a new instance of Round.



6
7
8
9
10
11
12
13
# File 'lib/meekster/round.rb', line 6

def initialize(parameters={})
  self.ballots = parameters[:ballots]
  self.candidates = parameters[:candidates]
  self.seats = parameters[:seats]
  self.omega = parameters[:omega]

  @candidate_elected_this_round = false
end

Instance Attribute Details

#ballotsObject

Returns the value of attribute ballots.



4
5
6
# File 'lib/meekster/round.rb', line 4

def ballots
  @ballots
end

#candidatesObject

Returns the value of attribute candidates.



4
5
6
# File 'lib/meekster/round.rb', line 4

def candidates
  @candidates
end

#omegaObject

Returns the value of attribute omega.



4
5
6
# File 'lib/meekster/round.rb', line 4

def omega
  @omega
end

#quotaObject

Returns the value of attribute quota.



4
5
6
# File 'lib/meekster/round.rb', line 4

def quota
  @quota
end

#seatsObject

Returns the value of attribute seats.



4
5
6
# File 'lib/meekster/round.rb', line 4

def seats
  @seats
end

#surplusObject

Returns the value of attribute surplus.



4
5
6
# File 'lib/meekster/round.rb', line 4

def surplus
  @surplus
end

Class Method Details

.round_up_to_nine_decimal_places(x) ⇒ Object



192
193
194
# File 'lib/meekster/round.rb', line 192

def self.round_up_to_nine_decimal_places(x)
  ((x * BigDecimal('1E9')).ceil)/BigDecimal('1E9')
end

.truncate_to_nine_decimal_places(x) ⇒ Object



196
197
198
# File 'lib/meekster/round.rb', line 196

def self.truncate_to_nine_decimal_places(x)
  ((x * BigDecimal('1E9')).floor)/BigDecimal('1E9')
end

Instance Method Details

#calculate_total_surplus!Object



118
119
120
121
122
123
124
125
126
127
128
129
# File 'lib/meekster/round.rb', line 118

def calculate_total_surplus!
  elected_candidates = candidates.select{|c| c.state == :elected}
  sum_of_surpluses = elected_candidates.inject(BigDecimal('0')){|memo, c| memo + (c.votes - quota)}
  log "Calculating total surplus. Sum of surpluses: #{sum_of_surpluses.to_f}"
  if sum_of_surpluses < 0
    self.surplus = 0
    @previous_surpluses.push(surplus)
  else
    self.surplus = sum_of_surpluses
    @previous_surpluses.push(surplus)
  end
end

#candidate_elected_this_round?Boolean

Returns:

  • (Boolean)


131
132
133
# File 'lib/meekster/round.rb', line 131

def candidate_elected_this_round?
  !!@candidate_elected_this_round
end

#count_complete?Boolean

Returns:

  • (Boolean)


57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
# File 'lib/meekster/round.rb', line 57

def count_complete?
  # Are all seats filled?
  elected_candidates_count = candidates.select{|c| c.state == :elected}.length
  if elected_candidates_count >= seats
    log "Count complete: enough elected candidates to fill the seats"
    return true
  end

  # Is number of elected plus hopeful candidates less than or equal to number of seats?
  hopeful_candidates_count = candidates.select{|c| c.state == :hopeful}.length
  if (elected_candidates_count + hopeful_candidates_count) <= seats
    log "Count complete: elected+hopeful candidates less than or equal to seats"
    return true
  end

  false
end

#defeat_low_candidate!Object



150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
# File 'lib/meekster/round.rb', line 150

def defeat_low_candidate!
  log "Defeating lowest candidate:"
  hopeful_candidates = candidates.select{|c| c.state == :hopeful}
  candidate_with_lowest_vote = nil
  hopeful_candidates.each do |hopeful_candidate|
    if candidate_with_lowest_vote.nil? || hopeful_candidate.votes < candidate_with_lowest_vote.votes
      candidate_with_lowest_vote = hopeful_candidate
    end
  end

  log "  Lowest candidate: #{candidate_with_lowest_vote.name}"

  # Detect ties

  hopeful_candidates.delete(candidate_with_lowest_vote)
  tied_candidates = hopeful_candidates.select{|c| c.votes <= (candidate_with_lowest_vote.votes + surplus)}

  if tied_candidates.empty?
    candidate_to_defeat = candidate_with_lowest_vote
  else
    candidates_with_lowest_votes = tied_candidates
    candidates_with_lowest_votes.push(candidate_with_lowest_vote)
    candidate_to_defeat = tiebreaker_select(candidates_with_lowest_votes)
  end

  candidate_to_defeat.state = :defeated
  candidate_to_defeat.keep_factor = BigDecimal.new('0')
end

#distribute_votes!Object



81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/meekster/round.rb', line 81

def distribute_votes!
  ballots.each do |ballot|
    ballot.weight = BigDecimal('1')

    ballot.ranking.each do |ranked_candidate|

      weight_times_keep_factor = ballot.weight * ranked_candidate.keep_factor
      weight_times_keep_factor = Meekster::Round.round_up_to_nine_decimal_places(weight_times_keep_factor)

      ranked_candidate.votes += weight_times_keep_factor

      ballot.weight -= weight_times_keep_factor

      break if ballot.weight <= 0
    end
  end
end

#find_winners!Object



108
109
110
111
112
113
114
115
116
# File 'lib/meekster/round.rb', line 108

def find_winners!
  candidates.select{|c| c.state == :hopeful}.each do |candidate|
    if candidate.votes >= quota
      log "Found winner: #{candidate.name}"
      candidate.state = :elected
      @candidate_elected_this_round = true
    end
  end
end

#log(msg) ⇒ Object

DEBUGGING



206
207
208
209
210
# File 'lib/meekster/round.rb', line 206

def log(msg)
  return # Comment out to enable debugging
  puts msg
  gets
end

#need_to_defeat_low_candidate?Boolean

Returns:

  • (Boolean)


135
136
137
138
139
140
141
142
143
144
145
146
147
148
# File 'lib/meekster/round.rb', line 135

def need_to_defeat_low_candidate?
  if surplus < omega
    log "Need to defeat low candidate."
    return true
  end

  if @previous_surpluses.length > 1 && (surplus >= @previous_surpluses[-2])
    log "Need to defeat low candidate (surplus greater than previous iteration)."
    return true
  end

  log "Do not need to defeat low candidate."
  false
end

#reset_votes!Object



75
76
77
78
79
# File 'lib/meekster/round.rb', line 75

def reset_votes!
  candidates.each do |candidate|
    candidate.votes = BigDecimal('0')
  end
end

#run!Object



15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# File 'lib/meekster/round.rb', line 15

def run!
  if count_complete?
    return # actually return something?
  end

  @previous_surpluses = []

  while true do
    log "STARTING NEW ITERATION"

    log "Keep factors:"
    candidates.each do |candidate|
      log "  #{candidate.name} | #{candidate.keep_factor.to_f}"
    end

    reset_votes!

    distribute_votes!

    log "Counted votes:"
    candidates.each do |candidate|
      log "  #{candidate.name} | #{candidate.votes.to_f}"
    end


    update_quota!
    find_winners!
    calculate_total_surplus!

    if candidate_elected_this_round?
      return
    end

    if need_to_defeat_low_candidate?
      defeat_low_candidate!
      return
    end

    update_keep_factors!
  end
end

#tiebreaker_select(array) ⇒ Object



200
201
202
# File 'lib/meekster/round.rb', line 200

def tiebreaker_select(array)
  array[rand(array.length)]
end

#update_keep_factors!Object



179
180
181
182
183
184
185
186
187
188
189
190
# File 'lib/meekster/round.rb', line 179

def update_keep_factors!
  log "Updating keep factors:"
  elected_candidates = candidates.select{|c| c.state == :elected}
  quota_rounded = Meekster::Round.round_up_to_nine_decimal_places(quota)
  elected_candidates.each do |elected_candidate|
    elected_candidate_votes_rounded = Meekster::Round.round_up_to_nine_decimal_places(elected_candidate.votes)
    new_keep_factor = elected_candidate.keep_factor * quota_rounded
    new_keep_factor = new_keep_factor / elected_candidate_votes_rounded
    log "  Candidate #{elected_candidate.name}: #{elected_candidate.keep_factor.to_f} -> #{new_keep_factor.to_f}"
    elected_candidate.keep_factor = new_keep_factor
  end
end

#update_quota!Object



99
100
101
102
103
104
105
106
# File 'lib/meekster/round.rb', line 99

def update_quota!
  sum_of_votes = candidates.inject(BigDecimal('0')){|sum, c| sum + c.votes}
  new_quota = sum_of_votes / (seats + 1)
  # TODO truncate to nine decimal places
  new_quota += BigDecimal('0.000000001')
  log "Updating quota: #{new_quota.to_f}"
  self.quota = new_quota
end