sciolyff 0.10.0 → 0.11.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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