File Coverage

blib/lib/Sport/Analytics/NHL/Report/PL.pm
Criterion Covered Total %
statement 252 550 45.8
branch 96 286 33.5
condition 46 133 34.5
subroutine 25 36 69.4
pod 21 21 100.0
total 440 1026 42.8


line stmt bran cond sub pod time code
1             package Sport::Analytics::NHL::Report::PL;
2              
3 19     19   76409 use v5.10.1;
  19         106  
4 19     19   80 use strict;
  19         33  
  19         380  
5 19     19   69 no strict 'refs';
  19         35  
  19         498  
6 19     19   78 use warnings FATAL => 'all';
  19         32  
  19         669  
7 19     19   92 use experimental qw(smartmatch);
  19         40  
  19         116  
8              
9 19     19   1543 use parent 'Sport::Analytics::NHL::Report';
  19         235  
  19         122  
10 19     19   1114 use warnings FATAL => 'all';
  19         39  
  19         580  
11 19     19   89 use experimental qw(smartmatch);
  19         27  
  19         73  
12              
13 19     19   802 use Storable qw(dclone);
  19         40  
  19         982  
14              
15 19     19   106 use Sport::Analytics::NHL::Config;
  19         116  
  19         3498  
16 19     19   169 use Sport::Analytics::NHL::Errors;
  19         92  
  19         2932  
17 19     19   141 use Sport::Analytics::NHL::Tools;
  19         30  
  19         2759  
18 19     19   109 use Sport::Analytics::NHL::Util;
  19         33  
  19         1161  
19              
20 19     19   109 use Data::Dumper;
  19         33  
  19         1067  
21              
22             =head1 NAME
23              
24             Sport::Analytics::NHL::Report::PL - Class for the Boxscore HTML PBP report
25              
26             =head1 SYNOPSYS
27              
28             Class for the Boxscore HTML PBP report.Should not be constructed directly, but via Sport::Analytics::NHL::Report (q.v.)
29             As with any other HTML report, there are two types: old (pre-2007) and new (2007 and on). Parsers of them may have something in common but may turn out to be completely different more often than not.
30              
31             This module is the heaviest one of the reports due to vast amounts of data and poor structure of the document. Handle with care.
32              
33             =head1 METHODS
34              
35             =over 2
36              
37             =item C
38              
39             Adds a missing GEND event to a game.
40              
41             Arguments: none
42             Returns: void. The object is manipulated from within.
43              
44             =item C
45              
46             Removes extra white spaces from old event's properties
47              
48             Arguments: the event
49             Returns: void. The event is altered.
50              
51             =item C
52              
53             Adds on-ice information to certain broken-format old goal events.
54              
55             Arguments: none
56             Returns: void. The events arrayref in the
57             object is manipulated from within.
58              
59             =item C
60              
61             Finds the events in the boxscore that were explicitly marked as broken and corrects them with the manual data.
62              
63             Arguments: none
64             Returns: void. The events arrayref in the
65             object is manipulated from within.
66              
67             =item C
68              
69             Fills the event with derived values from the boxscore object (e.g. season, stage)
70              
71             Arguments: the event
72             Returns: void. The event is altered.
73              
74             =item C
75              
76             Applies additional processing to the event from an old report.
77             Arguments: the event
78             Returns: void. The event is altered.
79              
80              
81             =item C
82              
83             Makes the best attempt to fix the broken lines in the old reports, including known typos.
84              
85             Arguments: the line
86             Returns: void. The event is altered.
87              
88             =item C
89              
90             Cleans, standardizes and provides default values to events after the parsing.
91              
92             Arguments: none
93             Returns: void. The events arrayref in the
94             object is manipulated from within.
95              
96             =item C
97              
98             Wrapper dispatching the actual parsing either to read_playbyplay (q.v.) or read_playbyplay_old (q.v.) depending on the type of the report.
99              
100             Arguments: none
101             Returns: void.
102              
103             =item C
104              
105             Parses the description of the event of the new report for specific information.
106              
107             Arguments: the event
108             Returns: void. The event is altered.
109              
110             =item C
111              
112             Parses the description of the event of the old report for specific information.
113              
114             Arguments: the event
115             Returns: void. The event is altered.
116              
117             =item C
118              
119             Parses the on-ice information of the event of the new report.
120              
121             Arguments: the event
122             Returns: void. The event is altered.
123              
124             =item C
125              
126             Parses the penalty event of the new report which requires its own function due to complexity of the matching regexp.
127              
128             Arguments: the event
129             Returns: void. The event is altered.
130              
131             =item C
132              
133             Parses the penalty event of the old report which requires its own function due to complexity of the matching regexp.
134              
135             Arguments: the event
136             Returns: void. The event is altered.
137              
138             =item C
139              
140             Reads the event from the event table row of the new report.
141              
142             Arguments: the row HTML element
143             Returns: the read event.
144              
145             =item C
146              
147             Reads the consecutive block of event lines of the old report.
148              
149             Arguments:
150             * the block of HTML elements containing the lines.
151             * the number of the block
152             * the adjustment flag (due to poor structure)
153             Returns: the array of lines with the events
154              
155             =item C
156              
157             Reads a line of the old report and parses it into the event.
158              
159             Argument: the line of text.
160             Returns: the event.
161              
162             =item C
163              
164             Reads the on-ice information of the old report event (goals only)
165              
166             Arguments: the line with the information
167             Returns: void. The event is updated within the object.
168              
169             =item C
170              
171             Actually parses the new report
172              
173             Arguments: none
174             Returns: void. It's all in $self.
175              
176             =item C
177              
178             Actually parses the old report
179              
180             Arguments: none
181             Returns: void. It's all in $self.
182              
183             =item C
184              
185             Flags if the event is out of place and should be skipped.
186              
187             Argument: the event
188             Returns: 1 if the event should be skipped, 0 if not.
189              
190             =back
191              
192             =cut
193              
194 19     19   92 use parent qw(Sport::Analytics::NHL::Report Exporter);
  19         46  
  19         80  
195              
196             our @EXPORT = qw(
197             @KNOWN_EVENT_TYPES @IGNORED_EVENT_TYPES
198             $VALID_SHOTS $VALID_MISSES $VALID_ZONES
199             );
200             our @event_fields = qw(id period strength time type description on_ice1 on_ice2);
201             our %OLD_EVENT_TYPES = (
202             'GIVEAWAY' => 'GIVE',
203             'MISSED SHOT' => 'MISS',
204             'PENALTY' => 'PENL',
205             'STOPPAGE' => 'STOP',
206             'HIT' => 'HIT',
207             'FACE-OFF' => 'FAC',
208             'GOAL' => 'GOAL',
209             'BLOCKED SHOT' => 'BLOCK',
210             'TAKEAWAY' => 'TAKE',
211             'SHOT' => 'SHOT',
212             'Penalty Shot' => 'SHOT',
213             'GOALIE' => 'GPUL',
214             );
215             our @KNOWN_EVENT_TYPES = qw(GOAL SHOT MISS BLOCK HIT FAC GIVE TAKE PENL STOP PSTR GEND PEND CHL);
216             our @IGNORED_EVENT_TYPES = qw(PGSTR PGEND ANTHEM GOFF SOC EIEND EISTR EGT EGPID);
217              
218             our $ID_INDEX = 0;
219             our $PERIOD_INDEX = 1;
220             our $STR_INDEX = 2;
221             our $TIME_INDEX = 3;
222             our $TYPE_INDEX = 4;
223             our $DESCRIPTION_INDEX = 5;
224             our $ON_ICE1_INDEX = 6;
225             our $ON_ICE2_INDEX = 7;
226              
227             our @EVENT_INDICES = (
228             $ID_INDEX, $TYPE_INDEX, $PERIOD_INDEX, $STR_INDEX, $TIME_INDEX,
229             $DESCRIPTION_INDEX, $ON_ICE1_INDEX, $ON_ICE2_INDEX,
230             );
231              
232             our $VALID_MISSES = q(Wide|Over|Crossbar|Goalpost);
233             our $VALID_SHOTS = q(Wrist|Slap|Snap|Tip-In|Wrap-around|Deflected|Backhand);
234             our $VALID_ZONES = q(Off|Neu|Def);
235              
236             sub parse_penalty_old ($$) {
237              
238 0     0 1 0 my $self = shift;
239 0         0 my $event = shift;
240              
241 0         0 $event->{location} = 'Unk';
242 0         0 $event->{team1} = $event->{team};
243 0         0 $event->{novictim} = 1;
244 0 0       0 if ($event->{description} =~ /^(\d+).*\,\s*(\S.*\S)\s*\,\s*(\d+)/) {
    0          
    0          
    0          
245 0         0 $event->{player1} = $1;
246 0         0 $event->{penalty} = $2;
247 0         0 $event->{length} = $3;
248 0         0 $event->{player2} = $UNKNOWN_PLAYER_ID;
249 0         0 $event->{team2} = 'OTH';
250 0         0 delete $event->{novictim};
251             }
252             elsif ($event->{description} =~ /^Team Penalty\,\s*(\S.*\S)\s*\,\s*(\d+) min\, Served By\s+(\d+)/) {
253 0 0       0 $event->{player1} = $event->{description} =~ /\bcoach\b/ ? $COACH_PLAYER_ID : $BENCH_PLAYER_ID;
254 0         0 $event->{penalty} = $1;
255 0         0 $event->{length} = $2;
256 0         0 $event->{servedby} = $3;
257             }
258             elsif ($event->{description} =~ /^Abuse of officials - bench\,\s*(\d+) min\, Served By\s+(\d+)/) {
259 0         0 $event->{player1} = $BENCH_PLAYER_ID;
260 0         0 $event->{penalty} = 'Abuse of officials';
261 0         0 $event->{length} = $1;
262 0         0 $event->{servedby} = $2;
263             }
264             elsif ($event->{description} =~ /^Team Penalty\,\s*(\S.*\S)\s*\,\s*(\d+) min/) {
265 0         0 $event->{player1} = $BENCH_PLAYER_ID;
266 0         0 $event->{penalty} = $1;
267 0         0 $event->{length} = $2;
268 0         0 $event->{servedby} = $UNKNOWN_PLAYER_ID;
269             }
270 0 0       0 $event->{misconduct} = 1 if $event->{description} =~ /(misconduct|unsportsmanlike)/i;
271             }
272              
273             sub parse_penalty ($$) {
274              
275 32     32 1 51 my $self = shift;
276 32         45 my $event = shift;
277              
278 32         47 my $desc;
279 32         45 my $use_servedby = 0;
280 32         192 ($event->{team1}, $desc) = ($event->{description} =~ /^\s*(\S\S\S)\s+(\S.*)/);
281 32 50       96 if (! $event->{team1}) {
282             die "Strange no team in penalty " . Dumper($event)
283 0 0       0 unless $event->{description} =~ /team/i;
284 0         0 $event->{team1} = 'UNK';
285 0         0 $desc = $event->{description};
286             }
287             else {
288 32 50       95 if ($desc =~ /^\#(\s+)/) {
289 0         0 $desc =~ s/^\#(\s+)/\#00 UNKNOWN /;
290 0         0 $use_servedby = 1;
291             }
292 32         179 $desc =~ s/^(\#\d+)(\D.*?)\s+(?:PS\-)?([A-Z][a-z])/"$1 $3"/e;
  30         137  
293             }
294 32         167 ($event->{player1}, $desc) = ($desc =~ /^\#?(\d+|TEAM|\s)\s*(\S.*)/i);
295 32         276 ($event->{penalty}, $event->{length}, $desc) = ($desc =~ /^([A-Z][a-z].*\S)\((\d+) min\)(.*)/);
296 32 50       86 die "Bad description $event->{id}/$event->{description}" unless defined $desc;
297 32 100       128 if ($desc =~ /Drawn.By: (\S\S\S) #(\d+)/i) {
298 30         72 $event->{team2} = $1; $event->{player2} = $2;
  30         63  
299             }
300             else {
301 2         6 $event->{novictim} = 1;
302             }
303 32 100       82 $event->{servedby} = $1 if $desc =~ /Served.By: #(\d+)/;
304 32 50       66 $event->{player1} = delete $event->{servedby} if $use_servedby;
305 32 100       143 $event->{location} = $1 if $desc =~ /(\w\w\w). Zone/;
306 32 50       317 $event->{misconduct} = 1 if $event->{description} =~ /(misconduct|unsportsmanlike)/i;
307 32   50     65 $event->{player1} ||= '';
308 32 50 33     136 if (! $event->{player1} && $event->{servedby}) {
    100          
    50          
309 0         0 $event->{player1} = delete $event->{servedby};
310             }
311             elsif ($event->{player1} =~ /team/i) {
312 2         7 $event->{player1} = $BENCH_PLAYER_ID;
313             }
314             elsif ($event->{player1} eq ' ') {
315 0 0       0 $event->{player1} = $event->{description} =~ /(Team Staff|\bcoach\b)/i ?
316             $COACH_PLAYER_ID : $BENCH_PLAYER_ID;
317             }
318 32   100     138 $event->{location} ||= 'Unk';
319 32 50 66     97 delete $event->{servedby} if $event->{servedby} && $event->{servedby} =~ /^80/;
320 32         71 $event;
321             }
322              
323             sub parse_description_old ($$) {
324              
325 0     0 1 0 my $self = shift;
326 0         0 my $event = shift;
327              
328             $event->{location} = ucfirst(substr($1, 0, 3)) if
329 0 0       0 $event->{description} =~ /(offensive|neutral|defensive) zone/;
330 0 0       0 $event->{distance} = $1 if $event->{description} =~ /(\d+)\s+ft/;
331 0 0       0 if ($event->{description} =~ /^\-?(\d+)/) {
332 0         0 $event->{team1} = $event->{team};
333 0         0 $event->{player1} = $1;
334             }
335 0 0       0 $event->{strength} = 'EV' if $event->{strength} eq 'SO';
336 0         0 for ($event->{type}) {
337 0         0 when ([qw(GIVE TAKE)]) {
338 0   0     0 $event->{location} ||= 'Unk';
339             }
340 0         0 when ('FAC') {
341 0         0 $event->{description} =~ /(\S\S\S) won/;
342 0         0 $event->{winning_team} = $event->{team}= $1;
343 0         0 $event->{description} =~ /(\S\S\S)\s+(\d+)\s+\S.*\S\s+vs\s+(\S\S\S)\s+(\d+)\s+\S+/;
344 0 0       0 if ($event->{winning_team} eq $1) {
    0          
345 0         0 $event->{team1} = $1;
346 0         0 $event->{team2} = $3;
347 0         0 $event->{player1} = $2;
348 0         0 $event->{player2} = $4;
349             }
350             elsif ($event->{winning_team} eq $3) {
351 0         0 $event->{team1} = $3;
352 0         0 $event->{team2} = $1;
353 0         0 $event->{player1} = $4;
354 0         0 $event->{player2} = $2;
355             }
356             else {
357 0         0 die "$event->{winning_team} / $event->{team1} / $event->{team2} FACEOFF MISMATCH";
358             }
359             }
360 0         0 when ('BLOCK') {
361 0         0 $event->{location} = 'Def';
362 0         0 $event->{player2} = $event->{player1};
363 0         0 $event->{player1} = $UNKNOWN_PLAYER_ID;
364 0         0 $event->{team2} = $event->{team1};
365 0         0 $event->{team1} = 'OTH';
366 0         0 $event->{shot_type} = 'Unknown';
367             }
368 0         0 when('HIT') {
369 0         0 $event->{location} = 'Unk';
370 0         0 $event->{player2} = $UNKNOWN_PLAYER_ID;
371 0         0 $event->{team2} = 'OTH';
372             }
373 0         0 when ('MISS') {
374 0 0       0 if ($event->{description} =~ /($VALID_MISSES|Penalty)/) {
375 0 0       0 if ($1 eq 'Penalty') {
376 0         0 $event->{penaltyshot} = 1;
377 0         0 $event->{miss} = 'Unknown';
378             }
379             else {
380 0         0 $event->{miss} = $1;
381             }
382             }
383             else {
384 0         0 $event->{miss} = 'Unknown';
385             }
386 0         0 $event->{location} = 'Off';
387 0         0 $event->{shot_type} = 'Unknown';
388 0         0 $event->{distance} = 999;
389             }
390 0         0 when ('SHOT') {
391 0         0 $event->{description} =~ s/Wrap\,/Wrap-around\,/;
392 0         0 $event->{description} =~ /($VALID_SHOTS|Unsuccessful Penalty Shot)/;
393 0         0 $event->{shot_type} = $1;
394 0 0       0 if ($event->{shot_type} eq 'Unsuccessful Penalty Shot') {
395 0   0     0 $event->{distance} ||= 999;
396 0         0 $event->{shot_type} = 'Unknown';
397             }
398 0         0 $event->{location} = 'Off';
399             }
400 0         0 when ('GOAL') {
401 0         0 $event->{description} =~ s/Wrap\,/Wrap-around\,/;
402 0         0 $event->{description} =~ s/Tip-in/Tip-In/;
403 0         0 $event->{description} =~ /($VALID_SHOTS)/;
404 0         0 $event->{shot_type} = $1;
405             $event->{location} =
406 0 0       0 $event->{distance} > 120 ? 'Def' : $event->{distance} > 72 ? 'Neu' : 'Off';
    0          
407 0 0       0 if ($event->{description} =~ /A\:\s+(\d+)\s+(\S+)\,\s+(\d+)\s+(\S+)/) {
    0          
408 0         0 $event->{assist1} = $1;
409 0         0 $event->{assist2} = $3;
410             }
411             elsif ($event->{description} =~ /A\:\s+(\d+)\s+(\S+)/) {
412 0         0 $event->{assist1} = $1;
413             }
414             }
415 0         0 when ('STOP') {
416 0         0 $event->{stopreason} = $event->{description};
417             }
418             }
419             }
420              
421             sub parse_description ($$) {
422              
423 632     632 1 771 my $self = shift;
424 632         670 my $event = shift;
425              
426 632         1245 $event->{description} =~ tr/ / /;
427 632         1123 my $evx = $BROKEN_EVENTS{PL}->{$self->{_id}};
428 632 0 33     1159 $event->{description} = $evx->{$event->{id}}{description} if defined $evx->{$event->{id}} && $evx->{$event->{id}}{description};
429              
430 632 100       1364 return $self->parse_penalty($event) if $event->{type} eq 'PENL';
431              
432 600         1663 my @items = split(/\,/, $event->{description});
433 600         964 for my $item (@items) {
434 1504         3496 $item =~ s/^\s+//;
435 1504         3154 $item =~ s/\s+$//;
436             }
437 600 50       1094 if ($event->{type} eq 'CHL') {
438 0         0 $event->{description} =~ /^(\S+)\s*Challenge\W*(\S.*)\s.*\-\s.*Result: (.*)/;
439 0   0     0 $event->{team1} = $1 || 'League';
440 0         0 $event->{challenge} = $2;
441 0         0 $event->{result} = $3;
442             }
443 600 100       1002 if ($event->{type} ne 'FAC') {
444 470 100       1434 if ($items[-1] =~ /^(\d+) ft./) {
445 200         589 $event->{distance} = $1;
446 200         254 pop @items;
447             }
448 470 100       1996 if ($items[-1] =~ /^($VALID_ZONES)\. Zone/) {
449 364         980 $event->{location} = $1;
450 364         458 pop @items;
451             }
452 470 100       1597 if ($items[-1] =~ /$VALID_MISSES/) {
453 64         136 $event->{miss} = $items[-1];
454 64         78 pop @items;
455             }
456 470 100       2099 if ($items[-1] =~ /^$VALID_SHOTS$/) {
457 262         489 $event->{shot_type} = $items[-1];
458 262         312 pop @items;
459             }
460 470         1137 $items[0] =~ s/ (ONGOAL|TAKEAWAY|GIVEAWAY) \-//g;
461 470         1302 $items[0] =~ s/ (\d+) /" #$1 "/ge;
  0         0  
462             }
463             else {
464 130 50       1133 $event->{location} = $1 if $event->{description} =~ /($VALID_ZONES)\. Zone/;
465             }
466 600         886 my $t = 1;
467 600         2128 while ($items[0] =~ /(\S\S\S) \#(\d+)/gc) {
468 750         1974 $event->{"team$t"} = $1;
469 750         1587 $event->{"player$t"} = $2;
470 750         1849 $t++;
471             }
472 600 50       1167 $event->{penaltyshot} = 1 if $event->{description} =~ /Penalty Shot/;
473 600 100 50     1411 $event->{shot_type} ||= 'Unknown' if $event->{type} =~ /^(GOAL|MISS|SHOT|BLOCK)$/;
474 600 100 50     1059 $event->{miss} ||= 'Unknown' if $event->{type} eq 'MISS';
475             $event->{location} = $event->{type} =~ /(GOAL|SHOT|MISS|BLOCK)/ ? 'Off' : 'Def'
476 600 50       1175 if ! $event->{location};
    100          
477 600         976 for ($event->{type}) {
478 600         1124 when ('GOAL') {
479 10 50       63 if ($event->{description} =~ /Assists: #(\d+) .* #(\d+)/) {
    0          
480 10         46 $event->{assist1} = $1;
481 10         53 $event->{assist2} = $2;
482             }
483             elsif ($event->{description} =~ /Assist: #(\d+)/) {
484 0         0 $event->{assist1} = $1;
485             }
486             }
487 590         1469 when ([qw(PEND GEND PSTR)]) {
488 8         36 $event->{description} =~ /time: (\d+:\d+)/;
489 8         48 $event->{timestamp} = $1;
490             }
491 582         1117 when ('STOP') {
492 98         352 $event->{description} =~ /^\s*(\S.*\S)\s*$/;
493 98         401 $event->{stopreason} = $1;
494             }
495 484         653 when ('FAC') {
496 130         354 $event->{description} =~ /(\S\S\S) won/;
497 130 50       271 return undef unless $1;
498 130         248 $event->{winning_team} = $1;
499 130 100       388 if ($event->{winning_team} ne $event->{team1}) {
500 64         92 my $x = $event->{player2};
501 64         95 $event->{player2} = $event->{player1};
502 64         86 $event->{player1} = $x;
503 64         113 $x = $event->{team2};
504 64         95 $event->{team2} = $event->{team1};
505 64         291 $event->{team1} = $x;
506             }
507             }
508 354         692 when ('BLOCK') {
509 62         113 my $x = $event->{player2};
510 62         91 $event->{player2} = $event->{player1};
511 62         106 $event->{player1} = $x;
512 62         86 $x = $event->{team2};
513 62         86 $event->{team2} = $event->{team1};
514 62         245 $event->{team1} = $x;
515             }
516             }
517             }
518              
519             sub parse_on_ice ($$) {
520              
521 630     630 1 748 my $self = shift;
522 630         691 my $event = shift;
523              
524 630         852 for my $team (1,2) {
525 1260         2641 my $on_ice = delete $event->{"on_ice$team"};
526 1260 50       2435 if (ref $on_ice eq 'ARRAY') {
527 0   0     0 $event->{on_ice} ||= [];
528 0         0 $event->{on_ice}[$team-1] = $on_ice;
529             }
530             else {
531 1260         3028 my $on_ice_table = $self->get_sub_tree(0, [0], $on_ice);
532 1260 50       3046 return unless ref $on_ice_table->{_content};
533 1260         1472 my $num = scalar @{$on_ice_table->{_content}};
  1260         1778  
534 1260   100     3121 $event->{on_ice} ||= [];
535 1260         2276 $event->{on_ice}[$team-1] = [];
536 1260         2393 for (my $i = 0; $i < $num; $i+=2) {
537 7272         16050 my $on_ice_font = $self->get_sub_tree(0, [$i,0,0,0,0], $on_ice_table);
538 7272   50     14479 my $name = $on_ice_font->attr('title') || '';
539 7272         71216 $event->{description} .= " $name";
540 7272         15220 my $on_ice_cell = $self->get_sub_tree(0, [$i,0,0,0,0,0], $on_ice_table);
541 7272 50       12838 next unless defined $on_ice_cell;
542 7272 50       17830 $on_ice_cell =
543             $self->get_sub_tree(0, [$i,0,0,1,0,0], $on_ice_table) if $on_ice_cell !~ /^\d+$/;
544 7272         7792 push(@{$event->{on_ice}[$team-1]}, $on_ice_cell);
  7272         19605  
545             }
546             }
547             }
548             }
549              
550             sub read_old_block ($$$) {
551              
552 0     0 1 0 my $self = shift;
553 0         0 my $row = shift;
554 0         0 my $r = shift;
555 0         0 my $adjust = shift;
556              
557             my $block = ref $row->{_content}[$r] ?
558             $row->{_content}[$r]{_content}[0] :
559 0 0       0 $row->{_content}[$adjust ? 0 : $r];
    0          
560              
561 0 0       0 my $split_char = $block =~ /\r/ ? "\r\n" : "\n";
562 0         0 my @lines = split(/$split_char/, $block);
563              
564 0         0 @lines;
565             }
566              
567             sub read_old_on_ice ($$) {
568              
569 0     0 1 0 my $self = shift;
570 0         0 my $line = shift;
571              
572 0         0 $self->{events}[-1]{description} .= $line;
573 0         0 $line =~ /^\s+(\S{3}):\s+(\S.*)/;
574 0         0 my $team = resolve_team($1, 1);
575 0   0     0 $self->{events}[-1]{on_ice} ||= [];
576 0         0 my $on_ice_text = $2;
577 0         0 my @on_ice = split(/\,/, $on_ice_text);
578 0         0 my $index;
579 0 0       0 if ($team eq $self->{teams}[0]{name}) {
    0          
580 0         0 $index = 0;
581             }
582             elsif ($team eq $self->{teams}[1]{name}) {
583 0         0 $index = 1;
584             }
585             else {
586 0         0 die "Couldn't map team $team";
587             }
588 0         0 $self->{events}[-1]{on_ice}[$index] = [ map { s/\D+//g; $_ } @on_ice ];
  0         0  
  0         0  
589 0         0 $self->{goal_mode}--;
590 0 0       0 $self->{goal_mode}-- if $BROKEN_EVENTS{PL}->{$self->{_id}}->{$self->{events}[-1]{id}}{on_ice2};
591             }
592              
593             sub fix_old_line ($$) {
594              
595 0     0 1 0 my $self = shift;
596 0         0 my $line = shift;
597              
598 0 0       0 my $id = $self->{events}[-1] ? $self->{events}[-1]{id}+1 : 1;
599 0         0 $line =~ s/^\s+//;
600 0         0 $line = sprintf("%5s %s", $id, $line);
601 0         0 $line =~ s/\t/ /g;
602 0         0 $line =~ s/ ATL/ATL /;
603 0         0 $line =~ s/PENALTY\s+(\S{3})\s/"PENALTY $1"/e;
  0         0  
604              
605 0         0 $line;
606             }
607              
608             sub read_old_line ($$) {
609              
610 0     0 1 0 my $self = shift;
611 0         0 my $line = shift;
612              
613 0 0       0 if ($line =~ /Shootout/) {
614 0         0 $self->{so} = 1;
615 0         0 return;
616             }
617             return undef if
618             $line !~ /\w/ || $line =~ /^\<\!\-\-/ || $line =~ /^\s*(\-+|\#)/ ||
619 0 0 0     0 ! $self->{goal_mode} && $line !~ /^\s*(\d+|SO\s|F\s)/;
      0        
      0        
      0        
620 0         0 $line =~ s/\r//g;
621 0         0 my $was_missed = 0;
622 0 0       0 if ($self->{goal_mode}) {
623 0         0 $self->read_old_on_ice($line);
624 0         0 return;
625             }
626 0 0       0 if ($line =~ /^\s+\d+\s+\d+:\d+/) {
627 0         0 $line = $self->fix_old_line($line);
628 0         0 $was_missed = 1;
629             }
630 0         0 my $event = {};
631 0 0       0 $event->{id} = $self->{so} ? $self->{events}[-1]{id}+3 : substr($line, 0, 5);
632 0         0 $event->{id} =~ s/\s//g;
633 0         0 $event->{id} += $self->{missed_events};
634             return undef if
635             defined $BROKEN_EVENTS{PL}->{$self->{_id}}->{$event->{id}} &&
636             (! $BROKEN_EVENTS{PL}->{$self->{_id}}->{$event->{id}} ||
637 0 0 0     0 $BROKEN_EVENTS{PL}->{$self->{_id}}->{$event->{id}}{special});
      0        
638 0 0       0 if ($self->{so}) {
639 0         0 $line =~ s/^\s+(\S+)/" "x(9-length($1)).$1/e
  0         0  
640             }
641 0 0       0 $event->{period} = $self->{so} ? 5 : substr($line, 5, 5);
642 0         0 $event->{period} =~ s/\s//g;
643 0 0 0     0 return undef if $event->{period} > 5 && $self->{stage} == $REGULAR;
644 0         0 $event->{type} = substr($line, 16, 16);
645 0         0 $event->{team} = substr($line, 34, 3);
646 0 0       0 return if $event->{type} =~ /GOALIE/i;
647 0         0 $event->{description} = substr($line, 43);
648 0 0       0 $event->{so} = $self->{so} ? 1 : 0;
649 0 0 0     0 $self->{shootout} = 1 if $event->{period} =~ /\d/ && $event->{period} == 5 && $self->{stage} == $REGULAR;
      0        
650 0 0       0 $event->{time} = $self->{so} ? '0:00' : substr($line, 9, 7);
651 0 0       0 $event->{strength} = $self->{so} ? 'SO' : substr($line, 37, 6);
652 0         0 $event->{old} = 1;
653 0         0 $self->{missed_events} += $was_missed;
654 0         0 $event;
655             }
656              
657             sub cleanup_old_event ($$) {
658              
659 0     0 1 0 my $self = shift;
660 0         0 my $event = shift;
661              
662 0         0 for (keys %{$event}) {
  0         0  
663 0         0 $event->{$_} =~ s/^\s+//g;
664 0         0 $event->{$_} =~ s/\s+$//g;
665             }
666 0 0       0 if ($event->{type} =~ /(.*)\s+\(\s*\S+\s*\)/) {
667 0         0 $event->{type} = $1;
668             }
669             }
670              
671             sub fix_old_event_type ($$) {
672              
673 0     0 1 0 my $self = shift;
674 0         0 my $event = shift;
675              
676 0 0       0 if ($event->{type} eq 'Penalty Shot') {
677 0         0 $event->{strength} = 'EV';
678 0         0 $event->{penaltyshot} = 1;
679             }
680 0         0 $event->{type} = $OLD_EVENT_TYPES{$event->{type}};
681 0 0       0 if ($event->{type} eq 'GOAL') {
682 0 0       0 $self->{goal_mode} = $self->{shootout} ? 0 : 2;
683             }
684              
685             }
686              
687             sub configure_old_events ($) {
688              
689 0     0 1 0 my $self = shift;
690              
691 0         0 my $e = 0;
692 0         0 while ($self->{events}[$e]{type} ne 'FAC') {
693 0         0 $e++;
694             }
695 0         0 for my $event (@{$self->{events}}) {
  0         0  
696 0 0       0 next if $event->{special};
697 0 0       0 if ($event->{type} eq 'GOAL') {
698 0 0       0 if ($BROKEN_EVENTS{PL}->{$self->{_id}}->{$event->{id}}{on_ice}) {
    0          
    0          
699 0         0 $event->{on_ice} = $BROKEN_EVENTS{PL}->{$self->{_id}}->{$event->{id}}{on_ice};
700             }
701             elsif ($BROKEN_EVENTS{PL}->{$self->{_id}}->{$event->{id}}{on_ice1}) {
702 0         0 $event->{on_ice}[0] = $BROKEN_EVENTS{PL}->{$self->{_id}}->{$event->{id}}{on_ice1};
703             }
704             elsif ($BROKEN_EVENTS{PL}->{$self->{_id}}->{$event->{id}}{on_ice}) {
705 0         0 $event->{on_ice}[1] = $BROKEN_EVENTS{PL}->{$self->{_id}}->{$event->{id}}{on_ice2};
706             }
707             }
708             }
709             }
710              
711             sub read_playbyplay_old ($) {
712              
713 0     0 1 0 my $self = shift;
714              
715 0 0       0 my $row = $self->get_sub_tree(0, [(@{$self->{head}} == 2 ? (3, 0) : (3)), 0, 0, 0]);
  0         0  
716 0         0 my $r = 0;
717 0         0 $self->{teams}[0]{name} = resolve_team($self->{teams}[0]{name}, 1);
718 0         0 $self->{teams}[1]{name} = resolve_team($self->{teams}[1]{name}, 1);
719 0         0 $self->{events} = [];
720 0         0 $self->{missed_events} = 0;
721 0         0 my $event_cache = {};
722 0         0 while ($row->{_content}[$r]) {
723 0         0 my $adjust = 0;
724 0 0 0     0 if ($r == 1 && ! @{$self->{events}}) {
  0         0  
725 0         0 $row = $row->{_content}[$r];
726 0         0 $adjust = 1;
727             }
728 0         0 my @lines = $self->read_old_block($row, $r, $adjust);
729 0         0 for my $line (@lines) {
730 0         0 my $event = $self->read_old_line($line, $self);
731 0 0       0 next unless $event;
732 0 0       0 next if $event_cache->{$event->{id}};
733 0         0 $event_cache->{$event->{id}} = 1;
734 0         0 $self->cleanup_old_event($event);
735 0         0 my $evx = $BROKEN_EVENTS{PL}->{$self->{_id}}->{$event->{id}};
736 0 0 0     0 if ($evx && $evx->{special}) {
737 0         0 push(@{$self->{events}}, $evx);
  0         0  
738 0         0 next;
739             }
740             die "Unknown old event type: " . Dumper($event) . $line . "\n"
741 0 0       0 unless $OLD_EVENT_TYPES{$event->{type}};
742 0         0 $self->fix_old_event_type($event, $self);
743             $event->{description} = $evx->{description} if
744 0 0       0 defined $evx->{description};
745 0 0       0 $event->{type} eq 'PENL' ?
746             $self->parse_penalty_old($event) : $self->parse_description_old($event);
747 0 0       0 if ($event->{strength} eq '-') {
748 0 0       0 $event->{strength} = @{$self->{events}} ? $self->{events}[-1]{strength} : 'EV';
  0         0  
749             }
750 0         0 fill_broken($event, $evx);
751 0         0 $self->fill_event_values($event);
752 0 0       0 next if $event->{period} > 11;
753 0         0 push(@{$self->{events}}, $event);
  0         0  
754             }
755 0         0 $r++;
756 0 0 0     0 $row = $row->{_parent} if @{$self->{events}} && $self->{events}[-1]{special} || $adjust;
  0   0     0  
757             }
758 0 0       0 $self->configure_old_events() unless ($self->{events}[-1]{special});
759 0 0       0 if ($BROKEN_EVENTS{PL}->{$self->{_id}}->{-1}) {
760 0         0 push(@{$self->{events}}, @{$BROKEN_EVENTS{PL}->{$self->{_id}}->{-1}});
  0         0  
  0         0  
761             }
762             }
763              
764             sub read_event ($$) {
765              
766 648     648 1 815 my $self = shift;
767 648         735 my $play_row = shift;
768              
769 648         847 my $event = {};
770              
771 648         993 for my $pp (@EVENT_INDICES) {
772 5072         9687 my $event_cell = $self->get_sub_tree(0, [$pp,0], $play_row);
773 5072         7516 for ($pp) {
774 5072         6222 when ($ID_INDEX) {
775 648 100 33     2103 return undef if $event_cell eq '#' || $event_cell && ref $event_cell;
      66        
776             return undef if defined $BROKEN_EVENTS{PL}->{$self->{_id}}{$event_cell} &&
777 632 50 33     2026 ! $BROKEN_EVENTS{PL}->{$self->{_id}}{$event_cell};
778             }
779 4424         5281 when ($TYPE_INDEX) {
780 632 50       905 die "Bad event row: " . $event_cell if ! $event_cell;
781 632 50       1213 return undef if grep { $event_cell eq $_ } @IGNORED_EVENT_TYPES;
  5688         8006  
782             die "UNKNOWN event $event_cell / $event->{id} " . Dumper($event) . $play_row->dump
783 632 50       891 if ! grep { $event_cell eq $_ } @KNOWN_EVENT_TYPES;
  8848         12224  
784             }
785             }
786 5056         9492 $event->{$event_fields[$pp]} = $event_cell;
787 5056 100 100     9414 if ($pp == $DESCRIPTION_INDEX && $event->{type} eq 'GOAL') {
788 10         25 my $extra_description = $self->get_sub_tree(0, [$pp,2], $play_row);
789 10 50       47 $event->{description} .= ' ' . $extra_description if $extra_description;
790 10 50       38 if ($event->{id} == 1) {
791             # games stopped and resumed
792 0         0 $self->parse_description($event);
793 0         0 $event->{on_ice} = [[],[]];
794 0         0 $event->{time} = '0:00';
795 0         0 $event->{strength} = 'EV';
796 0         0 $event->{period} = 1;
797 0         0 $event->{special} = 1;
798 0         0 return $event;
799             }
800             }
801             }
802 632 50 66     1300 $event->{time} = '5:00' if $event->{type} eq 'PEND' && $event->{time} !~ /^\d+/;
803 632         927 $event;
804             }
805              
806             sub add_game_end ($) {
807              
808 0     0 1 0 my $self = shift;
809              
810 0         0 my $e = 1;
811             do {
812 0 0       0 if ($self->{events}[-$e]{type} eq 'PEND') {
813 0         0 my $gend_event = dclone $self->{events}[-$e];
814 0         0 $gend_event->{type} = 'GEND';
815 0         0 push(@{$self->{events}}, $gend_event);
  0         0  
816 0         0 return;
817             }
818 0         0 $e++;
819 0         0 } while ($self->{events}[-$e]{type} ne 'PSTR');
820 0 0 0     0 if ($self->{events}[-$e]{period} == 5 && $self->{stage} == $REGULAR) {
821 0         0 my $pend_event = dclone $self->{events}[-$e];
822 0         0 $pend_event->{type} = 'PEND'; $pend_event->{id} = $self->{events}[-1]{id}+1;
  0         0  
823 0         0 my $gend_event = dclone $self->{events}[-$e];
824 0         0 $gend_event->{type} = 'GEND'; $gend_event->{id} = $self->{events}[-1]{id}+2;
  0         0  
825 0         0 push(@{$self->{events}}, $pend_event, $gend_event);
  0         0  
826             }
827             }
828              
829             sub fill_broken_events ($) {
830              
831 2     2 1 4 my $self = shift;
832              
833 2         5 my $evx = $BROKEN_EVENTS{PL}->{$self->{_id}};
834 2 50       9 return unless defined $evx;
835 2 50       6 if ($evx->{-1}) {
836 0         0 unshift(@{$self->{events}}, @{$evx->{-1}});
  0         0  
  0         0  
837             }
838 2         13 for my $event (@{$self->{events}}) {
  2         8  
839 632 50       1407 next unless $evx->{$event->{id}};
840 0 0       0 next if $event->{special};
841 0 0       0 if ($evx->{$event->{id}}{on_ice}) {
    0          
    0          
842 0         0 $event->{on_ice} = $evx->{$event->{id}}{on_ice};
843             }
844             elsif ($evx->{$event->{id}}{on_ice1}) {
845 0         0 $event->{on_ice}[0] = $evx->{$event->{id}}{on_ice1};
846             }
847             elsif ($evx->{$event->{id}}{on_ice}) {
848 0         0 $event->{on_ice}[1] = $evx->{$event->{id}}{on_ice2};
849             }
850             }
851             }
852              
853             sub skip_event ($$) {
854              
855 632     632 1 745 my $self = shift;
856 632         668 my $event = shift;
857              
858 632 50 33     1772 return 1 if $event && $event->{period} > 11;
859             return 1 if
860             ($event->{type} eq 'PEND' || $event->{type} eq 'PSTR') &&
861 632 50 66     1859 @{$self->{events}} && $self->{events}[-1]{type} eq $event->{type};
  6   66     34  
      66        
862 632         1344 0;
863             }
864              
865             sub fill_event_values ($$) {
866              
867 632     632 1 730 my $self = shift;
868 632         755 my $event = shift;
869 632         1007 $event->{file} = $self->{file};
870 632         969 $event->{season} = $self->{season};
871 632         1342 $event->{game_id} = $self->{_id};
872 632         1094 $event->{stage} = $self->{stage};
873 632 50 33     1275 if ($event->{period} == 5 && $self->{stage} == $REGULAR) {
874 0         0 $event->{so} = 1;
875 0         0 $event->{penaltyshot} = 1;
876             }
877             }
878              
879             sub read_playbyplay ($) {
880              
881 2     2 1 5 my $self = shift;
882              
883 2         6 $self->{events} = [];
884 2         3 my $gend = 0;
885             do {
886 6         18 my $p = 3;
887 6         12 my $main_table = $self->get_sub_tree(0, [@{$self->{head}}]);
  6         21  
888 6 50       18 $gend = 1 unless $main_table;
889 6         16 while (my $play_row = $self->get_sub_tree(0, [++$p], $main_table)) {
890 704 100 100     1232 next unless ref $play_row && scalar @{$play_row->{_content}} >= @event_fields;
  700         2315  
891 648         1361 my $event = $self->read_event($play_row, $self->{_id});
892 648 100 66     1638 next if ! $event || $self->skip_event($event);
893 632         1360 $self->parse_description($event);
894 632 50 33     1314 next if $event->{type} eq 'CHL' && $event->{team1} eq 'html';
895 632 100       1808 $self->parse_on_ice($event) unless $event->{type} eq 'GEND';
896             my $evx = $BROKEN_EVENTS{PL}->{$self->{_id}}
897             ? $BROKEN_EVENTS{PL}->{$self->{_id}}{$event->{id}}
898 632 50       2098 : undef;
899 632         1661 fill_broken($event, $evx);
900 632 50 66     1169 delete $event->{on_ice1} if $event->{on_ice1} && ref $event->{on_ice1} && ref $event->{on_ice1} ne 'ARRAY';
      33        
901 632 50 66     1156 delete $event->{on_ice2} if $event->{on_ice2} && ref $event->{on_ice2} && ref $event->{on_ice2} ne 'ARRAY';
      33        
902 632         1339 $self->fill_event_values($event);
903 632         683 push(@{$self->{events}}, $event);
  632         1040  
904 632 100       2400 last if $self->{events}[-1]{type} eq 'GEND';
905             }
906 6         53 $self->{head}[-1]++;
907 2   66     4 } until ($self->{events}[-1]{type} eq 'GEND' || $gend);
908 2         12 $self->fill_broken_events();
909 2 50       12 $self->add_game_end() unless $self->{events}[-1]{type} eq 'GEND';
910 2 50       12 die "No gend" unless $self->{events}[-1]{type} eq 'GEND';
911             }
912              
913             sub normalize ($$) {
914              
915 2     2 1 11 my $self = shift;
916              
917 2         5 for my $event (@{$self->{events}}) {
  2         7  
918 632         948 $event->{file} = $self->{file};
919 632 100       887 if ($event->{penalty}) {
920 32 100 66     159 if ($event->{penalty} =~ /\bbench\b/i && $event->{penalty} !~ /leav/i) {
    50          
921 2         7 $event->{player1} = $BENCH_PLAYER_ID;
922 2         12 $event->{penalty} =~ s/\s*\-\s+bench//i;
923             }
924             elsif ($event->{penalty} =~ /(.*\w)\W*\bcoach\b/i) {
925 0         0 $event->{player1} = $COACH_PLAYER_ID;
926 0         0 $event->{penalty} = $1;
927             }
928 32         56 $event->{penalty} = uc ($event->{penalty});
929 32 50       115 if ($event->{penalty} =~ /(.*)\s+\(MAJ\)/i) {
    50          
930 0         0 $event->{penalty} = $1;
931 0         0 $event->{severity} = 'major';
932             }
933             elsif ($event->{penalty} =~ /(.*)\s+\(10 MIN\)/i) {
934 0         0 $event->{penalty} = $1;
935 0         0 $event->{severity} = 'misconduct';
936             }
937 32         45 $event->{penalty} =~ s/(game)-(\S)/"$1 - $2"/ie;
  0         0  
938             }
939 632         753 for my $v (qw(penalty miss shot_type stopreason strength)) {
940 3160 100       4533 next unless exists $event->{$v};
941 1088 100       1526 if ($v eq 'stopreason') {
    100          
942 98         281 $event->{$v} = [ split(/\,/, $event->{$v}) ];
943 98         131 for my $ev (@{$event->{$v}}) {
  98         152  
944 112         179 $ev = vocabulary_lookup($v, $ev);
945             }
946             }
947             elsif ($v eq 'penalty') {
948 32         64 $event->{$v} = normalize_penalty($event->{$v});
949             }
950             else {
951 958         1619 $event->{$v} = vocabulary_lookup($v, $event->{$v});
952             }
953             }
954 632 100       1034 if ($event->{assist1}) {
955 10         20 $event->{assists} = [ $event->{assist1} ];
956 10 50       22 push(@{$event->{assists}}, $event->{assist2}) if ($event->{assist2});
  10         19  
957             }
958 632 50 33     1115 if ($event->{period} == 5 && $self->{stage} == $REGULAR) {
959 0         0 $event->{so} = 1;
960 0         0 $self->{so} = 1;
961             }
962             }
963             }
964              
965             sub parse ($) {
966              
967 2     2 1 4 my $self = shift;
968              
969             $self->{old}
970 2 50       11 ? $self->read_playbyplay_old()
971             : $self->read_playbyplay();
972             }
973              
974             1;
975              
976             =head1 AUTHOR
977              
978             More Hockey Stats, C<< >>
979              
980             =head1 BUGS
981              
982             Please report any bugs or feature requests to C, or through
983             the web interface at L. I will be notified, and then you'll
984             automatically be notified of progress on your bug as I make changes.
985              
986              
987             =head1 SUPPORT
988              
989             You can find documentation for this module with the perldoc command.
990              
991             perldoc Sport::Analytics::NHL::Report::PL
992              
993             You can also look for information at:
994              
995             =over 4
996              
997             =item * RT: CPAN's request tracker (report bugs here)
998              
999             L
1000              
1001             =item * AnnoCPAN: Annotated CPAN documentation
1002              
1003             L
1004              
1005             =item * CPAN Ratings
1006              
1007             L
1008              
1009             =item * Search CPAN
1010              
1011             L
1012              
1013             =back