sciolyff 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: []