sciolyff-duosmium 0.13.0

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