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