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   157936 use v5.10.1;
  41         142  
4 41     41   173 use strict;
  41         64  
  41         878  
5 41     41   182 use warnings FATAL => 'all';
  41         97  
  41         1249  
6 41     41   192 use experimental qw(smartmatch);
  41         114  
  41         221  
7              
8 41     41   7828 use Storable;
  41         30492  
  41         1811  
9              
10 41     41   14391 use Date::Calc qw(Decode_Date_US Decode_Date_EU);
  41         198660  
  41         2604  
11 41     41   22894 use HTML::TreeBuilder;
  41         990891  
  41         370  
12 41     41   7875 use List::MoreUtils qw(firstval);
  41         128748  
  41         326  
13 41     41   42783 use Module::Pluggable require => 1, search_path => ['Sport::Analytics::NHL::Report'];
  41         325679  
  41         260  
14 41     41   8182 use Time::Local;
  41         17587  
  41         1860  
15              
16 41     41   5387 use Sport::Analytics::NHL::Util;
  41         89  
  41         2371  
17 41     41   6313 use Sport::Analytics::NHL::Tools;
  41         100  
  41         6426  
18 41     41   248 use Sport::Analytics::NHL::Config;
  41         78  
  41         6038  
19 41     41   9019 use Sport::Analytics::NHL::Errors;
  41         491  
  41         7270  
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   261 use Data::Dumper;
  41         71  
  41         131059  
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 51 my $args = shift;
227              
228 30 100 100     205 if (! $args->{data} && ! $args->{file}) {
229 2         56 print STDERR "Need to specify either file or data, choose one!\n";
230 2         14 return undef;
231             }
232 28 100 100     130 if ($args->{data} && $args->{file}) {
233 1         9 print STDERR "Cannot specify both data and file, choose one!\n";
234 1         5 return undef;
235             }
236             my $type = $args->{type} || (
237 27   100     288 $args->{file} ? ($args->{file} =~ m|/([A-Z]{2}).[a-z]{4}$| ? $1 : '') : ''
238             );
239 27 100       92 if (! $type) {
240 1         7 print STDERR "Unable to determine the type of the report, please specify explicitly\n";
241 1         6 return undef;
242             }
243 26         71 $args->{type} = $type;
244 26 100       153 $args->{data} = read_file($args->{file}) if ($args->{file});
245 26         99 1;
246             }
247              
248             sub new ($$) {
249              
250 30     30 1 7431 my $class = shift;
251 30   100     112 my $args = shift || {};
252              
253 30 100       104 set_args($args) || return undef;
254 26         62 my $self = {};
255 26         156 bless $self, $class;
256 26 100       211 $class .= "::$args->{type}" unless $class =~ /\:\:[A-Z]{2}$/;
257 26     92   281 my $plugin = firstval {$class eq $_} $self->plugins();
  92         130017  
258 26 100       158 if (! $plugin) {
259 1         33 print STDERR "Unknown report type $args->{type}\n";
260 1         12 return undef;
261             }
262             $self = $REPORT_TYPES{$args->{type}} eq 'json'
263             ? $plugin->new($args->{data})
264 25 100       326 : $plugin->html_new($args);
265 25         178 $self->{type} = $args->{type};
266 25         27394 $self;
267             }
268              
269             sub html_new ($$) {
270              
271 20     20 1 42 my $class = shift;
272 20         36 my $args = shift;
273              
274 20         221 $tb = HTML::TreeBuilder->new;
275 20         6630 my $self = {};
276 20 50       100 if ($args->{file}) {
277 20         52 my $tree = $args->{file};
278 20         92 $tree =~ s/html/tree/;
279 20 100 66     832 if (-f $tree && (stat($tree))[9] > (stat($args->{file}))[9]-2) {
280 15         90 debug "Using tree file";
281 15         83 $tb = retrieve $tree;
282 15         375628 $self->{html} = $tb->{_body};
283             }
284             }
285 20 100       109 if (! $self->{html}) {
286 5         29 $tb->ignore_unknown(0);
287 5         83 $tb->implicit_tags(1);
288             # unidecode($args->{data);
289 5         1026 $args->{data} =~ tr/ / /;
290 5 50 66     218 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       11805775 if ($args->{file}) {
298 5         31 my $tree = $self->{file} = $args->{file};
299 5         29 $tree =~ s/html/tree/;
300 5         45 verbose "Storing tree file $tree";
301 5         29 store $tb, $tree;
302             }
303 5         114434 $self->{html} = $tb->{_body};
304             }
305 20         68 $self->{source} = $args->{data};
306 20         60 $self->{type} = $args->{type};
307 20         129 bless $self, $class;
308 20         115 $self;
309             }
310              
311             sub has_html ($$) {
312              
313 6     6 1 11 my $self = shift;
314              
315 6   33     65 return $self->{GS} || $self->{ES} || $self->{RO} || $self->{PL};
316             }
317              
318             sub read_status ($$$) {
319              
320 8     8 1 18 my $self = shift;
321 8         17 my $cell = shift;
322              
323 8         16 my $r = 0;
324 8 50       25 $cell = $self->get_sub_tree(0, [0,0], $cell) if $self->{old};
325 8         14 my $offset = 0;
326 8         13 my $no_att = 0;
327 8         27 while (my $row = $self->get_sub_tree(0, [$r], $cell)) {
328 72 50       169 my $content = $self->{old} ? $row : $self->get_sub_tree(0, [0,0], $row);
329 72         98 $r++;
330 72 100 66     175 next unless $content and ! ref($content);
331 56 0 33     117 if ($self->{old} && $r == 4 && $content =~ /\,/) {
      33        
332 0         0 $offset = 1 + $self->{old};
333             }
334 56 50       123 my $method = $self->{old} ? $HEADER_STATUS_METHODS_OLD[$r+$offset+$no_att] : $HEADER_STATUS_METHODS[$r+$offset];
335 56 50 33     297 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     111 if ($r == 11 && ! $self->{attendance}) {
341 0         0 $method = 'read_status_info';
342             }
343 56 50 33     147 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       122 next unless $method;
350 40         226 $self->$method($content);
351             }
352 8         62 $self->convert_time_date();
353 8   50     27 $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     70 || $self->{status} eq 'Period 4 (0:00 Remaining)';
      33        
358             }
359              
360             sub read_date_info ($$$) {
361              
362 8     8 1 24 my $self = shift;
363 8         17 my $date = shift;
364              
365 8         63 ($date) = ($date =~ /\S+,.*?(\S.*)$/);
366 8         24 $date =~ s/Sept\./Sep/g;
367 8         17 $date =~ s/Fev\.\S*/Feb/g;
368 8         20 $date =~ s/Avr\.\S*/Apr/g;
369 8         18 $date =~ s/Mai\S*/May/g;
370 8         32 $self->{date} = $date;
371             }
372              
373             sub read_time_info ($$$) {
374              
375 8     8 1 18 my $self = shift;
376 8         14 my $time = shift;
377              
378 8         38 $self->{time} = $time;
379             }
380              
381             sub read_arena_info ($$$) {
382              
383 8     8 1 18 my $self = shift;
384 8         16 my $arena_info = shift;
385              
386 8         16 my $stadium;
387             my $attendance;
388              
389 8         28 $arena_info =~ tr/\xA0/ /;
390 8 50       97 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         17 my $sep;
404 8         54 ($attendance, $sep, $stadium) = ($arena_info =~ /(\S+\d).*?(at\b|\@).*?(\w.*)/);
405 8 50       23 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         36 $attendance =~ s/\D//g;
413             }
414             }
415 8         23 $self->{attendance} = $attendance;
416 8         20 $stadium =~ s/^\s+//;
417 8         21 $stadium =~ s/\s+$//;
418 8         26 $stadium =~ s/\s+/ /g;
419 8         33 $self->{location} = $stadium;
420             }
421              
422             sub read_game_info ($$$) {
423              
424 8     8 1 19 my $self = shift;
425 8         26 my $game_info = shift;
426              
427 8         51 $game_info =~ /(Game|NHL)\D*(\d{4})/;
428 8         35 $self->{season_id} = $2;
429 8         28 return;
430             }
431              
432             sub read_status_info ($$$) {
433              
434 8     8 1 23 my $self = shift;
435 8         15 my $status_info = shift;
436              
437 8         23 $status_info =~ s/^\s+//;
438 8         20 $status_info =~ s/\s+$//;
439 8         17 $self->{status} = $status_info;
440 8 50       35 if ($status_info =~ / (\d+) \- (\S.*)/) {
441 0         0 $self->{season_id} = $1;
442 0         0 $self->{status} = $2;
443             }
444             else {
445 8         31 $self->{status} = $status_info;
446             }
447             }
448              
449             sub read_team ($$$$) {
450              
451 16     16 1 26 my $self = shift;
452 16         22 my $cell = shift;
453 16         24 my $idx = shift;
454              
455             my $name = $self->{old} ?
456 16 50       70 $self->get_sub_tree(0, [0,0,6], $cell) :
457             $self->get_sub_tree(0, [2,0,0], $cell);
458 16 0 33     39 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       58 ], $cell->{_parent}) : $self->get_sub_tree(0, [1,0,0,0,1,0], $cell);
467 16 50       107 $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     93 $score = $self->get_sub_tree(0, [9,0,0], $cell->{_parent}) if !defined $score || $score !~ /^\d{1,2}\s*$/;
469 16 50 33     81 if (!defined $score || $score !~ /^\s*\d{1,2}\s*$/) {
470 0         0 die "Unreadable header";
471             }
472 16 50       32 if ($name) {
473 16         39 $name =~ s/^\s+//g;
474 16         43 $name =~ s/\s+$//g;
475 16         63 $name =~ s/\s+/ /g;
476 16 50       42 $name = 'MONTREAL CANADIENS' if $name eq 'CANADIENS MONTREAL';
477 16         48 $self->{teams}[$idx]{name} = $name;
478             }
479 16         32 $score =~ s/\D//g;
480 16         46 $self->{teams}[$idx]{score} = $score;
481             }
482              
483             sub get_header ($) {
484              
485 8     8 1 14 my $self = shift;
486              
487 8         13 my $i = 0;
488 8         19 $self->{head} = [];
489 8         21 $self->{teams} = [];
490 8         141 while(my $base_element = $self->get_sub_tree(0, [$i])) {
491 24 50       43 last unless ref $base_element;
492 24 100       61 if ($base_element->tag eq 'table') {
493 8         50 push(@{$self->{head}}, $i);
  8         23  
494 8 50       31 push(@{$self->{head}}, 0) if $base_element->{_content}[0]->tag eq 'tbody';
  0         0  
495 8         82 last;
496             }
497 16         119 $i++;
498             }
499             }
500              
501             sub read_header ($) {
502              
503 8     8 1 17 my $self = shift;
504              
505 8         38 $self->get_header();
506              
507 8         21 my $main_table = $self->get_sub_tree(0, [@{$self->{head}}]);
  8         27  
508 8         16 my $gameinfo_table;
509 8         11 my $offset = 0;
510 8 50       29 if ($main_table->attr('class')) {
511 8         120 my $content_table = $self->get_sub_tree(0, [0,0,0],$main_table);
512 8         28 $gameinfo_table = $self->get_sub_tree(0, [0,0,0], $content_table);
513 8         20 $self->{content_table} = $content_table;
514 8         17 $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         28 my $gameinfo_row = $self->get_sub_tree(0, [0], $gameinfo_table);
523 8         15 my $gameinfo_size = @{$gameinfo_row->{_content}};
  8         20  
524 8         28 $self->{gs} = $gameinfo_size;
525 8         31 for my $i (0..2) {
526 24         40 my $cell;
527 24 50 33     58 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         75 $offset = $i + $i*$self->{old}*$gameinfo_size/2 + $self->{old}*(1-2*$i);
532 24 50       51 $offset += 1-$i if $gameinfo_size == 12;
533 24 50       44 $offset += 1-$i if $gameinfo_size == 14;
534 24 50       82 $cell = $self->get_sub_tree(0, [ $self->{old} ? $offset : ($offset, 0), ], $gameinfo_row);
535             }
536 24 100       143 ($i % 2) ? $self->read_status($cell) : $self->read_team($cell, $i / 2);
537             }
538 8 50 33     30 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       25 $self->{season}-- if ($self->{month} < 9);
543 8 50 33     137 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         23 $self->{stage} = $REGULAR;
551             }
552             $self->{teams}[0]{name} = 'MONTREAL CANADIENS'
553 8 50       29 if $self->{teams}[0]{name} eq 'CANADIENS MONTREAL';
554             $self->{teams}[1]{name} = 'MONTREAL CANADIENS'
555 8 50       25 if $self->{teams}[1]{name} eq 'CANADIENS MONTREAL';
556 8         32 $self->{_id} = $self->{season} * 100000 + $self->{stage} * 10000 + $self->{season_id};
557 8   50     49 $self->{periods} ||= [{},{},{}];
558 8         18 delete $self->{gs};
559 8         70 $self->fill_broken($BROKEN_HEADERS{$self->{_id}});
560 8   50     27 $self->{attendance} ||= 0;
561 8         42 ref ($self) =~ /\:\:(\w\w)$/;
562 8         25 $self->{type} = $1;
563 8         24 $self->{status} = uc $self->{status};
564             }
565              
566             sub convert_time_date ($;$) {
567              
568 10     10 1 22 my $self = shift;
569 10   100     46 my $force_us = shift || 0;
570            
571 10         25 my $date = $self->{date};
572 10         20 my $time = $self->{time};
573 10 50 66     136 my ($year, $month, $day) = $date =~ /^\d/ && ! $force_us
574             ? Decode_Date_EU($date)
575             : Decode_Date_US($date);
576              
577 10   66     53 $self->{season} ||= $year;
578 10         34 $self->{month} = $month;
579 10         20 $year -= 1900;
580 10         17 $month--;
581 10         66 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       34 unless ($end_h) {
584 2         5 ($start_h, $start_m, $start_tz) = ($time =~ /(\d+):(\d+)\W*(\w{1,2}T)\W*/);
585 2 50       4 unless ($start_h) {
586 2         4 $start_h = 12;
587 2         2 $start_m = 0;
588 2         4 $start_tz = 'EDT';
589             }
590 2         4 $end_h = $start_h + 3;
591 2         4 $end_m = $start_m;
592 2         3 $end_tz = $start_tz;
593             }
594 10 100       33 $start_h += 12 if $start_h < 12;
595 10 100       25 $end_h += 12 if $end_h < $start_h;
596 10         68 $self->{start_ts} = timelocal(0, $start_m, $start_h, $day, $month, $year);
597 10 50       1022 if ($end_h > 23) {
598 0         0 $self->{end_ts} = $self->{start_ts} + 10800;
599             }
600             else {
601 10         37 $self->{end_ts} = timegm(0, $end_m, $end_h, $day, $month, $year);
602             }
603 10   33     236 $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 31515 my $self = shift;
633 27436         27960 my $want_content = shift;
634 27436         28026 my $walk = shift;
635 27436   33     40313 my $tree = shift || $self->{html} || $self;
636              
637 27436 50       39209 print "Walking ",join(".", @{$walk}), "\n" if $ENV{SHOW_WALK};
  0         0  
638 27436         30509 my $tpointer = \$tree;
639 27436         30145 for my $node (@{$walk}) {
  27436         35134  
640 102416 100 100     220419 return undef unless $$tpointer && ref $$tpointer;
641 102156         104444 my $tc = ${$tpointer}->{_content}[$node];
  102156         160033  
642 102156         129630 $tpointer = \$tc;
643             }
644 27176         32859 my $tcopy = $$tpointer;
645 27176 50       59636 return $want_content ? $tcopy->{_content} : $tcopy;
646             }
647              
648             sub process ($) {
649              
650 10     10 1 3246 my $self = shift;
651              
652 10 100       78 $self->read_header() unless $self->{type} eq 'BH';
653 10         59 $self->parse();
654 10         60 $self->normalize();
655 10         88 $self->{html}->delete();
656 10         828383 delete $self->{source};
657             }
658              
659             sub set_event_extra_data ($) {
660              
661 14     14 1 47774 my $self = shift;
662 14         29 for my $event (@{$self->{events}}) {
  14         44  
663 3525         9286 $event->{sources} = {$self->{type} => 1};
664 3525 50       6024 $event->{game_id} = delete $event->{game} if $event->{game};
665 3525 100 33     5608 $event->{player1} ||= $BENCH_PLAYER_ID if ($event->{penalty});
666 3525         4004 my $t = -1;
667 3525 100       5911 if ($event->{team1}) {
668             $event->{team1} = resolve_team($event->{team1}) if
669 2924 50       7199 $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       9061 ? 1
    100          
674             : -1;
675             }
676 3525 100 66     8086 $event->{team2} = resolve_team($event->{team2}) if $event->{team2} && $event->{team2} ne 'OTH';
677 3525         6108 $event->{t} = $t;
678             $event->{ts} =
679             $event->{special} ? 0 :
680             $event->{stage} == $PLAYOFF || $event->{stage} == $REGULAR && $event->{period} < 5 ?
681 3525 50 33     17996 ($event->{period}-1) * 1200 + get_seconds($event->{time}) : 3900;
    50          
682             }
683 14 50       39 $self->{no_events} unless @{$self->{events}};
  14         86  
684             }
685              
686             END {
687 41 100   41   80403 $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