sciolyff-duosmium 0.13.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +92 -0
  4. data/bin/sciolyff +38 -0
  5. data/lib/sciolyff.rb +9 -0
  6. data/lib/sciolyff/interpreter.rb +89 -0
  7. data/lib/sciolyff/interpreter/bids.rb +24 -0
  8. data/lib/sciolyff/interpreter/event.rb +69 -0
  9. data/lib/sciolyff/interpreter/html.rb +65 -0
  10. data/lib/sciolyff/interpreter/html/helpers.rb +230 -0
  11. data/lib/sciolyff/interpreter/html/main.css +1 -0
  12. data/lib/sciolyff/interpreter/html/main.js +74 -0
  13. data/lib/sciolyff/interpreter/html/template.html.erb +480 -0
  14. data/lib/sciolyff/interpreter/model.rb +29 -0
  15. data/lib/sciolyff/interpreter/penalty.rb +19 -0
  16. data/lib/sciolyff/interpreter/placing.rb +127 -0
  17. data/lib/sciolyff/interpreter/raw.rb +50 -0
  18. data/lib/sciolyff/interpreter/subdivisions.rb +72 -0
  19. data/lib/sciolyff/interpreter/team.rb +111 -0
  20. data/lib/sciolyff/interpreter/tiebreaks.rb +34 -0
  21. data/lib/sciolyff/interpreter/tournament.rb +194 -0
  22. data/lib/sciolyff/validator.rb +89 -0
  23. data/lib/sciolyff/validator/canonical.rb +23 -0
  24. data/lib/sciolyff/validator/checker.rb +33 -0
  25. data/lib/sciolyff/validator/events.rb +106 -0
  26. data/lib/sciolyff/validator/logger.rb +48 -0
  27. data/lib/sciolyff/validator/penalties.rb +19 -0
  28. data/lib/sciolyff/validator/placings.rb +136 -0
  29. data/lib/sciolyff/validator/range.rb +15 -0
  30. data/lib/sciolyff/validator/raws.rb +40 -0
  31. data/lib/sciolyff/validator/sections.rb +56 -0
  32. data/lib/sciolyff/validator/subdivisions.rb +64 -0
  33. data/lib/sciolyff/validator/teams.rb +108 -0
  34. data/lib/sciolyff/validator/top_level.rb +23 -0
  35. data/lib/sciolyff/validator/tournament.rb +138 -0
  36. data/sciolyff.gemspec +22 -0
  37. metadata +121 -0
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SciolyFF
4
+ # Prints extra information produced by validation process
5
+ class Validator::Logger
6
+ ERROR = 0
7
+ WARN = 1
8
+ INFO = 2
9
+ DEBUG = 3
10
+
11
+ attr_reader :log, :options
12
+
13
+ def initialize(loglevel, **options)
14
+ @loglevel = loglevel
15
+ @options = options
16
+ flush
17
+ end
18
+
19
+ def flush
20
+ @log = String.new
21
+ end
22
+
23
+ def error(msg)
24
+ return false if @loglevel < ERROR
25
+
26
+ @log << "ERROR (invalid SciolyFF): #{msg}\n"
27
+ false # convenient for using logging the error as return value
28
+ end
29
+
30
+ def warn(msg)
31
+ return true if @loglevel < WARN
32
+
33
+ @log << "WARNING (still valid SciolyFF): #{msg}\n"
34
+ end
35
+
36
+ def info(msg)
37
+ return true if @loglevel < INFO
38
+
39
+ @log << "INFO: #{msg}\n"
40
+ end
41
+
42
+ def debug(msg)
43
+ return true if @loglevel < DEBUG
44
+
45
+ @log << "DEBUG (possible intentional exception): #{msg}\n"
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sciolyff/validator/checker'
4
+ require 'sciolyff/validator/sections'
5
+
6
+ module SciolyFF
7
+ # Checks for one penalty in the Penalties section of a SciolyFF file
8
+ class Validator::Penalties < Validator::Checker
9
+ include Validator::Sections
10
+
11
+ REQUIRED = {
12
+ team: Integer,
13
+ points: Integer
14
+ }.freeze
15
+
16
+ OPTIONAL = {
17
+ }.freeze
18
+ end
19
+ end
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sciolyff/validator/checker'
4
+ require 'sciolyff/validator/sections'
5
+
6
+ module SciolyFF
7
+ # Checks for one placing in the Placings section of a SciolyFF file
8
+ class Validator::Placings < Validator::Checker
9
+ include Validator::Sections
10
+
11
+ REQUIRED = {
12
+ event: String,
13
+ team: Integer
14
+ }.freeze
15
+
16
+ OPTIONAL = {
17
+ place: Integer,
18
+ participated: [true, false],
19
+ disqualified: [true, false],
20
+ exempt: [true, false],
21
+ tie: [true, false],
22
+ unknown: [true, false],
23
+ raw: Hash
24
+ }.freeze
25
+
26
+ def initialize(rep)
27
+ @events_by_name = group(rep[:Events], :name)
28
+ @teams_by_number = group(rep[:Teams], :number)
29
+ @event_names = @events_by_name.keys
30
+ @team_numbers = @teams_by_number.keys
31
+ @placings = rep[:Placings]
32
+ @maximum_place = rep[:Tournament][:'maximum place']
33
+ @has_places = rep[:Placings].any? { |p| p[:place] }
34
+ end
35
+
36
+ def matching_event?(placing, logger)
37
+ return true if @event_names.include? placing[:event]
38
+
39
+ logger.error "'event: #{placing[:event]}' in Placings "\
40
+ 'does not match any event name in Events'
41
+ end
42
+
43
+ def matching_team?(placing, logger)
44
+ return true if @team_numbers.include? placing[:team]
45
+
46
+ logger.error "'team: #{placing[:team]}' in Placings "\
47
+ 'does not match any team number in Teams'
48
+ end
49
+
50
+ def unique_event_and_team?(placing, logger)
51
+ return true if @placings.count do |other|
52
+ placing[:event] == other[:event] && placing[:team] == other[:team]
53
+ end == 1
54
+
55
+ logger.error "duplicate #{placing_log(placing)}"
56
+ end
57
+
58
+ def having_a_place_makes_sense?(placing, logger)
59
+ return true unless placing[:place] &&
60
+ (placing[:participated] == false ||
61
+ placing[:disqualified] ||
62
+ placing[:unknown] ||
63
+ placing[:raw])
64
+
65
+ logger.error 'having a place does not make sense for '\
66
+ "#{placing_log(placing)}"
67
+ end
68
+
69
+ def having_a_raw_makes_sense?(placing, logger)
70
+ return true unless placing[:raw] &&
71
+ (placing[:participated] == false ||
72
+ placing[:disqualified] ||
73
+ placing[:unknown] ||
74
+ placing[:place])
75
+
76
+ logger.error 'having raw section does not make sense for '\
77
+ "#{placing_log(placing)}"
78
+ end
79
+
80
+ def having_a_tie_makes_sense?(placing, logger)
81
+ return true unless placing.key?(:tie) && placing[:raw]
82
+
83
+ logger.error 'having a tie value does make sense for '\
84
+ "#{placing_log(placing)}"
85
+ end
86
+
87
+ def possible_participated_disqualified_combination?(placing, logger)
88
+ return true unless placing[:participated] == false &&
89
+ placing[:disqualified]
90
+
91
+ logger.error 'impossible participation-disqualified combination for '\
92
+ "#{placing_log(placing)}"
93
+ end
94
+
95
+ def possible_unknown_disqualified_combination?(placing, logger)
96
+ return true unless placing[:unknown] && placing[:disqualified]
97
+
98
+ logger.error 'impossible unknown-disqualified combination for '\
99
+ "#{placing_log(placing)}"
100
+ end
101
+
102
+ def unknown_allowed?(placing, logger)
103
+ event = @events_by_name[placing[:event]]
104
+ team = @teams_by_number[placing[:team]]
105
+ return true unless invalid_unknown?(placing, event, team)
106
+
107
+ logger.error "unknown place not allowed for #{placing_log(placing)} "\
108
+ '(either placing must be exempt or event must be trial/trialed)'
109
+ end
110
+
111
+ def no_mix_of_raws_and_places(placing, logger)
112
+ return true unless @has_places && placing.key?(:raw)
113
+
114
+ logger.error "cannot mix 'raw:' and 'place:' in same file"
115
+ end
116
+
117
+ private
118
+
119
+ def placing_log(placing)
120
+ "placing with 'team: #{placing[:team]}' and 'event: #{placing[:event]}'"
121
+ end
122
+
123
+ def invalid_unknown?(placing, event, team)
124
+ placing[:unknown] &&
125
+ @maximum_place.nil? &&
126
+ !placing[:exempt] &&
127
+ !event[:trial] &&
128
+ !event[:trialed] &&
129
+ !team[:exhibition]
130
+ end
131
+
132
+ def group(arr, key)
133
+ arr.group_by { |e| e[key] }.transform_values(&:first)
134
+ end
135
+ end
136
+ end
@@ -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
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sciolyff/validator/checker'
4
+ require 'sciolyff/validator/sections'
5
+
6
+ module SciolyFF
7
+ # Checks for one raw of a placing in the Placings section of a SciolyFF file
8
+ class Validator::Raws < Validator::Checker
9
+ include Validator::Sections
10
+
11
+ REQUIRED = {
12
+ score: Float
13
+ }.freeze
14
+
15
+ OPTIONAL = {
16
+ 'tier': Integer,
17
+ 'tiebreaker rank': Integer
18
+ }.freeze
19
+
20
+ def score_is_not_nan?(raw, logger)
21
+ return true unless raw[:score].nan?
22
+
23
+ logger.error "'score: .nan' (not a number) is not permitted"
24
+ end
25
+
26
+ def positive_tier?(raw, logger)
27
+ tier = raw[:tier]
28
+ return true if tier.nil? || tier.positive?
29
+
30
+ logger.error "'tier: #{tier}' is not positive"
31
+ end
32
+
33
+ def positive_tiebreaker_rank?(raw, logger)
34
+ rank = raw[:'tiebreaker rank']
35
+ return true if rank.nil? || rank.positive?
36
+
37
+ logger.error "'tiebreaker rank: #{rank}' is not positive"
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SciolyFF
4
+ # Generic tests for (sub-)sections and types. Including classes must have two
5
+ # hashes REQUIRED and OPTIONAL (see other files in this dir for examples)
6
+ module Validator::Sections
7
+ def rep_is_hash?(rep, logger)
8
+ return true if rep.instance_of? Hash
9
+
10
+ logger.error "entry in #{section_name} is not a Hash"
11
+ end
12
+
13
+ def all_required_sections?(rep, logger)
14
+ missing = self.class::REQUIRED.keys - rep.keys
15
+ return true if missing.empty?
16
+
17
+ logger.error "missing required sections: #{missing.join ', '}"
18
+ end
19
+
20
+ def no_extra_sections?(rep, logger)
21
+ extra = rep.keys - (self.class::REQUIRED.keys + self.class::OPTIONAL.keys)
22
+ return true if extra.empty?
23
+
24
+ logger.error "extra section(s) found: #{extra.join ', '}"
25
+ end
26
+
27
+ def sections_are_correct_type?(rep, logger)
28
+ correct_types = self.class::REQUIRED.merge self.class::OPTIONAL
29
+ rep.all? do |key, value|
30
+ type = correct_types[key]
31
+ next true if correct_type?(type, value)
32
+
33
+ logger.error "#{key}: #{value} is not #{type}"
34
+ end
35
+ end
36
+
37
+ private
38
+
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)
48
+ rescue StandardError
49
+ false
50
+ end
51
+
52
+ def section_name
53
+ self.class.to_s.split('::').last
54
+ end
55
+ end
56
+ 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
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sciolyff/validator/checker'
4
+ require 'sciolyff/validator/sections'
5
+ require 'sciolyff/validator/canonical'
6
+
7
+ module SciolyFF
8
+ # Checks for one team in the Teams section of a SciolyFF file
9
+ class Validator::Teams < Validator::Checker
10
+ include Validator::Sections
11
+
12
+ REQUIRED = {
13
+ number: Integer,
14
+ school: String,
15
+ state: String
16
+ }.freeze
17
+
18
+ OPTIONAL = {
19
+ 'school abbreviation': String,
20
+ subdivision: String,
21
+ suffix: String,
22
+ city: String,
23
+ disqualified: [true, false],
24
+ exhibition: [true, false]
25
+ }.freeze
26
+
27
+ def initialize(rep)
28
+ initialize_teams_info(rep[:Teams])
29
+ @subdivisions = rep[:Subdivisions]&.map { |s| s[:name] } || []
30
+ @placings = rep[:Placings].group_by { |p| p[:team] }
31
+ @exempt = rep[:Tournament][:'exempt placings'] || 0
32
+ end
33
+
34
+ def unique_number?(team, logger)
35
+ return true if @numbers.count(team[:number]) == 1
36
+
37
+ logger.error "duplicate team number: #{team[:number]}"
38
+ end
39
+
40
+ def unique_suffix_per_school?(team, logger)
41
+ full_school = [team[:school], team[:city], team[:state]]
42
+ return true if @schools[full_school].count do |other|
43
+ !other[:suffix].nil? && other[:suffix] == team[:suffix]
44
+ end <= 1
45
+
46
+ logger.error "team number #{team[:number]} has the same suffix "\
47
+ 'as another team from the same school'
48
+ end
49
+
50
+ def suffix_needed?(team, logger)
51
+ rep = [team[:school], team[:city], team[:state]]
52
+ return true unless team[:suffix] && @schools[rep].count == 1
53
+
54
+ logger.warn "team number #{team[:number]} may have unnecessary "\
55
+ "suffix: #{team[:suffix]}"
56
+ end
57
+
58
+ def unambiguous_cities_per_school?(team, logger)
59
+ return true unless @schools.keys.find do |other|
60
+ team[:city].nil? && !other[1].nil? &&
61
+ team[:school] == other[0] &&
62
+ team[:state] == other[2]
63
+ end
64
+
65
+ logger.error "city for team number #{team[:number]} is ambiguous, "\
66
+ 'value is required for unambiguity'
67
+ end
68
+
69
+ def correct_number_of_exempt_placings?(team, logger)
70
+ count = @placings[team[:number]].count { |p| p[:exempt] }
71
+ return true if count == @exempt || team[:exhibition]
72
+
73
+ logger.error "'team: #{team[:number]}' has incorrect number of "\
74
+ "exempt placings (#{count} insteand of #{@exempt})"
75
+ end
76
+
77
+ def matching_subdivision?(team, logger)
78
+ sub = team[:subdivision]
79
+ return true if sub.nil? || @subdivisions.include?(sub)
80
+
81
+ logger.error "'subdivision: #{sub}' does not match any name in "\
82
+ 'section Subdivisions'
83
+ end
84
+
85
+ def in_a_subdivision_if_possible?(team, logger)
86
+ return true unless !@subdivisions.empty? && !team[:subdivision]
87
+
88
+ logger.warn "missing subdivision for 'team: #{team[:number]}'"
89
+ end
90
+
91
+ include Validator::Canonical
92
+
93
+ def in_canonical_list?(team, logger)
94
+ rep = [team[:school], team[:city], team[:state]]
95
+ return true if canonical?(rep, 'schools.csv', logger)
96
+
97
+ location = rep[1..-1].compact.join ', '
98
+ logger.warn "non-canonical school: #{team[:school]} in #{location}"
99
+ end
100
+
101
+ private
102
+
103
+ def initialize_teams_info(teams)
104
+ @numbers = teams.map { |t| t[:number] }
105
+ @schools = teams.group_by { |t| [t[:school], t[:city], t[:state]] }
106
+ end
107
+ end
108
+ end