Skip to content

Commit

Permalink
Version 1.5.1: Fix order text when validating voters for multiple votes
Browse files Browse the repository at this point in the history
  • Loading branch information
hbiede committed Jun 25, 2024
1 parent 3d601d3 commit 5a23e02
Show file tree
Hide file tree
Showing 2 changed files with 71 additions and 44 deletions.
50 changes: 31 additions & 19 deletions tests/vote_parser_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ def test_validate_vote
vote = ['George Washington', 'John Adams', 'Thomas Jefferson', 'Alexander Hamilton']
alt_vote = ['George Washington', 'Aaron Burr', 'Thomas Jefferson', 'Someone Else']
abstain_vote = ['George Washington', '', '', '']
short_abstain_cote = ['George Washington']
short_abstain_vote = ['George Washington']
token_mapping = { 'abc' => 'A', 'bcd' => 'B', 'cde' => 'C', 'def' => 'D', 'efg' => 'E', 'fgh' => 'F' }

assert_equal('', VoteParser.validate_vote(vote_count, used_tokens, ['abc'].concat(vote), token_mapping))
Expand Down Expand Up @@ -197,7 +197,7 @@ def test_validate_vote
4 => { 'Alexander Hamilton' => 2, 'Someone Else' => 2 }
}, vote_count)

assert_equal('', VoteParser.validate_vote(vote_count, used_tokens, ['fgh'].concat(short_abstain_cote), token_mapping))
assert_equal('', VoteParser.validate_vote(vote_count, used_tokens, ['fgh'].concat(short_abstain_vote), token_mapping))
assert_equal({ 'abc' => true, 'bcd' => true, 'cde' => true, 'def' => true, 'efg' => true, 'fgh' => true }, used_tokens)
assert_equal({
1 => { 'George Washington' => 6 },
Expand All @@ -206,7 +206,7 @@ def test_validate_vote
4 => { 'Alexander Hamilton' => 2, 'Someone Else' => 2 }
}, vote_count)

assert_equal("fgh (F) voted multiple times. Using latest.\n", VoteParser.validate_vote(vote_count, used_tokens, ['fgh'].concat(short_abstain_cote), token_mapping))
assert_equal("fgh (F) voted multiple times. Using latest.\n", VoteParser.validate_vote(vote_count, used_tokens, ['fgh'].concat(short_abstain_vote), token_mapping))
assert_equal({ 'abc' => true, 'bcd' => true, 'cde' => true, 'def' => true, 'efg' => true, 'fgh' => true }, used_tokens)
assert_equal({
1 => { 'George Washington' => 6 },
Expand All @@ -218,26 +218,25 @@ def test_validate_vote

def test_generate_vote_totals
# Messages tests
assert_equal('', VoteParser.generate_vote_totals({}, {}, [['abc', '']], { 'abc' => 'A' }, true))
assert_equal('', VoteParser.generate_vote_totals({}, {}, [['abc', '']], { 'abc' => 'A' }))
assert_equal(
"abc (A) voted multiple times. Using latest.\n",
VoteParser.generate_vote_totals({}, { 'abc' => true }, [['abc', '']], { 'abc' => 'A' }, true)
VoteParser.generate_vote_totals({}, { 'abc' => true }, [['abc', '']], { 'abc' => 'A' })
)
assert_equal(
"abc (A) voted multiple times. Using latest.\nabc (A) voted multiple times. Using latest.\n",
VoteParser.generate_vote_totals({}, { 'abc' => true }, [['abc', ''], ['abc', '']], { 'abc' => 'A' }, true)
VoteParser.generate_vote_totals({}, { 'abc' => true }, [['abc', ''], ['abc', '']], { 'abc' => 'A' })
)
assert_equal(
"xyz is an invalid token. Vote not counted.\nabc (A) voted multiple times. Using latest.\nabc (A) voted multiple times. Using latest.\n",
VoteParser.generate_vote_totals({}, { 'abc' => true }, [['abc', ''], ['abc', ''], ['xyz', '']], { 'abc' => 'A' }, true))
VoteParser.generate_vote_totals({}, { 'abc' => true }, [['abc', ''], ['abc', ''], ['xyz', '']], { 'abc' => 'A' }))
assert_equal(
"xyz is an invalid token. Vote not counted.\nabc (A) voted multiple times. Using latest.\nabc (A) voted multiple times. Using latest.\nxyz2 is an invalid token. Vote not counted.\n",
VoteParser.generate_vote_totals(
{},
{ 'abc' => true },
[['xyz2', ''], ['abc', ''], ['abc', ''], ['xyz', '']],
{ 'abc' => 'A' },
true
{ 'abc' => 'A' }
)
)

Expand All @@ -248,8 +247,7 @@ def test_generate_vote_totals
vote_counts,
used_tokens,
[%w[xyz2 AVote1 BVote1], %w[abc AVote2 BVote2], %w[abc AVote3 BVote3], %w[xyz AVote4 BVote4]],
{ 'abc' => 'A' },
true
{ 'abc' => 'A' }
)
assert_equal({ 1 => { 'AVote3' => 1 }, 2 => { 'BVote3' => 1 } }, vote_counts)
assert_equal({ 'abc' => true }, used_tokens)
Expand All @@ -260,8 +258,7 @@ def test_generate_vote_totals
vote_counts,
used_tokens,
[%w[xyz2 AVote1 BVote1], %w[abc AVote2 BVote2], %w[abc AVote3 BVote3], %w[xyz AVote4 BVote4]],
{ 'abc' => 'A', 'xyz' => 'X', 'xyz2' => 'X2' },
true
{ 'abc' => 'A', 'xyz' => 'X', 'xyz2' => 'X2' }
)
assert_equal(
{
Expand Down Expand Up @@ -296,8 +293,7 @@ def test_generate_vote_totals
'hi4' => 'H',
'xyz' => 'X',
'xyz2' => 'X2'
},
true
}
)
assert_equal("fake is an invalid token. Vote not counted.\nabc (A) voted multiple times. Using latest.\n", warning)
assert_equal(
Expand All @@ -314,8 +310,25 @@ def test_generate_vote_totals
vote_counts,
used_tokens,
[%w[xyz2 AVote1 BVote1], %w[abc AVote2 BVote2], %w[abc AVote3 BVote3], %w[xyz AVote4 BVote4]],
{ 'abc' => 'A', 'xyz' => 'X', 'xyz2' => 'X2' },
false
{ 'abc' => 'A', 'xyz' => 'X', 'xyz2' => 'X2' }
)
assert_equal(
{
1 => { 'AVote1' => 1, 'AVote3' => 1, 'AVote4' => 1 },
2 => { 'BVote1' => 1, 'BVote3' => 1, 'BVote4' => 1 }
},
vote_counts)
assert_equal({ 'abc' => true, 'xyz' => true, 'xyz2' => true }, used_tokens)

# Reset

vote_counts = {}
used_tokens = {}
VoteParser.generate_vote_totals(
vote_counts,
used_tokens,
[%w[xyz AVote4 BVote4], %w[abc AVote3 BVote3], %w[abc AVote2 BVote2], %w[xyz2 AVote1 BVote1]],
{ 'abc' => 'A', 'xyz' => 'X', 'xyz2' => 'X2' }
)
assert_equal(
{
Expand Down Expand Up @@ -402,8 +415,7 @@ def test_process_votes
'hi4' => 'H',
'xyz' => 'X',
'xyz2' => 'X2'
},
true
}
)
assert_equal("fake is an invalid token. Vote not counted.\nabc (A) voted multiple times. Using latest.\n", result[:Warning])
assert_equal(
Expand Down
65 changes: 40 additions & 25 deletions vote_parser.rb
Original file line number Diff line number Diff line change
@@ -1,23 +1,33 @@
# frozen_string_literal: true

# Author: Hundter Biede (hbiede.com)
# Version: 1.5
# Version: 1.5.1
# License: MIT

require 'csv'
require 'optparse'
require 'singleton'

# Parses CLI flags
class OptionHandler
include Singleton

def initialize
@options = { reverse: true }
OptionParser.new do |opt|
opt.on(
'-o',
'--in-order',
TrueClass,
'If the votes should be counted in chronological order, keeping the first (defaults to false)'
) { |o| @options[:reverse] = o }
end.parse!
end

# :nocov:
options = { reverse: true }
OptionParser.new do |opt|
opt.on(
'-o',
'--in-order',
TrueClass,
'If the votes should be counted in chronological order, keeping the first (defaults to false)'
) { |o| options[:reverse] = o }
end.parse!
# :nocov:
def reversed?
@options[:reverse]
end
end

# Strip timestamps from vote results
#
Expand Down Expand Up @@ -109,6 +119,15 @@ def self.parse_single_vote(vote_counts, vote, position)
end
end

# @param [String] token The multi-voting token
# @param [String] school The school the token is from
# @return [String] the warning associated with the vote
def self.get_double_vote_string(token, school)
order_string = OptionHandler.instance.reversed? ? 'latest' : 'first'
format("%<ID>s (%<School>s) voted multiple times. Using %<Time>s.\n", ID: token, School: school,
Time: order_string)
end

# Validate an entire ballot and parse out its component votes
#
# @param [Hash{Integer => Hash{String => Integer}}] vote_counts The mapping of a
Expand All @@ -119,10 +138,9 @@ def self.parse_single_vote(vote_counts, vote, position)
# @return [String] the warning associated with the vote
def self.validate_vote(vote_counts, used_tokens, vote, token_mapping)
if used_tokens.include?(vote[0])
format("%<ID>s (%<School>s) voted multiple times. Using latest.\n", ID: vote[0], School: token_mapping[vote[0]])
else
get_double_vote_string(vote[0], token_mapping[vote[0]])
else # token hasn't been used. count votes
used_tokens.store(vote[0], true)
# token hasn't been used. count votes
(1...vote.length).each do |position|
next if vote[position].nil? || vote[position].empty?

Expand All @@ -139,11 +157,10 @@ def self.validate_vote(vote_counts, used_tokens, vote, token_mapping)
# @param [Hash{String => Boolean}] used_tokens A collection of all the tokens already used
# @param [Array[Array[String]]] votes The 2D array interpretation of the CSV
# @param [Hash{String => String}] token_mapping The mapping of the token onto a school
# @param [Boolean] reverse True iff the last vote for a token should be counted, else the first
# @return [String] the warnings generated
def self.generate_vote_totals(vote_counts, used_tokens, votes, token_mapping, reverse)
def self.generate_vote_totals(vote_counts, used_tokens, votes, token_mapping)
warning = ''
(reverse ? votes.reverse : votes).each do |vote|
(OptionHandler.instance.reversed? ? votes.reverse : votes).each do |vote|
warning += if token_mapping.key?(vote[0])
validate_vote(vote_counts, used_tokens, vote, token_mapping)
else
Expand Down Expand Up @@ -177,17 +194,16 @@ def self.init(vote_file, token_file)
# rows representing individual ballots and columns representing entries votes
# for a given position
# @param [Hash{String => String}] token_mapping The mapping of the token onto a school
# @param [Boolean] reverse True iff the last vote for a token should be counted, else the first
# @return [Hash{Symbol=>Integer,String,Hash{Integer=>Hash{String=>Integer}}] A
# collection of the primary output and all warnings
def self.process_votes(votes, token_mapping, reverse)
def self.process_votes(votes, token_mapping)
# @type [Hash{Integer=>Hash{String=>Integer}}]
vote_counts = {}

# @type [Hash{String => Boolean}]
used_tokens = {}

warning = generate_vote_totals(vote_counts, used_tokens, votes, token_mapping, reverse)
warning = generate_vote_totals(vote_counts, used_tokens, votes, token_mapping)
{ TotalVoterCount: used_tokens.length, VoteCounts: vote_counts, Warning: warning }
end
end
Expand Down Expand Up @@ -449,13 +465,12 @@ def self.write_output(election_report, warning, file)

# :nocov:
# Manage the program
def main(opt)
def main
VoteParser.vote_arg_count_validator ARGV
input = VoteParser.init(ARGV[0], ARGV[1])
# noinspection RubyMismatchedParameterType
# @type [Hash{Symbol=>Integer,String,Hash{Integer=>Hash{String=>Integer}}]
processed_values = VoteParser.process_votes(input[:Votes], input[:TokenMapping],
opt[:reverse])
processed_values = VoteParser.process_votes(input[:Votes], input[:TokenMapping])
# noinspection RubyMismatchedParameterType
election_report = OutputPrinter.vote_report(
processed_values[:TotalVoterCount],
Expand All @@ -465,5 +480,5 @@ def main(opt)
OutputPrinter.write_output(election_report, processed_values[:Warning], ARGV[2])
end

main(options) if __FILE__ == $PROGRAM_NAME
main if __FILE__ == $PROGRAM_NAME
# :nocov:

0 comments on commit 5a23e02

Please sign in to comment.