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,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SciolyFF
4
+ # Parent class for other nested classes within Interpreter
5
+ class Interpreter::Model
6
+ def initialize(rep, index)
7
+ @rep = rep[pluralize_for_key(self.class)][index]
8
+ end
9
+
10
+ def link_to_other_models(interpreter)
11
+ @tournament = interpreter.tournament
12
+ end
13
+
14
+ attr_reader :tournament
15
+
16
+ # prevents infinite loop due caused by intentional circular references
17
+ def inspect
18
+ to_s.delete_suffix('>') + " @rep=#{@rep}>"
19
+ end
20
+
21
+ private
22
+
23
+ def pluralize_for_key(klass)
24
+ name = klass.name.split('::').last
25
+ name = name.delete_suffix('y') + 'ie' if name.end_with?('y')
26
+ (name + 's').to_sym
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sciolyff/interpreter/model'
4
+
5
+ module SciolyFF
6
+ # Models a team penalty for a Science Olympiad team at a tournament
7
+ class Interpreter::Penalty < Interpreter::Model
8
+ def link_to_other_models(interpreter)
9
+ super
10
+ @team = interpreter.teams.find { |t| t.number == @rep[:team] }
11
+ end
12
+
13
+ attr_reader :team
14
+
15
+ def points
16
+ @rep[:points]
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sciolyff/interpreter/model'
4
+
5
+ module SciolyFF
6
+ # Models the result of a team participating (or not) in an event
7
+ class Interpreter::Placing < Interpreter::Model
8
+ require 'sciolyff/interpreter/raw'
9
+
10
+ def link_to_other_models(interpreter)
11
+ super
12
+ @event = interpreter.events.find { |e| e.name == @rep[:event] }
13
+ @team = interpreter.teams .find { |t| t.number == @rep[:team] }
14
+
15
+ link_to_placing_in_subdivision_interpreter(interpreter)
16
+ end
17
+
18
+ attr_reader :event, :team, :subdivision_placing
19
+
20
+ def participated?
21
+ @rep[:participated] || @rep[:participated].nil?
22
+ end
23
+
24
+ def disqualified?
25
+ @rep[:disqualified] || false
26
+ end
27
+
28
+ def exempt?
29
+ @rep[:exempt] || false
30
+ end
31
+
32
+ def unknown?
33
+ @rep[:unknown] || false
34
+ end
35
+
36
+ def tie?
37
+ raw? ? @tie ||= event.raws.count(raw) > 1 : @rep[:tie] == true
38
+ end
39
+
40
+ def place
41
+ raw? ? @place ||= event.raws.find_index(raw) + 1 : @rep[:place]
42
+ end
43
+
44
+ def raw
45
+ @raw ||= Raw.new(@rep[:raw], event.low_score_wins?) if raw?
46
+ end
47
+
48
+ def raw?
49
+ @rep.key? :raw
50
+ end
51
+
52
+ def did_not_participate?
53
+ !participated?
54
+ end
55
+
56
+ def participation_only?
57
+ participated? && !place && !disqualified? && !unknown?
58
+ end
59
+
60
+ def dropped_as_part_of_worst_placings?
61
+ team.worst_placings_to_be_dropped.include?(self)
62
+ end
63
+
64
+ def points
65
+ @points ||= if !considered_for_team_points? then 0
66
+ else isolated_points
67
+ end
68
+ end
69
+
70
+ def isolated_points
71
+ max_place = event.maximum_place
72
+ n = max_place + tournament.n_offset
73
+
74
+ if disqualified? then n + 2
75
+ elsif did_not_participate? then n + 1
76
+ elsif participation_only? || unknown? then n
77
+ else [calculate_points, max_place].min
78
+ end
79
+ end
80
+
81
+ def considered_for_team_points?
82
+ initially_considered_for_team_points? &&
83
+ !dropped_as_part_of_worst_placings?
84
+ end
85
+
86
+ def initially_considered_for_team_points?
87
+ !(event.trial? || event.trialed? || exempt?)
88
+ end
89
+
90
+ def points_affected_by_exhibition?
91
+ considered_for_team_points? && place && !exhibition_placings_behind.zero?
92
+ end
93
+
94
+ def points_limited_by_maximum_place?
95
+ tournament.custom_maximum_place? &&
96
+ (unknown? ||
97
+ (place &&
98
+ (calculate_points > event.maximum_place ||
99
+ calculate_points == event.maximum_place && tie?
100
+ )))
101
+ end
102
+
103
+ private
104
+
105
+ def calculate_points
106
+ return place if event.trial?
107
+
108
+ place - exhibition_placings_behind
109
+ end
110
+
111
+ def exhibition_placings_behind
112
+ @exhibition_placings_behind ||= event.placings.count do |p|
113
+ (p.exempt? || p.team.exhibition?) &&
114
+ p.place &&
115
+ p.place < place
116
+ end
117
+ end
118
+
119
+ def link_to_placing_in_subdivision_interpreter(interpreter)
120
+ return @subdivision_placing = nil unless (sub = team.subdivision)
121
+
122
+ @subdivision_placing = interpreter.subdivisions[sub].placings.find do |p|
123
+ p.event.name == event.name && p.team.number == team.number
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SciolyFF
4
+ # Models the raw score representation for a Placing
5
+ class Interpreter::Placing::Raw
6
+ def initialize(rep, low_score_wins)
7
+ @rep = rep
8
+ @low_score_wins = low_score_wins
9
+ end
10
+
11
+ def score
12
+ @rep[:score]
13
+ end
14
+
15
+ def tiered?
16
+ tier > 1
17
+ end
18
+
19
+ def tier
20
+ @rep[:tier] || 1
21
+ end
22
+
23
+ def lost_tiebreaker?
24
+ tiebreaker_rank > 1
25
+ end
26
+
27
+ def tiebreaker_rank
28
+ @rep[:'tiebreaker rank'] || 1
29
+ end
30
+
31
+ def ==(other)
32
+ score == other.score &&
33
+ tier == other.tier &&
34
+ tiebreaker_rank == other.tiebreaker_rank
35
+ end
36
+
37
+ def <=>(other)
38
+ [
39
+ tier,
40
+ @low_score_wins ? score : -score,
41
+ tiebreaker_rank
42
+ ] <=>
43
+ [
44
+ other.tier,
45
+ @low_score_wins ? other.score : -other.score,
46
+ other.tiebreaker_rank
47
+ ]
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SciolyFF
4
+ # Subdivision logic, to be used in the Interpreter class
5
+ module Interpreter::Subdivisions
6
+ private
7
+
8
+ def subdivision_rep(sub)
9
+ # make a deep copy of rep
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) unless raws?
16
+ rep
17
+ end
18
+
19
+ def remove_teams_not_in_subdivision(rep, sub)
20
+ rep[:Teams].select! { |t| t.delete(:subdivision) == sub }
21
+
22
+ team_numbers = rep[:Teams].map { |t| t[:number] }
23
+ rep[:Placings].select! { |p| team_numbers.include? p[:team] }
24
+ end
25
+
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
+ tournament_rep.delete(:'bids per school')
34
+ rep.delete(:Subdivisions)
35
+ end
36
+
37
+ def replace_tournament_fields(tournament_rep, sub_rep)
38
+ [:medals, :trophies, :'maximum place'].each do |key|
39
+ tournament_rep[key] = sub_rep[key] if sub_rep.key?(key)
40
+ end
41
+ end
42
+
43
+ def limit_maximum_place(rep)
44
+ max_place = rep[:Tournament][:'maximum place']
45
+ team_count = rep[:Teams].count { |t| !t[:exhibition] }
46
+
47
+ rep[:Tournament].delete(:'maximum place') if
48
+ !max_place.nil? && max_place > team_count
49
+ end
50
+
51
+ def fix_placings_for_existing_teams(rep)
52
+ rep[:Placings]
53
+ .group_by { |p| p[:event] }
54
+ .each { |_, ep| fix_event_placings(ep) }
55
+ end
56
+
57
+ def fix_event_placings(event_placings)
58
+ event_placings
59
+ .select { |p| p[:place] }
60
+ .sort_by { |p| p[:place] }
61
+ .each_with_index { |p, i| p[:temp_place] = i + 1 }
62
+ .each { |p| fix_placing_ties(p, event_placings) }
63
+ .each { |p| p.delete(:temp_place) }
64
+ end
65
+
66
+ def fix_placing_ties(placing, event_placings)
67
+ ties = event_placings.select { |o| o[:place] == placing[:place] }
68
+ placing[:place] = ties.map { |t| t[:temp_place] }.max - ties.count + 1
69
+ ties.count > 1 ? placing[:tie] = true : placing.delete(:tie)
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sciolyff/interpreter/model'
4
+
5
+ module SciolyFF
6
+ # Models an instance of a Science Olympiad team at a specific tournament
7
+ class Interpreter::Team < Interpreter::Model
8
+ def link_to_other_models(interpreter)
9
+ super
10
+ @placings = interpreter.placings .select { |p| p.team == self }
11
+ @penalties = interpreter.penalties.select { |p| p.team == self }
12
+ @placings_by_event =
13
+ @placings.group_by(&:event).transform_values!(&:first)
14
+
15
+ link_to_team_in_subdivision_interpreter(interpreter)
16
+ end
17
+
18
+ attr_reader :placings, :penalties, :subdivision_team
19
+
20
+ def school
21
+ @rep[:school]
22
+ end
23
+
24
+ def school_abbreviation
25
+ @rep[:'school abbreviation']
26
+ end
27
+
28
+ def suffix
29
+ @rep[:suffix]
30
+ end
31
+
32
+ def subdivision
33
+ @rep[:subdivision]
34
+ end
35
+
36
+ def exhibition?
37
+ @rep[:exhibition] || false
38
+ end
39
+
40
+ def disqualified?
41
+ @rep[:disqualified] || false
42
+ end
43
+
44
+ def number
45
+ @rep[:number]
46
+ end
47
+
48
+ def city
49
+ @rep[:city]
50
+ end
51
+
52
+ def state
53
+ @rep[:state]
54
+ end
55
+
56
+ def placing_for(event)
57
+ @placings_by_event[event]
58
+ end
59
+
60
+ def rank
61
+ @tournament.teams.find_index(self) + 1
62
+ end
63
+
64
+ def points
65
+ @points ||= placings.sum(&:points) + penalties.sum(&:points)
66
+ end
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
+
73
+ def worst_placings_to_be_dropped
74
+ return [] if @tournament.worst_placings_dropped.zero?
75
+
76
+ placings
77
+ .select(&:initially_considered_for_team_points?)
78
+ .sort_by(&:isolated_points)
79
+ .reverse
80
+ .take(@tournament.worst_placings_dropped)
81
+ end
82
+
83
+ def trial_event_points
84
+ placings.select { |p| p.event.trial? }.sum(&:isolated_points)
85
+ end
86
+
87
+ def medal_counts
88
+ (1..(@tournament.teams.count + 2)).map do |medal_points|
89
+ placings.select(&:considered_for_team_points?)
90
+ .count { |p| p.points == medal_points }
91
+ end
92
+ end
93
+
94
+ def trial_event_medal_counts
95
+ (1..(@tournament.teams.count + 2)).map do |medal_points|
96
+ placings.select { |p| p.event.trial? }
97
+ .count { |p| p.isolated_points == medal_points }
98
+ end
99
+ end
100
+
101
+ private
102
+
103
+ def link_to_team_in_subdivision_interpreter(interpreter)
104
+ return @subdivision_team = nil unless (sub = subdivision)
105
+
106
+ @subdivision_team = interpreter.subdivisions[sub].teams.find do |t|
107
+ t.number == number
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SciolyFF
4
+ # Tie-breaking logic for teams, to be used in the Interpreter class
5
+ module Interpreter::Tiebreaks
6
+ private
7
+
8
+ def sort_teams_by_points(teams)
9
+ teams.sort do |team_a, team_b|
10
+ cmp = team_a.points <=> team_b.points
11
+ cmp.zero? ? break_tie(team_a, team_b) : cmp
12
+ end
13
+ end
14
+
15
+ def break_tie(team_a, team_b)
16
+ team_a.medal_counts
17
+ .zip(team_b.medal_counts)
18
+ .map { |counts| counts.last - counts.first }
19
+ .find(proc { break_second_tie(team_a, team_b) }, &:nonzero?)
20
+ end
21
+
22
+ def break_second_tie(team_a, team_b)
23
+ cmp = team_a.trial_event_points <=> team_b.trial_event_points
24
+ cmp.zero? ? break_third_tie(team_a, team_b) : cmp
25
+ end
26
+
27
+ def break_third_tie(team_a, team_b)
28
+ team_a.trial_event_medal_counts
29
+ .zip(team_b.trial_event_medal_counts)
30
+ .map { |counts| counts.last - counts.first }
31
+ .find(proc { team_a.number <=> team_b.number }, &:nonzero?)
32
+ end
33
+ end
34
+ end