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.
@@ -18,19 +18,19 @@ module SciolyFF
18
18
  attr_reader :event, :team, :subdivision_placing
19
19
 
20
20
  def participated?
21
- @rep[:participated] == true || @rep[:participated].nil?
21
+ @rep[:participated] || @rep[:participated].nil?
22
22
  end
23
23
 
24
24
  def disqualified?
25
- @rep[:disqualified] == true
25
+ @rep[:disqualified] || false
26
26
  end
27
27
 
28
28
  def exempt?
29
- @rep[:exempt] == true
29
+ @rep[:exempt] || false
30
30
  end
31
31
 
32
32
  def unknown?
33
- @rep[:unknown] == true
33
+ @rep[:unknown] || false
34
34
  end
35
35
 
36
36
  def tie?
@@ -25,7 +25,7 @@ module SciolyFF
25
25
  end
26
26
 
27
27
  def tiebreaker_rank
28
- @rep[:'tiebreaker rank'] || Float::INFINITY
28
+ @rep[:'tiebreaker rank'] || 1
29
29
  end
30
30
 
31
31
  def ==(other)
@@ -6,15 +6,45 @@ module SciolyFF
6
6
  private
7
7
 
8
8
  def subdivision_rep(sub)
9
- # make a deep copy of rep and remove teams not in the subdivision
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
- fix_placings_for_existing_teams(rep)
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] == true
37
+ @rep[:exhibition] || false
38
38
  end
39
39
 
40
40
  def disqualified?
41
- @rep[:disqualified] == true
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
- def name
25
- @rep[:name]
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 location
33
- @rep[:location]
34
- end
35
-
36
- def level
37
- @rep[:level]
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 state
41
- @rep[:state]
48
+ def medals
49
+ @rep[:medals] || [calc_medals, maximum_place].min
42
50
  end
43
51
 
44
- def division
45
- @rep[:division]
52
+ def trophies
53
+ @rep[:trophies] || [calc_trophies, nonexhibition_teams_count].min
46
54
  end
47
55
 
48
- def year
49
- @rep[:year]
56
+ def bids
57
+ @rep[:bids] || 0
50
58
  end
51
59
 
52
- def date
53
- @date ||= if @rep[:date].instance_of?(Date)
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 != @teams.count { |t| !t.exhibition? }
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
- @teams.count { |t| !t.exhibition? }
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
@@ -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 = Logger::WARN)
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)
@@ -8,10 +8,11 @@ module SciolyFF
8
8
  INFO = 2
9
9
  DEBUG = 3
10
10
 
11
- attr_reader :log
11
+ attr_reader :log, :options
12
12
 
13
- def initialize(loglevel)
13
+ def initialize(loglevel, **options)
14
14
  @loglevel = loglevel
15
+ @options = options
15
16
  flush
16
17
  end
17
18
 
@@ -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
- correct = correct_types[key]
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 #{correct}"
33
+ logger.error "#{key}: #{value} is not #{type}"
37
34
  end
38
35
  end
39
36
 
40
37
  private
41
38
 
42
- def correct_date?(correct, value)
43
- correct == Date && Date.parse(value)
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