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 41     41   190030 use v5.10.1;
  41         170  
4 41     41   193 use strict;
  41         69  
  41         961  
5 41     41   210 use warnings FATAL => 'all';
  41         90  
  41         1374  
6 41     41   247 use experimental qw(smartmatch);
  41         104  
  41         278  
7              
8 41     41   9326 use Storable;
  41         37958  
  41         2066  
9              
10 41     41   16379 use Date::Calc qw(Decode_Date_US Decode_Date_EU);
  41         226210  
  41         2916  
11 41     41   27284 use HTML::TreeBuilder;
  41         1118645  
  41         365  
12 41     41   9548 use List::MoreUtils qw(firstval);
  41         150535  
  41         357  
13 41     41   47534 use Module::Pluggable require => 1, search_path => ['Sport::Analytics::NHL::Report'];
  41         369306  
  41         316  
14 41     41   9428 use Time::Local;
  41         20926  
  41         2044  
15              
16 41     41   6144 use Sport::Analytics::NHL::Util;
  41         114  
  41         2630  
17 41     41   7106 use Sport::Analytics::NHL::Tools;
  41         103  
  41         7461  
18 41     41   287 use Sport::Analytics::NHL::Config;
  41         91  
  41         6785  
19 41     41   10875 use Sport::Analytics::NHL::Errors;
  41         526  
  41         8478  
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 41     41   277 use Data::Dumper;
  41         94  
  41         148225  
182              
183             our %REPORT_TYPES = (
184             BS => 'json',
185             PB => 'json',
186             PL => 'html',
187             RO => 'html',
188             GS => 'html',
189             BH => 'html',
190             ES => 'html',
191             );
192              
193             our @HEADER_STATUS_METHODS = (
194             undef,
195             undef,
196             undef,
197             undef,
198             qw(
199             read_date_info
200             read_arena_info
201             read_time_info
202             read_game_info
203             read_status_info
204             ),
205             );
206             our @HEADER_STATUS_METHODS_OLD = (
207             undef,
208             undef,
209             undef,
210             undef,
211             'read_game_info',
212             undef,
213             'read_date_info',
214             undef,
215             'read_arena_info',
216             undef,
217             'read_time_info',
218             undef,
219             'read_status_info',
220             );
221              
222             our $tb;
223              
224             sub set_args ($) {
225              
226 30     30 1 63 my $args = shift;
227              
228 30 100 100     222 if (! $args->{data} && ! $args->{file}) {
229 2         228 print STDERR "Need to specify either file or data, choose one!\n";
230 2         20 return undef;
231             }
232 28 100 100     148 if ($args->{data} && $args->{file}) {
233 1         62 print STDERR "Cannot specify both data and file, choose one!\n";
234 1         8 return undef;
235             }
236             my $type = $args->{type} || (
237 27   100     334 $args->{file} ? ($args->{file} =~ m|/([A-Z]{2}).[a-z]{4}$| ? $1 : '') : ''
238             );
239 27 100       103 if (! $type) {
240 1         64 print STDERR "Unable to determine the type of the report, please specify explicitly\n";
241 1         8 return undef;
242             }
243 26         78 $args->{type} = $type;
244 26 100       177 $args->{data} = read_file($args->{file}) if ($args->{file});
245 26         109 1;
246             }
247              
248             sub new ($$) {
249              
250 30     30 1 7978 my $class = shift;
251 30   100     139 my $args = shift || {};
252              
253 30 100       117 set_args($args) || return undef;
254 26         72 my $self = {};
255 26         128 bless $self, $class;
256 26 100       251 $class .= "::$args->{type}" unless $class =~ /\:\:[A-Z]{2}$/;
257 26     92   350 my $plugin = firstval {$class eq $_} $self->plugins();
  92         145357  
258 26 100       194 if (! $plugin) {
259 1         104 print STDERR "Unknown report type $args->{type}\n";
260 1         13 return undef;
261             }
262             $self = $REPORT_TYPES{$args->{type}} eq 'json'
263             ? $plugin->new($args->{data})
264 25 100       419 : $plugin->html_new($args);
265 25         219 $self->{type} = $args->{type};
266 25         27742 $self;
267             }
268              
269             sub html_new ($$) {
270              
271 20     20 1 52 my $class = shift;
272 20         45 my $args = shift;
273              
274 20         210 $tb = HTML::TreeBuilder->new;
275 20         6347 my $self = {};
276 20 50       108 if ($args->{file}) {
277 20         58 my $tree = $args->{file};
278 20         108 $tree =~ s/html/tree/;
279 20 100 66     939 if (-f $tree && (stat($tree))[9] > (stat($args->{file}))[9]-2) {
280 15         123 debug "Using tree file";
281 15         126 $tb = retrieve $tree;
282 15         424210 $self->{html} = $tb->{_body};
283             }
284             }
285 20 100       129 if (! $self->{html}) {
286 5         33 $tb->ignore_unknown(0);
287 5         82 $tb->implicit_tags(1);
288             # unidecode($args->{data);
289 5         1143 $args->{data} =~ tr/ / /;
290 5 50 66     214 if ($args->{type} eq 'ES' &&
      33        
291             $args->{data} =~ /width\=100\%/i &&
292             $args->{data} !~ /width\=100\%\>/i
293             ) {
294 0         0 $args->{data} =~ s/width\=100\%/width\=100\%\>/ig
295             }
296 5         85 $tb->parse($args->{data});
297 5 50       12194029 if ($args->{file}) {
298 5         32 my $tree = $self->{file} = $args->{file};
299 5         30 $tree =~ s/html/tree/;
300 5         64 verbose "Storing tree file $tree";
301 5         32 store $tb, $tree;
302             }
303 5         114540 $self->{html} = $tb->{_body};
304             }
305 20         88 $self->{source} = $args->{data};
306 20         72 $self->{type} = $args->{type};
307 20         148 bless $self, $class;
308 20         134 $self;
309             }
310              
311             sub has_html ($$) {
312              
313 6     6 1 15 my $self = shift;
314              
315 6   33     84 return $self->{GS} || $self->{ES} || $self->{RO} || $self->{PL};
316             }
317              
318             sub read_status ($$$) {
319              
320 8     8 1 21 my $self = shift;
321 8         35 my $cell = shift;
322              
323 8         20 my $r = 0;
324 8 50       26 $cell = $self->get_sub_tree(0, [0,0], $cell) if $self->{old};
325 8         23 my $offset = 0;
326 8         19 my $no_att = 0;
327 8         28 while (my $row = $self->get_sub_tree(0, [$r], $cell)) {
328 72 50       220 my $content = $self->{old} ? $row : $self->get_sub_tree(0, [0,0], $row);
329 72         107 $r++;
330 72 100 66     216 next unless $content and ! ref($content);
331 56 0 33     134 if ($self->{old} && $r == 4 && $content =~ /\,/) {
      33        
332 0         0 $offset = 1 + $self->{old};
333             }
334 56 50       153 my $method = $self->{old} ? $HEADER_STATUS_METHODS_OLD[$r+$offset+$no_att] : $HEADER_STATUS_METHODS[$r+$offset];
335 56 50 33     400 if ($content && $content =~ /\s*(attendance|attd)\s+(\d+\S+\d+)\s*$/i) {
336 0         0 $self->{attendance} = $2;
337 0         0 $self->{attendance} =~ s/\D//g;
338 0         0 next;
339             }
340 56 50 33     124 if ($r == 11 && ! $self->{attendance}) {
341 0         0 $method = 'read_status_info';
342             }
343 56 50 33     172 if ($content && $content =~ /^\s*(\d+\:\d+)\s+(\S\S)\s+(\S\S)\s+at\s+(.*)/) {
344 0         0 $self->{time} = "$1 $3";
345 0         0 $self->{tz} = $3;
346 0         0 $self->{location} = $4;
347 0         0 next;
348             }
349 56 100       155 next unless $method;
350 40         272 $self->$method($content);
351             }
352 8         85 $self->convert_time_date();
353 8   50     37 $self->{status} ||= 'Preview';
354             $self->{status} = 'Final' if
355             $self->{status} eq 'End of Game'
356             || $self->{status} eq 'End of Period 4'
357 8 50 33     84 || $self->{status} eq 'Period 4 (0:00 Remaining)';
      33        
358             }
359              
360             sub read_date_info ($$$) {
361              
362 8     8 1 37 my $self = shift;
363 8         23 my $date = shift;
364              
365 8         62 ($date) = ($date =~ /\S+,.*?(\S.*)$/);
366 8         32 $date =~ s/Sept\./Sep/g;
367 8         19 $date =~ s/Fev\.\S*/Feb/g;
368 8         30 $date =~ s/Avr\.\S*/Apr/g;
369 8         18 $date =~ s/Mai\S*/May/g;
370 8         40 $self->{date} = $date;
371             }
372              
373             sub read_time_info ($$$) {
374              
375 8     8 1 21 my $self = shift;
376 8         22 my $time = shift;
377              
378 8         44 $self->{time} = $time;
379             }
380              
381             sub read_arena_info ($$$) {
382              
383 8     8 1 29 my $self = shift;
384 8         22 my $arena_info = shift;
385              
386 8         23 my $stadium;
387             my $attendance;
388              
389 8         35 $arena_info =~ tr/\xA0/ /;
390 8 50       100 if ($arena_info !~ /att/i) {
    50          
391 0         0 $stadium = $arena_info;
392 0 0       0 if ($arena_info =~ /(\d+\:\d+ \w\w \w\w) (at|\@) (.*)/) {
393 0         0 $self->{time} = $1;
394 0         0 $stadium = $3;
395             }
396 0         0 $attendance = 0;
397             }
398             elsif ($arena_info =~ /attendance.*?(\d+)\,(\d+)\s*$/i) {
399 0         0 $stadium = 'Unknown';
400 0         0 $attendance = $1*1000+$2;
401             }
402             else {
403 8         29 my $sep;
404 8         64 ($attendance, $sep, $stadium) = ($arena_info =~ /(\S+\d).*?(at\b|\@).*?(\w.*)/);
405 8 50       29 unless ($attendance) {
406 0         0 $attendance = 0;
407 0 0       0 if ($arena_info =~ /(at|\@).*?(\w.*)/) {
408 0         0 $stadium = $2,
409             }
410             }
411             else {
412 8         39 $attendance =~ s/\D//g;
413             }
414             }
415 8         31 $self->{attendance} = $attendance;
416 8         26 $stadium =~ s/^\s+//;
417 8         26 $stadium =~ s/\s+$//;
418 8         41 $stadium =~ s/\s+/ /g;
419 8         40 $self->{location} = $stadium;
420             }
421              
422             sub read_game_info ($$$) {
423              
424 8     8 1 54 my $self = shift;
425 8         24 my $game_info = shift;
426              
427 8         50 $game_info =~ /(Game|NHL)\D*(\d{4})/;
428 8         34 $self->{season_id} = $2;
429 8         27 return;
430             }
431              
432             sub read_status_info ($$$) {
433              
434 8     8 1 22 my $self = shift;
435 8         16 my $status_info = shift;
436              
437 8         24 $status_info =~ s/^\s+//;
438 8         30 $status_info =~ s/\s+$//;
439 8         22 $self->{status} = $status_info;
440 8 50       31 if ($status_info =~ / (\d+) \- (\S.*)/) {
441 0         0 $self->{season_id} = $1;
442 0         0 $self->{status} = $2;
443             }
444             else {
445 8         34 $self->{status} = $status_info;
446             }
447             }
448              
449             sub read_team ($$$$) {
450              
451 16     16 1 28 my $self = shift;
452 16         26 my $cell = shift;
453 16         20 my $idx = shift;
454              
455             my $name = $self->{old} ?
456 16 50       83 $self->get_sub_tree(0, [0,0,6], $cell) :
457             $self->get_sub_tree(0, [2,0,0], $cell);
458 16 0 33     56 if (ref $name && $self->{old}) {
459 0         0 $name = $self->get_sub_tree(0, [0,0,5], $cell);
460             }
461             my $score = $self->{old} ?
462             $self->get_sub_tree(0, [
463 0         0 2 - (scalar(@{$self->{head}})-1)*(1-$idx)
464 0         0 + $idx*($self->{gs}-5)-(scalar(@{$self->{head}})-1)*2*$idx,
465             ,0,0
466 16 50       71 ], $cell->{_parent}) : $self->get_sub_tree(0, [1,0,0,0,1,0], $cell);
467 16 50       129 $score = $self->get_sub_tree(0, [2+5*$idx+($self->{gs}>=12)*(1-$idx),0,0], $cell->{_parent}) if $score !~ /^\d{1,2}\s*$/;
468 16 50 33     92 $score = $self->get_sub_tree(0, [9,0,0], $cell->{_parent}) if !defined $score || $score !~ /^\d{1,2}\s*$/;
469 16 50 33     91 if (!defined $score || $score !~ /^\s*\d{1,2}\s*$/) {
470 0         0 die "Unreadable header";
471             }
472 16 50       46 if ($name) {
473 16         41 $name =~ s/^\s+//g;
474 16         53 $name =~ s/\s+$//g;
475 16         70 $name =~ s/\s+/ /g;
476 16 50       47 $name = 'MONTREAL CANADIENS' if $name eq 'CANADIENS MONTREAL';
477 16         52 $self->{teams}[$idx]{name} = $name;
478             }
479 16         43 $score =~ s/\D//g;
480 16         48 $self->{teams}[$idx]{score} = $score;
481             }
482              
483             sub get_header ($) {
484              
485 8     8 1 17 my $self = shift;
486              
487 8         22 my $i = 0;
488 8         26 $self->{head} = [];
489 8         23 $self->{teams} = [];
490 8         174 while(my $base_element = $self->get_sub_tree(0, [$i])) {
491 24 50       55 last unless ref $base_element;
492 24 100       76 if ($base_element->tag eq 'table') {
493 8         59 push(@{$self->{head}}, $i);
  8         26  
494 8 50       29 push(@{$self->{head}}, 0) if $base_element->{_content}[0]->tag eq 'tbody';
  0         0  
495 8         92 last;
496             }
497 16         143 $i++;
498             }
499             }
500              
501             sub read_header ($) {
502              
503 8     8 1 20 my $self = shift;
504              
505 8         46 $self->get_header();
506              
507 8         20 my $main_table = $self->get_sub_tree(0, [@{$self->{head}}]);
  8         29  
508 8         20 my $gameinfo_table;
509 8         18 my $offset = 0;
510 8 50       35 if ($main_table->attr('class')) {
511 8         157 my $content_table = $self->get_sub_tree(0, [0,0,0],$main_table);
512 8         45 $gameinfo_table = $self->get_sub_tree(0, [0,0,0], $content_table);
513 8         27 $self->{content_table} = $content_table;
514 8         24 $self->{old} = 0;
515 8         18 $offset = 0;
516             }
517             else {
518 0         0 $gameinfo_table = $main_table;
519 0         0 $self->{old} = 1;
520 0         0 $offset = 2;
521             }
522 8         29 my $gameinfo_row = $self->get_sub_tree(0, [0], $gameinfo_table);
523 8         45 my $gameinfo_size = @{$gameinfo_row->{_content}};
  8         27  
524 8         27 $self->{gs} = $gameinfo_size;
525 8         34 for my $i (0..2) {
526 24         37 my $cell;
527 24 50 33     73 if ($self->{old} && @{$self->{head}} == 2) {
  0         0  
528 0         0 $cell = $self->get_sub_tree(0, [ $i*$self->{old}*5 + $self->{old}*(2-$i) - 1 ], $gameinfo_row);
529             }
530             else {
531 24         87 $offset = $i + $i*$self->{old}*$gameinfo_size/2 + $self->{old}*(1-2*$i);
532 24 50       57 $offset += 1-$i if $gameinfo_size == 12;
533 24 50       50 $offset += 1-$i if $gameinfo_size == 14;
534 24 50       80 $cell = $self->get_sub_tree(0, [ $self->{old} ? $offset : ($offset, 0), ], $gameinfo_row);
535             }
536 24 100       172 ($i % 2) ? $self->read_status($cell) : $self->read_team($cell, $i / 2);
537             }
538 8 50 33     47 if ($self->{status} =~ /end.*period (3|4)/i
539             && $self->{teams}[0]{score} != $self->{teams}[1]{score}) {
540 0         0 $self->{status} = 'final';
541             }
542 8 50       26 $self->{season}-- if ($self->{month} < 9);
543 8 50 33     140 if (
      33        
      33        
      33        
      33        
544             ($self->{season} != 2012 && $self->{month} > 3 &&
545             $self->{month} < 8 && $self->{season_id} <= $LAST_PLAYOFF_GAME_INDEX) ||
546             ($self->{season} == 2012 && $self->{start_ts} >= $LATE_START_IN_2012)) {
547 0         0 $self->{stage} = $PLAYOFF;
548             }
549             else {
550 8         33 $self->{stage} = $REGULAR;
551             }
552             $self->{teams}[0]{name} = 'MONTREAL CANADIENS'
553 8 50       33 if $self->{teams}[0]{name} eq 'CANADIENS MONTREAL';
554             $self->{teams}[1]{name} = 'MONTREAL CANADIENS'
555 8 50       28 if $self->{teams}[1]{name} eq 'CANADIENS MONTREAL';
556 8         41 $self->{_id} = $self->{season} * 100000 + $self->{stage} * 10000 + $self->{season_id};
557 8   50     65 $self->{periods} ||= [{},{},{}];
558 8         21 delete $self->{gs};
559 8         81 $self->fill_broken($BROKEN_HEADERS{$self->{_id}});
560 8   50     33 $self->{attendance} ||= 0;
561 8         50 ref ($self) =~ /\:\:(\w\w)$/;
562 8         27 $self->{type} = $1;
563 8         34 $self->{status} = uc $self->{status};
564             }
565              
566             sub convert_time_date ($;$) {
567              
568 10     10 1 25 my $self = shift;
569 10   100     51 my $force_us = shift || 0;
570            
571 10         34 my $date = $self->{date};
572 10         21 my $time = $self->{time};
573 10 50 66     163 my ($year, $month, $day) = $date =~ /^\d/ && ! $force_us
574             ? Decode_Date_EU($date)
575             : Decode_Date_US($date);
576              
577 10   66     68 $self->{season} ||= $year;
578 10         40 $self->{month} = $month;
579 10         24 $year -= 1900;
580 10         20 $month--;
581 10         73 my ($start_h, $start_m, $start_tz, $end_h, $end_m, $end_tz) =
582             ($time =~ /(\d+):(\d+)\W*(\w{1,2}T)\s*\;\D*(\d+):(\d+)\W*(\w{1,2}T)/);
583 10 100       35 unless ($end_h) {
584 2         6 ($start_h, $start_m, $start_tz) = ($time =~ /(\d+):(\d+)\W*(\w{1,2}T)\W*/);
585 2 50       6 unless ($start_h) {
586 2         2 $start_h = 12;
587 2         5 $start_m = 0;
588 2         4 $start_tz = 'EDT';
589             }
590 2         4 $end_h = $start_h + 3;
591 2         2 $end_m = $start_m;
592 2         5 $end_tz = $start_tz;
593             }
594 10 100       35 $start_h += 12 if $start_h < 12;
595 10 100       38 $end_h += 12 if $end_h < $start_h;
596 10         74 $self->{start_ts} = timelocal(0, $start_m, $start_h, $day, $month, $year);
597 10 50       1153 if ($end_h > 23) {
598 0         0 $self->{end_ts} = $self->{start_ts} + 10800;
599             }
600             else {
601 10         45 $self->{end_ts} = timegm(0, $end_m, $end_h, $day, $month, $year);
602             }
603 10   33     289 $self->{tz} ||= $start_tz;
604             }
605              
606 0     0 1 0 sub parse ($) { die "Overload me" }
607       2 1   sub normalize ($) { }
608              
609             sub force_decision ($$) {
610              
611 0     0 1 0 my $self = shift;
612 0         0 my $team = shift;
613              
614             my @goalies = sort {
615             get_seconds($b->{timeOnIce}) <=> get_seconds($a->{timeOnIce})
616 0         0 } grep { $_->{position} eq 'G' } @{$team->{roster}};
  0         0  
  0         0  
  0         0  
617 0         0 my $goalie = $goalies[0];
618 0 0       0 if ($self->{_score}[0] == $self->{_score}[1]) {
    0          
619 0         0 $goalie->{decision} = 'T';
620             }
621             elsif ($self->{_score}[$self->{_t}] > $self->{_score}[1 - $self->{_t}]) {
622 0         0 $goalie->{decision} = 'W';
623             }
624             else {
625 0 0 0     0 $goalie->{decision} = $self->{ot} || $self->{so} ? 'O' : 'L';
626             }
627              
628             }
629              
630             sub get_sub_tree ($$$;$) {
631              
632 27436     27436 1 34552 my $self = shift;
633 27436         30480 my $want_content = shift;
634 27436         30067 my $walk = shift;
635 27436   33     42213 my $tree = shift || $self->{html} || $self;
636              
637 27436 50       42034 print "Walking ",join(".", @{$walk}), "\n" if $ENV{SHOW_WALK};
  0         0  
638 27436         32834 my $tpointer = \$tree;
639 27436         31655 for my $node (@{$walk}) {
  27436         37187  
640 102416 100 100     241080 return undef unless $$tpointer && ref $$tpointer;
641 102156         112435 my $tc = ${$tpointer}->{_content}[$node];
  102156         194437  
642 102156         142275 $tpointer = \$tc;
643             }
644 27176         34885 my $tcopy = $$tpointer;
645 27176 50       65330 return $want_content ? $tcopy->{_content} : $tcopy;
646             }
647              
648             sub process ($) {
649              
650 10     10 1 3379 my $self = shift;
651              
652 10 100       95 $self->read_header() unless $self->{type} eq 'BH';
653 10         58 $self->parse();
654 10         80 $self->normalize();
655 10         145 $self->{html}->delete();
656 10         880424 delete $self->{source};
657             }
658              
659             sub set_event_extra_data ($) {
660              
661 14     14 1 66418 my $self = shift;
662 14         32 for my $event (@{$self->{events}}) {
  14         47  
663 3525         10697 $event->{sources} = {$self->{type} => 1};
664 3525 50       6484 $event->{game_id} = delete $event->{game} if $event->{game};
665 3525 100 33     6217 $event->{player1} ||= $BENCH_PLAYER_ID if ($event->{penalty});
666 3525         4193 my $t = -1;
667 3525 100       6461 if ($event->{team1}) {
668             $event->{team1} = resolve_team($event->{team1}) if
669 2924 50       7394 $event->{team1} ne 'OTH';
670             $t = $event->{team1} eq $self->{teams}[0]{name}
671             ? 0
672             : $event->{team1} eq $self->{teams}[1]{name}
673 2924 100       8729 ? 1
    100          
674             : -1;
675             }
676 3525 100 66     8874 $event->{team2} = resolve_team($event->{team2}) if $event->{team2} && $event->{team2} ne 'OTH';
677 3525         6309 $event->{t} = $t;
678             $event->{ts} =
679             $event->{special} ? 0 :
680             $event->{stage} == $PLAYOFF || $event->{stage} == $REGULAR && $event->{period} < 5 ?
681 3525 50 33     19783 ($event->{period}-1) * 1200 + get_seconds($event->{time}) : 3900;
    50          
682             }
683 14 50       58 $self->{no_events} unless @{$self->{events}};
  14         112  
684             }
685              
686             END {
687 41 100   41   101136 $tb->delete if defined $tb;
688             }
689              
690             1;
691              
692             =head1 AUTHOR
693              
694             More Hockey Stats, C<< >>
695              
696             =head1 BUGS
697              
698             Please report any bugs or feature requests to C, or through
699             the web interface at L. I will be notified, and then you'll
700             automatically be notified of progress on your bug as I make changes.
701              
702              
703             =head1 SUPPORT
704              
705             You can find documentation for this module with the perldoc command.
706              
707             perldoc Sport::Analytics::NHL::Report
708              
709              
710             You can also look for information at:
711              
712             =over 4
713              
714             =item * RT: CPAN's request tracker (report bugs here)
715              
716             L
717              
718             =item * AnnoCPAN: Annotated CPAN documentation
719              
720             L
721              
722             =item * CPAN Ratings
723              
724             L
725              
726             =item * Search CPAN
727              
728             L
729              
730             =back
731