sciolyff 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 557d9424fccd8e1ef1cf039ac09a7f775475ab041238ce73a1eca5ce0e2ad7a1
4
+ data.tar.gz: 79c56f1488d33f5fa250c08216492da04dbfac9f980cf953673f3b3df48421a6
5
+ SHA512:
6
+ metadata.gz: a85a02fcb51e980d635d63a612fde179217118a81385e157001d91d733dbeea359b5d35fa24d94317555ce10ea72c1e26e70c829b2dade87689d4afe1bbbec35
7
+ data.tar.gz: 603d152c2efc7de191c050703add7803c4561c4c91b29a4ca5c9f893527b53d1259db11029508bf54d8410f4946b13925993370cd763eca998c6ed394d83d9fc
data/bin/sciolyff ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'optimist'
5
+ require 'yaml'
6
+ require 'sciolyff'
7
+
8
+ opts = Optimist.options do
9
+ version 'sciolyff 0.2.0'
10
+ banner <<~STRING
11
+ Checks if a given file is in the Scioly File Format
12
+
13
+ Usage:
14
+ #{File.basename(__FILE__)} [options] <file>
15
+
16
+ where [options] are:
17
+ STRING
18
+ opt :verbose, 'Run Minitest in verbose mode'
19
+ end
20
+
21
+ if ARGV.first.nil? || !File.exist?(ARGV.first)
22
+ puts "Error: file '#{ARGV.first}' not found."
23
+ puts 'Try --help for help.'
24
+ exit 1
25
+ end
26
+
27
+ puts 'More than one file given, ignoring all but first.' if ARGV.length > 1
28
+
29
+ SciolyFF.validate_file(ARGV.first, opts: opts)
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'minitest/test'
4
+ require 'set'
5
+
6
+ module SciolyFF
7
+ # Tests that also serve as the specification for the sciolyff file format
8
+ #
9
+ class Events < Minitest::Test
10
+ def setup
11
+ skip unless SciolyFF.rep.instance_of? Hash
12
+ @events = SciolyFF.rep[:Events]
13
+ skip unless @events.instance_of? Array
14
+ end
15
+
16
+ def test_has_valid_events
17
+ @events.each do |event|
18
+ assert_instance_of Hash, event
19
+ end
20
+ end
21
+
22
+ def test_each_event_does_not_have_extra_info
23
+ @events.select { |e| e.instance_of? Hash }.each do |event|
24
+ info = Set.new %i[name trial trialed scoring tiers]
25
+ assert Set.new(event.keys).subset? info
26
+ end
27
+ end
28
+
29
+ def test_each_event_has_valid_name
30
+ @events.select { |e| e.instance_of? Hash }.each do |event|
31
+ assert_instance_of String, event[:name]
32
+ end
33
+ end
34
+
35
+ def test_each_event_has_valid_trial
36
+ @events.select { |e| e.instance_of? Hash }.each do |event|
37
+ assert_includes [true, false], event[:trial] if event.key? :trial
38
+ end
39
+ end
40
+
41
+ def test_each_event_has_valid_trialed
42
+ @events.select { |e| e.instance_of? Hash }.each do |event|
43
+ assert_includes [true, false], event[:trialed] if event.key? :trialed
44
+ end
45
+ end
46
+
47
+ def test_each_event_has_valid_scoring
48
+ @events.select { |e| e.instance_of? Hash }.each do |event|
49
+ assert_includes %w[high low], event[:scoring] if event.key? :scoring
50
+ end
51
+ end
52
+
53
+ def test_each_event_has_valid_tiers
54
+ @events.select { |e| e.instance_of? Hash }.each do |event|
55
+ assert_instance_of Integer, event[:tiers] if event.key? :tiers
56
+ assert_includes (1..), event[:tiers] if event.key? :tiers
57
+ end
58
+ end
59
+
60
+ def test_each_event_has_unique_name
61
+ names = @events.select { |e| e.instance_of? Hash }
62
+ .map { |e| e[:name] }
63
+ assert_nil names.uniq!
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'minitest/test'
4
+ require 'set'
5
+
6
+ module SciolyFF
7
+ # Tests that also serve as the specification for the sciolyff file format
8
+ #
9
+ class Penalties < Minitest::Test
10
+ def setup
11
+ skip unless SciolyFF.rep.instance_of? Hash
12
+ @penalties = SciolyFF.rep[:Penalties]
13
+ skip unless @penalties.instance_of? Array
14
+ end
15
+
16
+ def test_has_valid_penalties
17
+ @penalties.each do |penalty|
18
+ assert_instance_of Hash, penalty
19
+ end
20
+ end
21
+
22
+ def test_each_penalty_does_not_have_extra_info
23
+ @penalties.select { |p| p.instance_of? Hash }.each do |penalty|
24
+ info = Set.new %i[team points]
25
+ assert Set.new(penalty.keys).subset? info
26
+ end
27
+ end
28
+
29
+ def test_each_penalty_has_valid_team
30
+ @penalties.select { |p| p.instance_of? Hash }.each do |penalty|
31
+ assert_instance_of Integer, penalty[:team]
32
+ skip unless SciolyFF.rep[:Teams].instance_of? Array
33
+
34
+ team_numbers = SciolyFF.rep[:Teams].map { |t| t[:number] }
35
+ assert_includes team_numbers, penalty[:team]
36
+ end
37
+ end
38
+
39
+ def test_each_penalty_has_valid_points
40
+ @penalties.select { |p| p.instance_of? Hash }.each do |penalty|
41
+ assert_instance_of Integer, penalty[:points]
42
+ end
43
+ end
44
+
45
+ def test_penalties_are_unique_for_team
46
+ teams = @penalties.select { |p| p.instance_of? Hash }
47
+ .map { |p| p[:team] }
48
+ assert_nil teams.uniq!
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'minitest/test'
4
+ require 'set'
5
+
6
+ module SciolyFF
7
+ # Tests that also serve as the specification for the sciolyff file format
8
+ #
9
+ class Placings < Minitest::Test
10
+ def setup
11
+ skip unless SciolyFF.rep.instance_of? Hash
12
+ @placings = SciolyFF.rep[:Placings]
13
+ skip unless @placings.instance_of? Array
14
+ end
15
+
16
+ def test_has_valid_placings
17
+ @placings.each do |placing|
18
+ assert_instance_of Hash, placing
19
+ end
20
+ end
21
+
22
+ def test_each_placing_does_not_have_extra_info
23
+ @placings.select { |p| p.instance_of? Hash }.each do |placing|
24
+ info = Set.new %i[event team participated disqualified place]
25
+ assert Set.new(placing.keys).subset? info
26
+ end
27
+ end
28
+
29
+ def test_each_placing_has_valid_event
30
+ @placings.select { |p| p.instance_of? Hash }.each do |placing|
31
+ assert_instance_of String, placing[:event]
32
+ skip unless SciolyFF.rep[:Events].instance_of? Array
33
+
34
+ event_names = SciolyFF.rep[:Events].map { |e| e[:name] }
35
+ assert_includes event_names, placing[:event]
36
+ end
37
+ end
38
+
39
+ def test_each_placing_has_valid_team
40
+ @placings.select { |p| p.instance_of? Hash }.each do |placing|
41
+ assert_instance_of Integer, placing[:team]
42
+ skip unless SciolyFF.rep[:Teams].instance_of? Array
43
+
44
+ team_numbers = SciolyFF.rep[:Teams].map { |t| t[:number] }
45
+ assert_includes team_numbers, placing[:team]
46
+ end
47
+ end
48
+
49
+ def test_each_placing_has_valid_participated
50
+ @placings.select { |p| p.instance_of? Hash }.each do |placing|
51
+ if placing.key? :participated
52
+ assert_includes [true, false], placing[:participated]
53
+ end
54
+ end
55
+ end
56
+
57
+ def test_each_placing_has_valid_disqualified
58
+ @placings.select { |p| p.instance_of? Hash }.each do |placing|
59
+ if placing.key? :disqualified
60
+ assert_includes [true, false], placing[:disqualified]
61
+ end
62
+ end
63
+ end
64
+
65
+ def test_each_placing_has_valid_place
66
+ @placings.select { |p| p.instance_of? Hash }.each do |placing|
67
+ next if placing[:disqualified] == true ||
68
+ placing.key?(:participated)
69
+
70
+ assert_instance_of Integer, placing[:place]
71
+ max_place = @placings.count { |p| p[:event] == placing[:event] }
72
+ assert_includes 1..max_place, placing[:place]
73
+ end
74
+ end
75
+
76
+ def test_placings_are_unique_for_event_and_place
77
+ skip unless SciolyFF.rep[:Events].instance_of? Array
78
+
79
+ SciolyFF.rep[:Events].each do |event|
80
+ next unless event.instance_of? Hash
81
+
82
+ places = @placings.select { |p| p[:event] == event[:name] }
83
+ .map { |p| p[:place] }
84
+ .compact
85
+
86
+ dups = places.select.with_index do |p, i|
87
+ places.index(p) != i
88
+ end
89
+
90
+ assert_empty dups, "The event #{event[:name]} has ties at #{dups}"
91
+ end
92
+ end
93
+
94
+ def test_placings_are_unique_for_event_and_team
95
+ skip unless SciolyFF.rep[:Teams].instance_of? Array
96
+
97
+ SciolyFF.rep[:Teams].each do |team|
98
+ next unless team.instance_of? Hash
99
+
100
+ assert_nil @placings.select { |p| p[:team] == team[:number] }
101
+ .map { |p| p[:event] }
102
+ .compact
103
+ .uniq!
104
+ end
105
+ end
106
+
107
+ def test_each_team_has_placings_for_all_events
108
+ skip unless SciolyFF.rep[:Teams].instance_of?(Array) &&
109
+ SciolyFF.rep[:Events].instance_of?(Array)
110
+
111
+ SciolyFF.rep[:Teams].select { |t| t.instance_of? Hash }.each do |team|
112
+ events = SciolyFF.rep[:Events].select { |e| e.instance_of? Hash }
113
+ .map { |e| e[:name] }
114
+
115
+ events_with_placings =
116
+ @placings.select { |p| p.instance_of? Hash }
117
+ .select { |p| p[:team] == team[:number] }
118
+ .map { |p| p[:event] }
119
+
120
+ assert_equal events, events_with_placings
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'minitest/test'
4
+ require 'set'
5
+
6
+ module SciolyFF
7
+ # Tests that also serve as the specification for the sciolyff file format
8
+ #
9
+ class Scores < Minitest::Test
10
+ def setup
11
+ skip unless SciolyFF.rep.instance_of? Hash
12
+ @scores = SciolyFF.rep[:Scores]
13
+ skip unless @scores.instance_of? Array
14
+ end
15
+
16
+ def test_has_valid_scores
17
+ @scores.each do |score|
18
+ assert_instance_of Hash, score
19
+ end
20
+ end
21
+
22
+ def test_each_score_does_not_have_extra_info
23
+ @scores.select { |s| s.instance_of? Hash }.each do |score|
24
+ a = %i[event team participated disqualified score tier]
25
+ a << :'tiebreaker place'
26
+ info = Set.new a
27
+ assert Set.new(score.keys).subset? info
28
+ end
29
+ end
30
+
31
+ def test_each_score_has_valid_event
32
+ @scores.select { |s| s.instance_of? Hash }.each do |score|
33
+ assert_instance_of String, score[:event]
34
+ skip unless SciolyFF.rep[:Events].instance_of? Array
35
+
36
+ event_names = SciolyFF.rep[:Events].map { |e| e[:name] }
37
+ assert_includes event_names, score[:event]
38
+ end
39
+ end
40
+
41
+ def test_each_score_has_valid_team
42
+ @scores.select { |s| s.instance_of? Hash }.each do |score|
43
+ assert_instance_of Integer, score[:team]
44
+ skip unless SciolyFF.rep[:Teams].instance_of? Array
45
+
46
+ team_numbers = SciolyFF.rep[:Teams].map { |t| t[:number] }
47
+ assert_includes team_numbers, score[:team]
48
+ end
49
+ end
50
+
51
+ def test_each_score_has_valid_participated
52
+ @scores.select { |s| s.instance_of? Hash }.each do |score|
53
+ if score.key? :participated
54
+ assert_includes [true, false], score[:participated]
55
+ end
56
+ end
57
+ end
58
+
59
+ def test_each_score_has_valid_disqualified
60
+ @scores.select { |s| s.instance_of? Hash }.each do |score|
61
+ if score.key? :disqualified
62
+ assert_includes [true, false], score[:disqualified]
63
+ end
64
+ end
65
+ end
66
+
67
+ def test_each_score_has_valid_score
68
+ @scores.select { |s| s.instance_of? Hash }.each do |score|
69
+ next if score[:disqualified] == true ||
70
+ score[:participated] == false
71
+
72
+ assert_kind_of Numeric, score[:score]
73
+ end
74
+ end
75
+
76
+ def test_each_score_has_valid_tiebreaker_place
77
+ @scores.select { |s| s.instance_of? Hash }.each do |score|
78
+ next unless score.key? 'tiebreaker place'
79
+
80
+ max_place = @scores.count do |s|
81
+ s[:event] == score[:event] &&
82
+ s[:score] == score[:score]
83
+ end
84
+ assert_instance_of Integer, score['tiebreaker place']
85
+ assert_includes 1..max_place, score['tiebreaker place']
86
+ end
87
+ end
88
+
89
+ def test_each_score_has_valid_tier
90
+ skip unless SciolyFF.rep[:Events].instance_of? Array
91
+
92
+ @scores.select { |s| s.instance_of? Hash }.each do |score|
93
+ next unless score.key? :tier
94
+
95
+ assert_instance_of Integer, score[:tier]
96
+
97
+ max_tier = SciolyFF.rep[:Events].find do |event|
98
+ event[:name] == score[:event]
99
+ end[:tiers]
100
+
101
+ refute_nil max_tier, "#{score[:event]} does not have tiers"
102
+ assert_includes 1..max_tier, score[:tier]
103
+ end
104
+ end
105
+
106
+ def test_scores_are_unique_for_event_and_team
107
+ skip unless SciolyFF.rep[:Teams].instance_of? Array
108
+
109
+ SciolyFF.rep[:Teams].each do |team|
110
+ next unless team.instance_of? Hash
111
+
112
+ assert_nil @scores.select { |s| s[:team] == team[:number] }
113
+ .map { |s| s[:event] }
114
+ .compact
115
+ .uniq!
116
+ end
117
+ end
118
+
119
+ def test_each_team_has_scores_for_all_events
120
+ skip unless SciolyFF.rep[:Teams].instance_of?(Array) &&
121
+ SciolyFF.rep[:Events].instance_of?(Array)
122
+
123
+ SciolyFF.rep[:Teams].select { |t| t.instance_of? Hash }.each do |team|
124
+ events = SciolyFF.rep[:Events].select { |e| e.instance_of? Hash }
125
+ .map { |e| e[:name] }
126
+
127
+ events_with_scores = @scores.select { |s| s.instance_of? Hash }
128
+ .select { |s| s[:team] == team[:number] }
129
+ .map { |s| s[:event] }
130
+
131
+ assert_equal events, events_with_scores
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'minitest/test'
4
+ require 'set'
5
+
6
+ module SciolyFF
7
+ # Tests that also serve as the specification for the sciolyff file format
8
+ #
9
+ class Sections < Minitest::Test
10
+ def setup
11
+ @rep = SciolyFF.rep
12
+ skip unless @rep.instance_of? Hash
13
+ end
14
+
15
+ def test_has_tournament
16
+ assert_instance_of Hash, @rep[:Tournament]
17
+ end
18
+
19
+ def test_has_events
20
+ assert_instance_of Array, @rep[:Events]
21
+ end
22
+
23
+ def test_has_teams
24
+ assert_instance_of Array, @rep[:Teams]
25
+ end
26
+
27
+ def test_has_placings
28
+ assert_instance_of Array, @rep[:Placings]
29
+ end
30
+
31
+ def test_has_penalties
32
+ assert_instance_of Array, @rep[:Penalties] if @rep.key? :Penalties
33
+ end
34
+
35
+ def test_does_not_have_extra_sections
36
+ sections = Set.new %i[Tournament Events Teams Placings Scores Penalties]
37
+ assert Set.new(@rep.keys).subset? sections
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'minitest/test'
4
+ require 'set'
5
+
6
+ module SciolyFF
7
+ # Tests that also serve as the specification for the sciolyff file format
8
+ #
9
+ class Teams < Minitest::Test
10
+ def setup
11
+ skip unless SciolyFF.rep.instance_of? Hash
12
+ @teams = SciolyFF.rep[:Teams]
13
+ skip unless @teams.instance_of? Array
14
+ end
15
+
16
+ def test_has_valid_teams
17
+ @teams.each do |team|
18
+ assert_instance_of Hash, team
19
+ end
20
+ end
21
+
22
+ def test_each_team_does_not_have_extra_info
23
+ @teams.select { |t| t.instance_of? Hash }.each do |team|
24
+ fo = Set.new %i[school suffix subdivision exhibition number city state]
25
+ assert Set.new(team.keys).subset? fo
26
+ end
27
+ end
28
+
29
+ def test_each_team_has_valid_school
30
+ @teams.select { |t| t.instance_of? Hash }.each do |team|
31
+ assert_instance_of String, team[:school]
32
+ end
33
+ end
34
+
35
+ def test_each_team_has_valid_suffix
36
+ @teams.select { |t| t.instance_of? Hash }.each do |team|
37
+ assert_instance_of String, team[:suffix] if team.key? :suffix
38
+ end
39
+ end
40
+
41
+ def test_each_team_has_valid_subdivision
42
+ @teams.select { |t| t.instance_of? Hash }.each do |team|
43
+ assert_instance_of String, team[:subdivision] \
44
+ if team.key? :subdivision
45
+ end
46
+ end
47
+
48
+ def test_each_team_has_valid_exhibition
49
+ @teams.select { |t| t.instance_of? Hash }.each do |team|
50
+ assert_includes [true, false], team[:exhibition] \
51
+ if team.key? :exhibition
52
+ end
53
+ end
54
+
55
+ def test_each_team_has_valid_number
56
+ @teams.select { |t| t.instance_of? Hash }.each do |team|
57
+ assert_instance_of Integer, team[:number]
58
+ end
59
+ end
60
+
61
+ def test_each_team_has_valid_city
62
+ @teams.select { |t| t.instance_of? Hash }.each do |team|
63
+ assert_instance_of String, team[:city] if team.key? :city
64
+ end
65
+ end
66
+
67
+ def test_each_team_has_valid_state
68
+ @teams.select { |t| t.instance_of? Hash }.each do |team|
69
+ assert_instance_of String, team[:state]
70
+ end
71
+ end
72
+
73
+ def test_each_team_has_unique_number
74
+ numbers = @teams.select { |t| t.instance_of? Hash }
75
+ .map { |t| t[:number] }
76
+ assert_nil numbers.uniq!
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'minitest/test'
4
+
5
+ module SciolyFF
6
+ # Tests that also serve as the specification for the sciolyff file format
7
+ #
8
+ class TopLevel < Minitest::Test
9
+ def test_is_hash
10
+ assert_instance_of Hash, SciolyFF.rep
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'minitest/test'
4
+ require 'set'
5
+
6
+ module SciolyFF
7
+ # Tests that also serve as the specification for the sciolyff file format
8
+ #
9
+ class Tournament < Minitest::Test
10
+ def setup
11
+ skip unless SciolyFF.rep.instance_of? Hash
12
+ @tournament = SciolyFF.rep[:Tournament]
13
+ skip unless @tournament.instance_of? Hash
14
+ end
15
+
16
+ def test_has_info
17
+ refute_nil @tournament[:location]
18
+ refute_nil @tournament[:level]
19
+ refute_nil @tournament[:division]
20
+ refute_nil @tournament[:year]
21
+ refute_nil @tournament[:date]
22
+ end
23
+
24
+ def test_does_not_have_extra_info
25
+ info = Set.new %i[name location level division state year date]
26
+ assert Set.new(@tournament.keys).subset? info
27
+ end
28
+
29
+ def test_has_valid_name
30
+ skip unless @tournament.key? :name
31
+ assert_instance_of String, @tournament[:name]
32
+ end
33
+
34
+ def test_has_valid_location
35
+ skip unless @tournament.key? :location
36
+ assert_instance_of String, @tournament[:location]
37
+ end
38
+
39
+ def test_has_valid_level
40
+ skip unless @tournament.key? :level
41
+ level = @tournament[:level]
42
+ assert_includes %w[Invitational Regionals States Nationals], level
43
+ end
44
+
45
+ def test_has_valid_division
46
+ skip unless @tournament.key? :division
47
+ assert_includes %w[A B C], @tournament[:division]
48
+ end
49
+
50
+ def test_has_valid_state
51
+ skip unless @tournament.key? :level
52
+ level = @tournament[:level]
53
+ skip unless %w[Regionals States].include? level
54
+ assert_instance_of String, @tournament[:state]
55
+ end
56
+
57
+ def test_has_valid_year
58
+ skip unless @tournament.key? :year
59
+ assert_instance_of Integer, @tournament[:year]
60
+ end
61
+
62
+ def test_has_valid_date
63
+ skip unless @tournament.key? :date
64
+ assert_instance_of Date, @tournament[:date]
65
+ end
66
+ end
67
+ end
data/lib/sciolyff.rb ADDED
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+ require 'sciolyff/top_level'
5
+ require 'sciolyff/sections'
6
+ require 'sciolyff/tournament'
7
+ require 'sciolyff/events'
8
+ require 'sciolyff/teams'
9
+ require 'sciolyff/placings'
10
+ require 'sciolyff/scores'
11
+ require 'sciolyff/penalties'
12
+
13
+ # API methods for the Scioly File Format
14
+ #
15
+ module SciolyFF
16
+ class << self
17
+ attr_accessor :rep
18
+ end
19
+
20
+ # Assumes rep is the output of YAML.load
21
+ def self.validate(rep, opts: {})
22
+ SciolyFF.rep = rep
23
+
24
+ mt_args = []
25
+ mt_args << '--verbose' if opts[:verbose]
26
+
27
+ Minitest.run mt_args
28
+ end
29
+
30
+ def self.validate_file(path, opts: {})
31
+ file = File.read(path)
32
+ rep = YAML.safe_load(file, permitted_classes: [Date], symbolize_names: true)
33
+ rescue StandardError => e
34
+ puts 'Error: could not read file as YAML.'
35
+ warn e.message
36
+ else
37
+ puts FILE_VALIDATION_MESSAGE
38
+ validate(rep, opts: opts)
39
+ end
40
+
41
+ FILE_VALIDATION_MESSAGE = <<~STRING
42
+ Validating file with Minitest...
43
+
44
+ Overkill? Probably.
45
+ Doesn't give line numbers from original file? Yeah.
46
+
47
+ STRING
48
+
49
+ # Wrapper class around a SciolyFF Ruby object representation with utility
50
+ # methods to help in displaying results
51
+ class Helper
52
+ attr_reader :rep, :tournament, :events_by_name, :teams_by_number
53
+ attr_reader :placings_by_event, :placings_by_team, :penalties_by_team
54
+
55
+ def initialize(rep)
56
+ @rep = rep
57
+ @tournament = rep[:Tournament]
58
+ @events_by_name = index_array(rep[:Events], [:name])
59
+ @teams_by_number = index_array(rep[:Teams], [:number])
60
+ @placings_by_event = index_array(rep[:Placings], %i[event team])
61
+ @placings_by_team = index_array(rep[:Placings], %i[team event])
62
+ return unless rep[:Penalties]
63
+
64
+ @penalties_by_team = index_array(rep[:Penalties], [:team])
65
+ end
66
+
67
+ def event_points(team_number, event_name)
68
+ placing = @placings_by_event[event_name][team_number]
69
+
70
+ if placing[:disqualified] then @teams_by_number.count + 2
71
+ elsif placing[:participated] == false then @teams_by_number.count + 1
72
+ elsif placing[:place].nil? then @teams_by_number.count
73
+ else calculate_event_points(placing)
74
+ end
75
+ end
76
+
77
+ def team_points(team_number)
78
+ @placings_by_team[team_number]
79
+ .values
80
+ .reject { |p| @events_by_name[p[:event]][:trial] }
81
+ .reject { |p| @events_by_name[p[:event]][:trialed] }
82
+ .sum { |p| event_points(team_number, p[:event]) } \
83
+ + team_points_from_penalties(team_number)
84
+ end
85
+
86
+ def sort_teams_by_rank
87
+ @teams_by_number
88
+ .values
89
+ .sort do |a, b|
90
+ cmp = team_points(a[:number]) - team_points(b[:number])
91
+ cmp.zero? ? break_tie(a[:number], b[:number]) : cmp
92
+ end
93
+ end
94
+
95
+ def medal_counts(team_number)
96
+ (1..(@teams_by_number.count + 2)).map do |m|
97
+ @events_by_name
98
+ .values
99
+ .reject { |e| e[:trial] || e[:trialed] }
100
+ .count { |e| event_points(team_number, e[:name]) == m }
101
+ end
102
+ end
103
+
104
+ private
105
+
106
+ def index_array(arr, index_keys)
107
+ return arr.first if index_keys.empty?
108
+
109
+ indexed_hash = arr.group_by { |x| x[index_keys.first] }
110
+
111
+ indexed_hash.transform_values do |a|
112
+ index_array(a, index_keys.drop(1))
113
+ end
114
+ end
115
+
116
+ def calculate_event_points(placing)
117
+ # Points is place minus number of exhibition teams with a better place
118
+ placing[:place] -
119
+ @placings_by_event[placing[:event]]
120
+ .values
121
+ .select { |p| @teams_by_number[p[:team]][:exhibition] && p[:place] }
122
+ .count { |p| p[:place] < placing[:place] }
123
+ end
124
+
125
+ def team_points_from_penalties(team_number)
126
+ if @penalties_by_team.nil? || @penalties_by_team[team_number].nil? then 0
127
+ else @penalties_by_team[team_number][:points]
128
+ end
129
+ end
130
+
131
+ def break_tie(team_number_a, team_number_b)
132
+ medal_counts(team_number_a)
133
+ .zip(medal_counts(team_number_b))
134
+ .map { |count| count.last - count.first }
135
+ .find(proc { team_number_a <=> team_number_b }, &:nonzero?)
136
+ end
137
+ end
138
+ end
metadata ADDED
@@ -0,0 +1,95 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sciolyff
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Em Zhan
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-05-15 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: minitest
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '5.11'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '5.11'
27
+ - !ruby/object:Gem::Dependency
28
+ name: optimist
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '12.3'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '12.3'
55
+ description:
56
+ email:
57
+ executables:
58
+ - sciolyff
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - bin/sciolyff
63
+ - lib/sciolyff.rb
64
+ - lib/sciolyff/events.rb
65
+ - lib/sciolyff/penalties.rb
66
+ - lib/sciolyff/placings.rb
67
+ - lib/sciolyff/scores.rb
68
+ - lib/sciolyff/sections.rb
69
+ - lib/sciolyff/teams.rb
70
+ - lib/sciolyff/top_level.rb
71
+ - lib/sciolyff/tournament.rb
72
+ homepage: https://github.com/zqianem/sciolyff
73
+ licenses:
74
+ - MIT
75
+ metadata: {}
76
+ post_install_message:
77
+ rdoc_options: []
78
+ require_paths:
79
+ - lib
80
+ required_ruby_version: !ruby/object:Gem::Requirement
81
+ requirements:
82
+ - - ">="
83
+ - !ruby/object:Gem::Version
84
+ version: '0'
85
+ required_rubygems_version: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ requirements: []
91
+ rubygems_version: 3.0.3
92
+ signing_key:
93
+ specification_version: 4
94
+ summary: A file format for Science Olympiad tournament results.
95
+ test_files: []