sciolyff-duosmium 0.13.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.
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