sciolyff 0.10.0 → 0.11.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 +4 -4
- data/README.md +2 -2
- data/bin/sciolyff +6 -2
- data/lib/sciolyff.rb +2 -0
- data/lib/sciolyff/interpreter/bids.rb +24 -0
- data/lib/sciolyff/interpreter/event.rb +5 -5
- data/lib/sciolyff/interpreter/html/helpers.rb +47 -0
- data/lib/sciolyff/interpreter/html/main.css +1 -0
- data/lib/sciolyff/interpreter/html/main.js +74 -0
- data/lib/sciolyff/interpreter/html/template.html.erb +29 -101
- data/lib/sciolyff/interpreter/placing.rb +4 -4
- data/lib/sciolyff/interpreter/raw.rb +1 -1
- data/lib/sciolyff/interpreter/subdivisions.rb +33 -3
- data/lib/sciolyff/interpreter/team.rb +7 -2
- data/lib/sciolyff/interpreter/tournament.rb +43 -23
- data/lib/sciolyff/validator.rb +4 -3
- data/lib/sciolyff/validator/canonical.rb +23 -0
- data/lib/sciolyff/validator/events.rb +10 -0
- data/lib/sciolyff/validator/logger.rb +3 -2
- data/lib/sciolyff/validator/range.rb +15 -0
- data/lib/sciolyff/validator/raws.rb +14 -0
- data/lib/sciolyff/validator/sections.rb +12 -8
- data/lib/sciolyff/validator/subdivisions.rb +64 -0
- data/lib/sciolyff/validator/teams.rb +29 -2
- data/lib/sciolyff/validator/top_level.rb +1 -0
- data/lib/sciolyff/validator/tournament.rb +51 -4
- data/sciolyff.gemspec +1 -1
- metadata +8 -2
@@ -18,19 +18,19 @@ module SciolyFF
|
|
18
18
|
attr_reader :event, :team, :subdivision_placing
|
19
19
|
|
20
20
|
def participated?
|
21
|
-
@rep[:participated]
|
21
|
+
@rep[:participated] || @rep[:participated].nil?
|
22
22
|
end
|
23
23
|
|
24
24
|
def disqualified?
|
25
|
-
@rep[:disqualified]
|
25
|
+
@rep[:disqualified] || false
|
26
26
|
end
|
27
27
|
|
28
28
|
def exempt?
|
29
|
-
@rep[:exempt]
|
29
|
+
@rep[:exempt] || false
|
30
30
|
end
|
31
31
|
|
32
32
|
def unknown?
|
33
|
-
@rep[:unknown]
|
33
|
+
@rep[:unknown] || false
|
34
34
|
end
|
35
35
|
|
36
36
|
def tie?
|
@@ -6,15 +6,45 @@ module SciolyFF
|
|
6
6
|
private
|
7
7
|
|
8
8
|
def subdivision_rep(sub)
|
9
|
-
# make a deep copy of rep
|
9
|
+
# make a deep copy of rep
|
10
10
|
rep = Marshal.load(Marshal.dump(@rep))
|
11
|
+
|
12
|
+
remove_teams_not_in_subdivision(rep, sub)
|
13
|
+
fix_subdivision_tournament_fields(rep, sub)
|
14
|
+
limit_maximum_place(rep)
|
15
|
+
fix_placings_for_existing_teams(rep)
|
16
|
+
rep
|
17
|
+
end
|
18
|
+
|
19
|
+
def remove_teams_not_in_subdivision(rep, sub)
|
11
20
|
rep[:Teams].select! { |t| t.delete(:subdivision) == sub }
|
12
21
|
|
13
22
|
team_numbers = rep[:Teams].map { |t| t[:number] }
|
14
23
|
rep[:Placings].select! { |p| team_numbers.include? p[:team] }
|
24
|
+
end
|
15
25
|
|
16
|
-
|
17
|
-
rep
|
26
|
+
def fix_subdivision_tournament_fields(rep, sub)
|
27
|
+
tournament_rep = rep[:Tournament]
|
28
|
+
sub_rep = rep[:Subdivisions].find { |s| s[:name] == sub }
|
29
|
+
|
30
|
+
replace_tournament_fields(tournament_rep, sub_rep)
|
31
|
+
|
32
|
+
tournament_rep.delete(:bids)
|
33
|
+
rep.delete(:Subdivisions)
|
34
|
+
end
|
35
|
+
|
36
|
+
def replace_tournament_fields(tournament_rep, sub_rep)
|
37
|
+
[:medals, :trophies, :'maximum place'].each do |key|
|
38
|
+
tournament_rep[key] = sub_rep[key] if sub_rep.key?(key)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def limit_maximum_place(rep)
|
43
|
+
max_place = rep[:Tournament][:'maximum place']
|
44
|
+
team_count = rep[:Teams].count { |t| !t[:exhibition] }
|
45
|
+
|
46
|
+
rep[:Tournament].delete(:'maximum place') if
|
47
|
+
!max_place.nil? && max_place > team_count
|
18
48
|
end
|
19
49
|
|
20
50
|
def fix_placings_for_existing_teams(rep)
|
@@ -34,11 +34,11 @@ module SciolyFF
|
|
34
34
|
end
|
35
35
|
|
36
36
|
def exhibition?
|
37
|
-
@rep[:exhibition]
|
37
|
+
@rep[:exhibition] || false
|
38
38
|
end
|
39
39
|
|
40
40
|
def disqualified?
|
41
|
-
@rep[:disqualified]
|
41
|
+
@rep[:disqualified] || false
|
42
42
|
end
|
43
43
|
|
44
44
|
def number
|
@@ -65,6 +65,11 @@ module SciolyFF
|
|
65
65
|
@points ||= placings.sum(&:points) + penalties.sum(&:points)
|
66
66
|
end
|
67
67
|
|
68
|
+
def earned_bid?
|
69
|
+
school_rank = @tournament.teams_eligible_for_bids.find_index(self)
|
70
|
+
!school_rank.nil? && school_rank < @tournament.bids
|
71
|
+
end
|
72
|
+
|
68
73
|
def worst_placings_to_be_dropped
|
69
74
|
return [] if @tournament.worst_placings_dropped.zero?
|
70
75
|
|
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'sciolyff/interpreter/model'
|
4
|
+
require 'sciolyff/interpreter/bids'
|
4
5
|
|
5
6
|
module SciolyFF
|
6
7
|
# Models a Science Olympiad tournament
|
@@ -21,40 +22,43 @@ module SciolyFF
|
|
21
22
|
|
22
23
|
undef tournament
|
23
24
|
|
24
|
-
|
25
|
-
|
25
|
+
%i[
|
26
|
+
name
|
27
|
+
location
|
28
|
+
level
|
29
|
+
state
|
30
|
+
division
|
31
|
+
year
|
32
|
+
].each do |sym|
|
33
|
+
define_method(sym) { @rep[sym] }
|
26
34
|
end
|
27
35
|
|
28
36
|
def short_name
|
29
37
|
@rep[:'short name']
|
30
38
|
end
|
31
39
|
|
32
|
-
def
|
33
|
-
@rep[:
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
40
|
+
def date
|
41
|
+
@date ||= if @rep[:date].instance_of?(Date)
|
42
|
+
@rep[:date]
|
43
|
+
else
|
44
|
+
Date.parse(@rep[:date])
|
45
|
+
end
|
38
46
|
end
|
39
47
|
|
40
|
-
def
|
41
|
-
@rep[:
|
48
|
+
def medals
|
49
|
+
@rep[:medals] || [calc_medals, maximum_place].min
|
42
50
|
end
|
43
51
|
|
44
|
-
def
|
45
|
-
@rep[:
|
52
|
+
def trophies
|
53
|
+
@rep[:trophies] || [calc_trophies, nonexhibition_teams_count].min
|
46
54
|
end
|
47
55
|
|
48
|
-
def
|
49
|
-
@rep[:
|
56
|
+
def bids
|
57
|
+
@rep[:bids] || 0
|
50
58
|
end
|
51
59
|
|
52
|
-
def
|
53
|
-
@
|
54
|
-
@rep[:date]
|
55
|
-
else
|
56
|
-
Date.parse(@rep[:date])
|
57
|
-
end
|
60
|
+
def bids_per_school
|
61
|
+
@rep[:'bids per school'] || 1
|
58
62
|
end
|
59
63
|
|
60
64
|
def worst_placings_dropped?
|
@@ -74,17 +78,17 @@ module SciolyFF
|
|
74
78
|
end
|
75
79
|
|
76
80
|
def custom_maximum_place?
|
77
|
-
maximum_place !=
|
81
|
+
maximum_place != nonexhibition_teams_count
|
78
82
|
end
|
79
83
|
|
80
84
|
def maximum_place
|
81
85
|
return @rep[:'maximum place'] if @rep[:'maximum place']
|
82
86
|
|
83
|
-
|
87
|
+
nonexhibition_teams_count
|
84
88
|
end
|
85
89
|
|
86
90
|
def per_event_n?
|
87
|
-
@rep[:'per-event n']
|
91
|
+
@rep[:'per-event n'] || false
|
88
92
|
end
|
89
93
|
|
90
94
|
def n_offset
|
@@ -104,5 +108,21 @@ module SciolyFF
|
|
104
108
|
def subdivisions?
|
105
109
|
!@subdivisions.empty?
|
106
110
|
end
|
111
|
+
|
112
|
+
def nonexhibition_teams_count
|
113
|
+
@nonexhibition_teams_count ||= @teams.count { |t| !t.exhibition? }
|
114
|
+
end
|
115
|
+
|
116
|
+
include Interpreter::Bids
|
117
|
+
|
118
|
+
private
|
119
|
+
|
120
|
+
def calc_medals
|
121
|
+
[(nonexhibition_teams_count / 10r).ceil, 3].max
|
122
|
+
end
|
123
|
+
|
124
|
+
def calc_trophies
|
125
|
+
[(nonexhibition_teams_count / 6r).ceil, 3].max
|
126
|
+
end
|
107
127
|
end
|
108
128
|
end
|
data/lib/sciolyff/validator.rb
CHANGED
@@ -10,14 +10,15 @@ module SciolyFF
|
|
10
10
|
|
11
11
|
require 'sciolyff/validator/top_level'
|
12
12
|
require 'sciolyff/validator/tournament'
|
13
|
+
require 'sciolyff/validator/subdivisions'
|
13
14
|
require 'sciolyff/validator/events'
|
14
15
|
require 'sciolyff/validator/teams'
|
15
16
|
require 'sciolyff/validator/placings'
|
16
17
|
require 'sciolyff/validator/penalties'
|
17
18
|
require 'sciolyff/validator/raws'
|
18
19
|
|
19
|
-
def initialize(loglevel
|
20
|
-
@logger = Logger.new loglevel
|
20
|
+
def initialize(loglevel: Logger::WARN, canonical: true)
|
21
|
+
@logger = Logger.new loglevel, canonical_checks: canonical
|
21
22
|
@checkers = {}
|
22
23
|
end
|
23
24
|
|
@@ -64,7 +65,7 @@ module SciolyFF
|
|
64
65
|
def check_all(rep, logger)
|
65
66
|
check(TopLevel, rep, rep, logger) &&
|
66
67
|
check(Tournament, rep, rep[:Tournament], logger) &&
|
67
|
-
[Events, Teams, Placings, Penalties].all? do |klass|
|
68
|
+
[Subdivisions, Events, Teams, Placings, Penalties].all? do |klass|
|
68
69
|
check_list(klass, rep, logger)
|
69
70
|
end &&
|
70
71
|
rep[:Placings].map { |p| p[:raw] }.compact.all? do |r|
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SciolyFF
|
4
|
+
# Logic for checking against canonical lists of events and schools
|
5
|
+
module Validator::Canonical
|
6
|
+
BASE = 'https://raw.githubusercontent.com/unosmium/canonical-names/master/'
|
7
|
+
|
8
|
+
private
|
9
|
+
|
10
|
+
def canonical?(rep, file, logger)
|
11
|
+
return true if @canonical_warned || !logger.options[:canonical_checks]
|
12
|
+
|
13
|
+
@canonical_list ||= CSV.parse(URI.open(BASE + file))
|
14
|
+
# don't try to make this more efficient, harder than it looks because of
|
15
|
+
# nil comparisons
|
16
|
+
@canonical_list.include?(rep)
|
17
|
+
rescue StandardError => e
|
18
|
+
logger.warn "could not read canonical names file: #{BASE + file}"
|
19
|
+
logger.debug "#{e}\n #{e.backtrace.first}"
|
20
|
+
@canonical_warned = true
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
require 'sciolyff/validator/checker'
|
4
4
|
require 'sciolyff/validator/sections'
|
5
|
+
require 'sciolyff/validator/canonical'
|
5
6
|
|
6
7
|
module SciolyFF
|
7
8
|
# Checks for one event in the Events section of a SciolyFF file
|
@@ -78,6 +79,15 @@ module SciolyFF
|
|
78
79
|
"#{lowest_place} instead of 1"
|
79
80
|
end
|
80
81
|
|
82
|
+
include Validator::Canonical
|
83
|
+
|
84
|
+
def in_canonical_list?(event, logger)
|
85
|
+
rep = [event[:name]]
|
86
|
+
return true if canonical?(rep, 'events.csv', logger)
|
87
|
+
|
88
|
+
logger.warn "non-canonical event: #{event[:name]}"
|
89
|
+
end
|
90
|
+
|
81
91
|
private
|
82
92
|
|
83
93
|
def placings_by_place(event)
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SciolyFF
|
4
|
+
# Helper method to check for value in range and report error if not
|
5
|
+
module Validator::Range
|
6
|
+
private
|
7
|
+
|
8
|
+
def within_range?(rep, key, logger, min, max)
|
9
|
+
value = rep[key]
|
10
|
+
return true if value.nil? || value.between?(min, max)
|
11
|
+
|
12
|
+
logger.error "'#{key}: #{value}' is not within range [#{min}, #{max}]"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -16,5 +16,19 @@ module SciolyFF
|
|
16
16
|
'tier': Integer,
|
17
17
|
'tiebreaker rank': Integer
|
18
18
|
}.freeze
|
19
|
+
|
20
|
+
def positive_tier?(raw, logger)
|
21
|
+
tier = raw[:tier]
|
22
|
+
return true if tier.nil? || tier.positive?
|
23
|
+
|
24
|
+
logger.error "'tier: #{tier}' is not positive"
|
25
|
+
end
|
26
|
+
|
27
|
+
def positive_tiebreaker_rank?(raw, logger)
|
28
|
+
rank = raw[:'tiebreaker rank']
|
29
|
+
return true if rank.nil? || rank.positive?
|
30
|
+
|
31
|
+
logger.error "'tiebreaker rank: #{rank}' is not positive"
|
32
|
+
end
|
19
33
|
end
|
20
34
|
end
|
@@ -27,20 +27,24 @@ module SciolyFF
|
|
27
27
|
def sections_are_correct_type?(rep, logger)
|
28
28
|
correct_types = self.class::REQUIRED.merge self.class::OPTIONAL
|
29
29
|
rep.all? do |key, value|
|
30
|
-
|
31
|
-
next true if
|
32
|
-
(correct.instance_of?(Array) && correct.include?(value)) ||
|
33
|
-
(correct.instance_of?(Class) && value.instance_of?(correct)) ||
|
34
|
-
correct_date?(correct, value)
|
30
|
+
type = correct_types[key]
|
31
|
+
next true if correct_type?(type, value)
|
35
32
|
|
36
|
-
logger.error "#{key}: #{value} is not #{
|
33
|
+
logger.error "#{key}: #{value} is not #{type}"
|
37
34
|
end
|
38
35
|
end
|
39
36
|
|
40
37
|
private
|
41
38
|
|
42
|
-
def
|
43
|
-
|
39
|
+
def correct_type?(type, value)
|
40
|
+
type.nil? ||
|
41
|
+
(type.instance_of?(Array) && type.include?(value)) ||
|
42
|
+
(type.instance_of?(Class) && value.instance_of?(type)) ||
|
43
|
+
correct_date?(type, value)
|
44
|
+
end
|
45
|
+
|
46
|
+
def correct_date?(type, value)
|
47
|
+
type == Date && Date.parse(value)
|
44
48
|
rescue StandardError
|
45
49
|
false
|
46
50
|
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'sciolyff/validator/checker'
|
4
|
+
require 'sciolyff/validator/sections'
|
5
|
+
require 'sciolyff/validator/range'
|
6
|
+
|
7
|
+
module SciolyFF
|
8
|
+
# Checks for one subdivision in the Subdivisions section of a SciolyFF file
|
9
|
+
class Validator::Subdivisions < Validator::Checker
|
10
|
+
include Validator::Sections
|
11
|
+
|
12
|
+
REQUIRED = {
|
13
|
+
name: String
|
14
|
+
}.freeze
|
15
|
+
|
16
|
+
OPTIONAL = {
|
17
|
+
medals: Integer,
|
18
|
+
trophies: Integer,
|
19
|
+
'maximum place': Integer
|
20
|
+
}.freeze
|
21
|
+
|
22
|
+
def initialize(rep)
|
23
|
+
@names = rep[:Subdivisions].map { |s| s[:name] }
|
24
|
+
@teams = rep[:Teams].group_by { |t| t[:subdivision] }
|
25
|
+
end
|
26
|
+
|
27
|
+
def unique_name?(subdivision, logger)
|
28
|
+
return true if @names.count(subdivision[:name]) == 1
|
29
|
+
|
30
|
+
logger.error "duplicate subdivision name: #{subdivision[:name]}"
|
31
|
+
end
|
32
|
+
|
33
|
+
def matching_teams?(subdivision, logger)
|
34
|
+
name = subdivision[:name]
|
35
|
+
return true if @teams[name]
|
36
|
+
|
37
|
+
logger.error "subdivision with 'name: #{name}' has no teams"
|
38
|
+
end
|
39
|
+
|
40
|
+
include Validator::Range
|
41
|
+
|
42
|
+
def maximum_place_within_range?(subdivision, logger)
|
43
|
+
max = team_count(subdivision)
|
44
|
+
within_range?(subdivision, :'maximum place', logger, 1, max)
|
45
|
+
end
|
46
|
+
|
47
|
+
def medals_within_range?(subdivision, logger)
|
48
|
+
max = [team_count(subdivision), subdivision[:'maximum place']].compact.min
|
49
|
+
within_range?(subdivision, :medals, logger, 1, max)
|
50
|
+
end
|
51
|
+
|
52
|
+
def trophies_within_range?(subdivision, logger)
|
53
|
+
within_range?(subdivision, :trophies, logger, 1, team_count(subdivision))
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def team_count(subdivision)
|
59
|
+
@teams[subdivision[:name]].count do |t|
|
60
|
+
t[:subdivision] == subdivision[:name] && !t[:exhibition]
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|