diff --git a/gen_tokens.rb b/gen_tokens.rb index 0552ece..908e211 100644 --- a/gen_tokens.rb +++ b/gen_tokens.rb @@ -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! @@ -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 @@ -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) @@ -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>] column The columns containing pertinent info # @param [Hash Array>] 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 @@ -219,7 +225,8 @@ def self.determine_header_columns(columns, line) # # @param [Hash Array>] all_tokens The mapping into which the tokens will be inserted # @param [Array>] 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>] columns = { Org: 0, Delegates: 0 } @@ -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 @@ -254,13 +261,14 @@ 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 Array>] 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 @@ -268,5 +276,5 @@ def self.main(generate_pdfs) end end -TokenGenerator.main generate_pdfs if __FILE__ == $PROGRAM_NAME +TokenGenerator.main(generate_pdfs, token_char_count) if __FILE__ == $PROGRAM_NAME # :nocov: diff --git a/tests/gen_tokens_test.rb b/tests/gen_tokens_test.rb index f849d21..f34b5dd 100644 --- a/tests/gen_tokens_test.rb +++ b/tests/gen_tokens_test.rb @@ -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 @@ -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 diff --git a/tests/vote_parser_test.rb b/tests/vote_parser_test.rb index 7c4791c..dd1f645 100644 --- a/tests/vote_parser_test.rb +++ b/tests/vote_parser_test.rb @@ -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 ) ) @@ -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) @@ -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( { @@ -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( @@ -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( diff --git a/vote_parser.rb b/vote_parser.rb index bbab752..46535d7 100644 --- a/vote_parser.rb +++ b/vote_parser.rb @@ -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 # @@ -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) @@ -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] vote A collection of the individuals receiving votes # @param [Integer] position The index of the vote position in vote @@ -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] vote A collection of the individuals receiving votes @@ -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 @@ -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 @@ -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], @@ -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: