ahl_scraper 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (94) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +12 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +85 -0
  5. data/.ruby-version +1 -0
  6. data/.solargraph.yml +15 -0
  7. data/.travis.yml +6 -0
  8. data/.vscode/settings.json +11 -0
  9. data/CODE_OF_CONDUCT.md +74 -0
  10. data/Gemfile +25 -0
  11. data/Gemfile.lock +114 -0
  12. data/LICENSE.txt +21 -0
  13. data/README.md +235 -0
  14. data/Rakefile +8 -0
  15. data/ahl_scraper.gemspec +27 -0
  16. data/bin/console +16 -0
  17. data/bin/setup +8 -0
  18. data/lib/ahl_scraper.rb +55 -0
  19. data/lib/ahl_scraper/fetchers/division_data_fetcher.rb +19 -0
  20. data/lib/ahl_scraper/fetchers/game_data_fetcher.rb +29 -0
  21. data/lib/ahl_scraper/fetchers/game_event_data_fetcher.rb +19 -0
  22. data/lib/ahl_scraper/fetchers/player_data_fetcher.rb +25 -0
  23. data/lib/ahl_scraper/fetchers/player_games/data_fetcher.rb +30 -0
  24. data/lib/ahl_scraper/fetchers/playoff_bracket_data_fetcher.rb +19 -0
  25. data/lib/ahl_scraper/fetchers/season_data_fetcher.rb +15 -0
  26. data/lib/ahl_scraper/fetchers/season_end_date_fetcher.rb +34 -0
  27. data/lib/ahl_scraper/fetchers/season_game_ids_fetcher.rb +21 -0
  28. data/lib/ahl_scraper/fetchers/season_start_date_fetcher.rb +30 -0
  29. data/lib/ahl_scraper/fetchers/season_type_fetcher.rb +33 -0
  30. data/lib/ahl_scraper/fetchers/team_data_fetcher.rb +19 -0
  31. data/lib/ahl_scraper/fetchers/team_games/data_fetcher.rb +22 -0
  32. data/lib/ahl_scraper/fetchers/team_roster_data_fetcher.rb +67 -0
  33. data/lib/ahl_scraper/fixed_games/1001050.json +2182 -0
  34. data/lib/ahl_scraper/fixed_games/1018774.json +2573 -0
  35. data/lib/ahl_scraper/fixed_games/1020527.json +2483 -0
  36. data/lib/ahl_scraper/games.rb +46 -0
  37. data/lib/ahl_scraper/games/events.rb +8 -0
  38. data/lib/ahl_scraper/helpers/birthdate_helper.rb +27 -0
  39. data/lib/ahl_scraper/helpers/elapsed_time_helper.rb +24 -0
  40. data/lib/ahl_scraper/helpers/ice_time_helper.rb +18 -0
  41. data/lib/ahl_scraper/helpers/parameterize_helper.rb +35 -0
  42. data/lib/ahl_scraper/helpers/period_time_helper.rb +25 -0
  43. data/lib/ahl_scraper/helpers/season_dates_helper.rb +14 -0
  44. data/lib/ahl_scraper/player_games.rb +19 -0
  45. data/lib/ahl_scraper/players.rb +12 -0
  46. data/lib/ahl_scraper/playoff_brackets.rb +17 -0
  47. data/lib/ahl_scraper/resource.rb +38 -0
  48. data/lib/ahl_scraper/resources/game.rb +373 -0
  49. data/lib/ahl_scraper/resources/game_list_item.rb +57 -0
  50. data/lib/ahl_scraper/resources/games/coach.rb +23 -0
  51. data/lib/ahl_scraper/resources/games/events/shot.rb +10 -0
  52. data/lib/ahl_scraper/resources/games/goal.rb +161 -0
  53. data/lib/ahl_scraper/resources/games/goalie.rb +106 -0
  54. data/lib/ahl_scraper/resources/games/info.rb +83 -0
  55. data/lib/ahl_scraper/resources/games/on_ice_skater.rb +35 -0
  56. data/lib/ahl_scraper/resources/games/overtime.rb +48 -0
  57. data/lib/ahl_scraper/resources/games/penalty.rb +101 -0
  58. data/lib/ahl_scraper/resources/games/penalty_shot.rb +82 -0
  59. data/lib/ahl_scraper/resources/games/period.rb +31 -0
  60. data/lib/ahl_scraper/resources/games/referee.rb +23 -0
  61. data/lib/ahl_scraper/resources/games/shootout_attempt.rb +61 -0
  62. data/lib/ahl_scraper/resources/games/skater.rb +94 -0
  63. data/lib/ahl_scraper/resources/games/star.rb +47 -0
  64. data/lib/ahl_scraper/resources/games/team.rb +85 -0
  65. data/lib/ahl_scraper/resources/goalie_game_list_item.rb +69 -0
  66. data/lib/ahl_scraper/resources/player.rb +71 -0
  67. data/lib/ahl_scraper/resources/playoff_bracket.rb +21 -0
  68. data/lib/ahl_scraper/resources/playoff_brackets/game.rb +43 -0
  69. data/lib/ahl_scraper/resources/playoff_brackets/round.rb +31 -0
  70. data/lib/ahl_scraper/resources/playoff_brackets/series.rb +73 -0
  71. data/lib/ahl_scraper/resources/playoff_brackets/team.rb +39 -0
  72. data/lib/ahl_scraper/resources/roster_player.rb +71 -0
  73. data/lib/ahl_scraper/resources/season.rb +93 -0
  74. data/lib/ahl_scraper/resources/season_list_item.rb +30 -0
  75. data/lib/ahl_scraper/resources/seasons/team.rb +77 -0
  76. data/lib/ahl_scraper/resources/skater_game_list_item.rb +65 -0
  77. data/lib/ahl_scraper/resources/team_game_list_item.rb +64 -0
  78. data/lib/ahl_scraper/resources/team_list_item.rb +20 -0
  79. data/lib/ahl_scraper/roster_players.rb +11 -0
  80. data/lib/ahl_scraper/seasons.rb +31 -0
  81. data/lib/ahl_scraper/services/games/create_skaters_service.rb +72 -0
  82. data/lib/ahl_scraper/services/games/on_ice_statlines_service.rb +117 -0
  83. data/lib/ahl_scraper/services/games/penalty_shot_statlines_service.rb +38 -0
  84. data/lib/ahl_scraper/services/games/penalty_shots_service.rb +30 -0
  85. data/lib/ahl_scraper/services/games/penalty_statlines_service.rb +62 -0
  86. data/lib/ahl_scraper/services/games/scoring_statlines_service.rb +115 -0
  87. data/lib/ahl_scraper/services/games/shootout_statlines_service.rb +42 -0
  88. data/lib/ahl_scraper/services/games/team_on_ice_goals_service.rb +152 -0
  89. data/lib/ahl_scraper/services/games/time_splits_service.rb +70 -0
  90. data/lib/ahl_scraper/services/seasons/teams_service.rb +20 -0
  91. data/lib/ahl_scraper/team_games.rb +17 -0
  92. data/lib/ahl_scraper/teams.rb +11 -0
  93. data/lib/ahl_scraper/version.rb +5 -0
  94. metadata +138 -0
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ahl_scraper/games/events"
4
+
5
+ require "ahl_scraper/services/games/create_skaters_service"
6
+ require "ahl_scraper/services/games/on_ice_statlines_service"
7
+ require "ahl_scraper/services/games/penalty_shots_service"
8
+ require "ahl_scraper/services/games/penalty_shot_statlines_service"
9
+ require "ahl_scraper/services/games/penalty_statlines_service"
10
+ require "ahl_scraper/services/games/scoring_statlines_service"
11
+ require "ahl_scraper/services/games/shootout_statlines_service"
12
+ require "ahl_scraper/services/games/team_on_ice_goals_service"
13
+ require "ahl_scraper/services/games/time_splits_service"
14
+
15
+ require "ahl_scraper/resources/games/coach"
16
+ require "ahl_scraper/resources/games/goal"
17
+ require "ahl_scraper/resources/games/goalie"
18
+ require "ahl_scraper/resources/games/info"
19
+ require "ahl_scraper/resources/games/on_ice_skater"
20
+ require "ahl_scraper/resources/games/overtime"
21
+ require "ahl_scraper/resources/games/penalty_shot"
22
+ require "ahl_scraper/resources/games/penalty"
23
+ require "ahl_scraper/resources/games/period"
24
+ require "ahl_scraper/resources/games/referee"
25
+ require "ahl_scraper/resources/games/shootout_attempt"
26
+ require "ahl_scraper/resources/games/skater"
27
+ require "ahl_scraper/resources/games/star"
28
+ require "ahl_scraper/resources/games/team"
29
+
30
+ require "ahl_scraper/resources/games/events/shot"
31
+
32
+ module AhlScraper
33
+ module Games
34
+ @season_games = {}
35
+
36
+ class << self
37
+ def retrieve(game_id, season_type = nil)
38
+ Game.new(game_id, season_type: season_type)
39
+ end
40
+
41
+ def list(season_id)
42
+ @season_games[season_id.to_s] ||= SeasonGameIdsFetcher.new(season_id).call&.map { |game_data| GameListItem.new(game_data) }
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AhlScraper
4
+ module Games
5
+ module Events
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+
5
+ module AhlScraper
6
+ class BirthdateHelper
7
+ attr_reader :birthdate
8
+
9
+ def initialize(birthdate)
10
+ @birthdate = Date.parse(birthdate)
11
+ end
12
+
13
+ def draft_year
14
+ return (birthdate + (19 * 365)).year if (birthdate.month == 9 && birthdate.day > 15) || birthdate.month > 9
15
+
16
+ (birthdate + (18 * 365)).year
17
+ end
18
+
19
+ def current_age(round: 2)
20
+ ((Time.now.utc.to_datetime - birthdate.to_datetime).to_i / BigDecimal("365")).round(round)
21
+ end
22
+
23
+ def age_on_date(date, round: 2)
24
+ ((Date.parse(date).to_datetime - birthdate.to_datetime).to_i / BigDecimal("365")).round(round)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AhlScraper
4
+ class ElapsedTimeHelper
5
+ attr_reader :time
6
+
7
+ def initialize(time)
8
+ @time = time.to_i
9
+ end
10
+
11
+ def to_minutes
12
+ "#{time / 60}:#{(time % 60).to_s.rjust(2, '0')}"
13
+ end
14
+
15
+ def to_minutes_with_period
16
+ period_elapsed = time % 1200
17
+
18
+ ["#{period_elapsed / 60}:#{period_elapsed % 60}", (time / 1200) + 1]
19
+ end
20
+
21
+ alias to_min to_minutes
22
+ alias to_min_with_per to_minutes_with_period
23
+ end
24
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AhlScraper
4
+ class IceTimeHelper
5
+ attr_reader :time
6
+
7
+ def initialize(time)
8
+ @time = time
9
+ end
10
+
11
+ def to_seconds
12
+ ice_time = time.split(":")
13
+ ice_time[0].to_i * 60 + ice_time[1].to_i
14
+ end
15
+
16
+ alias to_sec to_seconds
17
+ end
18
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AhlScraper
4
+ class ParameterizeHelper
5
+ attr_reader :string, :separator, :preserve_case
6
+
7
+ def initialize(string, separator: "-", preserve_case: false)
8
+ @string = string
9
+ @separator = separator
10
+ @preserve_case = preserve_case
11
+ end
12
+
13
+ def call
14
+ parameterized_string = string.dup.to_s
15
+
16
+ parameterized_string.gsub!(/[^a-zA-Z0-9\-_]+/, separator)
17
+
18
+ unless separator.nil? || separator.empty?
19
+ if separator == "-"
20
+ re_duplicate_separator = /-{2,}/
21
+ re_leading_trailing_separator = /^-|-$/
22
+ else
23
+ re_sep = Regexp.escape(separator)
24
+ re_duplicate_separator = /#{re_sep}{2,}/
25
+ re_leading_trailing_separator = /^#{re_sep}|#{re_sep}$/
26
+ end
27
+ parameterized_string.gsub!(re_duplicate_separator, separator)
28
+ parameterized_string.gsub!(re_leading_trailing_separator, "")
29
+ end
30
+
31
+ parameterized_string.downcase! unless preserve_case
32
+ parameterized_string
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AhlScraper
4
+ class PeriodTimeHelper
5
+ attr_reader :time, :period
6
+
7
+ def initialize(time, period = nil)
8
+ @time = time
9
+ @period = period.to_i
10
+ end
11
+
12
+ def to_period_seconds
13
+ period_time = time.split(":")
14
+ period_time[0].to_i * 60 + period_time[1].to_i
15
+ end
16
+
17
+ def to_time_elapsed
18
+ period_time = time.split(":")
19
+ period_time[0].to_i * 60 + period_time[1].to_i + ((period - 1) * 1200)
20
+ end
21
+
22
+ alias to_sec to_period_seconds
23
+ alias to_elapsed to_time_elapsed
24
+ end
25
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AhlScraper
4
+ class SeasonDatesHelper
5
+ DATE_EXCEPTIONS = {
6
+ 68 => { start_date: "Mon, Feb 1", end_date: "Sat, Jun 8" },
7
+ }.freeze
8
+
9
+ SEASON_MONTH_KEY = {
10
+ regular: { start_month: "10", end_month: "4" },
11
+ playoffs: { start_month: "4", end_month: "6" },
12
+ }.freeze
13
+ end
14
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ahl_scraper/fetchers/player_games/data_fetcher"
4
+
5
+ module AhlScraper
6
+ module PlayerGames
7
+ @season_games = {}
8
+
9
+ class << self
10
+ def list(player_id, season_id)
11
+ @season_games["#{player_id}-#{season_id}"] ||= DataFetcher.new(player_id, season_id).call&.map do |player_data|
12
+ player_data[:position] == "G" ? GoalieGameListItem.new(player_data) : SkaterGameListItem.new(player_data)
13
+ end
14
+
15
+ @season_games["#{player_id}-#{season_id}"]
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AhlScraper
4
+ module Players
5
+ class << self
6
+ def retrieve(player_id)
7
+ player_data = PlayerDataFetcher.new(player_id).call
8
+ Player.new(player_data)
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ahl_scraper/resources/playoff_brackets/game"
4
+ require "ahl_scraper/resources/playoff_brackets/round"
5
+ require "ahl_scraper/resources/playoff_brackets/series"
6
+ require "ahl_scraper/resources/playoff_brackets/team"
7
+
8
+ module AhlScraper
9
+ module PlayoffBrackets
10
+ class << self
11
+ def retrieve(season_id)
12
+ bracket_data = PlayoffBracketDataFetcher.new(season_id).call
13
+ PlayoffBracket.new(bracket_data)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AhlScraper
4
+ class Resource
5
+ include Enumerable
6
+
7
+ def initialize(raw_data, opts = {})
8
+ @raw_data = raw_data
9
+ @opts = opts
10
+ end
11
+
12
+ def values
13
+ @values ||= (self.class.instance_methods(false) - %i[to_json inspect each keys [] values]).map do |m|
14
+ [m, send(m)]
15
+ end.to_h.transform_keys(&:to_sym)
16
+ end
17
+
18
+ def inspect
19
+ "#<#{self.class.to_s.split('::').last}:0x#{object_id.to_s(16)} #{values}>"
20
+ end
21
+
22
+ def [](key)
23
+ values[key.to_sym]
24
+ end
25
+
26
+ def keys
27
+ values.keys
28
+ end
29
+
30
+ def to_json(*_opts)
31
+ JSON.generate(values)
32
+ end
33
+
34
+ def each(&blk)
35
+ values.each(&blk)
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,373 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AhlScraper
4
+ class Game < Resource # rubocop:disable Metrics/ClassLength
5
+ include Enumerable
6
+
7
+ IRREGULAR_GAMES = {
8
+ "1022174" => { status: "result_void", start_time_in_seconds: 0, end_time_in_seconds: 1200 },
9
+ }.freeze
10
+
11
+ ATTRIBUTES = %i[
12
+ game_id
13
+ season_type
14
+ info
15
+ current_time
16
+ current_period
17
+ current_period_number
18
+ season_id
19
+ played
20
+ in_progress
21
+ referees
22
+ home_coaches
23
+ away_coaches
24
+ home_team
25
+ away_team
26
+ winning_team
27
+ three_stars
28
+ home_skaters
29
+ away_skaters
30
+ home_goalies
31
+ away_goalies
32
+ home_roster
33
+ away_roster
34
+ goals
35
+ penalties
36
+ penalty_shots
37
+ periods
38
+ overtimes
39
+ overtime
40
+ home_shootout_attempts
41
+ away_shootout_attempts
42
+ home_penalty_shots
43
+ away_penalty_shots
44
+ shootout
45
+ ].freeze
46
+
47
+ attr_reader(*ATTRIBUTES)
48
+
49
+ def initialize(game_id, opts = {})
50
+ @game_id = game_id
51
+ @raw_data = GameDataFetcher.new(game_id).call
52
+ @raw_event_data = GameEventDataFetcher.new(game_id).call
53
+ @season_type = opts[:season_type] || SeasonTypeFetcher.new(@raw_data[:details][:seasonId]).call
54
+ end
55
+
56
+ def values
57
+ @values ||= ATTRIBUTES.index_with do |m|
58
+ send(m)
59
+ end.transform_keys(&:to_sym)
60
+ end
61
+
62
+ def status
63
+ @status ||= set_game_status
64
+ end
65
+
66
+ def info
67
+ @info ||= Games::Info.new(@raw_data[:details], { name: "#{away_team.abbreviation} @ #{home_team.abbreviation}" })
68
+ end
69
+
70
+ def current_time
71
+ @current_time ||= set_current_game_time
72
+ end
73
+
74
+ def current_period
75
+ @current_period ||= set_current_game_period
76
+ end
77
+
78
+ def current_period_number
79
+ @current_period_number ||= @raw_data[:periods].length
80
+ end
81
+
82
+ def season_id
83
+ @season_id ||= @raw_data[:details][:seasonId].to_i
84
+ end
85
+
86
+ def referees
87
+ @referees ||= Array((@raw_data[:referees] + @raw_data[:linesmen])).uniq { |referee| referee.values_at(:lastName, :firstName, :jerseyNumber) }
88
+ .map { |r| Games::Referee.new(r) }
89
+ end
90
+
91
+ def three_stars
92
+ @three_stars ||= Array(@raw_data[:mostValuablePlayers]).map.with_index { |t, i| Games::Star.new(t, { number: i + 1 }) }
93
+ end
94
+
95
+ def home_coaches
96
+ @home_coaches ||= Array(@raw_data[:homeTeam][:coaches]).uniq { |coach| coach.values_at(:lastName, :firstName) }
97
+ .map { |coach| Games::Coach.new(coach, { team_id: home_team.id }) }
98
+ end
99
+
100
+ def away_coaches
101
+ @away_coaches ||= Array(@raw_data[:visitingTeam][:coaches]).uniq { |coach| coach.values_at(:lastName, :firstName) }
102
+ .map { |coach| Games::Coach.new(coach, { team_id: away_team.id }) }
103
+ end
104
+
105
+ def home_team
106
+ @home_team ||= Games::Team.new(
107
+ @raw_data[:homeTeam],
108
+ {
109
+ home_team: true,
110
+ goals: raw_goals,
111
+ current_state: current_state,
112
+ game_properties: game_properties,
113
+ shots: raw_period_shots,
114
+ goal_totals: raw_period_goals,
115
+ winning_team_id: winning_team_id,
116
+ }
117
+ )
118
+ end
119
+
120
+ def away_team
121
+ @away_team ||= Games::Team.new(
122
+ @raw_data[:visitingTeam],
123
+ {
124
+ home_team: false,
125
+ goals: raw_goals,
126
+ current_state: current_state,
127
+ game_properties: game_properties,
128
+ shots: raw_period_shots,
129
+ goal_totals: raw_period_goals,
130
+ winning_team_id: winning_team_id,
131
+ }
132
+ )
133
+ end
134
+
135
+ def winning_team
136
+ @winning_team ||=
137
+ if status == "finished"
138
+ winning_team_id == home_team.id ? home_team : away_team
139
+ end
140
+ end
141
+
142
+ def home_skaters
143
+ @home_skaters ||= Games::CreateSkatersService.new(
144
+ @raw_data[:homeTeam][:skaters],
145
+ raw_goals,
146
+ raw_penalties,
147
+ raw_home_shootout_attempts,
148
+ @raw_data[:penaltyShots][:homeTeam],
149
+ { home_team: true, team_id: home_team.id, team_abbreviation: home_team.abbreviation, game_date: @raw_data[:details][:date] }
150
+ ).call
151
+ end
152
+
153
+ def away_skaters
154
+ @away_skaters ||= Games::CreateSkatersService.new(
155
+ Array(@raw_data[:visitingTeam][:skaters]),
156
+ raw_goals,
157
+ raw_penalties,
158
+ raw_away_shootout_attempts,
159
+ @raw_data[:penaltyShots][:visitingTeam],
160
+ { home_team: false, team_id: away_team.id, team_abbreviation: away_team.abbreviation, game_date: @raw_data[:details][:date] }
161
+ ).call
162
+ end
163
+
164
+ def home_goalies
165
+ @home_goalies ||= Array(@raw_data[:homeTeam][:goalies]).map do |g|
166
+ Games::Goalie.new(
167
+ g,
168
+ {
169
+ team_id: home_team.id,
170
+ home_team: true,
171
+ shootout_data: @raw_data.dig(:shootoutDetails, :visitingTeamShots),
172
+ penalty_shot_data: @raw_data.dig(:penaltyShots, :visitingTeam),
173
+ game_date: @raw_data[:details][:date],
174
+ }
175
+ )
176
+ end
177
+ end
178
+
179
+ def away_goalies
180
+ @away_goalies ||= Array(@raw_data[:visitingTeam][:goalies]).map do |g|
181
+ Games::Goalie.new(
182
+ g,
183
+ {
184
+ team_id: away_team.id,
185
+ home_team: false,
186
+ shootout_data: @raw_data.dig(:shootoutDetails, :homeTeamShots),
187
+ penalty_shot_data: @raw_data.dig(:penaltyShots, :homeTeam),
188
+ game_date: @raw_data[:details][:date],
189
+ }
190
+ )
191
+ end
192
+ end
193
+
194
+ def home_roster
195
+ @home_roster ||= [*home_skaters, *home_goalies]
196
+ end
197
+
198
+ def away_roster
199
+ @away_roster ||= [*away_skaters, *away_goalies]
200
+ end
201
+
202
+ def goals
203
+ @goals ||= raw_goals.map.with_index { |g, i| Games::Goal.new(g, { number: i + 1 }) }
204
+ end
205
+
206
+ def penalties
207
+ @penalties ||= raw_penalties.map.with_index { |pn, i| Games::Penalty.new(pn, { number: i + 1 }) }
208
+ end
209
+
210
+ def penalty_shots
211
+ @penalty_shots ||= (home_penalty_shots || []) + (away_penalty_shots || [])
212
+ end
213
+
214
+ def home_penalty_shots
215
+ @home_penalty_shots ||= Games::PenaltyShotsService.new(@raw_data[:penaltyShots][:homeTeam]).call
216
+ end
217
+
218
+ def away_penalty_shots
219
+ @away_penalty_shots ||= Games::PenaltyShotsService.new(@raw_data[:penaltyShots][:visitingTeam]).call
220
+ end
221
+
222
+ def periods
223
+ @periods ||= Array(@raw_data[:periods][0..2]).map { |per| Games::Period.new(per) }
224
+ end
225
+
226
+ def overtimes
227
+ @overtimes ||= Array(@raw_data[:periods][3..-1]).map { |o| Games::Overtime.new(o, { regular_season: season_type == :regular }) }
228
+ end
229
+
230
+ def overtime?
231
+ @overtime ||= overtimes.length.positive?
232
+ end
233
+
234
+ def shootout_attempts
235
+ @shootout_attempts ||= [*home_shootout_attempts, *away_shootout_attempts]
236
+ end
237
+
238
+ def home_shootout_attempts
239
+ @home_shootout_attempts ||= raw_home_shootout_attempts&.map&.with_index do |att, i|
240
+ Games::ShootoutAttempt.new(att, { number: i + 1, opposing_team: find_opposing_team(att[:shooterTeam][:id].to_i) })
241
+ end || []
242
+ end
243
+
244
+ def away_shootout_attempts
245
+ @away_shootout_attempts ||= raw_away_shootout_attempts&.map&.with_index do |att, i|
246
+ Games::ShootoutAttempt.new(att, { number: i + 1, opposing_team: find_opposing_team(att[:shooterTeam][:id].to_i) })
247
+ end || []
248
+ end
249
+
250
+ def shootout?
251
+ @shootout ||= @raw_data[:hasShootout] == true
252
+ end
253
+
254
+ def played?
255
+ @played ||= status == "finished"
256
+ end
257
+
258
+ def in_progress?
259
+ @in_progress ||= status == "in_progress"
260
+ end
261
+
262
+ def ended_in
263
+ @ended_in ||=
264
+ if shootout?
265
+ "SO"
266
+ elsif overtime?
267
+ "OT"
268
+ else
269
+ "REG"
270
+ end
271
+ end
272
+
273
+ private
274
+
275
+ def raw_goals
276
+ @raw_goals ||= Array(@raw_data[:periods]).map { |a| a[:goals] }.flatten
277
+ end
278
+
279
+ def raw_penalties
280
+ @raw_penalties ||= Array(@raw_data[:periods]).map { |a| a[:penalties] }.flatten
281
+ end
282
+
283
+ def raw_period_shots
284
+ @raw_period_shots ||= Array(@raw_data[:periods]).map do |period|
285
+ { home: period[:stats][:homeShots].to_i, away: period[:stats][:visitingShots].to_i }
286
+ end
287
+ end
288
+
289
+ def raw_period_goals
290
+ @raw_period_goals ||= Array(@raw_data[:periods]).map do |period|
291
+ { home: period[:stats][:homeGoals].to_i, away: period[:stats][:visitingGoals].to_i }
292
+ end
293
+ end
294
+
295
+ def raw_home_shootout_attempts
296
+ @raw_home_shootout_attempts ||= @raw_data.dig(:shootoutDetails, :homeTeamShots)
297
+ end
298
+
299
+ def raw_away_shootout_attempts
300
+ @raw_away_shootout_attempts ||= @raw_data.dig(:shootoutDetails, :visitingTeamShots)
301
+ end
302
+
303
+ def find_opposing_team(team_id)
304
+ team_id == home_team.id ? away_team : home_team
305
+ end
306
+
307
+ def winning_team_id
308
+ @winning_team_id ||= status == "finished" ? set_winning_team_id : nil
309
+ end
310
+
311
+ def set_winning_team_id
312
+ if @raw_data[:homeTeam][:stats][:goals] > @raw_data[:visitingTeam][:stats][:goals]
313
+ @raw_data[:homeTeam][:info][:id]
314
+ elsif @raw_data[:homeTeam][:stats][:goals] < @raw_data[:visitingTeam][:stats][:goals]
315
+ @raw_data[:visitingTeam][:info][:id]
316
+ else
317
+ @raw_data[:shootoutDetails][:winningTeam][:id]
318
+ end
319
+ end
320
+
321
+ def current_state
322
+ @current_state ||= {
323
+ status: status,
324
+ period: current_period,
325
+ period_number: current_period_number,
326
+ time: current_time,
327
+ shootout: shootout?,
328
+ overtime: overtime?,
329
+ }
330
+ end
331
+
332
+ def game_properties
333
+ @game_properties ||= {
334
+ playoffs: @season_type == :playoffs,
335
+ periods: periods.length,
336
+ overtime_periods: overtimes.length,
337
+ game_start_time_in_seconds: IRREGULAR_GAMES.dig(game_id.to_s, :start_time_in_seconds),
338
+ game_end_time_in_seconds: IRREGULAR_GAMES.dig(game_id.to_s, :end_time_in_seconds),
339
+ }
340
+ end
341
+
342
+ def set_game_status
343
+ irregular_game_status = IRREGULAR_GAMES.dig(game_id.to_s, :status)
344
+ return irregular_game_status if irregular_game_status
345
+
346
+ # return "forfeited" if game is a forfeit, need to figure this out if it happens
347
+
348
+ return "finished" if @raw_data[:details][:final] == "1"
349
+
350
+ return "in_progress" if @raw_data[:details][:started] == "1"
351
+
352
+ "not_started"
353
+ end
354
+
355
+ def set_current_game_time
356
+ return if /Final/.match?(@raw_data[:details][:status])
357
+
358
+ game_time = @raw_data[:details][:status].match(/\d{1,2}:\d{2}/).to_s
359
+ return if game_time.empty?
360
+
361
+ game_time
362
+ end
363
+
364
+ def set_current_game_period
365
+ return if /Final/.match?(@raw_data[:details][:status])
366
+
367
+ period = @raw_data[:details][:status].match(/(1st|2nd|3rd|OT|[1-9]?[0-9]OT|SO)/).to_s
368
+ return if period.empty?
369
+
370
+ period
371
+ end
372
+ end
373
+ end