File Coverage

blib/lib/Sport/Analytics/NHL/Report/GS.pm
Criterion Covered Total %
statement 300 529 56.7
branch 106 256 41.4
condition 50 162 30.8
subroutine 21 26 80.7
pod 15 15 100.0
total 492 988 49.8


line stmt bran cond sub pod time code
1             package Sport::Analytics::NHL::Report::GS;
2              
3 20     20   83763 use v5.10.1;
  20         83  
4 20     20   101 use strict;
  20         39  
  20         417  
5 20     20   102 use warnings FATAL => 'all';
  20         38  
  20         652  
6 20     20   97 use experimental qw(smartmatch);
  20         35  
  20         101  
7              
8 20     20   1385 use parent 'Sport::Analytics::NHL::Report';
  20         444  
  20         107  
9              
10 20     20   1977 use utf8;
  20         137  
  20         145  
11              
12 20     20   519 use Sport::Analytics::NHL::Config;
  20         39  
  20         3843  
13 20     20   141 use Sport::Analytics::NHL::Errors;
  20         41  
  20         3521  
14 20     20   137 use Sport::Analytics::NHL::Util;
  20         40  
  20         1262  
15 20     20   125 use Sport::Analytics::NHL::Tools;
  20         40  
  20         3417  
16              
17 20     20   121 use Data::Dumper;
  20         51  
  20         118492  
18              
19             =head1 NAME
20              
21             Sport::Analytics::NHL::Report::GS - Class for the NHL HTML GS report.
22              
23             =head1 SYNOPSYS
24              
25             Class for the NHL HTML GS report. Should not be constructed directly, but via Sport::Analytics::NHL::Report (q.v.)
26             As with any other HTML report, there are two types: old (pre-2007) and new (2007 and on). Parsers of them may have something in common but may turn out to be completely different more often than not.
27              
28             =head1 METHODS
29              
30             =over 2
31              
32             =item C
33              
34             Cleaning up and standardizing the parsed data.
35              
36             Arguments: none
37             Returns: void. Everything is in the $self.
38              
39             =item C
40              
41             Cleaning up and standardizing the parsed data from the new report.
42              
43             Arguments: none
44             Returns: void. Everything is in the $self.
45              
46             =item C
47              
48             Cleaning up and standardizing the parsed data from the old report.
49              
50             Arguments: none
51             Returns: void. Everything is in the $self.
52              
53             =item C
54              
55             Apply specific normalization to a scoring event.
56              
57             Arguments: the scoring event hashref
58             Returns: void. Everything is fixed within the hashref.
59              
60             =item C
61              
62             Parse the GS html tree into a boxscore object
63              
64             Arguments: none
65             Returns: void. Everything is in the $self.
66              
67             =item C
68              
69             Parse the goaltending summary in the new GS report
70              
71             Arguments: the goaltender summary HTML element
72             Returns: void. It's in the $self
73              
74             =item C
75              
76             Parse the misc information summary in the old GS report
77              
78             Arguments: the misc summary HTML element
79             Returns: void.
80              
81             =item C
82              
83             Parse the misc information summary in the new GS report
84              
85             Arguments: the misc summary HTML element
86             Returns: void.
87              
88             =item C
89              
90             Parse the entry of a penalty event in the new GS report
91              
92             Arguments: the HTML element of the penalty
93             Returns: the parsed event
94              
95             =item C
96              
97             =item C
98              
99             Parse the entry of a penalty event in the old GS report
100              
101             Arguments: the HTML element of the penalty
102             Returns: the parsed event
103              
104             =item C
105              
106             Parse penalty summary in the both old and new GS reports
107              
108             Arguments:
109             * the HTML element with the summary
110             * the flag of format (old/new)
111              
112             =item C
113              
114             Parse powerplay success summary in the new GS report
115              
116             Arguments: the HTML element with the PP summary
117             Returns: void
118              
119             =item C
120              
121             Parse the entry of a scoring event in the both new and old GS reports.
122              
123             Arguments: the HTML element of the goal
124             Returns: the parsed event
125              
126             =item C
127              
128             Parse scoring summary in the both old and new GS reports
129              
130             Arguments:
131             * the HTML element with the summary
132             * the flag of format (old/new)
133             Returns: void.
134              
135             =back
136              
137             =cut
138              
139             my %NORMAL_FIELDS = (
140             pp => 'powerPlayTimeOnIce', sh => 'shortHandedTimeOnIce', ev => 'evenTimeOnIce',
141             toitot => 'timeOnIce', wl => 'decision',
142             );
143              
144             our $is_special = 0;
145              
146             sub parse_scoring_event ($$;$) {
147              
148 20     20 1 29 my $self = shift;
149 20         28 my $row = shift;
150 20         27 my $is_new = shift;
151              
152 20         254 my $event = {on_ice => []};
153              
154 20 50       63 my $is_goal = $self->get_sub_tree(0, [$is_new ? (0,0) : (0,0,0)], $row);
155 20 50       88 $event->{type} = $is_goal =~ /\d/ ? 'GOAL' : 'MISS';
156 20 50       45 my $offset = $event->{type} eq 'MISS' ? 1 : 0;
157 20 50       153 $event->{period} = $self->get_sub_tree(0, [$is_new ? (1,0) : (1,0,0)], $row);
158 20 50 33     95 return undef if !$is_special && $event->{period} !~ /\w/;
159 20 50       64 $event->{time} = $self->get_sub_tree(0, [$is_new ? (2,0) : (2,0,0)], $row);
160 20 50       74 $event->{team1} = $self->get_sub_tree(0, [$is_new ? (4,0) : (3,0,0)], $row);
161 20 50       57 $event->{player1} = $self->get_sub_tree(0, [$is_new ? (5,0) : (4,0,0)], $row);
162 20 50       49 if ($event->{type} eq 'GOAL') {
163 20 50       62 $event->{assist1} = $self->get_sub_tree(0, [$is_new ? (6,0) : (5,0,0)], $row);
164 20 50       64 $event->{assist2} = $self->get_sub_tree(0, [$is_new ? (7,0) : (6,0,0)], $row);
165             }
166 20 50       45 if ($is_special) {
167 0         0 $event->{special} = 1;
168 0         0 $is_special = 0;
169 0         0 return $event;
170             }
171 20 50       58 $event->{strength} = $self->get_sub_tree(0, [$is_new ? (3,0) : (9,0,0)], $row);
172 20 50 33     91 if ($event->{period} eq 'OT') {
    50          
173 0         0 $event->{period} = 4;
174             }
175             elsif ($event->{period} eq 'SO' || $event->{period} eq 'F') {
176 0         0 $event->{period} = 5;
177 0         0 $event->{strength} = 'PS';
178 0         0 $event->{time} = '0:00';
179 0         0 $event->{assist1} = 'unassisted';
180 0         0 $event->{penaltyshot} = 1;
181             }
182 20 50 33     84 if ($event->{assist1} && $event->{assist1} =~ /unsuccessful/i) {
183 0         0 $event->{type} = 'MISS';
184 0         0 $offset = 1;
185             }
186 20 50       44 if (ref $event->{assist2}) {
187 0         0 $event->{assist2} = undef;
188 0         0 $offset = 1;
189             }
190 20 50       39 if ($event->{type} eq 'MISS') {
191 0         0 $event->{description} = 'Missed Penalty Shot';
192 0         0 $event->{assist1} = $event->{assist2} = undef;
193             }
194              
195 20 50 33     76 if ($event->{strength} =~ /(.*)-EN/) {
    50          
196 0         0 $event->{strength} = $1;
197 0         0 $event->{en} = 1;
198             }
199             elsif ($event->{strength} =~ /(.*)-\s*PS/ || $event->{type} eq 'MISS') {
200 0 0       0 $event->{strength} = $1 if $1;
201 0         0 $event->{penaltyshot} = 1;
202 0         0 $event->{shot_type} = 'Unknown';
203 0         0 $event->{assist1} = undef;
204 0         0 $event->{location} = 'Off';
205 0         0 $event->{distance} = 999;
206 0         0 $event->{miss} = 'Unknown';
207 0         0 $event->{team1} =~ s/\s//g;
208             }
209 20   50     79 $event->{en} ||= 0;
210 20 50       31 if ($is_new) {
211 20         36 for my $i (8,9) {
212 40         95 my $on_ice = $self->get_sub_tree(0, [$i-$offset], $row);
213 40         62 my $n = 0;
214 40         88 while (my $on_ice_num = $self->get_sub_tree(0, [$n,0], $on_ice)) {
215 240   100     557 $event->{on_ice}[$i-8] ||= [];
216 240         276 push(@{$event->{on_ice}[$i-8]}, $on_ice_num);
  240         394  
217 240         579 $n += 2;
218             }
219             }
220             }
221             else {
222 0   0     0 $event->{on_ice}[0] = [ $self->get_sub_tree(0, [7,0,0], $row) || $self->get_sub_tree(0, [8,0], $row) ];
223 0   0     0 $event->{on_ice}[1] = [ $self->get_sub_tree(0, [8,0,0], $row) || $self->get_sub_tree(0, [9,0], $row) ];
224             }
225 20 50       49 return undef if ref $event->{on_ice}[0][0];
226 20         39 $event->{shot_type} = 'Unknown';
227 20         42 $event->{location} = 'Unk';
228 20         34 $event->{distance} = 999;
229 20         37 $event;
230             }
231              
232             sub normalize_scoring_event ($$) {
233              
234 0     0 1 0 my $self = shift;
235 0         0 my $event = shift;
236              
237 0         0 my @keys = keys %{$event};
  0         0  
238 0         0 for my $key (@keys) {
239 0 0       0 if ($key eq 'on_ice') {
240 0         0 for my $on_ice (@{$event->{$key}}) {
  0         0  
241 0         0 $on_ice =~ s/^\s//;
242 0         0 $on_ice =~ s/\s$//;
243 0         0 $on_ice = [split(/\s+/, $on_ice)];
244             }
245             }
246             else {
247 0         0 $event->{$key} =~ s/^\s//;
248 0         0 $event->{$key} =~ s/\s$//;
249 0 0 0     0 if ($key =~ /^assist/ && ! $event->{$key}) {
250 0         0 delete $event->{$key};
251 0         0 next;
252             }
253 0         0 $event->{$key} =~ s/^(.*)\s+\(\d+\)/$1/ge;
  0         0  
254             }
255             }
256             }
257              
258             sub parse_scoring_summary ($$;$) {
259              
260 4     4 1 7 my $self = shift;
261 4         9 my $scoring_summary = shift;
262 4   50     13 my $is_new = shift || 0;
263              
264 4         10 my $events = [];
265 4 50       11 my $r = $is_new ? 1 : 2;
266 4         13 while (my $row = $self->get_sub_tree(0, [$r], $scoring_summary)) {
267 24 100 66     78 last unless $row && ref $row;
268 20         27 $r++;
269 20         43 my $event = $self->parse_scoring_event($row, $is_new);
270 20 50       41 push(@{$events}, $event) if $event;
  20         69  
271             }
272 4         13 $events;
273             }
274              
275             sub parse_new_penalty_event ($$) {
276              
277 64     64 1 81 my $self = shift;
278 64         81 my $row = shift;
279              
280 64         103 my $event = {};
281              
282 64         132 $event->{type} = 'PENL';
283 64         139 $event->{period} = $self->get_sub_tree(0, [1,0], $row);
284 64 50       212 $event->{period} = 4 if $event->{period} eq 'OT';
285 64 50       120 $event->{period} = 5 if $event->{period} eq 'SO';
286 64         139 $event->{time} = $self->get_sub_tree(0, [2,0], $row);
287 64 50       243 return undef if $event->{time} !~ /:/;
288 64         166 $event->{player1} = $self->get_sub_tree(0, [3,0,0,0,0], $row);
289 64         167 $event->{name} = $self->get_sub_tree(0, [3,0,0,3,0], $row);
290 64         162 $event->{length} = $self->get_sub_tree(0, [4,0], $row);
291 64         156 $event->{penalty} = $self->get_sub_tree(0, [5,0], $row);
292 64 50       157 $event->{misconduct} = 1 if $event->{penalty} =~ /conduct/;
293 64 100       145 $event->{player1} = $BENCH_PLAYER_ID if $event->{penalty} =~ /\-\s+bench/;
294 64 50       182 $event->{player1} = $COACH_PLAYER_ID if $event->{penalty} =~ /\bcoach\b/i;
295 64 100 66     245 $event->{player1} = $BENCH_PLAYER_ID if $event->{name} && $event->{name} =~ /\bteam\b/i;
296 64         106 delete $event->{name};
297 64         103 $event;
298             }
299              
300             sub parse_penalty_event ($$$) {
301              
302 0     0 1 0 my $self = shift;
303 0         0 my $row = shift;
304 0         0 my $t = shift;
305              
306 0         0 my $event = {};
307              
308 0         0 $event->{type} = 'PENL';
309 0         0 $event->{period} = $self->get_sub_tree(0, [1+7*$t,0,0], $row);
310 0 0       0 return undef unless $event->{period};
311 0 0       0 return undef unless $event->{period} =~ /\w/;
312 0 0       0 $event->{period} = 4 if $event->{period} eq 'OT';
313 0         0 $event->{time} = $self->get_sub_tree(0, [2+7*$t,0,0], $row);
314 0         0 $event->{team1} = $t;
315 0         0 $event->{number} = $self->get_sub_tree(0, [3+7*$t,0,0], $row);
316 0         0 $event->{player1} = $self->get_sub_tree(0, [4+7*$t,0,0], $row);
317 0         0 $event->{length} = $self->get_sub_tree(0, [5+7*$t,0,0], $row);
318 0         0 $event->{penalty} = $self->get_sub_tree(0, [6+7*$t,0,0], $row);
319 0 0       0 $event->{misconduct} = 1 if $event->{penalty} =~ /conduct/i;
320 0 0       0 $event->{player1} = $BENCH_PLAYER_ID if $event->{penalty} =~ /\-\s+bench/i;
321 0 0       0 $event->{player1} = $COACH_PLAYER_ID if $event->{penalty} =~ /\-\s+coach/i;
322 0         0 $event;
323             }
324              
325             sub parse_penalty_summary ($$;$) {
326              
327 4     4 1 8 my $self = shift;
328 4         10 my $penalty_summary = shift;
329 4   50     14 my $is_new = shift || 0;
330              
331 4         24 my $events = [];
332 4 50       44 my @penalty_tables = $is_new ? (
333             $self->get_sub_tree(0, [ (1,0,0,0,0,0,0,0,0) ], $penalty_summary),
334             $self->get_sub_tree(0, [ (1,0,0,0,0,0,0,3,0) ], $penalty_summary),
335             ) : ( $penalty_summary );
336 4 50       19 if ($is_new) {
337 4 50       15 if (! ref $penalty_tables[0]) {
338 0         0 $penalty_tables[0] = $self->get_sub_tree(0, [ (1,0,0,0,0,0) ], $penalty_summary);
339 0         0 $penalty_tables[1] = $self->get_sub_tree(0, [ (1,0,0,0,0,3) ], $penalty_summary);
340             }
341             }
342 4         9 my $p = 0;
343 4         11 for my $penalty_table (@penalty_tables) {
344 8 50       22 next unless defined $penalty_table;
345 8         16 my $r = 2 - $is_new;
346 8         29 while (my $row = $self->get_sub_tree(0, [$r], $penalty_table)) {
347 72 100 66     220 last unless $row && ref $row;
348 64         82 $r++;
349 64 50       88 if ($is_new) {
350 64         114 my $event = $self->parse_new_penalty_event($row);
351 64         136 $event->{team1} = $p;
352 64 50 33     187 push(@{$events}, $event) if $event && $event->{type};
  64         230  
353             }
354             else {
355 0         0 for my $t (0,1) {
356 0         0 my $event = $self->parse_penalty_event($row, $t);
357 0 0       0 push(@{$events}, $event) if $event->{type};
  0         0  
358             }
359             }
360             }
361 8         20 $p++;
362             }
363 4         20 $events;
364             }
365              
366             sub parse_new_pp_summary ($$$) {
367              
368 4     4 1 9 my $self = shift;
369 4         20 my $pp_summary = shift;
370              
371 4         12 for my $t (0,1) {
372 8         25 my $pp_team_summary = $self->get_sub_tree(0, [ (1,0,0,0,$t) ], $pp_summary);
373 8         24 $self->{teams}[$t]{pptype} = {};
374 8         16 my $pp = 0;
375 8         17 for my $pptype (qw(5v4 5v3 4v3)) {
376 24   33     58 $self->{teams}[$t]{pptype}{$pptype} =
377             $self->get_sub_tree(0, [0,1,$pp,0], $pp_team_summary) ||
378             $self->get_sub_tree(0, [0,0,0,0,0,0,0,1,$pp,0], $pp_team_summary);
379 24         53 $pp++;
380             }
381             }
382             }
383              
384             sub parse_pp_summary ($$$) {
385              
386 0     0 1 0 my $self = shift;
387 0         0 my $pp_summary = shift;
388              
389 0         0 $self->{teams}[0]{pp} = [];
390 0         0 $self->{teams}[1]{pp} = [];
391 0         0 my $r = 2;
392 0         0 while (my $row = $self->get_sub_tree(0, [$r], $pp_summary)) {
393 0         0 $r++;
394 0 0       0 next unless ref $row;
395 0         0 my $period = $self->get_sub_tree(0, [9,0,0], $row);
396 0 0       0 if ($period =~ /(\d+)/) {
    0          
397 0         0 $self->{last_period} = $1;
398             }
399             elsif ($period eq 'OT') {
400 0         0 $self->{last_period} = 4;
401             }
402 0 0 0     0 if ($period =~ /(\d+)/ && $period > 0) {
    0          
403 0         0 $period = $1 - 1;
404 0         0 my $pp0 = $self->get_sub_tree(0, [10,0,0], $row);
405 0         0 my $pp1 = $self->get_sub_tree(0, [11,0,0], $row);
406 0         0 $self->{teams}[0]{pp}[$period] = $pp0;
407 0         0 $self->{teams}[1]{pp}[$period] = $pp1;
408             }
409             elsif ($period =~ /time/i) {
410 0         0 my $pp0 = $self->get_sub_tree(0, [10,0,0], $row);
411 0         0 my $pp1 = $self->get_sub_tree(0, [11,0,0], $row);
412 0         0 push(@{$self->{teams}[0]{pp}}, $pp0);
  0         0  
413 0         0 push(@{$self->{teams}[1]{pp}}, $pp1);
  0         0  
414             }
415             }
416             }
417              
418             sub parse_new_misc_summary ($$$) {
419              
420 4     4 1 10 my $self = shift;
421 4         9 my $misc_summary = shift;
422              
423 4         26 my $officials_table = $self->get_sub_tree(0, [1,0,0], $misc_summary);
424             $self->{officials} = {
425 4   33     24 referees => [
      33        
426             $self->get_sub_tree(0, [1,0,0,0,0,0], $officials_table),
427             $self->get_sub_tree(0, [1,0,0,1,0,0], $officials_table) || (),
428             ],
429             linesmen => [
430             $self->get_sub_tree(0, [1,1,0,0,0,0], $officials_table),
431             $self->get_sub_tree(0, [1,1,0,1,0,0], $officials_table) || (),
432             ],
433             };
434 4 50       21 unless ($self->{officials}{referees}[0]) {
435             $self->{officials} = {
436 0   0     0 referees => [
      0        
437             $self->get_sub_tree(0, [0,1,0,0,0,0], $officials_table),
438             $self->get_sub_tree(0, [0,1,0,1,0,0], $officials_table) || (),
439             ],
440             linesmen => [
441             $self->get_sub_tree(0, [0,3,0,0,0,0], $officials_table),
442             $self->get_sub_tree(0, [0,3,0,1,0,0], $officials_table) || (),
443             ],
444             };
445             }
446 4         17 my $stars_table = $self->get_sub_tree(0, [1,1,0], $misc_summary);
447 4         28 $self->{stars} = [];
448 4         25 my $star1 = $self->get_sub_tree(0, [0,0,0,0,1,0], $stars_table);
449 4 50 33     33 my $star_offset = $star1 && $star1 eq 'Team' ? 1 : 0;
450 4         12 my $t = 0;
451 4         15 for my $s (0..2) {
452 12         41 my $team = $self->get_sub_tree(0, [0,0,0,$s+$star_offset,1,0], $stars_table);
453 12 50 33     92 unless ($team && $team =~ /[A-Z]\s*$/) {
454 0         0 $t--;
455 0         0 next;
456             }
457 12         47 $self->{stars}[$s]{team} = $self->get_sub_tree(0, [0,0,0,$s+$star_offset+$t,1,0], $stars_table);
458 12         42 $self->{stars}[$s]{position} = $self->get_sub_tree(0, [0,0,0,$s+$star_offset+$t,2,0], $stars_table);
459 12         87 $self->{stars}[$s]{name} = $self->get_sub_tree(0, [0,0,0,$s+$star_offset+$t,3,0], $stars_table);
460             }
461             }
462              
463             sub parse_misc_summary ($$$) {
464              
465 0     0 1 0 my $self = shift;
466 0         0 my $misc_summary = shift;
467              
468 0         0 my $goalies_header = $self->get_sub_tree(0, [0,0], $misc_summary);
469 0   0     0 my $g_span = $goalies_header->attr('colspan') || $goalies_header->attr('colSpan');
470              
471 0         0 my $g = 2;
472 0         0 $self->{goalies} = [];
473 0         0 while (my $goalies_row = $self->get_sub_tree(0, [$g], $misc_summary)) {
474 0 0       0 my $goalie = {
475             team_decision => $self->get_sub_tree(0, [0,0,0], $goalies_row),
476             name => $self->get_sub_tree(0, [1,0,0], $goalies_row),
477             p1 => $self->get_sub_tree(0, [2,0,0], $goalies_row),
478             p2 => $self->get_sub_tree(0, [3,0,0], $goalies_row),
479             p3 => $self->get_sub_tree(0, [4,0,0], $goalies_row),
480             pot => $self->get_sub_tree(0, [5,0,0], $goalies_row),
481             pt => $self->get_sub_tree(0, [6,0,0], $goalies_row),
482             toi => $self->get_sub_tree(0, [7,0,0], $goalies_row),
483             $g_span == 8 ?
484             () : (so_stats => $self->get_sub_tree(0, [$g_span-1,0,0], $goalies_row)),
485             };
486 0 0 0     0 unless ($goalie->{name} && $goalie->{name} =~ /[a-z]/i) {
487 0         0 $g++;
488 0         0 next;
489             }
490 0 0       0 $goalie->{pt} = delete $goalie->{pot} if $goalie->{pt} !~ /\d/;
491 0 0       0 if ($goalie->{pt} =~ /:/) {
492 0         0 $goalie->{toi} = $goalie->{pt};
493 0         0 $goalie->{pt} = delete $goalie->{pot};
494             }
495 0         0 push(@{$self->{goalies}}, $goalie);
  0         0  
496 0         0 $g++;
497             }
498 0         0 $self->{stars} = [];
499 0         0 my $t = 0;
500 0         0 for my $s (0..2) {
501 0         0 my $name = $self->get_sub_tree(0, [$s+1, $g_span+2,0,0], $misc_summary);
502 0 0 0     0 unless ($name && $name =~ /[A-Z]\s*$/i) {
503 0         0 $t++;
504 0         0 next;
505             }
506 0         0 $self->{stars}[$s-$t]{team} = $self->get_sub_tree(0, [$s+1, $g_span ,0,0], $misc_summary);
507 0         0 $self->{stars}[$s-$t]{team} =~ s/.*\d+\s+(\S.*)$/$1/e;
  0         0  
508 0         0 $self->{stars}[$s-$t]{number} = $self->get_sub_tree(0, [$s+1, $g_span+1,0,0], $misc_summary);
509 0         0 $self->{stars}[$s-$t]{number} =~ s/\s//g;
510 0         0 $self->{stars}[$s-$t]{name} = $name;
511             }
512 0         0 for my $r (0..3) {
513 0         0 my $type = $self->get_sub_tree(0, [$r+1,$g_span+3,0,0], $misc_summary);
514 0 0       0 next unless $type;
515 0 0       0 next unless $type =~ /\w/;
516 0 0       0 $type = $type =~ /R|A/ ? 'referees' : 'linesmen';
517 0   0     0 $self->{officials}{$type} ||= [];
518 0         0 my $name = $self->get_sub_tree(0, [$r+1,$g_span+4,0,0], $misc_summary);
519 0 0       0 push(@{$self->{officials}{$type}}, { name => $name, number => 0}) if $name =~ /\w/;
  0         0  
520             }
521             }
522              
523             sub parse_goaltender_summary ($$$) {
524              
525 4     4 1 10 my $self = shift;
526 4         9 my $goaltender_summary = shift;
527              
528 4         12 $self->{goalies} = [];
529 4         26 my $g = 2;
530 4         9 my $t = 0;
531 4         19 while (my $goalies_row = $self->get_sub_tree(0, [$g], $goaltender_summary)) {
532 48 100 66     136 last unless $goalies_row && ref $goalies_row;
533 44         101 my $number = $self->get_sub_tree(0, [0,0], $goalies_row);
534 44 100       131 if ($number =~ /^\d+$/) {
535 16 100       32 $t = 1 if $t;
536 16         46 my $goalie = {
537             number => $number,
538             team => $t,
539             position => 'G',
540             name_decision => $self->get_sub_tree(0, [2,0], $goalies_row),
541             ev => $self->get_sub_tree(0, [3,0], $goalies_row),
542             pp => $self->get_sub_tree(0, [4,0], $goalies_row),
543             sh => $self->get_sub_tree(0, [5,0], $goalies_row),
544             toitot => $self->get_sub_tree(0, [6,0], $goalies_row),
545             };
546 16         35 my $s = 1;
547 16         44 while (my $shots_period = $self->get_sub_tree(0, [$s+6,0], $goalies_row)) {
548 64         156 $goalie->{"SHOT$s"} = $shots_period;
549 64         91 $self->{last_period} = $s;
550 64         166 $s++;
551             };
552 16         33 $self->{last_period}--;
553 16         23 $s--;
554 16         52 $goalie->{"SHOT"} = delete $goalie->{"SHOT$s"};
555 16         26 push(@{$self->{goalies}}, $goalie);
  16         33  
556             }
557             else {
558 28         38 $t++;
559             }
560 44         118 $g++;
561             }
562             }
563              
564             sub parse ($$) {
565              
566 4     4 1 10 my $self = shift;
567              
568 4         8 my $events = [];
569 4 100       7 $is_special = (grep {$_ == $self->{_id} && $BROKEN_EVENTS{BS}->{$_}->{1}} keys %{$BROKEN_EVENTS{BS}})
  41 50       97  
  4         43  
570             ? 1 : 0;
571 4         10 my $main_table_idx;
572 4 50       13 unless ($self->{old}) {
573 4         15 my $main_table = $self->get_sub_tree(0, [1]);
574 4 50       15 $main_table_idx = $main_table->tag eq 'table' ? 1 : 2;
575             }
576 4 50       42 my $scoring_summary = $self->get_sub_tree(0, [$self->{old} ? (3) : ($main_table_idx,3,0,0)]);
577 4 50       20 my $penalty_summary = $self->get_sub_tree(0, [$self->{old} ? (5) : ($main_table_idx,6,0,0)]);
578 4 50       24 my $pp_summary = $self->get_sub_tree(0, [$self->{old} ? (7) : ($main_table_idx,10,0,0)]);
579 4 50       25 my $misc_summary = $self->get_sub_tree(0, [$self->{old} ? (9) : ($main_table_idx,16,0,0)]);
580 4 50       14 $misc_summary = $self->get_sub_tree(0, [$main_table_idx,17,0,0]) unless ref $misc_summary;
581             $self->{events} = [
582 4         21 @{$self->parse_scoring_summary($scoring_summary, 1-$self->{old})},
583 4         7 @{$self->parse_penalty_summary($penalty_summary, 1-$self->{old})},
  4         26  
584             ];
585             $self->{old} ?
586 4 50       32 $self->parse_pp_summary($pp_summary) :
587             $self->parse_new_pp_summary($pp_summary);
588             $self->{old} ?
589 4 50       36 $self->parse_misc_summary($misc_summary) :
590             $self->parse_new_misc_summary($misc_summary);
591 4 50       16 unless ($self->{old}) {
592 4         19 my $goaltender_summary = $self->get_sub_tree(0, [2,14,0,0]);
593 4 50       20 if (ref $goaltender_summary) {
594 4         21 $self->parse_goaltender_summary($goaltender_summary);
595             }
596             else {
597 0         0 $self->{_gs_no_g} = 1;
598             }
599             }
600 4         11 for my $event (@{$self->{events}}) {
  4         16  
601 84         163 $event->{file} = $self->{file};
602 84         137 $event->{game_id} = $self->{_id};
603 84         158 $event->{stage} = $self->{stage};
604 84         130 $event->{season} = $self->{season};
605             }
606 4         14 for my $t (0,1) {
607 8         30 $self->{teams}[$t]{roster} = [];
608             }
609             }
610              
611             sub normalize_new ($$) {
612              
613 4     4 1 12 my $self = shift;
614              
615 4         7 for my $goalie (@{$self->{goalies}}) {
  4         14  
616 16         26 for my $field (keys %{$goalie}) {
  16         57  
617 192 100       561 if ($field eq 'name_decision') {
    100          
    100          
    100          
618 16 100       239 if ($goalie->{$field} =~ /^(\S+.*)\,\s+(\S+.*\S+)\s+\((W|L|OT)\)/) {
619 8         42 $goalie->{name} = "$2 $1";
620 8         28 $goalie->{wl} = $3;
621             }
622             else {
623 8         37 $goalie->{$field} =~ /^(\S+.*)\,\s+(\S+.*\S+)/;
624 8         49 $goalie->{name} = "$2 $1";
625 8         19 $goalie->{wl} = '';
626             }
627             }
628             elsif ($field eq 'team') {
629 16         42 $goalie->{$field} = $self->{teams}[$goalie->{$field}]{name};
630             }
631             elsif ($goalie->{$field} =~ /(\d+)\:(\d+)/) {
632 32         76 $goalie->{uc $field} = $goalie->{$field};
633 32         86 $goalie->{$field} = $1*60 + $2;
634             }
635             elsif ($goalie->{$field} =~ /(\d+)\-(\d+)/) {
636 32         90 $goalie->{$field} = [$1, $2];
637             }
638 192 100 66     554 if ($goalie->{$field} eq ' ' || ord($goalie->{$field}) == 160) {
639 64 100       179 $goalie->{$field} = $field =~ /SHOT/ ? [0,0] : 0;
640             }
641             }
642 16         40 delete $goalie->{name_decision};
643             }
644 4         12 my $t = 0;
645 4 50       10 if (@{$self->{events}}) {
  4         18  
646 4         21 my $last_time = $self->{events}[-1]{time};
647 4         31 $last_time =~ s/(\d+):(\d+)/$1*60+$2/eg;
  4         22  
648 4         16 for my $team (@{$self->{teams}}) {
  4         15  
649 8   66     42 $team->{strength}{ev}{time} ||= $last_time;
650 8   66     88 $self->{teams}[$t-1]{strength}{ev}{time} ||= $last_time;
651 8         15 for my $pptype (qw(5v4 5v3 4v3)) {
652 24 100       91 if ($team->{pptype}{$pptype} =~ /(\d+)\-(\d+)\/(\d+)\:(\d+)/) {
653 16         132 $team->{strength}{$pptype} = $self->{teams}[$t-1]{strength}{reverse $pptype} = {
654             goals => $1,
655             tries => $2,
656             time => $3*60+$4,
657             };
658 16         39 $team->{strength}{ev}{time} -= $team->{strength}{$pptype}{time};
659 16         43 $self->{teams}[$t-1]{strength}{ev}{time} -= $team->{strength}{$pptype}{time};
660             }
661             else {
662 8         40 $team->{strength}{$pptype} = $self->{teams}[$t-1]{strength}{reverse $pptype} = {
663             goals => 0,
664             tries => 0,
665             time => 0,
666             };
667             }
668             }
669 8         22 delete $team->{pptype};
670 8         17 $t++;
671             }
672             }
673 4         11 for my $type (keys %{$self->{officials}}) {
  4         17  
674 8         14 for my $official (@{$self->{officials}{$type}}) {
  8         20  
675 16 50       41 next unless $official;
676 16         55 $official =~ /\#(\d+)\s+(\S.*\S)/;
677 16         92 $official = { name => $2, number => $1 };
678             }
679             }
680 4         9 for my $star (@{$self->{stars}}) {
  4         13  
681 12 50 33     84 next unless defined $star && ref $star && defined $star->{name};
      33        
682 12         61 $star->{name} =~ /(\d+)\s+\S+.*\.(\S+.*\S+)/;
683 12         27 $star->{number} = $1;
684 12         28 $star->{name} = $2;
685             }
686 4         9 my $e = 1;
687 4         10 for my $event (@{$self->{events}}) {
  4         14  
688 84         100 for my $field (keys %{$event}) {
  84         361  
689 1044 100 100     2950 if ($event->{$field} && $event->{$field} =~ /^(\d+)\s+\D/) {
690 60         144 $event->{$field} = $1;
691             }
692             }
693 84   100     292 $event->{strength} ||= 'XX';
694 84   100     221 $event->{location} ||= 'Unk';
695 84         125 $event->{file} = $self->{file};
696 84         118 $event->{id} = $e++;
697 84 100 66     381 if (defined $event->{team1} && $event->{team1} =~ /^\d+$/) {
698 64         135 $event->{team1} = $self->{teams}[$event->{team1}]{name};
699             }
700 84 50 33     230 $event->{assist1} = undef if $event->{assist1} && (lc($event->{assist1}) eq 'unassisted' || $event->{assist1} =~ /unsuccessful/i || $event->{assist1} =~ /penalty shot/i);
      66        
701 84 100 66     221 $event->{assist2} = undef unless defined $event->{assist2} && $event->{assist2} =~ /\w/;
702 84   50     139 $event->{player1} ||= 0;
703 84         142 $event->{player1} =~ s/^\s+//g;
704 84         134 $event->{player1} =~ s/\s+$//g;
705 84 50       169 $event->{player1} = $NAME_TYPOS{$event->{player1}} if $NAME_TYPOS{$event->{player1}};
706 84 50 66     168 $event->{assist1} = $NAME_TYPOS{$event->{assist1}} if $event->{assist1} && $NAME_TYPOS{$event->{assist1}};
707 84 50 66     196 $event->{assist2} = $NAME_TYPOS{$event->{assist2}} if $event->{assist2} && $NAME_TYPOS{$event->{assist2}};
708             }
709 4         9 for my $goalie (@{$self->{goalies}}) {
  4         15  
710 16         32 $goalie->{goals} = $goalie->{SHOT}[0];
711 16         31 $goalie->{shots} = $goalie->{SHOT}[1];
712 16         47 $goalie->{saves} = $goalie->{SHOT}[1] - $goalie->{SHOT}[0];
713 16         45 for my $field (keys %NORMAL_FIELDS) {
714 80         178 $goalie->{$NORMAL_FIELDS{$field}} = delete $goalie->{$field};
715             }
716 16 100       69 my $t = $goalie->{team} eq $self->{teams}[0]{name} ? 0 : 1;
717 16 100       45 $self->{teams}[$t]{_decision} = $goalie->{decision} if ($goalie->{decision});
718 16         23 push(@{$self->{teams}[$t]{roster}}, $goalie);
  16         42  
719             }
720             $self->{_score} = [
721             $self->{teams}[0]{score},
722             $self->{teams}[1]{score},
723 4         19 ];
724              
725 4         13 $self->{_t} = 0;
726 4         10 for my $team (@{$self->{teams}}) {
  4         13  
727 8 50       20 $self->force_decision($team) unless $team->{_decision};
728 8         18 $self->{_t}++;
729             }
730              
731             }
732              
733             sub normalize_old ($$) {
734              
735 0     0 1 0 my $self = shift;
736              
737 0         0 for my $event (@{$self->{events}}) {
  0         0  
738 0         0 $event->{old} = 1;
739 0         0 $event->{file} = $self->{file};
740 0 0 0     0 if ($event->{type} eq 'GOAL' || $event->{type} eq 'MISS') {
    0          
741 0         0 $event->{player1} =~ s/^\s*(\S.*\S)\s*\(.*/$1/e;
  0         0  
742 0 0 0     0 if ($event->{assist1} && $event->{assist1} =~ /\w/) {
743 0         0 $event->{assist1} =~ s/^\s*(\S.*\S)\s*\(.*/$1/e;
  0         0  
744 0 0 0     0 if ($event->{assist2} && $event->{assist2} =~ /\w/) {
745 0         0 $event->{assist2} =~ s/^\s*(\S.*\S)\s*\(.*/$1/e;
  0         0  
746             }
747             else {
748 0         0 delete $event->{assist2};
749             }
750             }
751             else {
752 0         0 delete $event->{assist1};
753             }
754 0 0 0     0 if ($SPECIAL_EVENTS{$self->{_id}} && !$event->{on_ice}[0][0] ||
      0        
755             $event->{on_ice}[0][0] =~ /Data/) {
756 0         0 $event->{on_ice} = [[($UNKNOWN_PLAYER_ID)x6],[($UNKNOWN_PLAYER_ID)x6]];
757             }
758             else {
759 0         0 for my $on_ice (@{$event->{on_ice}}) {
  0         0  
760 0         0 for my $on_ice_n (@{$on_ice}) {
  0         0  
761 0         0 $on_ice_n =~ s/^\s+//;
762 0         0 $on_ice_n =~ s/\s+$//;
763 0         0 $on_ice = [split(/\s+/, $on_ice_n)];
764             }
765             }
766             }
767             }
768             elsif ($event->{type} eq 'PENL') {
769 0         0 $event->{penalty} =~ s/(\- obstruction)//i;
770 0         0 $event->{length} =~ s/^(\d+)\:.*/$1/e;
  0         0  
771 0         0 $event->{strength} = 'XX';
772 0         0 $event->{location} = 'UNK';
773 0 0 0     0 if ($event->{player1} && $event->{player1} =~ /\D/) {
774 0         0 $event->{name} = $event->{player1};
775 0         0 $event->{player1} = $event->{number};
776             }
777             }
778 0 0 0     0 if (defined $event->{team1} && $event->{team1} =~ /^\d+$/) {
779 0         0 $event->{team1} = $self->{teams}[$event->{team1}]{name};
780             }
781 0         0 $event->{player1} =~ s/^\s+//g;
782 0         0 $event->{player1} =~ s/\s+$//g;
783 0 0       0 $event->{player1} = $NAME_TYPOS{$event->{player1}} if $NAME_TYPOS{$event->{player1}};
784 0 0 0     0 $event->{assist1} = $NAME_TYPOS{$event->{assist1}} if $event->{assist1} && $NAME_TYPOS{$event->{assist1}};
785 0 0 0     0 $event->{assist2} = $NAME_TYPOS{$event->{assist2}} if $event->{assist2} && $NAME_TYPOS{$event->{assist2}};
786 0 0 0     0 delete $event->{assist1} if $event->{assist1} && ($event->{assist1} =~ /unassisted/i || $event->{assist1} !~ /[a-z]/i);
      0        
787 0 0 0     0 delete $event->{assist2} if $event->{assist2} && ($event->{assist2} =~ /unassisted/i || $event->{assist2} !~ /[a-z]/i);
      0        
788             }
789 0         0 for my $e (1..@{$self->{events}}) {
  0         0  
790 0         0 $self->{events}[$e-1]{id} = $e;
791 0   0     0 $self->{events}[$e-1]{location} ||= 'Unk';
792             }
793 0         0 my $t0 = '';
794 0         0 my $t = 0;
795 0         0 for my $goalie (@{$self->{goalies}}) {
  0         0  
796 0         0 $goalie->{pt} =~ /(\d+)\-(\d+)/;
797 0         0 $goalie->{goals} = $1;
798 0         0 $goalie->{shots} = $2;
799 0         0 $goalie->{saves} = $2 - $1;
800 0         0 $goalie->{timeOnIce} = get_seconds(delete $goalie->{toi});
801 0         0 $goalie->{old} = 1;
802 0         0 $goalie->{position} = 'G';
803 0 0       0 if ($goalie->{team_decision} =~ /^(\S{3})\(([A-Z])\)/) {
804 0 0       0 if (! $t0) {
    0          
805 0         0 $t = 0;
806 0         0 $t0 = $1;
807             }
808             elsif ($t0 ne $1) {
809 0         0 $t = 1;
810 0         0 $t0 = $1;
811             }
812 0         0 $goalie->{decision} = $2;
813 0         0 delete $goalie->{team_decision};
814             }
815             else {
816 0         0 $goalie->{team_decision} =~ /^(\S{3})/;
817 0         0 delete $goalie->{team_decision};
818 0 0       0 if (! $t0) {
    0          
819 0         0 $t = 0;
820 0         0 $t0 = $1;
821             }
822             elsif ($t0 ne $1) {
823 0         0 $t = 1;
824 0         0 $t0 = $1;
825             }
826             }
827 0 0       0 $self->{teams}[$t]{_decision} = $goalie->{decision} if ($goalie->{decision});
828 0 0       0 if ($goalie->{name} eq 'EMPTY NET') {
829 0         0 $goalie->{_id} = $EMPTY_NET_ID;
830 0         0 $goalie->{number} = 0;
831             }
832 0         0 push(@{$self->{teams}[$t]{roster}}, $goalie);
  0         0  
833             }
834             $self->{_score} = [
835             $self->{teams}[0]{score},
836             $self->{teams}[1]{score},
837 0         0 ];
838              
839 0         0 $self->{_t} = 0;
840 0         0 for my $team (@{$self->{teams}}) {
  0         0  
841 0 0       0 $self->force_decision($team) unless $team->{_decision};
842 0         0 $self->{_t}++;
843             }
844             }
845              
846             sub normalize ($) {
847              
848 4     4 1 11 my $self = shift;
849              
850             $self->{old} ?
851 4 50       27 $self->normalize_old($self) :
852             $self->normalize_new($self);
853 4         75 @{$self->{events}} = grep { $_->{type} ne 'PENL' } @{$self->{events}}
  84         143  
  4         13  
854 4 50       17 unless $ENV{GS_KEEP_PENL};
855 4         11 for my $event (@{$self->{events}}) {
  4         13  
856 20 50       81 if (my $evx = $BROKEN_EVENTS{GS}->{$self->{_id}}->{$event->{id}}) {
857 0         0 for my $key (keys %{$evx}) {
  0         0  
858 0         0 $event->{$key} = $evx->{$key};
859             }
860 0         0 next;
861             }
862 20 50       44 if ($event->{type} eq 'PENL') {
863 0         0 $event->{penalty} =~ s/(\- double minor)//i;
864 0         0 $event->{penalty} =~ s/(\- obstruction)//i;
865 0         0 $event->{penalty} =~ s/(PS \- )//i;
866             }
867 20         30 for my $v (qw(strength shot_type penalty miss)) {
868 80 100       190 $event->{$v} = vocabulary_lookup($v, $event->{$v}) if exists $event->{$v};
869             }
870             }
871             }
872              
873             1;
874              
875             =head1 AUTHOR
876              
877             More Hockey Stats, C<< >>
878              
879             =head1 BUGS
880              
881             Please report any bugs or feature requests to C, or through
882             the web interface at L. I will be notified, and then you'll
883             automatically be notified of progress on your bug as I make changes.
884              
885              
886             =head1 SUPPORT
887              
888             You can find documentation for this module with the perldoc command.
889              
890             perldoc Sport::Analytics::NHL::Report::GS
891              
892             You can also look for information at:
893              
894             =over 4
895              
896             =item * RT: CPAN's request tracker (report bugs here)
897              
898             L
899              
900             =item * AnnoCPAN: Annotated CPAN documentation
901              
902             L
903              
904             =item * CPAN Ratings
905              
906             L
907              
908             =item * Search CPAN
909              
910             L
911              
912             =back