File Coverage

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


line stmt bran cond sub pod time code
1             package Sport::Analytics::NHL::Report::GS;
2              
3 19     19   75080 use v5.10.1;
  19         79  
4 19     19   94 use strict;
  19         46  
  19         410  
5 19     19   82 use warnings FATAL => 'all';
  19         39  
  19         579  
6 19     19   98 use experimental qw(smartmatch);
  19         36  
  19         121  
7              
8 19     19   1311 use parent 'Sport::Analytics::NHL::Report';
  19         297  
  19         129  
9              
10 19     19   2064 use utf8;
  19         82  
  19         126  
11              
12 19     19   570 use Sport::Analytics::NHL::Config;
  19         37  
  19         3292  
13 19     19   123 use Sport::Analytics::NHL::Errors;
  19         40  
  19         3179  
14 19     19   133 use Sport::Analytics::NHL::Util;
  19         38  
  19         1129  
15 19     19   109 use Sport::Analytics::NHL::Tools;
  19         35  
  19         3049  
16              
17 19     19   134 use Data::Dumper;
  19         47  
  19         114718  
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 10     10 1 14 my $self = shift;
149 10         14 my $row = shift;
150 10         15 my $is_new = shift;
151              
152 10         19 my $event = {on_ice => []};
153              
154 10 50       37 my $is_goal = $self->get_sub_tree(0, [$is_new ? (0,0) : (0,0,0)], $row);
155 10 50       42 $event->{type} = $is_goal =~ /\d/ ? 'GOAL' : 'MISS';
156 10 50       22 my $offset = $event->{type} eq 'MISS' ? 1 : 0;
157 10 50       31 $event->{period} = $self->get_sub_tree(0, [$is_new ? (1,0) : (1,0,0)], $row);
158 10 50 33     43 return undef if !$is_special && $event->{period} !~ /\w/;
159 10 50       29 $event->{time} = $self->get_sub_tree(0, [$is_new ? (2,0) : (2,0,0)], $row);
160 10 50       30 $event->{team1} = $self->get_sub_tree(0, [$is_new ? (4,0) : (3,0,0)], $row);
161 10 50       34 $event->{player1} = $self->get_sub_tree(0, [$is_new ? (5,0) : (4,0,0)], $row);
162 10 50       24 if ($event->{type} eq 'GOAL') {
163 10 50       29 $event->{assist1} = $self->get_sub_tree(0, [$is_new ? (6,0) : (5,0,0)], $row);
164 10 50       29 $event->{assist2} = $self->get_sub_tree(0, [$is_new ? (7,0) : (6,0,0)], $row);
165             }
166 10 50       22 if ($is_special) {
167 0         0 $event->{special} = 1;
168 0         0 $is_special = 0;
169 0         0 return $event;
170             }
171 10 50       27 $event->{strength} = $self->get_sub_tree(0, [$is_new ? (3,0) : (9,0,0)], $row);
172 10 50 33     60 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 10 50 33     42 if ($event->{assist1} && $event->{assist1} =~ /unsuccessful/i) {
183 0         0 $event->{type} = 'MISS';
184 0         0 $offset = 1;
185             }
186 10 50       19 if (ref $event->{assist2}) {
187 0         0 $event->{assist2} = undef;
188 0         0 $offset = 1;
189             }
190 10 50       18 if ($event->{type} eq 'MISS') {
191 0         0 $event->{description} = 'Missed Penalty Shot';
192 0         0 $event->{assist1} = $event->{assist2} = undef;
193             }
194              
195 10 50 33     39 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 10   50     34 $event->{en} ||= 0;
210 10 50       14 if ($is_new) {
211 10         18 for my $i (8,9) {
212 20         45 my $on_ice = $self->get_sub_tree(0, [$i-$offset], $row);
213 20         31 my $n = 0;
214 20         38 while (my $on_ice_num = $self->get_sub_tree(0, [$n,0], $on_ice)) {
215 120   100     249 $event->{on_ice}[$i-8] ||= [];
216 120         135 push(@{$event->{on_ice}[$i-8]}, $on_ice_num);
  120         241  
217 120         279 $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 10 50       21 return undef if ref $event->{on_ice}[0][0];
226 10         18 $event->{shot_type} = 'Unknown';
227 10         15 $event->{location} = 'Unk';
228 10         15 $event->{distance} = 999;
229 10         15 $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 2     2 1 4 my $self = shift;
261 2         3 my $scoring_summary = shift;
262 2   50     8 my $is_new = shift || 0;
263              
264 2         5 my $events = [];
265 2 50       6 my $r = $is_new ? 1 : 2;
266 2         6 while (my $row = $self->get_sub_tree(0, [$r], $scoring_summary)) {
267 12 100 66     37 last unless $row && ref $row;
268 10         20 $r++;
269 10         21 my $event = $self->parse_scoring_event($row, $is_new);
270 10 50       19 push(@{$events}, $event) if $event;
  10         26  
271             }
272 2         6 $events;
273             }
274              
275             sub parse_new_penalty_event ($$) {
276              
277 32     32 1 41 my $self = shift;
278 32         43 my $row = shift;
279              
280 32         50 my $event = {};
281              
282 32         68 $event->{type} = 'PENL';
283 32         66 $event->{period} = $self->get_sub_tree(0, [1,0], $row);
284 32 50       74 $event->{period} = 4 if $event->{period} eq 'OT';
285 32 50       56 $event->{period} = 5 if $event->{period} eq 'SO';
286 32         67 $event->{time} = $self->get_sub_tree(0, [2,0], $row);
287 32 50       99 return undef if $event->{time} !~ /:/;
288 32         81 $event->{player1} = $self->get_sub_tree(0, [3,0,0,0,0], $row);
289 32         73 $event->{name} = $self->get_sub_tree(0, [3,0,0,3,0], $row);
290 32         72 $event->{length} = $self->get_sub_tree(0, [4,0], $row);
291 32         70 $event->{penalty} = $self->get_sub_tree(0, [5,0], $row);
292 32 50       75 $event->{misconduct} = 1 if $event->{penalty} =~ /conduct/;
293 32 100       63 $event->{player1} = $BENCH_PLAYER_ID if $event->{penalty} =~ /\-\s+bench/;
294 32 50       84 $event->{player1} = $COACH_PLAYER_ID if $event->{penalty} =~ /\bcoach\b/i;
295 32 100 66     109 $event->{player1} = $BENCH_PLAYER_ID if $event->{name} && $event->{name} =~ /\bteam\b/i;
296 32         58 delete $event->{name};
297 32         42 $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 2     2 1 5 my $self = shift;
328 2         11 my $penalty_summary = shift;
329 2   50     7 my $is_new = shift || 0;
330              
331 2         5 my $events = [];
332 2 50       21 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 2 50       10 if ($is_new) {
337 2 50       17 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 2         4 my $p = 0;
343 2         5 for my $penalty_table (@penalty_tables) {
344 4 50       9 next unless defined $penalty_table;
345 4         7 my $r = 2 - $is_new;
346 4         12 while (my $row = $self->get_sub_tree(0, [$r], $penalty_table)) {
347 36 100 66     105 last unless $row && ref $row;
348 32         53 $r++;
349 32 50       39 if ($is_new) {
350 32         60 my $event = $self->parse_new_penalty_event($row);
351 32         49 $event->{team1} = $p;
352 32 50 33     89 push(@{$events}, $event) if $event && $event->{type};
  32         97  
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 4         8 $p++;
362             }
363 2         8 $events;
364             }
365              
366             sub parse_new_pp_summary ($$$) {
367              
368 2     2 1 18 my $self = shift;
369 2         14 my $pp_summary = shift;
370              
371 2         6 for my $t (0,1) {
372 4         13 my $pp_team_summary = $self->get_sub_tree(0, [ (1,0,0,0,$t) ], $pp_summary);
373 4         10 $self->{teams}[$t]{pptype} = {};
374 4         7 my $pp = 0;
375 4         8 for my $pptype (qw(5v4 5v3 4v3)) {
376 12   33     29 $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 12         22 $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 2     2 1 4 my $self = shift;
421 2         4 my $misc_summary = shift;
422              
423 2         8 my $officials_table = $self->get_sub_tree(0, [1,0,0], $misc_summary);
424             $self->{officials} = {
425 2   33     9 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 2 50       11 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 2         22 my $stars_table = $self->get_sub_tree(0, [1,1,0], $misc_summary);
447 2         5 $self->{stars} = [];
448 2         8 my $star1 = $self->get_sub_tree(0, [0,0,0,0,1,0], $stars_table);
449 2 50 33     11 my $star_offset = $star1 && $star1 eq 'Team' ? 1 : 0;
450 2         5 my $t = 0;
451 2         8 for my $s (0..2) {
452 6         18 my $team = $self->get_sub_tree(0, [0,0,0,$s+$star_offset,1,0], $stars_table);
453 6 50 33     38 unless ($team && $team =~ /[A-Z]\s*$/) {
454 0         0 $t--;
455 0         0 next;
456             }
457 6         25 $self->{stars}[$s]{team} = $self->get_sub_tree(0, [0,0,0,$s+$star_offset+$t,1,0], $stars_table);
458 6         26 $self->{stars}[$s]{position} = $self->get_sub_tree(0, [0,0,0,$s+$star_offset+$t,2,0], $stars_table);
459 6         20 $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 2     2 1 3 my $self = shift;
526 2         4 my $goaltender_summary = shift;
527              
528 2         11 $self->{goalies} = [];
529 2         5 my $g = 2;
530 2         4 my $t = 0;
531 2         7 while (my $goalies_row = $self->get_sub_tree(0, [$g], $goaltender_summary)) {
532 24 100 66     72 last unless $goalies_row && ref $goalies_row;
533 22         48 my $number = $self->get_sub_tree(0, [0,0], $goalies_row);
534 22 100       60 if ($number =~ /^\d+$/) {
535 8 100       24 $t = 1 if $t;
536 8         23 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 8         18 my $s = 1;
547 8         21 while (my $shots_period = $self->get_sub_tree(0, [$s+6,0], $goalies_row)) {
548 32         70 $goalie->{"SHOT$s"} = $shots_period;
549 32         49 $self->{last_period} = $s;
550 32         72 $s++;
551             };
552 8         20 $self->{last_period}--;
553 8         14 $s--;
554 8         115 $goalie->{"SHOT"} = delete $goalie->{"SHOT$s"};
555 8         13 push(@{$self->{goalies}}, $goalie);
  8         15  
556             }
557             else {
558 14         18 $t++;
559             }
560 22         111 $g++;
561             }
562             }
563              
564             sub parse ($$) {
565              
566 2     2 1 4 my $self = shift;
567              
568 2         4 my $events = [];
569 2 50       4 $is_special = (grep {$_ == $self->{_id} && $BROKEN_EVENTS{BS}->{$_}->{1}} keys %{$BROKEN_EVENTS{BS}})
  18 50       50  
  2         15  
570             ? 1 : 0;
571 2         5 my $main_table_idx;
572 2 50       7 unless ($self->{old}) {
573 2         8 my $main_table = $self->get_sub_tree(0, [1]);
574 2 50       8 $main_table_idx = $main_table->tag eq 'table' ? 1 : 2;
575             }
576 2 50       21 my $scoring_summary = $self->get_sub_tree(0, [$self->{old} ? (3) : ($main_table_idx,3,0,0)]);
577 2 50       12 my $penalty_summary = $self->get_sub_tree(0, [$self->{old} ? (5) : ($main_table_idx,6,0,0)]);
578 2 50       9 my $pp_summary = $self->get_sub_tree(0, [$self->{old} ? (7) : ($main_table_idx,10,0,0)]);
579 2 50       9 my $misc_summary = $self->get_sub_tree(0, [$self->{old} ? (9) : ($main_table_idx,16,0,0)]);
580 2 50       13 $misc_summary = $self->get_sub_tree(0, [$main_table_idx,17,0,0]) unless ref $misc_summary;
581             $self->{events} = [
582 2         10 @{$self->parse_scoring_summary($scoring_summary, 1-$self->{old})},
583 2         4 @{$self->parse_penalty_summary($penalty_summary, 1-$self->{old})},
  2         12  
584             ];
585             $self->{old} ?
586 2 50       15 $self->parse_pp_summary($pp_summary) :
587             $self->parse_new_pp_summary($pp_summary);
588             $self->{old} ?
589 2 50       22 $self->parse_misc_summary($misc_summary) :
590             $self->parse_new_misc_summary($misc_summary);
591 2 50       7 unless ($self->{old}) {
592 2         8 my $goaltender_summary = $self->get_sub_tree(0, [2,14,0,0]);
593 2 50       8 if (ref $goaltender_summary) {
594 2         7 $self->parse_goaltender_summary($goaltender_summary);
595             }
596             else {
597 0         0 $self->{_gs_no_g} = 1;
598             }
599             }
600 2         3 for my $event (@{$self->{events}}) {
  2         5  
601 42         62 $event->{file} = $self->{file};
602 42         58 $event->{game_id} = $self->{_id};
603 42         73 $event->{stage} = $self->{stage};
604 42         57 $event->{season} = $self->{season};
605             }
606 2         7 for my $t (0,1) {
607 4         13 $self->{teams}[$t]{roster} = [];
608             }
609             }
610              
611             sub normalize_new ($$) {
612              
613 2     2 1 3 my $self = shift;
614              
615 2         4 for my $goalie (@{$self->{goalies}}) {
  2         6  
616 8         11 for my $field (keys %{$goalie}) {
  8         33  
617 96 100       260 if ($field eq 'name_decision') {
    100          
    100          
    100          
618 8 100       133 if ($goalie->{$field} =~ /^(\S+.*)\,\s+(\S+.*\S+)\s+\((W|L|OT)\)/) {
619 4         16 $goalie->{name} = "$2 $1";
620 4         9 $goalie->{wl} = $3;
621             }
622             else {
623 4         13 $goalie->{$field} =~ /^(\S+.*)\,\s+(\S+.*\S+)/;
624 4         16 $goalie->{name} = "$2 $1";
625 4         17 $goalie->{wl} = '';
626             }
627             }
628             elsif ($field eq 'team') {
629 8         20 $goalie->{$field} = $self->{teams}[$goalie->{$field}]{name};
630             }
631             elsif ($goalie->{$field} =~ /(\d+)\:(\d+)/) {
632 16         36 $goalie->{uc $field} = $goalie->{$field};
633 16         45 $goalie->{$field} = $1*60 + $2;
634             }
635             elsif ($goalie->{$field} =~ /(\d+)\-(\d+)/) {
636 16         40 $goalie->{$field} = [$1, $2];
637             }
638 96 100 66     272 if ($goalie->{$field} eq ' ' || ord($goalie->{$field}) == 160) {
639 32 100       77 $goalie->{$field} = $field =~ /SHOT/ ? [0,0] : 0;
640             }
641             }
642 8         17 delete $goalie->{name_decision};
643             }
644 2         4 my $t = 0;
645 2 50       4 if (@{$self->{events}}) {
  2         10  
646 2         5 my $last_time = $self->{events}[-1]{time};
647 2         11 $last_time =~ s/(\d+):(\d+)/$1*60+$2/eg;
  2         11  
648 2         4 for my $team (@{$self->{teams}}) {
  2         6  
649 4   66     26 $team->{strength}{ev}{time} ||= $last_time;
650 4   66     58 $self->{teams}[$t-1]{strength}{ev}{time} ||= $last_time;
651 4         8 for my $pptype (qw(5v4 5v3 4v3)) {
652 12 100       45 if ($team->{pptype}{$pptype} =~ /(\d+)\-(\d+)\/(\d+)\:(\d+)/) {
653 8         49 $team->{strength}{$pptype} = $self->{teams}[$t-1]{strength}{reverse $pptype} = {
654             goals => $1,
655             tries => $2,
656             time => $3*60+$4,
657             };
658 8         17 $team->{strength}{ev}{time} -= $team->{strength}{$pptype}{time};
659 8         27 $self->{teams}[$t-1]{strength}{ev}{time} -= $team->{strength}{$pptype}{time};
660             }
661             else {
662 4         25 $team->{strength}{$pptype} = $self->{teams}[$t-1]{strength}{reverse $pptype} = {
663             goals => 0,
664             tries => 0,
665             time => 0,
666             };
667             }
668             }
669 4         9 delete $team->{pptype};
670 4         9 $t++;
671             }
672             }
673 2         4 for my $type (keys %{$self->{officials}}) {
  2         10  
674 4         7 for my $official (@{$self->{officials}{$type}}) {
  4         8  
675 8 50       22 next unless $official;
676 8         21 $official =~ /\#(\d+)\s+(\S.*\S)/;
677 8         33 $official = { name => $2, number => $1 };
678             }
679             }
680 2         12 for my $star (@{$self->{stars}}) {
  2         6  
681 6 50 33     35 next unless defined $star && ref $star && defined $star->{name};
      33        
682 6         26 $star->{name} =~ /(\d+)\s+\S+.*\.(\S+.*\S+)/;
683 6         13 $star->{number} = $1;
684 6         12 $star->{name} = $2;
685             }
686 2         4 my $e = 1;
687 2         7 for my $event (@{$self->{events}}) {
  2         9  
688 42         64 for my $field (keys %{$event}) {
  42         145  
689 522 100 100     1412 if ($event->{$field} && $event->{$field} =~ /^(\d+)\s+\D/) {
690 30         60 $event->{$field} = $1;
691             }
692             }
693 42   100     142 $event->{strength} ||= 'XX';
694 42   100     121 $event->{location} ||= 'Unk';
695 42         59 $event->{file} = $self->{file};
696 42         54 $event->{id} = $e++;
697 42 100 66     173 if (defined $event->{team1} && $event->{team1} =~ /^\d+$/) {
698 32         56 $event->{team1} = $self->{teams}[$event->{team1}]{name};
699             }
700 42 50 33     122 $event->{assist1} = undef if $event->{assist1} && (lc($event->{assist1}) eq 'unassisted' || $event->{assist1} =~ /unsuccessful/i || $event->{assist1} =~ /penalty shot/i);
      66        
701 42 100 66     99 $event->{assist2} = undef unless defined $event->{assist2} && $event->{assist2} =~ /\w/;
702 42   50     65 $event->{player1} ||= 0;
703 42         69 $event->{player1} =~ s/^\s+//g;
704 42         65 $event->{player1} =~ s/\s+$//g;
705 42 50       72 $event->{player1} = $NAME_TYPOS{$event->{player1}} if $NAME_TYPOS{$event->{player1}};
706 42 50 66     84 $event->{assist1} = $NAME_TYPOS{$event->{assist1}} if $event->{assist1} && $NAME_TYPOS{$event->{assist1}};
707 42 50 66     101 $event->{assist2} = $NAME_TYPOS{$event->{assist2}} if $event->{assist2} && $NAME_TYPOS{$event->{assist2}};
708             }
709 2         12 for my $goalie (@{$self->{goalies}}) {
  2         11  
710 8         16 $goalie->{goals} = $goalie->{SHOT}[0];
711 8         12 $goalie->{shots} = $goalie->{SHOT}[1];
712 8         36 $goalie->{saves} = $goalie->{SHOT}[1] - $goalie->{SHOT}[0];
713 8         19 for my $field (keys %NORMAL_FIELDS) {
714 40         77 $goalie->{$NORMAL_FIELDS{$field}} = delete $goalie->{$field};
715             }
716 8 100       21 my $t = $goalie->{team} eq $self->{teams}[0]{name} ? 0 : 1;
717 8 100       43 $self->{teams}[$t]{_decision} = $goalie->{decision} if ($goalie->{decision});
718 8         15 push(@{$self->{teams}[$t]{roster}}, $goalie);
  8         21  
719             }
720             $self->{_score} = [
721             $self->{teams}[0]{score},
722             $self->{teams}[1]{score},
723 2         18 ];
724              
725 2         6 $self->{_t} = 0;
726 2         4 for my $team (@{$self->{teams}}) {
  2         6  
727 4 50       9 $self->force_decision($team) unless $team->{_decision};
728 4         9 $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 2     2 1 4 my $self = shift;
849              
850             $self->{old} ?
851 2 50       16 $self->normalize_old($self) :
852             $self->normalize_new($self);
853 2         47 @{$self->{events}} = grep { $_->{type} ne 'PENL' } @{$self->{events}}
  42         66  
  2         9  
854 2 50       9 unless $ENV{GS_KEEP_PENL};
855 2         5 for my $event (@{$self->{events}}) {
  2         4  
856 10 50       30 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 10 50       22 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 10         15 for my $v (qw(strength shot_type penalty miss)) {
868 40 100       95 $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