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.
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,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 9f8d723a4b5265cf85a60231c83d8f1eeea0558cc0f6c8f2e51572bca2a86e6f
4
+ data.tar.gz: b733acad947c8c180011279e4681c086bc94d78efb920433f6e0d72333c3b8e9
5
+ SHA512:
6
+ metadata.gz: 62fddbdf4bb9e9e953678a9cf87487c9f68b97b082cc9be825f384933ae06093923dfbc1966aa7c1a5f8ff7fd60107216f43ef9014fb09d5d57b6eba9fff76cb
7
+ data.tar.gz: beef2bd6cad0cf430f7699b515a85459881ef1c3f4602ef622046e2f62024d52917eafe63418aa27825ea8dc41c5aa1601b57c327b8376456940c001e17816cf
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2020 Em Zhan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,92 @@
1
+ # SciolyFF (Science Olympiad File Format)
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/sciolyff.svg)](https://badge.fury.io/rb/sciolyff)
4
+
5
+ We propose a standardized file format called SciolyFF to represent Science
6
+ Olympiad tournament results. This will allow for a more universal record of
7
+ tournament performance and also make it easier to do sabermetric-like stats and
8
+ other fun stuff. The format is a subset of YAML for easy implementation of
9
+ parsers across many programming languages.
10
+
11
+ A website that generates results tables based off SciolyFF files can be found
12
+ [here](https://unosmium.org/results/) and the source code for the website
13
+ [here](https://github.com/unosmium/unosmium.org).
14
+
15
+ ## Specification
16
+
17
+ Reading through the demo file [here](examples/demo.yaml) is probably the fastest
18
+ way to get acquainted with the format. Officially, any file that passes the
19
+ validation (see Usage -- Validation) is valid, but the intentions of the format
20
+ outlined in the comments of the demo file should be respected.
21
+
22
+ ## Installation
23
+
24
+ ```
25
+ gem install sciolyff
26
+ ```
27
+
28
+ This gem is currently in an alpha stage. To get the latest changes before
29
+ official releases, build from source:
30
+
31
+ ```
32
+ git clone https://github.com/unosmium/sciolyff.git && cd sciolyff
33
+ gem build sciolyff.gemspec
34
+ gem install ./sciolyff-0.12.0.gem
35
+ ```
36
+
37
+ ## Usage
38
+
39
+ ### Validation
40
+
41
+ A Ruby gem provided in this repository contains a command line utility that can
42
+ validate if a given file meets the SciolyFF. The files found in
43
+ `lib/sciolyff/validator` also serve as the specification for the format.
44
+
45
+ From the command line, e.g.
46
+
47
+ ```
48
+ sciolyff 2017-05-20_nationals_c.yaml
49
+ ```
50
+
51
+ Inside Ruby code, e.g.
52
+
53
+ ```ruby
54
+ require 'sciolyff'
55
+
56
+ validator = SciolyFF::Validator.new
57
+ puts validator.valid? File.read('2017-05-20_nationals_c.yaml') #=> true
58
+ print validator.last_log #=> error or warning messages
59
+ ```
60
+
61
+ ### Parsing
62
+
63
+ Although the SciolyFF makes the results file human-readable without the
64
+ ambiguity of spreadsheet results, it can be a bit awkward to parse overall
65
+ results -- for instance, when trying to regenerate a results spreadsheet from a
66
+ SciolyFF file.
67
+
68
+ To make this easier, a `SciolyFF::Interpreter` class has been provided to wrap
69
+ the output of Ruby's yaml parser. For example:
70
+
71
+ ```ruby
72
+ require 'sciolyff'
73
+
74
+ i = SciolyFF::Interpreter.new(File.read('2017-05-20_nationals_c.yaml'))
75
+
76
+ a_and_p = i.events.find { |e| e.name == 'Anatomy and Physiology' }
77
+ a_and_p.trialed? #=> false
78
+
79
+ team_one = i.teams.find { |t| t.number == 1 }
80
+ team_one.placing_for(a_and_p).points #=> 7
81
+ team_one.points #=> 448
82
+
83
+ # sorted by rank
84
+ i.teams #=> [#<...{:school=>"Troy H.S.", :number=>3, :state=>"CA"}>, ... ]
85
+ ```
86
+
87
+ A fuller example can be found here in the code for the Unosmium Results website,
88
+ found
89
+ [here](https://github.com/unosmium/unosmium.org/blob/master/source/results/template.html.erb).
90
+ There is also of course the
91
+ [documentation](https://www.rubydoc.info/gems/sciolyff/0.12.0), a bit sparse
92
+ currently.
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'optimist'
5
+ require 'sciolyff'
6
+
7
+ opts = Optimist.options do
8
+ version 'sciolyff 0.13.0'
9
+ banner <<~STRING
10
+ Checks if a given file is in the Scioly File Format
11
+
12
+ Usage:
13
+ #{File.basename(__FILE__)} [options] <file>
14
+
15
+ where [options] are:
16
+ STRING
17
+ opt :loglevel, 'Log verbosity from 0 to 3', default: 1
18
+ opt :nocanon, 'Disable canonical name checks'
19
+ end
20
+
21
+ Optimist.educate if ARGV.empty?
22
+ puts 'More than one file given, ignoring all but first.' if ARGV.length > 1
23
+
24
+ begin
25
+ contents = File.read(ARGV.first)
26
+ rescue StandardError => e
27
+ puts "Could not read file: #{e}"
28
+ exit 1
29
+ end
30
+
31
+ validator = SciolyFF::Validator.new(
32
+ loglevel: opts[:loglevel],
33
+ canonical: !opts[:nocanon]
34
+ )
35
+ validity = validator.valid? contents
36
+ print validator.last_log
37
+
38
+ exit validity
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'csv'
4
+ require 'open-uri'
5
+ require 'psych'
6
+ require 'date'
7
+
8
+ require 'sciolyff/interpreter'
9
+ require 'sciolyff/validator'
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SciolyFF
4
+ # Interprets the YAML representation of a SciolyFF file through objects that
5
+ # respond to idiomatic Ruby method calls
6
+ class Interpreter
7
+ require 'sciolyff/interpreter/tournament'
8
+ require 'sciolyff/interpreter/event'
9
+ require 'sciolyff/interpreter/team'
10
+ require 'sciolyff/interpreter/placing'
11
+ require 'sciolyff/interpreter/penalty'
12
+
13
+ require 'sciolyff/interpreter/tiebreaks'
14
+ require 'sciolyff/interpreter/subdivisions'
15
+ require 'sciolyff/interpreter/html'
16
+
17
+ attr_reader :tournament, :events, :teams, :placings, :penalties
18
+
19
+ def initialize(rep)
20
+ if rep.instance_of? String
21
+ rep = Psych.safe_load(rep,
22
+ permitted_classes: [Date],
23
+ symbolize_names: true)
24
+ end
25
+ create_models(@rep = rep)
26
+ link_models(self)
27
+
28
+ sort_events_naturally
29
+ sort_teams_by_rank
30
+ end
31
+
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
39
+ end
40
+
41
+ def raws?
42
+ placings.any?(&:raw?)
43
+ end
44
+
45
+ private
46
+
47
+ def create_models(rep)
48
+ @tournament = Tournament.new(rep)
49
+ @events = map_array_to_models rep[:Events], Event, rep
50
+ @teams = map_array_to_models rep[:Teams], Team, rep
51
+ @placings = map_array_to_models rep[:Placings], Placing, rep
52
+ @penalties = map_array_to_models rep[:Penalties], Penalty, rep
53
+ end
54
+
55
+ def map_array_to_models(arr, object_class, rep)
56
+ return [] if arr.nil?
57
+
58
+ arr.map.with_index { |_, index| object_class.new(rep, index) }
59
+ end
60
+
61
+ def link_models(interpreter)
62
+ # models have to linked in reverse order because reasons
63
+ @penalties.each { |m| m.link_to_other_models(interpreter) }
64
+ @placings .each { |m| m.link_to_other_models(interpreter) }
65
+ @teams .each { |m| m.link_to_other_models(interpreter) }
66
+ @events .each { |m| m.link_to_other_models(interpreter) }
67
+ @tournament.link_to_other_models(interpreter)
68
+ end
69
+
70
+ def sort_events_naturally
71
+ @events.sort_by! { |e| [e.trial?.to_s, e.name] }
72
+ end
73
+
74
+ def sort_teams_by_rank
75
+ sorted =
76
+ @teams
77
+ .group_by { |t| [t.disqualified?.to_s, t.exhibition?.to_s] }
78
+ .map { |key, teams| [key, sort_teams_by_points(teams)] }
79
+ .sort_by(&:first)
80
+ .map(&:last)
81
+ .flatten
82
+ @teams.map!.with_index { |_, i| sorted[i] }
83
+ end
84
+
85
+ include Interpreter::Tiebreaks
86
+ include Interpreter::Subdivisions
87
+ include Interpreter::HTML
88
+ end
89
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SciolyFF
4
+ # Bid assignment logic, to be included in the Interpreter::Tournament class
5
+ module Interpreter::Bids
6
+ def top_teams_per_school
7
+ # explicitly relies on uniq traversing in order
8
+ @top_teams_per_school ||= @teams.uniq { |t| [t.school, t.city, t.state] }
9
+ end
10
+
11
+ def teams_eligible_for_bids
12
+ return top_teams_per_school if bids_per_school == 1
13
+
14
+ # doesn't rely on group_by preserving order
15
+ @teams_eligible_for_bids ||=
16
+ @teams
17
+ .group_by { |t| [t.school, t.city, t.state] }
18
+ .each_value { |teams| teams.sort_by!(&:rank) }
19
+ .map { |_, teams| teams.take(bids_per_school) }
20
+ .flatten
21
+ .sort_by(&:rank)
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sciolyff/interpreter/model'
4
+
5
+ module SciolyFF
6
+ # Models an instance of a Science Olympiad event at a specific tournament
7
+ class Interpreter::Event < Interpreter::Model
8
+ def link_to_other_models(interpreter)
9
+ super
10
+ @placings = interpreter.placings.select { |p| p.event == self }
11
+ @placings_by_team =
12
+ @placings.group_by(&:team).transform_values!(&:first)
13
+ @raws = @placings.select(&:raw?).map(&:raw).sort
14
+ end
15
+
16
+ attr_reader :placings, :raws
17
+
18
+ def name
19
+ @rep[:name]
20
+ end
21
+
22
+ def trial?
23
+ @rep[:trial] || false
24
+ end
25
+
26
+ def trialed?
27
+ @rep[:trialed] || false
28
+ end
29
+
30
+ def high_score_wins?
31
+ !low_score_wins?
32
+ end
33
+
34
+ def low_score_wins?
35
+ @rep[:scoring] == 'low'
36
+ end
37
+
38
+ def placing_for(team)
39
+ @placings_by_team[team]
40
+ end
41
+
42
+ def maximum_place
43
+ @maximum_place ||=
44
+ if trial?
45
+ placings.size
46
+ elsif tournament.per_event_n?
47
+ [per_event_maximum_place, tournament.maximum_place].min
48
+ else
49
+ tournament.maximum_place
50
+ end
51
+ end
52
+
53
+ private
54
+
55
+ def per_event_maximum_place
56
+ return competing_teams_count if tournament.per_event_n == 'participation'
57
+
58
+ placings.map(&:place).compact.max + 1
59
+ end
60
+
61
+ def competing_teams_count
62
+ return placings.count(&:participated?) if trial?
63
+
64
+ placings.count do |p|
65
+ p.participated? && !(p.team.exhibition? || p.exempt?)
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,65 @@
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 'erubi'
8
+ require 'sciolyff/interpreter/html/helpers'
9
+ require 'json'
10
+
11
+ def to_html(hide_raw: false, color: '#000000')
12
+ helpers = Interpreter::HTML::Helpers.new
13
+ src = Erubi::Engine.new(helpers.template).src
14
+ helpers.eval_with_binding(src, self, hide_raw, color)
15
+ .gsub(/^\s*$/, '') # remove empty lines
16
+ .gsub(/\s+$/, '') # remove trailing whitespace
17
+ end
18
+
19
+ def to_yaml(hide_raw: false)
20
+ rep = hide_raw ? hidden_raw_rep : @rep
21
+ stringify_keys(rep).to_yaml
22
+ end
23
+
24
+ def to_json(hide_raw: false, pretty: false)
25
+ rep = hide_raw ? hidden_raw_rep : @rep
26
+ return JSON.pretty_generate(rep) if pretty
27
+
28
+ rep.to_json
29
+ end
30
+
31
+ private
32
+
33
+ def hidden_raw_rep
34
+ @hidden_raw_rep || begin
35
+ @hidden_raw_rep = Marshal.load(Marshal.dump(@rep))
36
+ @hidden_raw_rep[:Placings].each { |p| hide_and_replace_raw(p) }
37
+ @hidden_raw_rep
38
+ end
39
+ end
40
+
41
+ def hide_and_replace_raw(placing_rep)
42
+ placing = placings.find do |p|
43
+ p.event.name == placing_rep[:event] &&
44
+ p.team.number == placing_rep[:team]
45
+ end
46
+ placing_rep.delete :raw
47
+ placing_rep[:tie] = true if placing.tie?
48
+ placing_rep[:place] = placing.place if placing.place
49
+ end
50
+
51
+ def stringify_keys(hash)
52
+ return hash unless hash.instance_of? Hash
53
+
54
+ hash.map do |k, v|
55
+ new_k = k.to_s
56
+ new_v = case v
57
+ when Array then v.map { |e| stringify_keys(e) }
58
+ when Hash then stringify_keys(v)
59
+ else v
60
+ end
61
+ [new_k, new_v]
62
+ end.to_h
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,230 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SciolyFF
4
+ # Holds helper methods used in template.html.erb
5
+ class Interpreter::HTML::Helpers
6
+ def template
7
+ File.read(File.join(__dir__, 'template.html.erb'))
8
+ end
9
+
10
+ def eval_with_binding(src, interpreter, hide_raw, color)
11
+ i = interpreter
12
+ css_file_content = File.read(File.join(__dir__, 'main.css'))
13
+ js_file_content = File.read(File.join(__dir__, 'main.js'))
14
+ eval(src)
15
+ end
16
+
17
+ private
18
+
19
+ STATES_BY_POSTAL_CODE = {
20
+ AK: 'Alaska',
21
+ AZ: 'Arizona',
22
+ AR: 'Arkansas',
23
+ CA: 'California',
24
+ nCA: 'Northern California',
25
+ sCA: 'Southern California',
26
+ CO: 'Colorado',
27
+ CT: 'Connecticut',
28
+ DE: 'Delaware',
29
+ DC: 'District of Columbia',
30
+ FL: 'Florida',
31
+ GA: 'Georgia',
32
+ HI: 'Hawaii',
33
+ ID: 'Idaho',
34
+ IL: 'Illinois',
35
+ IN: 'Indiana',
36
+ IA: 'Iowa',
37
+ KS: 'Kansas',
38
+ KY: 'Kentucky',
39
+ LA: 'Louisiana',
40
+ ME: 'Maine',
41
+ MD: 'Maryland',
42
+ MA: 'Massachusetts',
43
+ MI: 'Michigan',
44
+ MN: 'Minnesota',
45
+ MS: 'Mississippi',
46
+ MO: 'Missouri',
47
+ MT: 'Montana',
48
+ NE: 'Nebraska',
49
+ NV: 'Nevada',
50
+ NH: 'New Hampshire',
51
+ NJ: 'New Jersey',
52
+ NM: 'New Mexico',
53
+ NY: 'New York',
54
+ NC: 'North Carolina',
55
+ ND: 'North Dakota',
56
+ OH: 'Ohio',
57
+ OK: 'Oklahoma',
58
+ OR: 'Oregon',
59
+ PA: 'Pennsylvania',
60
+ RI: 'Rhode Island',
61
+ SC: 'South Carolina',
62
+ SD: 'South Dakota',
63
+ TN: 'Tennessee',
64
+ TX: 'Texas',
65
+ UT: 'Utah',
66
+ VT: 'Vermont',
67
+ VA: 'Virginia',
68
+ WA: 'Washington',
69
+ WV: 'West Virginia',
70
+ WI: 'Wisconsin',
71
+ WY: 'Wyoming'
72
+ }.freeze
73
+
74
+ def trophy_and_medal_colors
75
+ %w[
76
+ #ffee58
77
+ #cfd8dc
78
+ #d8bf99
79
+ #ffefc0
80
+ #dcedc8
81
+ #f8bbd0
82
+ #eeccff
83
+ #fdd5b4
84
+ #ebedd8
85
+ #d4f0f1
86
+ ]
87
+ end
88
+
89
+ def trophy_and_medal_css(trophies, medals)
90
+ trophy_and_medal_colors.map.with_index do |color, i|
91
+ [
92
+ ("td.event-points[data-points='#{i+1}'] div" if i < medals),
93
+ ("td.event-points-focus[data-points='#{i+1}'] div" if i < medals),
94
+ ("div#team-detail tr[data-points='#{i+1}']" if i < medals),
95
+ ("td.rank[data-points='#{i+1}'] div" if i < trophies)
96
+ ].compact.join(',') + "{background-color: #{color};border-radius: 1em;}"
97
+ end.join +
98
+ trophy_and_medal_colors.map.with_index do |color, i|
99
+ "div#team-detail tr[data-points='#{i+1}'] td:first-child" if i < medals
100
+ end.compact.join(',') + "{padding-left: 0.5em;}"
101
+ end
102
+
103
+ def tournament_title(t_info)
104
+ return t_info.name if t_info.name
105
+
106
+ case t_info.level
107
+ when 'Nationals'
108
+ 'Science Olympiad National Tournament'
109
+ when 'States'
110
+ "#{expand_state_name(t_info.state)} Science Olympiad State Tournament"
111
+ when 'Regionals'
112
+ "#{t_info.location} Regional Tournament"
113
+ when 'Invitational'
114
+ "#{t_info.location} Invitational"
115
+ end
116
+ end
117
+
118
+ def tournament_title_short(t_info)
119
+ case t_info.level
120
+ when 'Nationals'
121
+ 'National Tournament'
122
+ when 'States'
123
+ "#{t_info.state} State Tournament"
124
+ when 'Regionals', 'Invitational'
125
+ t_info.short_name
126
+ end
127
+ end
128
+
129
+ def expand_state_name(postal_code)
130
+ STATES_BY_POSTAL_CODE[postal_code.to_sym]
131
+ end
132
+
133
+ def format_school(team)
134
+ if team.school_abbreviation
135
+ abbr_school(team.school_abbreviation)
136
+ else
137
+ abbr_school(team.school)
138
+ end
139
+ end
140
+
141
+ def abbr_school(school)
142
+ school.sub('Elementary School', 'Elementary')
143
+ .sub('Elementary/Middle School', 'E.M.S.')
144
+ .sub('Middle School', 'M.S.')
145
+ .sub('Junior High School', 'J.H.S.')
146
+ .sub(%r{Middle[ /-]High School}, 'M.H.S')
147
+ .sub('Junior/Senior High School', 'Jr./Sr. H.S.')
148
+ .sub('High School', 'H.S.')
149
+ .sub('Secondary School', 'Secondary')
150
+ end
151
+
152
+ def full_school_name(team)
153
+ location = if team.city then "(#{team.city}, #{team.state})"
154
+ else "(#{team.state})"
155
+ end
156
+ [team.school, location].join(' ')
157
+ end
158
+
159
+ def full_team_name(team)
160
+ location = if team.city then "(#{team.city}, #{team.state})"
161
+ else "(#{team.state})"
162
+ end
163
+ [team.school, team.suffix, location].join(' ')
164
+ end
165
+
166
+ def team_attended?(team)
167
+ team
168
+ .placings
169
+ .map(&:participated?)
170
+ .any?
171
+ end
172
+
173
+ def summary_titles
174
+ %w[
175
+ Champion
176
+ Runner-up
177
+ Third-place
178
+ Fourth-place
179
+ Fifth-place
180
+ Sixth-place
181
+ ]
182
+ end
183
+
184
+ def sup_tag(placing)
185
+ exempt = placing.exempt? || placing.dropped_as_part_of_worst_placings?
186
+ tie = placing.tie? && !placing.points_limited_by_maximum_place?
187
+ return '' unless tie || exempt
188
+
189
+ "<sup>#{'◊' if exempt}#{'*' if tie}</sup>"
190
+ end
191
+
192
+ def bids_sup_tag(team)
193
+ return '' unless team.earned_bid?
194
+
195
+ "<sup>✧</sup>"
196
+ end
197
+
198
+ def bids_sup_tag_note(tournament)
199
+ next_tournament = if tournament.level == 'Regionals'
200
+ "#{tournament.state} State Tournament"
201
+ else
202
+ "National Tournament"
203
+ end
204
+ qualifiee = tournament.bids_per_school > 1 ? 'team' : 'school'
205
+ "Qualified #{qualifiee} for the #{tournament.year} #{next_tournament}"
206
+ end
207
+
208
+ def placing_notes(placing)
209
+ place = placing.place
210
+ points = placing.isolated_points
211
+ [
212
+ ('trial event' if placing.event.trial?),
213
+ ('trialed event' if placing.event.trialed?),
214
+ ('disqualified' if placing.disqualified?),
215
+ ('did not participate' if placing.did_not_participate?),
216
+ ('participation points only' if placing.participation_only?),
217
+ ('tie' if placing.tie?),
218
+ ('exempt' if placing.exempt?),
219
+ ('points limited' if placing.points_limited_by_maximum_place?),
220
+ ('unknown place' if placing.unknown?),
221
+ ('placed behind exhibition team'\
222
+ if placing.points_affected_by_exhibition? && place - points == 1),
223
+ ('placed behind exhibition teams'\
224
+ if placing.points_affected_by_exhibition? && place - points > 1),
225
+ ('dropped'\
226
+ if placing.dropped_as_part_of_worst_placings?)
227
+ ].compact.join(', ').capitalize
228
+ end
229
+ end
230
+ end