sciolyff 0.8.0 → 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|