require "rational"
# Set up the classes required to swallow the raw data for presentation to the
# rest of the program.
class ExtendedObject
# Put any methods here that I want to use just about all over the place.
def initialize
super
yield self if block_given?
end
end
class Person < ExtendedObject
attr_accessor :name
def inspect
if name
"{#{class_word} #{name}}"
else
"{a #{class_word}}"
end
end
end
class Voter < Person
attr_accessor :ranks_by_candidate, :scores_by_candidate, :weight
class Initializer
attr_accessor :target
class CandidateInitializer
attr_accessor :voter, :candidate
def rank a_rank
voter.ranks_by_candidate[candidate] = a_rank
end
def score a_score
voter.scores_by_candidate[candidate] = a_score
end
end
def weight shall_be
target.weight = shall_be
end
def candidate name, &y
# Accept data initialization concerning the voter's attitude to the named candidate.
target.scores_by_candidate ||= Hash.new
target.ranks_by_candidate ||= Hash.new
ci = CandidateInitializer.new
ci.voter = target
ci.candidate = $example.candidate name
y.call ci
true
end
end # Initilizer
def initializer
# Answer with an object that can initialize me from data declarations.
iobj = Initializer.new
iobj.target = self
self.weight = 1 # can be changed
iobj
end
def class_word
"voter"
end
end # Voter
class Candidate < Person
def class_word
"candidate"
end
end
$example ||= Object.new
class << $example
attr_accessor :data_initializer_thingy, :voters_by_name, :candidates_by_name
def inspect
"$example"
end
def data
# Accept a data declaration specified by the block.
yield data_initializer_thingy
true
end
def voter name
# Return an object to represent the voter whose name is given.
voters_by_name[name] ||= Voter.new {|v| v.name = name}
end
def candidate name
# Return an object to represent the candidate whose name is given.
candidates_by_name[name] ||= Candidate.new {|c| c.name = name}
end
def voters
voters_by_name.values
end
def candidates
candidates_by_name.values
end
end # class << $example
$example.candidates_by_name ||= Hash.new
$example.voters_by_name ||= Hash.new
$example.data_initializer_thingy = Object.new
class << $example.data_initializer_thingy
def inspect
"{the data initializer thingy}"
end
def voter voter_name, &y
# Accept a data declaration concerning the voter whose name is given by
# voter_name. Consult the block given for further details about the data to be associated
# to the voter.
y.call(($example.voter voter_name).initializer)
end
end
################################################################################
# #
# D A T A #
# #
################################################################################
# OK, the raw data are here. You can clone this code and stubstitute your own
# example here.
$example.data do |d|
d.voter "Ossipoff" do |v|
v.weight 1
v.candidate "fine Score" do |c|
c.score 1
end
v.candidate "coarse Score" do |c|
c.score Rational(75, 100)
end
v.candidate "Approval" do |c|
c.score Rational(74, 100)
end
v.candidate "ER-Bucklin (delay)" do |c|
c.score Rational(74, 100)
end
v.candidate "ER-Bucklin (no delay)" do |c|
c.score Rational(73, 100)
end
v.candidate "Majority Judgment" do |c|
c.score Rational(1, 100)
end
end
d.voter "Jennings" do |v|
v.candidate("coarse Score") {|c|c.score 1}
v.candidate("fine Score") {|c|c.score 1}
v.candidate("Majority Judgment") {|c|c.score 1}
v.candidate("Approval") {|c|c.score 1}
v.candidate("Condorcet") {|c|c.score Rational(75, 100)}
v.candidate("Instant Runoff") {|c|c.score Rational(35, 100)}
end
d.voter "Quinn" do |v|
v.candidate "ER-Bucklin (delay)" do |c|
c.score Rational(93, 100)
end
v.candidate "ER-Bucklin (no delay)" do |c|
c.score Rational(93, 100)
end
v.candidate("Ranked Pairs") {|c|c.score Rational(91, 100)} # Condorcet.
v.candidate("Improved Condorcet Top") {|c|c.score Rational(91, 100)} # Condorcet.
v.candidate("ERABW") {|c|c.score Rational(91, 100)} # Condorcet?
v.candidate("other Condorcet") {|c|c.score Rational(90, 100)}
v.candidate("coarse Score") {|c|c.score Rational(8, 10)}
v.candidate("fine Score") {|c|c.score Rational(94, 100)}
v.candidate("Runoff") {|c|c.score Rational(4, 10)}
v.candidate("Majority Judgment") {|c|c.score 1}
v.candidate("Continuous Majority Judgment") {|c|c.score 1}
v.candidate("SODA") {|c|c.score 1}
v.candidate("Approval") {|c|c.score Rational(94, 100)}
v.candidate("Condorcet") {|c|c.score Rational(90, 100)}
v.candidate("Instant Runoff") {|c|c.score Rational(20, 100)}
v.candidate("Score (selected alternative values)") {|c|c.score Rational(99, 100)}
end
d.voter "Gilson" do |v|
v.weight 1
v.candidate "coarse Score" do |c|
c.score 1
end
v.candidate "fine Score" do |c|
c.score Rational(75, 100)
end
v.candidate "Condorcet" do |c|
c.score Rational(75, 100)
end
v.candidate "Majority Judgment" do |c|
# c.rank 2 Not trying to rank these.
c.score Rational(90, 100)
end
v.candidate "Bucklin" do |c|
# c.rank 2 Not trying to rank these.
c.score Rational(90, 100)
end
v.candidate "Plurality" do |c|
# c.rank 2 Not trying to rank these.
c.score Rational(10, 100)
end
end
d.voter "Shentrup" do |v|
v.weight 1
v.candidate "coarse Score" do |c|
c.score 1
end
v.candidate "fine Score" do |c|
c.score Rational(100, 100)
end
v.candidate "Approval" do |c|
c.score Rational(90, 100)
end
v.candidate "Bucklin" do |c|
c.score Rational(75, 100)
end
v.candidate "Majority Judgment" do |c|
# c.rank 2 Not trying to rank these.
c.score Rational(70, 100)
end
v.candidate "Condorcet" do |c|
# c.rank 2 Not trying to rank these.
c.score Rational(67, 100)
end
v.candidate "Instant Runoff" do |c|
# c.rank 2 Not trying to rank these.
c.score Rational(30, 100)
end
end
d.voter "Le'vesque" do |v|
v.weight 1
v.candidate "coarse Score" do |c|
c.score 1
end
v.candidate "fine Score" do |c|
c.score Rational(100, 100)
end
v.candidate "Approval" do |c|
c.score Rational(85, 100)
end
v.candidate "Bucklin" do |c|
c.score Rational(70, 100)
end
v.candidate "Majority Judgment" do |c|
# c.rank 2 Not trying to rank these.
c.score Rational(75, 100)
end
v.candidate "Instant Runoff" do |c|
# c.rank 2 Not trying to rank these.
c.score Rational(30, 100)
end
end
end
################################################################################
# #
# R A N G E #
# #
################################################################################
# Score or range voting.
class ScoreElection < ExtendedObject
attr_accessor :max_score # What is the maximum score that a voter is allowed to assign to a
# candidate in this election?
# Code all over the place assumes the minimum score is zero.
def initialize
@max_score = Rational(1, 1)
super
end
def voters
$example.voters
end
def candidates
$example.candidates
end
def ballots
@ballots ||= voters.map do | a_voter |
ScoreBallot.new do |it|
it.election = self
it.voter = a_voter
end
end
end
def first_round
# Answer with my first round.
@first_round ||= ScoreRound.new do |it|
it.real_ballots = ballots
it.candidates = candidates
it.ordinal = 1 # for display
end
end
end
class << $example
# This is a re-open.
attr_accessor :score_election
def voters
voters_by_name.values
end
def candidates
candidates_by_name.values
end
end
$example.score_election = ScoreElection.new
class ScoreBallot < ExtendedObject
attr_writer :weight
attr_accessor :voter, :election
attr_writer :scores_by_candidate
def weight
@weight ||= voter.weight.to_r
end
def inspect
if voter
"{ballot of #{voter.inspect} wt:#{weight} #{scores_by_candidate.size} scores}"
else
"{a score ballot}"
end
end
def max_score
election.max_score
end
def scores_by_candidate
@scores_by_candidate ||= lambda do
r = Hash.new
voter.scores_by_candidate.each do | cand, score |
r[cand] = score.to_r
end
r
end.call
end
def weighted_score_for_candidate a_candidate
hit = scores_by_candidate[a_candidate]
if hit
hit * weight
else
nil
end
end
def []=(k, v)
scores_by_candidate[k] = v
end
def deweighted_with_winners some_winners
# Answer with a ballot similar to myself but with weight determined by
# some_winners as canddidates who have already won in prior rounds of the
# multi-winner election.
sum = some_winners.inject(0.to_r) do | acc, a_winner |
acc + ((weighted_score_for_candidate a_winner) || 0.to_r)
end
new_weight = Rational(1, 2) * voter.weight / (Rational(1, 2) + sum.to_r / max_score)
if new_weight == weight
self
else
n = self.class.allocate
n.scores_by_candidate = scores_by_candidate
n.voter = voter
n.election = election
n.weight = new_weight
n
end
end
end
class FakeBallot < ExtendedObject
# There exist versions of score election code that do not use this class.
attr_accessor :weight
def weighted_score_for_candidate any_candidate
0.to_r
end
def deweighted_with_winners some_winners
self
end
end
class RoundCandidateTally
# The purpose of a round candidate tally is to calculate the total score that
# a candidate receives in a round.
attr_accessor :candidate, :round
def score
@score ||= lambda do
acc = Rational(0)
base = Rational(0)
round.ballots.each do | a_ballot |
hit = a_ballot.weighted_score_for_candidate candidate
if hit
acc += hit
# base += a_ballot.weight
end
base += a_ballot.weight # even if candidate not mentioned on ballot.
end
acc / base
end.call
end
def inspect
if @candidate
"{#{score.to_f} #{candidate.name}}"
else
"{an uninitialized tally object}"
end
end
end
class ScoreRound < ExtendedObject
# An instance represents a round of tallying for a reweighted range (score) multi-winner
# election.
attr_accessor :ballots, :candidates, :ordinal
attr_writer :prior_winners
attr_writer :winners
# Can be set from outside to express decision between ties for first place.
# Expressed as tallies, not candidates (historical reasons).
# Probably should have just one element.
def real_ballots=(them)
# Receive them as the real ballots on which to base my work.
# Internally, our ballots could also include artificial ballots for
# the "better quorum" scheme http://r...content-available-to-author-only...g.org/BetterQuorum.html
# but that feature is commented out.
# fake = FakeBallot.new {|it| it.weight = [them.size, 1000].min.to_r}
self.ballots = them # + [fake]
end
def prior_winners
# What candidates already won on prior rounds?
@prior_winners ||= []
end
def follow prior_round
# Be the round that follows prior_round.
self.ordinal = prior_round.ordinal + 1
self.prior_winners = prior_round.prior_winners + prior_round.winners.map(&:candidate)
self.ballots = prior_round.ballots.map {|e|e.deweighted_with_winners prior_winners}
self.candidates = prior_round.candidates - prior_winners
true
end
def inspect
if ordinal
"{round #{ordinal}}"
else
"{a round}"
end
end
def tallies
@tallies ||= lambda do
candidates.map do | a_candidate |
n = RoundCandidateTally.new
n.round = self
n.candidate = a_candidate
n
end
end.call
end
def ordered_tallies
tallies.sort_by {|t| - t.score}
end
def winners
@winners ||= leaders # by default; can be set otherwise.
end
def leaders
# Answer with the tallies of the candidates in first place by score.
@leaders ||= lambda do
ordered_tallies = self.ordered_tallies
acc = []
unless ordered_tallies.empty?
cur = 1
lim = ordered_tallies.size
top_score = ordered_tallies.first.score
acc = [ordered_tallies.first]
while cur < lim && (hit = ordered_tallies[cur]).score == top_score
acc += [hit]
cur += 1
end # while
end # unless
acc
end.call
end # def
def successor
r = self.class.new
r.follow self
r
end
def next_round
successor
end
def next # Can do this with a keyword? Yes, but be careful with implied "self".
successor
end
end # class
################################################################################
# #
# O G L E #
# #
################################################################################
# Rank election by my best understanding of the method of James Ogle despite being
# somewhat confused by his communications about it.
# An Ogle election only has one round. However, to make the code parallel the
# code for a score election, I will objectify the round.
class OgleElection < ExtendedObject
def voters
$example.voters
end
def candidates
$example.candidates
end
def ballots
@ballots ||= voters.map do | a_voter |
OgleBallot.new do |it|
it.election = self
it.voter = a_voter
end
end
end
def first_round
# Answer with my first round.
@first_round ||= OgleRound.new do |it|
it.real_ballots = ballots
it.candidates = candidates
it.ordinal = 1 # for display
end
end
end
class << $example
attr_accessor :rank_election
end
$example.rank_election = OgleElection.new
class OgleTally
# An instance denotes the tally of a candidate across the ballots.
attr_accessor :candidate, :round
def high_order_part
do_tally unless @high_order_part
@high_order_part
end
def low_order_part
do_tally unless @low_order_part
@low_order_part
end
def inspect
if @candidate && @round
"{(#{high_order_part}, #{low_order_part}) #{candidate.name}}"
else
"{a tally}"
end
end
def <=> another
if high_order_part < another.high_order_part
1
elsif high_order_part > another.high_order_part
-1
elsif low_order_part < another.low_order_part
1
elsif low_order_part > another.low_order_part
-1
else
0
end
end
def do_tally
# Private.
high = 0
low = 0
round.ballots.each do | a_ballot |
hit = a_ballot.rank_for_candidate candidate
if hit
high += a_ballot.weight # tic count.
low -= hit * a_ballot.weight # sum rank numbers negated.
end
end
@high_order_part = high
@low_order_part = low
true
end
end
class OgleRound < ExtendedObject
attr_accessor :ballots, :candidates, :ordinal
attr_writer :prior_winners, :winners
def real_ballots=(them)
# Receive them as the real ballots on which to base my work.
self.ballots = them
end
def prior_winners
# What candidates already won on prior rounds?
@prior_winners ||= []
end
def follow prior_round
# Be the round that follows prior_round.
self.ordinal = prior_round.ordinal + 1
self.prior_winners = prior_round.prior_winners + prior_round.winners.map(&:candidate)
self.ballots = prior_round.ballots.map {|e|e.deweighted_with_winners prior_winners}
self.candidates = prior_round.candidates - prior_winners
true
end
def inspect
if ordinal
"{round #{ordinal}}"
else
"{a round}"
end
end
def tallies
@tallies ||= lambda do
candidates.map do | a_candidate |
n = OgleTally.new
n.round = self
n.candidate = a_candidate
n
end
end.call
end
def ordered_tallies
tallies.sort
end
def winners
@winners ||= leaders
end
def leaders
# Answer with the tallies of the candidates in first place by Ogle pair.
throw "Don't ask."
end # def
def successor
r = self.class.new
r.follow self
r
end
def next_round
successor
end
def next # Can do this with a keyword? Yes, but be careful with implied "self".
successor
end
end
class OgleBallot < ExtendedObject
attr_accessor :voter, :election
attr_writer :ranks_by_candidate
attr_writer :weight # for made-up examples where a ballot represents many.
def inspect
if voter
"{ballot of #{voter.inspect} #{ranks_by_candidate.size} ranks}"
else
"{a score ballot}"
end
end
def rank_for_candidate a_candidate
ranks_by_candidate[a_candidate]
end
def weight
@weight ||= voter.weight || 1
end
def ranks_by_candidate
@ranks_by_candidate ||= lambda do
r = voter.ranks_by_candidate
# Just do a sanity check.
candidates_by_rank = Array.new r.size
r.each {|k, v| candidates_by_rank[v - 1] = k}
candidates_by_rank.each do |e|
e || (throw "Bad rank ballot #{voter.inspect}.")
end
r
end.call
end
end
################################################################################
# #
# B U C K L I N #
# #
################################################################################
# What's it take to run a Bucklin election?
class BucklinElection < ExtendedObject
def electorate_size
@electorate_size ||= ballot_models.inject(0){| a, e | a + e.instance_count}
end
def half_electorate_size
@half_electorate_size ||= electorate_size * Rational(1, 2)
end
def finds_majority_in_score a_score
a_score > half_electorate_size
end
def candidates
$example.candidates
end
def assess_ballot_models
# Initialize myself according to what I find in the example ballot_models.
self.approval_grade_count = [ $example.voters.map do |b|
b.ranks_by_candidate.values.max
end.max, 3 ].max
true
end
def approval_grade_count= the_count
# Initialize myself to tally the election based on the given count of
# Bucklin grades or ranks indicating approval.
@approval_grade_count = the_count
end
def approval_grade_count
@approval_grade_count or assess_ballot_models
@approval_grade_count
end
def grade_from_one_based_rank_index an_index
# Given a one-based index to a grade viewed as a rank, where rank 1 is the
# most preferred rank and higher numbers correspond to the less-preferred
# ranks, answer with the abstrct grade object representing the corresponding
# grade in the Bucklin election.
grades_by_rank_index[an_index - 1]
end
def most_preferred_grade
@most_preferred_grade ||= grades_by_rank_index[0]
end
def grades_by_rank_index
@grades_by_rank_index ||= lambda do # called immediately.
they = Array.new approval_grade_count
(0..(approval_grade_count - 1)).each do|i|
they[i] = BucklinGrade.new {|n| n.rank_index = i}
end
(1..(approval_grade_count - 1)).each do|i|
they[i - 1].next_less_preferred = they[i]
end
they
end.call
end
def grades
grades_by_rank_index.values
end
def ballot_models
@ballot_models ||= $example.voters.map do | a_voter |
BucklinBallotModel.new do |b|
b.instance_count = a_voter.weight
a_dict = Hash.new
a_voter.ranks_by_candidate.each do | candidate, one_based_rank |
a_dict[candidate] = grade_from_one_based_rank_index one_based_rank
end
b.grades_by_candidate = a_dict
end
end
end
def first_round
BucklinRound.new {|n| n.election = self}
end
end
class BucklinBallotModel < ExtendedObject
# An instance represents a group of ballots all marked the same way.
attr_accessor :grades_by_candidate, :instance_count
end
class BucklinGrade < ExtendedObject
# An instance represents a grade or rank.
attr_accessor :rank_index # zero-based index where most preferred gets lowest.
attr_accessor :next_less_preferred
end
class BucklinCandidateTally < ExtendedObject
attr_accessor :candidate
attr_accessor :approval_accumulators_by_rank_index
end
class BucklinRound < ExtendedObject
# An instance represents a round in the election.
# It might be the only round.
attr_accessor :election
def finds_majority_in_score a_score
election.finds_majority_in_score a_score
end
def approval_grade_count
election.approval_grade_count
end
def ballot_models
election.ballot_models
end
def candidates
election.candidates
end
def most_preferred_grade
election.most_preferred_grade
end
def working_tallies
# Answer with data structures allocated for the candidates'
# tallies but not necessarily filled in with the totals yet.
@working_tallies ||= candidates.map do | c |
BucklinCandidateTally.new do |n|
n.candidate = c
n.approval_accumulators_by_rank_index = [0] * approval_grade_count
end
end
end
def working_tallies_by_candidate
@working_tallies_by_candidate ||= working_tallies.inject({}) do | a, e |
a.update({e.candidate => e})
end
end
def working_tally_for_candidate a_candidate
working_tallies_by_candidate[a_candidate]
end
def tallies
# Answer with the candidate tallies for this round.
@tallies or ballot_models.each do | ab |
ab.grades_by_candidate.each do | candidate, grade |
( working_tally_for_candidate candidate
).approval_accumulators_by_rank_index[grade.rank_index] +=
ab.instance_count
end
end
@tallies = @working_tallies
end
def first_miniround
BucklinMiniround.new do |n|
n.round = self
n.through_grade = self.most_preferred_grade
end
end
end
class BucklinMiniround < ExtendedObject
# A mini-round deals with the grades from the most preferred down through
# some given grade, and looks at the candidates with regard to their scores
# taken through that range of grades.
attr_accessor :round, :through_grade
def finds_majority_in_score a_score
round.finds_majority_in_score a_score
end
def tallies
@tallies ||= round.tallies.map do |e| BucklinTallyThroughGrade.new do |n|
n.tally = e
n.through_grade = through_grade
end end
end
def majority_tallies
tallies.select{|e| finds_majority_in_score e.score}
end
def next
ng = through_grade.next_less_preferred
ng and self.class.new do |n|
n.round = round
n.through_grade = ng
end
end
def inspect
"{a miniround}"
end
end
class BucklinTallyThroughGrade < ExtendedObject
# An instance holds a candidate's score summed down through a certain grade
# in a round of a Bucklin election.
attr_accessor :tally, :through_grade
def score
@score ||= (0..(through_grade.rank_index)).inject(0) do | a, i |
a + approval_accumulators_by_rank_index[i]
end
end
def approval_accumulators_by_rank_index
tally.approval_accumulators_by_rank_index
end
def inspect
if @tally && @through_grade
"{#{score} #{candidate.name}}"
else
super
end
end
def candidate
tally.candidate
end
end
################################################################################
# #
# Symmetric Improved Condorcet Top #
# #
################################################################################
class SymmetricImprovedCondercetTopElection < ExtendedObject
# Symmetric Improved-Condorcet-Top
# http://w...content-available-to-author-only...a.com/wiki/Symmetrical_ICT
class BallotModel < ExtendedObject
# An instance represents a group of ballots that are marked the same way.
attr_accessor :underlying # My basis in the raw data.
def ranks__over a, b
# Answer whether I rank candidate a over candidate b.
if ! score_for_candidate(a)
false
elsif ! score_for_candidate(b)
true
else
score_for_candidate(a) > score_for_candidate(b)
end # if
end # ranks__over
def score_for_candidate a_candidate
# Answer with the score that I assign to a_candidate.
# If none, answer nil.
underlying.scores_by_candidate[a_candidate.underlying]
end
def instance_count
# How many ballots are in me?
underlying.weight
end
def ranks_at_bottom a_candidate
# Answer whether I rank a_candidate at the bottom.
i = score_for a_candidate
! i || 0 == i
end
def ranks_at_top a_candidate
# Answer whether I rank a_candidate at the top.
underlying.scores_by_candidate.values.max == (score_for a_candidate)
end
def score_for a_candidate
score_for_candidate a_candidate
end
end # BallotModel
class Candidate < ExtendedObject
attr_accessor :underlying # the rawer data structure that underlies me.
attr_accessor :election # the election to which I belong.
def inspect
if @underlying
"{#{@underlying.name}}"
else
"{an unitialized Candidate structure for Symmetric ICT}"
end
end
def beats another
# Answer whether I beat the indicated other candidate.
(self % another).beats
end
def % another
# Answer with the pair of candidates consisting of myself and another
# in that order. Another should not be me.
another.inverse_percent self
end # %
def inverse_percent another
# Indicate the pair of which another is the left member and I am the
# right member. another should not be me.
inward_edges_by_candidate[another]
end
def is_unbeaten
# Answer whether I am unbeaten.
if nil == @is_unbeaten
inward_edges.each do | a_pair |
if a_pair.beats
return @is_unbeaten = false # Someone beat me.
end
end
# The above loop contains an early return.
@is_unbeaten = true
end
# This method contains an early return.
@is_unbeaten
end
def inward_edges
# Answer with the candidate pairs of which I am the right-hand candidate.
@inward_edges ||= others.map do | another |
CandidatePair.new{|n| n.left = another; n.right = self}
end
end
def inward_edges_by_candidate
@inward_edges_by_candidate ||= inward_edges.inject({}) do | a, e |
a[e.left] = e
a
end
end
def others
# Indicate the candidates who are not me.
all.select{|e| self != e}
end
def all
# Indicate the candidates in the election.
election.candidates
end
def top_count
# The count of ballots that rank me at the top.
@top_count_prim ||= ballot_models.inject(0) do
| a, e|
if e.ranks_at_top self
a + e.instance_count
else
a
end # if
end # do
end # top_count
def ballot_models; election.ballot_models; end
end # Candidate
class CandidatePair < ExtendedObject
# An instance represents an ordered pair of different candidates.
attr_accessor :left, :right
attr_reader :bottom_count_prim, :top_count_prim # for use with mates.
def bottom_count
# Return the count of ballots that rank both my members at the bottom.
@bottom_count_prim ||= transpose.bottom_count_prim || ballot_models.inject(0) do
| a, e|
if e.ranks_at_bottom left and e.ranks_at_bottom right
a + e.instance_count
else
a
end
end
end
def top_count
# Return the count of ballots that rank both my members at the top.
@top_count_prim ||= transpose.top_count_prim || ballot_models.inject(0) do
| a, e|
if e.ranks_at_top left and e.ranks_at_top right
a + e.instance_count
else
a
end
end
end
def election
right.election
end
def ballot_models
election.ballot_models
end
def beats
if approximately_beats
if transpose.approximately_beats
over_count > transpose.over_count
else
true
end
else
false
end
end # beats
def approximately_beats
# Answer whether by the first-cut definition of "beats", left would be
# said to beat right.
over_count + bottom_count > transpose.over_count + top_count
end
def over_count
# Return the count of ballots that rate left over right.
@over_count ||= ballot_models.inject(0) do | a, m |
if m.ranks__over(left, right)
a + m.instance_count
else
a
end
end
end # over_count
def transpose
# Answer with the candidate pair that has the same candidates I have, but
# in the opposite order.
@transpose ||= right % left
end
end # CandidatePair
def unbeaten_candidates_count
@unbeaten_candidates_count ||= unbeaten_candidates.size
end
def unbeaten_candidates
@unbeaten_candidates ||= candidates.select{|c|c.is_unbeaten}
end
def candidates_count
@candidates_count ||= candidates.size
end
def candidates
@candidates ||= underlying.candidates_by_name.values.map do | uc |
Candidate.new{|n| n.underlying = uc; n.election = self}
end
end
def leaders
# Answer with the candidates tied for the win.
if unbeaten_candidates_count == 1
unbeaten_candidates
elsif unbeaten_candidates_count == 0 or
unbeaten_candidates_count == candidates_count
# then
leaders_by_top_count_among candidates
else
leaders_by_top_count_among unbeaten_candidates
end
end
def print_leaders_with_explanation
# Print the names of the candidates tied for the win, with a top-level
# explanation of by what rule we arrive at that result.
if unbeaten_candidates_count == 1
puts "There is only one unbeaten candidate, #{unbeaten_candidates.first}."
elsif unbeaten_candidates_count == 0 or
unbeaten_candidates_count == candidates_count
# then
puts unbeaten_candidates_count == 0 ?
"There are no unbeaten candidates." :
"All #{candidates_count} candidates are unbeaten."
puts "The candidates who received top billing from the most ballots are" +
" " + (leaders_by_top_count_among candidates).inspect + "."
else
puts "Some, but not all, of the candidates are unbeaten."
puts "Among the unbeaten ones, these are the ones who received top"
puts "rankings from the highest count of ballots:"
puts leaders_by_top_count_among(unbeaten_candidates).inspect
end
end
def leaders_by_top_count_among some_candidates
# Answer with the candidates from among some_candidates who received top
# ranking from the most ballots.
leaders_among(some_candidates) {|e| e.top_count}
end
def leaders_among(some_objects, &y)
# Answer with those among some_objects to which the result of calling the
# given block on them is the highest.
max_r = some_objects.map(&y).max
some_objects.select{|e| max_r == y.call(e)}
end
def underlying
# Indicate the rawer data structure that underlies me.
$example
end
def ballot_models
@ballot_models ||= underlying.voters.map do |v| BallotModel.new do |n|
n.underlying = v
end end
end
end # SymmetricImprovedCondercetTopElection
################################################################################
# #
# Top-level Queries #
# #
################################################################################
# Who wins the first round of the score election? Let r1 be the first round.
r1 = $example.score_election.first_round
puts "Score election."
puts
puts "Round 1, ordered tallies:"
r1.ordered_tallies.each {|e| puts " " + e.inspect}
puts
puts "Round 1 leaders #{r1.leaders}"
# puts "Manual resolution of tie: fine Score used as the winner."
# r1.winners = [r1.leaders[0]]
# puts "r1.winners #{r1.winners}"
r2 = r1.next
puts "Round 2 leaders #{r2.leaders}."
r3 = r2.next
puts "Round 3 leaders #{r3.leaders}."
r4 = r3.next
puts "Round 4 leaders #{r4.leaders}."
puts
puts
puts "Symmetric Improved Condorcet Top election:"
puts
sicte = SymmetricImprovedCondercetTopElection.new
sicte.print_leaders_with_explanation