sciolyff 0.8.0 → 0.9.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 +4 -4
- data/bin/sciolyff +7 -10
- data/lib/sciolyff/interpreter/event.rb +20 -8
- data/lib/sciolyff/interpreter/html/helpers.rb +130 -0
- data/lib/sciolyff/interpreter/html/template.html.erb +558 -0
- data/lib/sciolyff/interpreter/html.rb +47 -0
- data/lib/sciolyff/interpreter/placing.rb +31 -6
- data/lib/sciolyff/interpreter/raw.rb +50 -0
- data/lib/sciolyff/interpreter/subdivisions.rb +41 -0
- data/lib/sciolyff/interpreter/team.rb +16 -3
- data/lib/sciolyff/interpreter/tiebreaks.rb +34 -0
- data/lib/sciolyff/interpreter/tournament.rb +28 -5
- data/lib/sciolyff/interpreter.rb +34 -44
- data/lib/sciolyff/validator/checker.rb +20 -0
- data/lib/sciolyff/validator/events.rb +96 -0
- data/lib/sciolyff/validator/logger.rb +47 -0
- data/lib/sciolyff/validator/penalties.rb +19 -0
- data/lib/sciolyff/validator/placings.rb +120 -0
- data/lib/sciolyff/validator/raws.rb +19 -0
- data/lib/sciolyff/validator/sections.rb +32 -0
- data/lib/sciolyff/validator/teams.rb +81 -0
- data/lib/sciolyff/validator/top_level.rb +22 -0
- data/lib/sciolyff/validator/tournament.rb +72 -0
- data/lib/sciolyff/validator.rb +88 -0
- data/lib/sciolyff.rb +2 -45
- metadata +20 -25
- data/lib/sciolyff/events.rb +0 -66
- data/lib/sciolyff/penalties.rb +0 -52
- data/lib/sciolyff/placings.rb +0 -198
- data/lib/sciolyff/scores.rb +0 -148
- data/lib/sciolyff/sections.rb +0 -40
- data/lib/sciolyff/teams.rb +0 -96
- data/lib/sciolyff/top_level.rb +0 -13
- data/lib/sciolyff/tournament.rb +0 -94
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SciolyFF
|
4
|
+
# Grants ability to convert a SciolyFF file into stand-alone HTML and other
|
5
|
+
# formats (YAML, JSON)
|
6
|
+
module Interpreter::HTML
|
7
|
+
require 'erb'
|
8
|
+
require 'sciolyff/interpreter/html/helpers'
|
9
|
+
require 'json'
|
10
|
+
|
11
|
+
def html
|
12
|
+
helpers = Interpreter::HTML::Helpers.new
|
13
|
+
ERB.new(
|
14
|
+
helpers.template,
|
15
|
+
trim_mode: '<>'
|
16
|
+
).result(helpers.get_binding(self))
|
17
|
+
.gsub(/^\s*$/, '') # remove empty lines
|
18
|
+
.gsub(/\s+$/, '') # remove trailing whitespace
|
19
|
+
end
|
20
|
+
|
21
|
+
def yaml
|
22
|
+
stringify_keys(@rep).to_yaml
|
23
|
+
end
|
24
|
+
|
25
|
+
def json(pretty: false)
|
26
|
+
return JSON.pretty_generate(@rep) if pretty
|
27
|
+
|
28
|
+
@rep.to_json
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def stringify_keys(hash)
|
34
|
+
return hash unless hash.instance_of? Hash
|
35
|
+
|
36
|
+
hash.map do |k, v|
|
37
|
+
new_k = k.to_s
|
38
|
+
new_v = case v
|
39
|
+
when Array then v.map { |e| stringify_keys(e) }
|
40
|
+
when Hash then stringify_keys(v)
|
41
|
+
else v
|
42
|
+
end
|
43
|
+
[new_k, new_v]
|
44
|
+
end.to_h
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -5,13 +5,17 @@ require 'sciolyff/interpreter/model'
|
|
5
5
|
module SciolyFF
|
6
6
|
# Models the result of a team participating (or not) in an event
|
7
7
|
class Interpreter::Placing < Interpreter::Model
|
8
|
+
require 'sciolyff/interpreter/raw'
|
9
|
+
|
8
10
|
def link_to_other_models(interpreter)
|
9
11
|
super
|
10
12
|
@event = interpreter.events.find { |e| e.name == @rep[:event] }
|
11
13
|
@team = interpreter.teams .find { |t| t.number == @rep[:team] }
|
14
|
+
|
15
|
+
link_to_placing_in_subdivision_interpreter(interpreter)
|
12
16
|
end
|
13
17
|
|
14
|
-
attr_reader :event, :team
|
18
|
+
attr_reader :event, :team, :subdivision_placing
|
15
19
|
|
16
20
|
def participated?
|
17
21
|
@rep[:participated] == true || @rep[:participated].nil?
|
@@ -30,11 +34,19 @@ module SciolyFF
|
|
30
34
|
end
|
31
35
|
|
32
36
|
def tie?
|
33
|
-
@rep[:tie] == true
|
37
|
+
raw? ? @tie ||= event.raws.count(raw) > 1 : @rep[:tie] == true
|
34
38
|
end
|
35
39
|
|
36
40
|
def place
|
37
|
-
@rep[: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
|
38
50
|
end
|
39
51
|
|
40
52
|
def did_not_participate?
|
@@ -56,12 +68,13 @@ module SciolyFF
|
|
56
68
|
end
|
57
69
|
|
58
70
|
def isolated_points
|
59
|
-
|
71
|
+
max_place = event.maximum_place
|
72
|
+
n = max_place + tournament.n_offset
|
60
73
|
|
61
74
|
if disqualified? then n + 2
|
62
75
|
elsif did_not_participate? then n + 1
|
63
76
|
elsif participation_only? || unknown? then n
|
64
|
-
else [calculate_points,
|
77
|
+
else [calculate_points, max_place].min
|
65
78
|
end
|
66
79
|
end
|
67
80
|
|
@@ -80,7 +93,11 @@ module SciolyFF
|
|
80
93
|
|
81
94
|
def points_limited_by_maximum_place?
|
82
95
|
tournament.custom_maximum_place? &&
|
83
|
-
(unknown? ||
|
96
|
+
(unknown? ||
|
97
|
+
(place &&
|
98
|
+
(calculate_points > event.maximum_place ||
|
99
|
+
calculate_points == event.maximum_place && tie?
|
100
|
+
)))
|
84
101
|
end
|
85
102
|
|
86
103
|
private
|
@@ -98,5 +115,13 @@ module SciolyFF
|
|
98
115
|
p.place < place
|
99
116
|
end
|
100
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
|
101
126
|
end
|
102
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'] || Float::INFINITY
|
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,41 @@
|
|
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 and remove teams not in the subdivision
|
10
|
+
rep = Marshal.load(Marshal.dump(@rep))
|
11
|
+
rep[:Teams].select! { |t| t.delete(:subdivision) == sub }
|
12
|
+
|
13
|
+
team_numbers = rep[:Teams].map { |t| t[:number] }
|
14
|
+
rep[:Placings].select! { |p| team_numbers.include? p[:team] }
|
15
|
+
|
16
|
+
fix_placings_for_existing_teams(rep)
|
17
|
+
rep
|
18
|
+
end
|
19
|
+
|
20
|
+
def fix_placings_for_existing_teams(rep)
|
21
|
+
rep[:Placings]
|
22
|
+
.group_by { |p| p[:event] }
|
23
|
+
.each { |_, ep| fix_event_placings(ep) }
|
24
|
+
end
|
25
|
+
|
26
|
+
def fix_event_placings(event_placings)
|
27
|
+
event_placings
|
28
|
+
.select { |p| p[:place] }
|
29
|
+
.sort_by { |p| p[:place] }
|
30
|
+
.each_with_index { |p, i| p[:temp_place] = i + 1 }
|
31
|
+
.each { |p| fix_placing_ties(p, event_placings) }
|
32
|
+
.each { |p| p.delete(:temp_place) }
|
33
|
+
end
|
34
|
+
|
35
|
+
def fix_placing_ties(placing, event_placings)
|
36
|
+
ties = event_placings.select { |o| o[:place] == placing[:place] }
|
37
|
+
placing[:place] = ties.map { |t| t[:temp_place] }.max - ties.count + 1
|
38
|
+
ties.count > 1 ? placing[:tie] = true : placing.delete(:tie)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -12,11 +12,10 @@ module SciolyFF
|
|
12
12
|
@placings_by_event =
|
13
13
|
@placings.group_by(&:event).transform_values!(&:first)
|
14
14
|
|
15
|
-
|
16
|
-
@penalties.freeze
|
15
|
+
link_to_team_in_subdivision_interpreter(interpreter)
|
17
16
|
end
|
18
17
|
|
19
|
-
attr_reader :placings, :penalties
|
18
|
+
attr_reader :placings, :penalties, :subdivision_team
|
20
19
|
|
21
20
|
def school
|
22
21
|
@rep[:school]
|
@@ -38,6 +37,10 @@ module SciolyFF
|
|
38
37
|
@rep[:exhibition] == true
|
39
38
|
end
|
40
39
|
|
40
|
+
def disqualified?
|
41
|
+
@rep[:disqualified] == true
|
42
|
+
end
|
43
|
+
|
41
44
|
def number
|
42
45
|
@rep[:number]
|
43
46
|
end
|
@@ -89,5 +92,15 @@ module SciolyFF
|
|
89
92
|
.count { |p| p.isolated_points == medal_points }
|
90
93
|
end
|
91
94
|
end
|
95
|
+
|
96
|
+
private
|
97
|
+
|
98
|
+
def link_to_team_in_subdivision_interpreter(interpreter)
|
99
|
+
return @subdivision_team = nil unless (sub = subdivision)
|
100
|
+
|
101
|
+
@subdivision_team = interpreter.subdivisions[sub].teams.find do |t|
|
102
|
+
t.number == number
|
103
|
+
end
|
104
|
+
end
|
92
105
|
end
|
93
106
|
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
|
@@ -14,9 +14,10 @@ module SciolyFF
|
|
14
14
|
@teams = interpreter.teams
|
15
15
|
@placings = interpreter.placings
|
16
16
|
@penalties = interpreter.penalties
|
17
|
+
@subdivisions = interpreter.subdivisions
|
17
18
|
end
|
18
19
|
|
19
|
-
attr_reader :events, :teams, :placings, :penalties
|
20
|
+
attr_reader :events, :teams, :placings, :penalties, :subdivisions
|
20
21
|
|
21
22
|
undef tournament
|
22
23
|
|
@@ -53,19 +54,19 @@ module SciolyFF
|
|
53
54
|
end
|
54
55
|
|
55
56
|
def worst_placings_dropped?
|
56
|
-
|
57
|
+
worst_placings_dropped.positive?
|
57
58
|
end
|
58
59
|
|
59
60
|
def worst_placings_dropped
|
60
|
-
|
61
|
+
@rep[:'worst placings dropped'] || 0
|
61
62
|
end
|
62
63
|
|
63
64
|
def exempt_placings?
|
64
|
-
|
65
|
+
exempt_placings.positive?
|
65
66
|
end
|
66
67
|
|
67
68
|
def exempt_placings
|
68
|
-
|
69
|
+
@rep[:'exempt placings'] || 0
|
69
70
|
end
|
70
71
|
|
71
72
|
def custom_maximum_place?
|
@@ -77,5 +78,27 @@ module SciolyFF
|
|
77
78
|
|
78
79
|
@teams.count { |t| !t.exhibition? }
|
79
80
|
end
|
81
|
+
|
82
|
+
def per_event_n?
|
83
|
+
@rep[:'per-event n']
|
84
|
+
end
|
85
|
+
|
86
|
+
def n_offset
|
87
|
+
@rep[:'n offset'] || 0
|
88
|
+
end
|
89
|
+
|
90
|
+
def ties?
|
91
|
+
@ties ||= placings.map(&:tie?).any?
|
92
|
+
end
|
93
|
+
|
94
|
+
def ties_outside_of_maximum_places?
|
95
|
+
@ties_outside_of_maximum_places ||= placings.map do |p|
|
96
|
+
p.tie? && !p.points_limited_by_maximum_place?
|
97
|
+
end.any?
|
98
|
+
end
|
99
|
+
|
100
|
+
def subdivisions?
|
101
|
+
!@subdivisions.empty?
|
102
|
+
end
|
80
103
|
end
|
81
104
|
end
|
data/lib/sciolyff/interpreter.rb
CHANGED
@@ -10,26 +10,42 @@ module SciolyFF
|
|
10
10
|
require 'sciolyff/interpreter/placing'
|
11
11
|
require 'sciolyff/interpreter/penalty'
|
12
12
|
|
13
|
+
require 'sciolyff/interpreter/tiebreaks'
|
14
|
+
require 'sciolyff/interpreter/subdivisions'
|
15
|
+
require 'sciolyff/interpreter/html'
|
16
|
+
|
13
17
|
attr_reader :tournament, :events, :teams, :placings, :penalties
|
14
18
|
|
15
19
|
def initialize(rep)
|
16
|
-
|
20
|
+
if rep.instance_of? String
|
21
|
+
rep = YAML.safe_load(File.read(rep),
|
22
|
+
permitted_classes: [Date],
|
23
|
+
symbolize_names: true)
|
24
|
+
end
|
25
|
+
create_models(@rep = rep)
|
17
26
|
link_models(self)
|
18
27
|
|
19
28
|
sort_events_naturally
|
20
29
|
sort_teams_by_rank
|
30
|
+
end
|
21
31
|
|
22
|
-
|
32
|
+
def subdivisions
|
33
|
+
@subdivisions ||=
|
34
|
+
teams.map(&:subdivision)
|
35
|
+
.uniq
|
36
|
+
.compact
|
37
|
+
.map { |sub| [sub, Interpreter.new(subdivision_rep(sub))] }
|
38
|
+
.to_h
|
23
39
|
end
|
24
40
|
|
25
41
|
private
|
26
42
|
|
27
43
|
def create_models(rep)
|
28
44
|
@tournament = Tournament.new(rep)
|
29
|
-
@events
|
30
|
-
@teams
|
31
|
-
@placings
|
32
|
-
@penalties
|
45
|
+
@events = map_array_to_models rep[:Events], Event, rep
|
46
|
+
@teams = map_array_to_models rep[:Teams], Team, rep
|
47
|
+
@placings = map_array_to_models rep[:Placings], Placing, rep
|
48
|
+
@penalties = map_array_to_models rep[:Penalties], Penalty, rep
|
33
49
|
end
|
34
50
|
|
35
51
|
def map_array_to_models(arr, object_class, rep)
|
@@ -47,49 +63,23 @@ module SciolyFF
|
|
47
63
|
@tournament.link_to_other_models(interpreter)
|
48
64
|
end
|
49
65
|
|
50
|
-
def freeze_models
|
51
|
-
@events.freeze
|
52
|
-
@teams.freeze
|
53
|
-
@placings.freeze
|
54
|
-
@penalties.freeze
|
55
|
-
end
|
56
|
-
|
57
66
|
def sort_events_naturally
|
58
|
-
@events.
|
59
|
-
next 1 if a.trial? && !b.trial?
|
60
|
-
next -1 if !a.trial? && b.trial?
|
61
|
-
|
62
|
-
a.name <=> b.name
|
63
|
-
end
|
67
|
+
@events.sort_by! { |e| [e.trial?.to_s, e.name] }
|
64
68
|
end
|
65
69
|
|
66
70
|
def sort_teams_by_rank
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
def break_tie(team_a, team_b)
|
77
|
-
team_a.medal_counts
|
78
|
-
.zip(team_b.medal_counts)
|
79
|
-
.map { |counts| counts.last - counts.first }
|
80
|
-
.find(proc { break_second_tie(team_a, team_b) }, &:nonzero?)
|
71
|
+
sorted =
|
72
|
+
@teams
|
73
|
+
.group_by { |t| [t.disqualified?.to_s, t.exhibition?.to_s] }
|
74
|
+
.map { |key, teams| [key, sort_teams_by_points(teams)] }
|
75
|
+
.sort_by(&:first)
|
76
|
+
.map(&:last)
|
77
|
+
.flatten
|
78
|
+
@teams.map!.with_index { |_, i| sorted[i] }
|
81
79
|
end
|
82
80
|
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
end
|
87
|
-
|
88
|
-
def break_third_tie(team_a, team_b)
|
89
|
-
team_a.trial_event_medal_counts
|
90
|
-
.zip(team_b.trial_event_medal_counts)
|
91
|
-
.map { |counts| counts.last - counts.first }
|
92
|
-
.find(proc { team_a.number <=> team_b.number }, &:nonzero?)
|
93
|
-
end
|
81
|
+
include Interpreter::Tiebreaks
|
82
|
+
include Interpreter::Subdivisions
|
83
|
+
include Interpreter::HTML
|
94
84
|
end
|
95
85
|
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SciolyFF
|
4
|
+
# An empty base class to ensure consistent inheritance. All instance methods
|
5
|
+
# in children classes should take the arguments rep and logger.
|
6
|
+
class Validator::Checker
|
7
|
+
def initialize(rep); end
|
8
|
+
|
9
|
+
# wraps method calls (always using send in Validator) so that exceptions
|
10
|
+
# in the check cause check to pass, as what caused the exception should
|
11
|
+
# cause some other check to fail if the SciolyFF is truly invalid
|
12
|
+
#
|
13
|
+
# this simplifies the checking code greatly, even if it is a bit hacky
|
14
|
+
def send(method, *args)
|
15
|
+
super
|
16
|
+
rescue StandardError => e
|
17
|
+
args[1].debug "#{e}\n #{e.backtrace.first}" # args[1] is the logger
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'sciolyff/validator/checker'
|
4
|
+
require 'sciolyff/validator/sections'
|
5
|
+
|
6
|
+
module SciolyFF
|
7
|
+
# Checks for one event in the Events section of a SciolyFF file
|
8
|
+
class Validator::Events < Validator::Checker
|
9
|
+
include Validator::Sections
|
10
|
+
|
11
|
+
REQUIRED = {
|
12
|
+
name: String
|
13
|
+
}.freeze
|
14
|
+
|
15
|
+
OPTIONAL = {
|
16
|
+
trial: [true, false],
|
17
|
+
trialed: [true, false],
|
18
|
+
scoring: %w[high low]
|
19
|
+
}.freeze
|
20
|
+
|
21
|
+
def initialize(rep)
|
22
|
+
@events = rep[:Events]
|
23
|
+
@names = @events.map { |e| e[:name] }
|
24
|
+
@placings = rep[:Placings].group_by { |p| p[:event] }
|
25
|
+
@teams = rep[:Teams].count
|
26
|
+
end
|
27
|
+
|
28
|
+
def unique_name?(event, logger)
|
29
|
+
return true if @names.count(event[:name]) == 1
|
30
|
+
|
31
|
+
logger.error "duplicate event name: #{event[:name]}"
|
32
|
+
end
|
33
|
+
|
34
|
+
def placings_for_all_teams?(event, logger)
|
35
|
+
count = @placings[event[:name]].count
|
36
|
+
return true if count == @teams
|
37
|
+
|
38
|
+
logger.error "'event: #{event[:name]}' has incorrect number of "\
|
39
|
+
"placings (#{count} instead of #{@teams})"
|
40
|
+
end
|
41
|
+
|
42
|
+
def ties_marked?(event, logger)
|
43
|
+
unmarked_ties = placings_by_place(event).select do |_place, placings|
|
44
|
+
placings.count { |p| !p[:tie] } > 1
|
45
|
+
end
|
46
|
+
return true if unmarked_ties.empty?
|
47
|
+
|
48
|
+
logger.error "'event: #{event[:name]}' has unmarked ties at "\
|
49
|
+
"place #{unmarked_ties.keys.join ', '}"
|
50
|
+
end
|
51
|
+
|
52
|
+
def ties_paired?(event, logger)
|
53
|
+
unpaired_ties = placings_by_place(event).select do |_place, placings|
|
54
|
+
placings.count { |p| p[:tie] } == 1
|
55
|
+
end
|
56
|
+
return true if unpaired_ties.empty?
|
57
|
+
|
58
|
+
logger.error "'event: #{event[:name]}' has unpaired ties at "\
|
59
|
+
"place #{unpaired_ties.keys.join ', '}"
|
60
|
+
end
|
61
|
+
|
62
|
+
def no_gaps_in_places?(event, logger)
|
63
|
+
places = places_with_expanded_ties(event)
|
64
|
+
return true if places.empty?
|
65
|
+
|
66
|
+
gaps = (places.min..places.max).to_a - places
|
67
|
+
return true if gaps.empty?
|
68
|
+
|
69
|
+
logger.error "'event: #{event[:name]}' has gaps in "\
|
70
|
+
"place #{gaps.join ', '}"
|
71
|
+
end
|
72
|
+
|
73
|
+
def places_start_at_one?(event, logger)
|
74
|
+
lowest_place = @placings[event[:name]].map { |p| p[:place] }.compact.min
|
75
|
+
return true if lowest_place == 1 || lowest_place.nil?
|
76
|
+
|
77
|
+
logger.error "places for 'event: #{event[:name]}' start at "\
|
78
|
+
"#{lowest_place} instead of 1"
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
def placings_by_place(event)
|
84
|
+
@placings[event[:name]]
|
85
|
+
.select { |p| p[:place] }
|
86
|
+
.group_by { |p| p[:place] }
|
87
|
+
end
|
88
|
+
|
89
|
+
def places_with_expanded_ties(event)
|
90
|
+
# e.g. [6, 6, 8] -> [6, 7, 8]
|
91
|
+
placings_by_place(event).map do |place, placings|
|
92
|
+
(place..(place + (placings.size - 1))).to_a
|
93
|
+
end.flatten
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SciolyFF
|
4
|
+
# Prints extra information produced by validation process
|
5
|
+
class Validator::Logger
|
6
|
+
ERROR = 0
|
7
|
+
WARN = 1
|
8
|
+
INFO = 2
|
9
|
+
DEBUG = 3
|
10
|
+
|
11
|
+
attr_reader :log
|
12
|
+
|
13
|
+
def initialize(loglevel)
|
14
|
+
@loglevel = loglevel
|
15
|
+
flush
|
16
|
+
end
|
17
|
+
|
18
|
+
def flush
|
19
|
+
@log = String.new
|
20
|
+
end
|
21
|
+
|
22
|
+
def error(msg)
|
23
|
+
return false if @loglevel < ERROR
|
24
|
+
|
25
|
+
@log << "ERROR (invalid SciolyFF): #{msg}\n"
|
26
|
+
false # convenient for using logging the error as return value
|
27
|
+
end
|
28
|
+
|
29
|
+
def warn(msg)
|
30
|
+
return true if @loglevel < WARN
|
31
|
+
|
32
|
+
@log << "WARNING (still valid SciolyFF): #{msg}\n"
|
33
|
+
end
|
34
|
+
|
35
|
+
def info(msg)
|
36
|
+
return true if @loglevel < INFO
|
37
|
+
|
38
|
+
@log << "INFO: #{msg}\n"
|
39
|
+
end
|
40
|
+
|
41
|
+
def debug(msg)
|
42
|
+
return true if @loglevel < DEBUG
|
43
|
+
|
44
|
+
@log << "DEBUG (possible intentional exception): #{msg}\n"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'sciolyff/validator/checker'
|
4
|
+
require 'sciolyff/validator/sections'
|
5
|
+
|
6
|
+
module SciolyFF
|
7
|
+
# Checks for one penalty in the Penalties section of a SciolyFF file
|
8
|
+
class Validator::Penalties < Validator::Checker
|
9
|
+
include Validator::Sections
|
10
|
+
|
11
|
+
REQUIRED = {
|
12
|
+
team: Integer,
|
13
|
+
points: Integer
|
14
|
+
}.freeze
|
15
|
+
|
16
|
+
OPTIONAL = {
|
17
|
+
}.freeze
|
18
|
+
end
|
19
|
+
end
|