File Coverage

blib/lib/Sport/Analytics/NHL/Report.pm
Criterion Covered Total %
statement 286 334 85.6
branch 93 150 62.0
condition 54 112 48.2
subroutine 35 37 94.5
pod 20 20 100.0
total 488 653 74.7


line stmt bran cond sub pod time code
1             package Sport::Analytics::NHL::Report;
2              
3 52     52   220286 use v5.10.1;
  52         200  
4 52     52   258 use strict;
  52         104  
  52         1284  
5 52     52   261 use warnings FATAL => 'all';
  52         103  
  52         1836  
6 52     52   316 use experimental qw(smartmatch);
  52         113  
  52         296  
7              
8 52     52   9674 use Storable;
  52         35173  
  52         2691  
9              
10 52     52   20053 use Date::Calc qw(Decode_Date_US Decode_Date_EU);
  52         284340  
  52         3838  
11 52     52   31102 use HTML::TreeBuilder;
  52         1454221  
  52         611  
12 52     52   10967 use List::MoreUtils qw(firstval);
  52         177787  
  52         530  
13 52     52   61942 use Module::Pluggable require => 1, search_path => ['Sport::Analytics::NHL::Report'];
  52         471362  
  52         404  
14 52     52   11517 use Time::Local;
  52         22318  
  52         2994  
15              
16 52     52   6434 use Sport::Analytics::NHL::Util;
  52         119  
  52         3623  
17 52     52   7689 use Sport::Analytics::NHL::Tools;
  52         129  
  52         10590  
18 52     52   367 use Sport::Analytics::NHL::Config;
  52         118  
  52         10162  
19 52     52   13929 use Sport::Analytics::NHL::Errors;
  52         312  
  52         10909  
20              
21             =head1 NAME
22              
23             Sport::Analytics::NHL::Report - Generic class for an NHL report
24              
25             =head1 SYNOPSYS
26              
27             Generic class for an NHL report
28              
29             Contains methods common for most (usually HTML) or all NHL reports.
30              
31             use Sport::Analytics::NHL::Report;
32             my $report = Sport::Analytics::NHL::Report->new($args);
33             $report->process();
34              
35             =head1 METHODS
36              
37             =over 2
38              
39             =item C
40              
41             Common constructor wrapper. Assigns the report plugin, and initializes the report object. For a json report usually an overloaded constructor is required. For an HTML report, the generic html_new (q.v.) method is usually sufficient.
42             Arguments: the arguments hashref
43             * file: the file with the report OR
44             * data: the scalar with the report
45             BUT NOT BOTH
46             * type: explicitly specify the type of the report
47             Returns: the blessed object of one of the Report's Plugins.
48              
49             The object represents an NHL game.
50              
51             =item C
52              
53             Specific constructor for the HTML reports. Parses the HTML using HTML::TreeBuilder, immediately storing the tree as another storable (.tree) for re-use. The tree resides in $obj->{html}. The raw HTML is stored in $obj->{source}. The type of the report is set in $obj->{type}.
54             Arguments: see new() (q.v.)
55             Returns: the blessed object.
56              
57             =item C
58              
59             Converts the NHL HTML header date strings of start and end of the game into $obj->{start_ts} and $obj->{end_ts} timestamps and sets the object's time zone in $obj->{tz} and the month in $obj->{month}.
60             Arguments: whether to force US date parsing or not.
61             Note: uses $self->{date} anf $self->{time} from get_header() (q.v.)
62             Returns: void. Sets object fields
63              
64             =item C
65              
66             Forces a decision setting on a goaltender in case the reports miss on it explicitly. Usually happens in tied games.
67             Arguments: the team to force the decision
68             Returns: void. Sets a team's goaltender with the decision.
69              
70             =item C
71              
72             Gets the HTML node path for the HTML report header (teams, score, location, attendance etc.)
73             Arguments: none
74             Returns: void. Sets the path in $obj->{head}
75              
76             =item C
77              
78             Gets the node in the HTML Tree as set by a path.
79             Arguments:
80             * flag 0|1 whether the node or its contents are wanted
81             * the walk path to the node as arrayref
82             * optional: the sub tree to walk (or $obj->{html})
83              
84             =item C
85              
86             A post-process function for the report that should be overloaded.
87              
88             =item C
89              
90             A processing function for the specific report that must be overloaded.
91              
92             =item C
93              
94             Read the boxscore: read the header, parse the rest (overloaded), normalize it (may be overloaded), delete the html tree to free the memory and delete the HTML source for the same purpose.
95             Arguments: none
96             Returns: void
97              
98             =item C
99              
100             Reads the arena information from the game header
101             Arguments: the HTML element with the arena information
102             Returns: void. Sets the arena and the attendance in the object.
103              
104             =item C
105              
106             Reads the date from the game header
107             Arguments: the HTML element with the date information
108             Returns: void. Sets the date in the object. Implies calling convert_date_time (q.v.) later.
109              
110             =item C
111              
112             Reads the NHL id from the game header
113             Arguments: the HTML element with the game id information
114             Returns: void. Sets the nhl season game id in the object.
115              
116             =item C
117              
118             Parses the header of the HTML report, dispatching the processing of the discovered information elements.
119             Arguments: none
120             Returns: void. Everything is set in the object.
121              
122             =item C
123              
124             Checks if one of the sources of the boxscore is an HTML report
125             Arguments: none
126             Returns: True|False
127              
128             =item C
129              
130             Reads the game status block from the game header
131             Arguments: the HTML element with the game status and other information
132             Returns: void. Sets the information in the object.
133              
134             =item C
135              
136             Reads the actual status of the game from the header
137             Arguments: the HTML element with the status information
138             Returns: void. Sets the status in the object.
139              
140             =item C
141              
142             Reads the team information from the game header
143             Arguments: the HTML element with the team information and the index of the team
144             Returns: void. Sets the team information in the object.
145              
146             =item C
147              
148             Reads the time from the game header
149             Arguments: the HTML element with the time information
150             Returns: void. Sets the date in the object. Implies calling convert_date_time (q.v.) later.
151              
152             =item C
153              
154             Sets the argument for the constructor. Juggles the data, file and type fields.
155             Arguments: the args hashref:
156             * the file to process OR
157             * the scalar with the data to process, BUT NOT BOTH.
158             * the explicit data type setting,
159             optional when 'file' is specified.
160             Returns: void. Updates the args hashref.
161              
162             =item C
163              
164             Sets extra data to already parsed events:
165              
166             * The file type as event source
167             * The game_id normalized
168             * Bench player in case of bench penalty
169             * Resolves teams to standard 3-letter codes
170             * Converts time to timestamp (ts)
171             * Sets field t for primary event team:
172             0 for away, 1 for home, -1 - noplay event
173              
174             Arguments: none
175             Returns: void. Updates the events in the object.
176              
177             =back
178              
179             =cut
180              
181 52     52   375 use Data::Dumper;
  52         118  
  52         185906  
182              
183             our %REPORT_TYPES = (
184             BS => 'json',
185             PB => 'json',
186             Player => 'json',
187             PL => 'html',
188             RO => 'html',
189             GS => 'html',
190             BH => 'html',
191             ES => 'html',
192             );
193              
194             our @HEADER_STATUS_METHODS = (
195             undef,
196             undef,
197             undef,
198             undef,
199             qw(
200             read_date_info
201             read_arena_info
202             read_time_info
203             read_game_info
204             read_status_info
205             ),
206             );
207             our @HEADER_STATUS_METHODS_OLD = (
208             undef,
209             undef,
210             undef,
211             undef,
212             'read_game_info',
213             undef,
214             'read_date_info',
215             undef,
216             'read_arena_info',
217             undef,
218             'read_time_info',
219             undef,
220             'read_status_info',
221             );
222              
223             our $tb;
224              
225             sub set_args ($) {
226              
227 41     41 1 100 my $args = shift;
228              
229 41 100 100     278 if (! $args->{data} && ! $args->{file}) {
230 2         58 print STDERR "Need to specify either file or data, choose one!\n";
231 2         21 return undef;
232             }
233 39 100 100     173 if ($args->{data} && $args->{file}) {
234 1         12 print STDERR "Cannot specify both data and file, choose one!\n";
235 1         8 return undef;
236             }
237             my $type = $args->{type} || (
238 38   100     468 $args->{file} ? ($args->{file} =~ m|/([A-Z]{2}).[a-z]{4}$| ? $1 : '') : ''
239             );
240 38 100       145 if (! $type) {
241 1         11 print STDERR "Unable to determine the type of the report, please specify explicitly\n";
242 1         7 return undef;
243             }
244 37         118 $args->{type} = $type;
245 37 100       269 $args->{data} = read_file($args->{file}) if ($args->{file});
246 37         149 1;
247             }
248              
249             sub new ($$) {
250              
251 41     41 1 10066 my $class = shift;
252 41   100     184 my $args = shift || {};
253              
254 41 100       161 set_args($args) || return undef;
255 37         97 my $self = {};
256 37         193 bless $self, $class;
257 37 100       325 $class .= "::$args->{type}" unless $class =~ /\:\:[A-Z]{2}$/;
258 37     141   444 my $plugin = firstval {$class eq $_} $self->plugins();
  141         259502  
259 37 100       268 if (! $plugin) {
260 1         28 print STDERR "Unknown report type $args->{type}\n";
261 1         18 return undef;
262             }
263             $self = $REPORT_TYPES{$args->{type}} eq 'json'
264             ? $plugin->new($args->{data})
265 36 100       635 : $plugin->html_new($args);
266 36         289 $self->{type} = $args->{type};
267 36         6574 $self;
268             }
269              
270             sub html_new ($$) {
271              
272 28     28 1 88 my $class = shift;
273 28         69 my $args = shift;
274              
275 28         295 $tb = HTML::TreeBuilder->new;
276 28         9405 my $self = {};
277 28 50       146 if ($args->{file}) {
278 28         79 my $tree = $args->{file};
279 28         161 $tree =~ s/html/tree/;
280 28 100 66     1244 if (-f $tree && (stat($tree))[9] > (stat($args->{file}))[9]-2) {
281 23         165 debug "Using tree file";
282 23         141 $tb = retrieve $tree;
283 23         606676 $self->{html} = $tb->{_body};
284             }
285             }
286 28 100       182 if (! $self->{html}) {
287 5         44 $tb->ignore_unknown(0);
288 5         100 $tb->implicit_tags(1);
289             # unidecode($args->{data);
290 5         1378 $args->{data} =~ tr/ / /;
291 5 50 66     220 if ($args->{type} eq 'ES' &&
      33        
292             $args->{data} =~ /width\=100\%/i &&
293             $args->{data} !~ /width\=100\%\>/i
294             ) {
295 0         0 $args->{data} =~ s/width\=100\%/width\=100\%\>/ig
296             }
297 5         105 $tb->parse($args->{data});
298 5 50       15370205 if ($args->{file}) {
299 5         27 my $tree = $self->{file} = $args->{file};
300 5         31 $tree =~ s/html/tree/;
301 5         38 verbose "Storing tree file $tree";
302 5         32 store $tb, $tree;
303             }
304 5         120262 $self->{html} = $tb->{_body};
305             }
306 28         111 $self->{source} = $args->{data};
307 28         86 $self->{type} = $args->{type};
308 28         206 bless $self, $class;
309 28         149 $self;
310             }
311              
312             sub has_html ($$) {
313              
314 6     6 1 12 my $self = shift;
315              
316 6   33     63 return $self->{GS} || $self->{ES} || $self->{RO} || $self->{PL};
317             }
318              
319             sub read_status ($$$) {
320              
321 16     16 1 37 my $self = shift;
322 16         33 my $cell = shift;
323              
324 16         40 my $r = 0;
325 16 50       55 $cell = $self->get_sub_tree(0, [0,0], $cell) if $self->{old};
326 16         40 my $offset = 0;
327 16         33 my $no_att = 0;
328 16         87 while (my $row = $self->get_sub_tree(0, [$r], $cell)) {
329 144 50       333 my $content = $self->{old} ? $row : $self->get_sub_tree(0, [0,0], $row);
330 144         194 $r++;
331 144 100 66     382 next unless $content and ! ref($content);
332 112 0 33     232 if ($self->{old} && $r == 4 && $content =~ /\,/) {
      33        
333 0         0 $offset = 1 + $self->{old};
334             }
335 112 50       255 my $method = $self->{old} ? $HEADER_STATUS_METHODS_OLD[$r+$offset+$no_att] : $HEADER_STATUS_METHODS[$r+$offset];
336 112 50 33     624 if ($content && $content =~ /\s*(attendance|attd)\s+(\d+\S+\d+)\s*$/i) {
337 0         0 $self->{attendance} = $2;
338 0         0 $self->{attendance} =~ s/\D//g;
339 0         0 next;
340             }
341 112 50 33     243 if ($r == 11 && ! $self->{attendance}) {
342 0         0 $method = 'read_status_info';
343             }
344 112 50 33     308 if ($content && $content =~ /^\s*(\d+\:\d+)\s+(\S\S)\s+(\S\S)\s+at\s+(.*)/) {
345 0         0 $self->{time} = "$1 $3";
346 0         0 $self->{tz} = $3;
347 0         0 $self->{location} = $4;
348 0         0 next;
349             }
350 112 100       254 next unless $method;
351 80         403 $self->$method($content);
352             }
353 16         114 $self->convert_time_date();
354 16   50     70 $self->{status} ||= 'Preview';
355             $self->{status} = 'Final' if
356             $self->{status} eq 'End of Game'
357             || $self->{status} eq 'End of Period 4'
358 16 50 33     159 || $self->{status} eq 'Period 4 (0:00 Remaining)';
      33        
359             }
360              
361             sub read_date_info ($$$) {
362              
363 16     16 1 35 my $self = shift;
364 16         31 my $date = shift;
365              
366 16         107 ($date) = ($date =~ /\S+,.*?(\S.*)$/);
367 16         48 $date =~ s/Sept\./Sep/g;
368 16         39 $date =~ s/Fev\.\S*/Feb/g;
369 16         38 $date =~ s/Avr\.\S*/Apr/g;
370 16         32 $date =~ s/Mai\S*/May/g;
371 16         73 $self->{date} = $date;
372             }
373              
374             sub read_time_info ($$$) {
375              
376 16     16 1 38 my $self = shift;
377 16         33 my $time = shift;
378              
379 16         59 $self->{time} = $time;
380             }
381              
382             sub read_arena_info ($$$) {
383              
384 16     16 1 33 my $self = shift;
385 16         35 my $arena_info = shift;
386              
387 16         35 my $stadium;
388             my $attendance;
389              
390 16         56 $arena_info =~ tr/\xA0/ /;
391 16 50       174 if ($arena_info !~ /att/i) {
    50          
392 0         0 $stadium = $arena_info;
393 0 0       0 if ($arena_info =~ /(\d+\:\d+ \w\w \w\w) (at|\@) (.*)/) {
394 0         0 $self->{time} = $1;
395 0         0 $stadium = $3;
396             }
397 0         0 $attendance = 0;
398             }
399             elsif ($arena_info =~ /attendance.*?(\d+)\,(\d+)\s*$/i) {
400 0         0 $stadium = 'Unknown';
401 0         0 $attendance = $1*1000+$2;
402             }
403             else {
404 16         35 my $sep;
405 16         102 ($attendance, $sep, $stadium) = ($arena_info =~ /(\S+\d).*?(at\b|\@).*?(\w.*)/);
406 16 50       44 unless ($attendance) {
407 0         0 $attendance = 0;
408 0 0       0 if ($arena_info =~ /(at|\@).*?(\w.*)/) {
409 0         0 $stadium = $2,
410             }
411             }
412             else {
413 16         70 $attendance =~ s/\D//g;
414             }
415             }
416 16         49 $self->{attendance} = $attendance;
417 16         61 $stadium =~ s/^\s+//;
418 16         48 $stadium =~ s/\s+$//;
419 16         63 $stadium =~ s/\s+/ /g;
420 16         81 $self->{location} = $stadium;
421             }
422              
423             sub read_game_info ($$$) {
424              
425 16     16 1 34 my $self = shift;
426 16         27 my $game_info = shift;
427              
428 16         88 $game_info =~ /(Game|NHL)\D*(\d{4})/;
429 16         77 $self->{season_id} = $2;
430 16         50 return;
431             }
432              
433             sub read_status_info ($$$) {
434              
435 16     16 1 39 my $self = shift;
436 16         29 my $status_info = shift;
437              
438 16         53 $status_info =~ s/^\s+//;
439 16         41 $status_info =~ s/\s+$//;
440 16         48 $self->{status} = $status_info;
441 16 50       68 if ($status_info =~ / (\d+) \- (\S.*)/) {
442 0         0 $self->{season_id} = $1;
443 0         0 $self->{status} = $2;
444             }
445             else {
446 16         64 $self->{status} = $status_info;
447             }
448             }
449              
450             sub read_team ($$$$) {
451              
452 32     32 1 59 my $self = shift;
453 32         49 my $cell = shift;
454 32         46 my $idx = shift;
455              
456             my $name = $self->{old} ?
457 32 50       115 $self->get_sub_tree(0, [0,0,6], $cell) :
458             $self->get_sub_tree(0, [2,0,0], $cell);
459 32 0 33     87 if (ref $name && $self->{old}) {
460 0         0 $name = $self->get_sub_tree(0, [0,0,5], $cell);
461             }
462             my $score = $self->{old} ?
463             $self->get_sub_tree(0, [
464 0         0 2 - (scalar(@{$self->{head}})-1)*(1-$idx)
465 0         0 + $idx*($self->{gs}-5)-(scalar(@{$self->{head}})-1)*2*$idx,
466             ,0,0
467 32 50       129 ], $cell->{_parent}) : $self->get_sub_tree(0, [1,0,0,0,1,0], $cell);
468 32 50       202 $score = $self->get_sub_tree(0, [2+5*$idx+($self->{gs}>=12)*(1-$idx),0,0], $cell->{_parent}) if $score !~ /^\d{1,2}\s*$/;
469 32 50 33     190 $score = $self->get_sub_tree(0, [9,0,0], $cell->{_parent}) if !defined $score || $score !~ /^\d{1,2}\s*$/;
470 32 50 33     167 if (!defined $score || $score !~ /^\s*\d{1,2}\s*$/) {
471 0         0 die "Unreadable header";
472             }
473 32 50       74 if ($name) {
474 32         84 $name =~ s/^\s+//g;
475 32         86 $name =~ s/\s+$//g;
476 32         132 $name =~ s/\s+/ /g;
477 32 50       87 $name = 'MONTREAL CANADIENS' if $name eq 'CANADIENS MONTREAL';
478 32         110 $self->{teams}[$idx]{name} = $name;
479             }
480 32         67 $score =~ s/\D//g;
481 32         98 $self->{teams}[$idx]{score} = $score;
482             }
483              
484             sub get_header ($) {
485              
486 16     16 1 33 my $self = shift;
487              
488 16         29 my $i = 0;
489 16         53 $self->{head} = [];
490 16         48 $self->{teams} = [];
491 16         141 while(my $base_element = $self->get_sub_tree(0, [$i])) {
492 48 50       88 last unless ref $base_element;
493 48 100       157 if ($base_element->tag eq 'table') {
494 16         108 push(@{$self->{head}}, $i);
  16         48  
495 16 50       59 push(@{$self->{head}}, 0) if $base_element->{_content}[0]->tag eq 'tbody';
  0         0  
496 16         118 last;
497             }
498 32         304 $i++;
499             }
500             }
501              
502             sub read_header ($) {
503              
504 16     16 1 37 my $self = shift;
505              
506 16         110 $self->get_header();
507              
508 16         45 my $main_table = $self->get_sub_tree(0, [@{$self->{head}}]);
  16         57  
509 16         33 my $gameinfo_table;
510 16         31 my $offset = 0;
511 16 50       74 if ($main_table->attr('class')) {
512 16         266 my $content_table = $self->get_sub_tree(0, [0,0,0],$main_table);
513 16         68 $gameinfo_table = $self->get_sub_tree(0, [0,0,0], $content_table);
514 16         50 $self->{content_table} = $content_table;
515 16         39 $self->{old} = 0;
516 16         35 $offset = 0;
517             }
518             else {
519 0         0 $gameinfo_table = $main_table;
520 0         0 $self->{old} = 1;
521 0         0 $offset = 2;
522             }
523 16         54 my $gameinfo_row = $self->get_sub_tree(0, [0], $gameinfo_table);
524 16         30 my $gameinfo_size = @{$gameinfo_row->{_content}};
  16         43  
525 16         50 $self->{gs} = $gameinfo_size;
526 16         56 for my $i (0..2) {
527 48         75 my $cell;
528 48 50 33     128 if ($self->{old} && @{$self->{head}} == 2) {
  0         0  
529 0         0 $cell = $self->get_sub_tree(0, [ $i*$self->{old}*5 + $self->{old}*(2-$i) - 1 ], $gameinfo_row);
530             }
531             else {
532 48         159 $offset = $i + $i*$self->{old}*$gameinfo_size/2 + $self->{old}*(1-2*$i);
533 48 50       102 $offset += 1-$i if $gameinfo_size == 12;
534 48 50       97 $offset += 1-$i if $gameinfo_size == 14;
535 48 50       156 $cell = $self->get_sub_tree(0, [ $self->{old} ? $offset : ($offset, 0), ], $gameinfo_row);
536             }
537 48 100       320 ($i % 2) ? $self->read_status($cell) : $self->read_team($cell, $i / 2);
538             }
539 16 50 33     74 if ($self->{status} =~ /end.*period (3|4)/i
540             && $self->{teams}[0]{score} != $self->{teams}[1]{score}) {
541 0         0 $self->{status} = 'final';
542             }
543 16 50       56 $self->{season}-- if ($self->{month} < 9);
544 16 50 33     209 if (
      33        
      33        
      33        
      33        
545             ($self->{season} != 2012 && $self->{month} > 3 &&
546             $self->{month} < 8 && $self->{season_id} <= $LAST_PLAYOFF_GAME_INDEX) ||
547             ($self->{season} == 2012 && $self->{start_ts} >= $LATE_START_IN_2012)) {
548 0         0 $self->{stage} = $PLAYOFF;
549             }
550             else {
551 16         50 $self->{stage} = $REGULAR;
552             }
553             $self->{teams}[0]{name} = 'MONTREAL CANADIENS'
554 16 50       53 if $self->{teams}[0]{name} eq 'CANADIENS MONTREAL';
555             $self->{teams}[1]{name} = 'MONTREAL CANADIENS'
556 16 50       44 if $self->{teams}[1]{name} eq 'CANADIENS MONTREAL';
557 16         65 $self->{_id} = $self->{season} * 100000 + $self->{stage} * 10000 + $self->{season_id};
558 16   50     108 $self->{periods} ||= [{},{},{}];
559 16         37 delete $self->{gs};
560 16         155 $self->fill_broken($BROKEN_HEADERS{$self->{_id}});
561 16   50     58 $self->{attendance} ||= 0;
562 16         80 ref ($self) =~ /\:\:(\w\w)$/;
563 16         55 $self->{type} = $1;
564 16         51 $self->{status} = uc $self->{status};
565             }
566              
567             sub convert_time_date ($;$) {
568              
569 18     18 1 32 my $self = shift;
570 18   100     99 my $force_us = shift || 0;
571            
572 18         45 my $date = $self->{date};
573 18         42 my $time = $self->{time};
574 18 50 66     228 my ($year, $month, $day) = $date =~ /^\d/ && ! $force_us
575             ? Decode_Date_EU($date)
576             : Decode_Date_US($date);
577              
578 18   66     118 $self->{season} ||= $year;
579 18         64 $self->{month} = $month;
580 18         41 $year -= 1900;
581 18         40 $month--;
582 18         136 my ($start_h, $start_m, $start_tz, $end_h, $end_m, $end_tz) =
583             ($time =~ /(\d+):(\d+)\W*(\w{1,2}T)\s*\;\D*(\d+):(\d+)\W*(\w{1,2}T)/);
584 18 100       60 unless ($end_h) {
585 2         6 ($start_h, $start_m, $start_tz) = ($time =~ /(\d+):(\d+)\W*(\w{1,2}T)\W*/);
586 2 50       8 unless ($start_h) {
587 2         4 $start_h = 12;
588 2         4 $start_m = 0;
589 2         5 $start_tz = 'EDT';
590             }
591 2         4 $end_h = $start_h + 3;
592 2         4 $end_m = $start_m;
593 2         5 $end_tz = $start_tz;
594             }
595 18 100       63 $start_h += 12 if $start_h < 12;
596 18 100       52 $end_h += 12 if $end_h < $start_h;
597 18         123 $self->{start_ts} = timelocal(0, $start_m, $start_h, $day, $month, $year);
598 18 50       1792 if ($end_h > 23) {
599 0         0 $self->{end_ts} = $self->{start_ts} + 10800;
600             }
601             else {
602 18         170 $self->{end_ts} = timegm(0, $end_m, $end_h, $day, $month, $year);
603             }
604 18   33     455 $self->{tz} ||= $start_tz;
605             }
606              
607 0     0 1 0 sub parse ($) { die "Overload me" }
608       4 1   sub normalize ($) { }
609              
610             sub force_decision ($$) {
611              
612 0     0 1 0 my $self = shift;
613 0         0 my $team = shift;
614              
615             my @goalies = sort {
616             get_seconds($b->{timeOnIce}) <=> get_seconds($a->{timeOnIce})
617 0         0 } grep { $_->{position} eq 'G' } @{$team->{roster}};
  0         0  
  0         0  
  0         0  
618 0         0 my $goalie = $goalies[0];
619 0 0       0 if ($self->{_score}[0] == $self->{_score}[1]) {
    0          
620 0         0 $goalie->{decision} = 'T';
621             }
622             elsif ($self->{_score}[$self->{_t}] > $self->{_score}[1 - $self->{_t}]) {
623 0         0 $goalie->{decision} = 'W';
624             }
625             else {
626 0 0 0     0 $goalie->{decision} = $self->{ot} || $self->{so} ? 'O' : 'L';
627             }
628              
629             }
630              
631             sub get_sub_tree ($$$;$) {
632              
633 52982     52982 1 62825 my $self = shift;
634 52982         54575 my $want_content = shift;
635 52982         53948 my $walk = shift;
636 52982   33     78711 my $tree = shift || $self->{html} || $self;
637              
638 52982 50       76762 print "Walking ",join(".", @{$walk}), "\n" if $ENV{SHOW_WALK};
  0         0  
639 52982         59555 my $tpointer = \$tree;
640 52982         56797 for my $node (@{$walk}) {
  52982         67423  
641 202676 100 100     437856 return undef unless $$tpointer && ref $$tpointer;
642 202158         203245 my $tc = ${$tpointer}->{_content}[$node];
  202158         301548  
643 202158         261733 $tpointer = \$tc;
644             }
645 52464         63651 my $tcopy = $$tpointer;
646 52464 50       117918 return $want_content ? $tcopy->{_content} : $tcopy;
647             }
648              
649             sub process ($) {
650              
651 18     18 1 3753 my $self = shift;
652              
653 18 100       163 $self->read_header() unless $self->{type} eq 'BH';
654 18         96 $self->parse();
655 18         126 $self->normalize();
656 18         159 $self->{html}->delete();
657 18         1649242 delete $self->{source};
658             }
659              
660             sub set_event_extra_data ($) {
661              
662 18     18 1 58414 my $self = shift;
663 18         39 for my $event (@{$self->{events}}) {
  18         57  
664 4186         11781 $event->{sources} = {$self->{type} => 1};
665 4186 50       7528 $event->{game_id} = delete $event->{game} if $event->{game};
666 4186 100 33     7248 $event->{player1} ||= $BENCH_PLAYER_ID if ($event->{penalty});
667 4186         4699 my $t = -1;
668 4186 100       7108 if ($event->{team1}) {
669             $event->{team1} = resolve_team($event->{team1}) if
670 3476 50       8929 $event->{team1} ne 'OTH';
671             $t = $event->{team1} eq $self->{teams}[0]{name}
672             ? 0
673             : $event->{team1} eq $self->{teams}[1]{name}
674 3476 100       11919 ? 1
    100          
675             : -1;
676             }
677 4186 100 66     10111 $event->{team2} = resolve_team($event->{team2}) if $event->{team2} && $event->{team2} ne 'OTH';
678 4186         7573 $event->{t} = $t;
679             $event->{ts} =
680             $event->{special} ? 0 :
681             $event->{stage} == $PLAYOFF || $event->{stage} == $REGULAR && $event->{period} < 5 ?
682 4186 50 33     23317 ($event->{period}-1) * 1200 + get_seconds($event->{time}) : 3900;
    50          
683             }
684 18 50       62 $self->{no_events} unless @{$self->{events}};
  18         123  
685             }
686              
687             END {
688 52 100   52   175845 $tb->delete if defined $tb;
689             }
690              
691             1;
692              
693             =head1 AUTHOR
694              
695             More Hockey Stats, C<< >>
696              
697             =head1 BUGS
698              
699             Please report any bugs or feature requests to C, or through
700             the web interface at L. I will be notified, and then you'll
701             automatically be notified of progress on your bug as I make changes.
702              
703              
704             =head1 SUPPORT
705              
706             You can find documentation for this module with the perldoc command.
707              
708             perldoc Sport::Analytics::NHL::Report
709              
710              
711             You can also look for information at:
712              
713             =over 4
714              
715             =item * RT: CPAN's request tracker (report bugs here)
716              
717             L
718              
719             =item * AnnoCPAN: Annotated CPAN documentation
720              
721             L
722              
723             =item * CPAN Ratings
724              
725             L
726              
727             =item * Search CPAN
728              
729             L
730              
731             =back
732