sciolyff 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|