Skip to content

Commit

Permalink
Version 1.4
Browse files Browse the repository at this point in the history
  • Loading branch information
hbiede committed Mar 4, 2024
1 parent 6afaaee commit fde055d
Show file tree
Hide file tree
Showing 4 changed files with 67 additions and 40 deletions.
34 changes: 21 additions & 13 deletions gen_tokens.rb
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
# frozen_string_literal: true

# Author: Hundter Biede (hbiede.com)
# Version: 1.3
# Version: 1.4
# License: MIT
require 'csv'
require 'optparse'

# how many characters to pad
token_char_count = 7
# Whether or not to generate PDFs
generate_pdfs = true
OptionParser.new do |opt|
opt.on(
'-cCOUNT',
'--chars=COUNT',
Integer,
'How many characters long should tokens be? (Default: 7)'
) { |o| token_char_count = o }
opt.on('-n', '--no-pdfs', 'Disable PDF generation') { generate_pdfs = false }
end.parse!

Expand All @@ -24,9 +32,6 @@
(basta)|(ar[s5])|(ana[l1])|(anu[s5])|(ba[l1][l1])|(b[l1][o0]w)|(b[o0][o0]b)|
([l1]mf?a[o0])/ix.freeze

# how many characters to pad
TOKEN_LENGTH = 7

# Writes tokens to PDFs
class PDFWriter
# Compile a unique PDF for a singular organization with its passwords and
Expand Down Expand Up @@ -173,7 +178,7 @@ def self.random_string(length)
# generated, used to prevent duplicates
# @param [Numeric] token_length The length of the token
# @return [String] the new token
def self.gen_token(all_tokens, token_length = TOKEN_LENGTH)
def self.gen_token(all_tokens, token_length = token_char_count)
new_token = ''
loop do
new_token = random_string(token_length)
Expand All @@ -188,14 +193,15 @@ def self.gen_token(all_tokens, token_length = TOKEN_LENGTH)
# @param [CSV::Row|Enumerator] line The elements from this line to be processed
# @param [Hash<Integer => integer>] column The columns containing pertinent info
# @param [Hash<String => Array<String>>] all_tokens
def self.process_chapter(line, column, all_tokens)
# @param [Numeric] token_length The length of the token
def self.process_chapter(line, column, all_tokens, token_char_count)
org = line[column[:Org]]
(0...line[column[:Delegates]].to_i).each do
# gen tokens and push to the csv
if all_tokens.include?(org)
all_tokens.fetch(org).push(gen_token(all_tokens))
all_tokens.fetch(org).push(gen_token(all_tokens, token_char_count))
else
all_tokens.store(org, [gen_token(all_tokens)])
all_tokens.store(org, [gen_token(all_tokens, token_char_count)])
end
end
end
Expand All @@ -219,7 +225,8 @@ def self.determine_header_columns(columns, line)
#
# @param [Hash<String => Array<String>>] all_tokens The mapping into which the tokens will be inserted
# @param [Array<Array<String>>] lines The lines from the delegate count CSV
def self.parse_organizations(all_tokens, lines)
# @param [Numeric] token_length The length of the token
def self.parse_organizations(all_tokens, lines, token_char_count)
# index of our two key columns (all other columns are ignored)
# @type [Hash<Integer => integer>]
columns = { Org: 0, Delegates: 0 }
Expand All @@ -232,7 +239,7 @@ def self.parse_organizations(all_tokens, lines)
# header line
determine_header_columns(columns, line)
else
process_chapter(line, columns, all_tokens)
process_chapter(line, columns, all_tokens, token_char_count)
end
end
end
Expand All @@ -254,19 +261,20 @@ def self.get_token_count_report(all_tokens)
#
# @param [Boolean] generate_pdfs True if the program should generate PDFs with
# the generated passwords
def self.main(generate_pdfs)
# @param [Numeric] token_length The length of the token
def self.main(generate_pdfs, token_char_count)
# @type [Hash<String => Array<String>>]
all_tokens = {}
token_arg_count_validator ARGV
lines = read_delegate_csv ARGV[0]

parse_organizations(all_tokens, lines)
parse_organizations(all_tokens, lines, token_char_count)
write_tokens_to_csv(all_tokens, ARGV[1])

puts get_token_count_report all_tokens
puts PDFWriter.create_pdfs(all_tokens, File.read('pdfs/template/voting.tex')) if generate_pdfs
end
end

TokenGenerator.main generate_pdfs if __FILE__ == $PROGRAM_NAME
TokenGenerator.main(generate_pdfs, token_char_count) if __FILE__ == $PROGRAM_NAME
# :nocov:
6 changes: 3 additions & 3 deletions tests/gen_tokens_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ def test_process_chapter
}
all_tokens = {}
lines.each do |line|
TokenGenerator.process_chapter(line, columns, all_tokens)
TokenGenerator.process_chapter(line, columns, all_tokens, 7)
# noinspection RubyNilAnalysis
assert_equal(line[1], all_tokens.fetch(line[0]).length)
end
Expand Down Expand Up @@ -250,12 +250,12 @@ def test_parse_organizations
%w[Test 1],
%w[Test2 10],
]
TokenGenerator.parse_organizations(all_tokens, lines)
TokenGenerator.parse_organizations(all_tokens, lines, 7)
assert_equal(1, all_tokens["Test"].length)
assert_equal(10, all_tokens["Test2"].length)

begin
TokenGenerator.parse_organizations({}, [["", ""], ["", ""]])
TokenGenerator.parse_organizations({}, [["", ""], ["", ""]], 7)
rescue SystemExit
assert_true true
else
Expand Down
23 changes: 14 additions & 9 deletions tests/vote_parser_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -218,25 +218,26 @@ def test_validate_vote

def test_generate_vote_totals
# Messages tests
assert_equal('', VoteParser.generate_vote_totals({}, {}, [['abc', '']], { 'abc' => 'A' }))
assert_equal('', VoteParser.generate_vote_totals({}, {}, [['abc', '']], { 'abc' => 'A' }, true))
assert_equal(
"abc (A) voted multiple times. Using latest.\n",
VoteParser.generate_vote_totals({}, { 'abc' => true }, [['abc', '']], { 'abc' => 'A' })
VoteParser.generate_vote_totals({}, { 'abc' => true }, [['abc', '']], { 'abc' => 'A' }, true)
)
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' })
VoteParser.generate_vote_totals({}, { 'abc' => true }, [['abc', ''], ['abc', '']], { 'abc' => 'A' }, true)
)
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' }))
VoteParser.generate_vote_totals({}, { 'abc' => true }, [['abc', ''], ['abc', ''], ['xyz', '']], { 'abc' => 'A' }, true))
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' }
{ 'abc' => 'A' },
true
)
)

Expand All @@ -247,7 +248,8 @@ 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' }
{ 'abc' => 'A' },
true
)
assert_equal({ 1 => { "AVote3" => 1 }, 2 => { "BVote3" => 1 } }, vote_counts)
assert_equal({ "abc" => true }, used_tokens)
Expand All @@ -258,7 +260,8 @@ 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' }
{ 'abc' => 'A', 'xyz' => 'X', 'xyz2' => 'X2' },
true
)
assert_equal(
{
Expand Down Expand Up @@ -293,7 +296,8 @@ 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 Down Expand Up @@ -381,7 +385,8 @@ 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
44 changes: 29 additions & 15 deletions vote_parser.rb
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
# frozen_string_literal: true

# Author: Hundter Biede (hbiede.com)
# Version: 1.2
# Version: 1.4
# License: MIT

require 'csv'
require 'optparse'

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!

# Read the contents of the given CSV file
#
Expand Down Expand Up @@ -54,7 +65,7 @@ def self.read_tokens(file)

# Adds a new Hash to vote_counts if necessary
#
# @param [Hash{String => Hash{String => Integer}}] vote_counts The mapping of a
# @param [Hash{Integer => Hash{String => Integer}}] vote_counts The mapping of a
# position to a set of votes
# @param [Integer] position The index of the vote position in vote
def self.add_position_to_vote_counts(vote_counts, position)
Expand All @@ -63,7 +74,7 @@ def self.add_position_to_vote_counts(vote_counts, position)

# Parses out a single vote and applies its totals to the valid vote counts
#
# @param [Hash{String => Hash{String => Integer}}] vote_counts The mapping of a
# @param [Hash{Integer => Hash{String => Integer}}] vote_counts The mapping of a
# position to a set of votes
# @param [Array<String>] vote A collection of the individuals receiving votes
# @param [Integer] position The index of the vote position in vote
Expand All @@ -79,7 +90,7 @@ def self.parse_single_vote(vote_counts, vote, position)

# Validate an entire ballot and parse out its component votes
#
# @param [Hash{String => Hash{String => Integer}}] vote_counts The mapping of a
# @param [Hash{Integer => Hash{String => Integer}}] vote_counts The mapping of a
# position to a set of votes
# @param [Hash{String => Boolean}] used_tokens A collection of all the tokens already used
# @param [Array<String>] vote A collection of the individuals receiving votes
Expand All @@ -102,15 +113,16 @@ def self.validate_vote(vote_counts, used_tokens, vote, token_mapping)

# Count the number of votes in each position
#
# @param [Hash{String => Hash{String => Integer}}] vote_counts The mapping of a
# @param [Hash{Integer => Hash{String => Integer}}] vote_counts The mapping of a
# position to a set of votes
# @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)
def self.generate_vote_totals(vote_counts, used_tokens, votes, token_mapping, reverse)
warning = ''
votes.reverse.each do |vote|
(reverse ? 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 @@ -144,15 +156,17 @@ 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
# @return [Hash{Symbol=>Integer,String,Hash{String}] A collection of the primary output and all warnings
def self.process_votes(votes, token_mapping)
# @type [Hash{String=> Hash{String=>Integer}}]
# @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)
# @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)
warning = generate_vote_totals(vote_counts, used_tokens, votes, token_mapping, reverse)
{ TotalVoterCount: used_tokens.length, VoteCounts: vote_counts, Warning: warning }
end
end
Expand Down Expand Up @@ -315,12 +329,12 @@ def self.write_output(election_report, warning, file)

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

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

0 comments on commit fde055d

Please sign in to comment.