sciolyff-duosmium 0.13.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +92 -0
- data/bin/sciolyff +38 -0
- data/lib/sciolyff.rb +9 -0
- data/lib/sciolyff/interpreter.rb +89 -0
- data/lib/sciolyff/interpreter/bids.rb +24 -0
- data/lib/sciolyff/interpreter/event.rb +69 -0
- data/lib/sciolyff/interpreter/html.rb +65 -0
- data/lib/sciolyff/interpreter/html/helpers.rb +230 -0
- data/lib/sciolyff/interpreter/html/main.css +1 -0
- data/lib/sciolyff/interpreter/html/main.js +74 -0
- data/lib/sciolyff/interpreter/html/template.html.erb +480 -0
- data/lib/sciolyff/interpreter/model.rb +29 -0
- data/lib/sciolyff/interpreter/penalty.rb +19 -0
- data/lib/sciolyff/interpreter/placing.rb +127 -0
- data/lib/sciolyff/interpreter/raw.rb +50 -0
- data/lib/sciolyff/interpreter/subdivisions.rb +72 -0
- data/lib/sciolyff/interpreter/team.rb +111 -0
- data/lib/sciolyff/interpreter/tiebreaks.rb +34 -0
- data/lib/sciolyff/interpreter/tournament.rb +194 -0
- data/lib/sciolyff/validator.rb +89 -0
- data/lib/sciolyff/validator/canonical.rb +23 -0
- data/lib/sciolyff/validator/checker.rb +33 -0
- data/lib/sciolyff/validator/events.rb +106 -0
- data/lib/sciolyff/validator/logger.rb +48 -0
- data/lib/sciolyff/validator/penalties.rb +19 -0
- data/lib/sciolyff/validator/placings.rb +136 -0
- data/lib/sciolyff/validator/range.rb +15 -0
- data/lib/sciolyff/validator/raws.rb +40 -0
- data/lib/sciolyff/validator/sections.rb +56 -0
- data/lib/sciolyff/validator/subdivisions.rb +64 -0
- data/lib/sciolyff/validator/teams.rb +108 -0
- data/lib/sciolyff/validator/top_level.rb +23 -0
- data/lib/sciolyff/validator/tournament.rb +138 -0
- data/sciolyff.gemspec +22 -0
- 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
|