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,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