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 +7 -0
- data/bin/sciolyff +29 -0
- data/lib/sciolyff/events.rb +66 -0
- data/lib/sciolyff/penalties.rb +51 -0
- data/lib/sciolyff/placings.rb +124 -0
- data/lib/sciolyff/scores.rb +135 -0
- data/lib/sciolyff/sections.rb +40 -0
- data/lib/sciolyff/teams.rb +79 -0
- data/lib/sciolyff/top_level.rb +13 -0
- data/lib/sciolyff/tournament.rb +67 -0
- data/lib/sciolyff.rb +138 -0
- metadata +95 -0
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: []
|