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   90701 use v5.10.1;
  19         125  
4 19     19   99 use strict;
  19         46  
  19         430  
5 19     19   81 no strict 'refs';
  19         39  
  19         591  
6 19     19   91 use warnings FATAL => 'all';
  19         37  
  19         718  
7 19     19   116 use experimental qw(smartmatch);
  19         38  
  19         152  
8              
9 19     19   1729 use parent 'Sport::Analytics::NHL::Report';
  19         291  
  19         133  
10 19     19   1297 use warnings FATAL => 'all';
  19         50  
  19         674  
11 19     19   98 use experimental qw(smartmatch);
  19         39  
  19         107  
12              
13 19     19   959 use Storable qw(dclone);
  19         46  
  19         1157  
14              
15 19     19   123 use Sport::Analytics::NHL::Config;
  19         176  
  19         4136  
16 19     19   189 use Sport::Analytics::NHL::Errors;
  19         106  
  19         3444  
17 19     19   174 use Sport::Analytics::NHL::Tools;
  19         37  
  19         3147  
18 19     19   139 use Sport::Analytics::NHL::Util;
  19         43  
  19         1335  
19              
20 19     19   136 use Data::Dumper;
  19         43  
  19         1288  
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   116 use parent qw(Sport::Analytics::NHL::Report Exporter);
  19         36  
  19         114  
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 57 my $self = shift;
276 32         63 my $event = shift;
277              
278 32         40 my $desc;
279 32         49 my $use_servedby = 0;
280 32         227 ($event->{team1}, $desc) = ($event->{description} =~ /^\s*(\S\S\S)\s+(\S.*)/);
281 32 50       92 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       120 if ($desc =~ /^\#(\s+)/) {
289 0         0 $desc =~ s/^\#(\s+)/\#00 UNKNOWN /;
290 0         0 $use_servedby = 1;
291             }
292 32         227 $desc =~ s/^(\#\d+)(\D.*?)\s+(?:PS\-)?([A-Z][a-z])/"$1 $3"/e;
  30         167  
293             }
294 32         196 ($event->{player1}, $desc) = ($desc =~ /^\#?(\d+|TEAM|\s)\s*(\S.*)/i);
295 32         315 ($event->{penalty}, $event->{length}, $desc) = ($desc =~ /^([A-Z][a-z].*\S)\((\d+) min\)(.*)/);
296 32 50       101 die "Bad description $event->{id}/$event->{description}" unless defined $desc;
297 32 100       159 if ($desc =~ /Drawn.By: (\S\S\S) #(\d+)/i) {
298 30         96 $event->{team2} = $1; $event->{player2} = $2;
  30         74  
299             }
300             else {
301 2         7 $event->{novictim} = 1;
302             }
303 32 100       106 $event->{servedby} = $1 if $desc =~ /Served.By: #(\d+)/;
304 32 50       97 $event->{player1} = delete $event->{servedby} if $use_servedby;
305 32 100       156 $event->{location} = $1 if $desc =~ /(\w\w\w). Zone/;
306 32 50       384 $event->{misconduct} = 1 if $event->{description} =~ /(misconduct|unsportsmanlike)/i;
307 32   50     100 $event->{player1} ||= '';
308 32 50 33     155 if (! $event->{player1} && $event->{servedby}) {
    100          
    50          
309 0         0 $event->{player1} = delete $event->{servedby};
310             }
311             elsif ($event->{player1} =~ /team/i) {
312 2         9 $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     158 $event->{location} ||= 'Unk';
319 32 50 66     124 delete $event->{servedby} if $event->{servedby} && $event->{servedby} =~ /^80/;
320 32         95 $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 857 my $self = shift;
424 632         811 my $event = shift;
425              
426 632         1287 $event->{description} =~ tr/ / /;
427 632         1140 my $evx = $BROKEN_EVENTS{PL}->{$self->{_id}};
428 632 0 33     1237 $event->{description} = $evx->{$event->{id}}{description} if defined $evx->{$event->{id}} && $evx->{$event->{id}}{description};
429              
430 632 100       1314 return $self->parse_penalty($event) if $event->{type} eq 'PENL';
431              
432 600         1790 my @items = split(/\,/, $event->{description});
433 600         929 for my $item (@items) {
434 1504         3610 $item =~ s/^\s+//;
435 1504         3282 $item =~ s/\s+$//;
436             }
437 600 50       1273 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       1001 if ($event->{type} ne 'FAC') {
444 470 100       1536 if ($items[-1] =~ /^(\d+) ft./) {
445 200         656 $event->{distance} = $1;
446 200         339 pop @items;
447             }
448 470 100       2113 if ($items[-1] =~ /^($VALID_ZONES)\. Zone/) {
449 364         988 $event->{location} = $1;
450 364         498 pop @items;
451             }
452 470 100       1701 if ($items[-1] =~ /$VALID_MISSES/) {
453 64         140 $event->{miss} = $items[-1];
454 64         97 pop @items;
455             }
456 470 100       2513 if ($items[-1] =~ /^$VALID_SHOTS$/) {
457 262         640 $event->{shot_type} = $items[-1];
458 262         332 pop @items;
459             }
460 470         1156 $items[0] =~ s/ (ONGOAL|TAKEAWAY|GIVEAWAY) \-//g;
461 470         1358 $items[0] =~ s/ (\d+) /" #$1 "/ge;
  0         0  
462             }
463             else {
464 130 50       1231 $event->{location} = $1 if $event->{description} =~ /($VALID_ZONES)\. Zone/;
465             }
466 600         992 my $t = 1;
467 600         2214 while ($items[0] =~ /(\S\S\S) \#(\d+)/gc) {
468 750         2202 $event->{"team$t"} = $1;
469 750         1720 $event->{"player$t"} = $2;
470 750         2249 $t++;
471             }
472 600 50       1208 $event->{penaltyshot} = 1 if $event->{description} =~ /Penalty Shot/;
473 600 100 50     1543 $event->{shot_type} ||= 'Unknown' if $event->{type} =~ /^(GOAL|MISS|SHOT|BLOCK)$/;
474 600 100 50     1120 $event->{miss} ||= 'Unknown' if $event->{type} eq 'MISS';
475             $event->{location} = $event->{type} =~ /(GOAL|SHOT|MISS|BLOCK)/ ? 'Off' : 'Def'
476 600 50       1229 if ! $event->{location};
    100          
477 600         1152 for ($event->{type}) {
478 600         1197 when ('GOAL') {
479 10 50       83 if ($event->{description} =~ /Assists: #(\d+) .* #(\d+)/) {
    0          
480 10         63 $event->{assist1} = $1;
481 10         48 $event->{assist2} = $2;
482             }
483             elsif ($event->{description} =~ /Assist: #(\d+)/) {
484 0         0 $event->{assist1} = $1;
485             }
486             }
487 590         1600 when ([qw(PEND GEND PSTR)]) {
488 8         60 $event->{description} =~ /time: (\d+:\d+)/;
489 8         62 $event->{timestamp} = $1;
490             }
491 582         1131 when ('STOP') {
492 98         362 $event->{description} =~ /^\s*(\S.*\S)\s*$/;
493 98         479 $event->{stopreason} = $1;
494             }
495 484         647 when ('FAC') {
496 130         421 $event->{description} =~ /(\S\S\S) won/;
497 130 50       340 return undef unless $1;
498 130         331 $event->{winning_team} = $1;
499 130 100       489 if ($event->{winning_team} ne $event->{team1}) {
500 64         124 my $x = $event->{player2};
501 64         103 $event->{player2} = $event->{player1};
502 64         118 $event->{player1} = $x;
503 64         137 $x = $event->{team2};
504 64         115 $event->{team2} = $event->{team1};
505 64         348 $event->{team1} = $x;
506             }
507             }
508 354         756 when ('BLOCK') {
509 62         135 my $x = $event->{player2};
510 62         120 $event->{player2} = $event->{player1};
511 62         88 $event->{player1} = $x;
512 62         104 $x = $event->{team2};
513 62         112 $event->{team2} = $event->{team1};
514 62         229 $event->{team1} = $x;
515             }
516             }
517             }
518              
519             sub parse_on_ice ($$) {
520              
521 630     630 1 884 my $self = shift;
522 630         738 my $event = shift;
523              
524 630         1151 for my $team (1,2) {
525 1260         2812 my $on_ice = delete $event->{"on_ice$team"};
526 1260 50       2580 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         3257 my $on_ice_table = $self->get_sub_tree(0, [0], $on_ice);
532 1260 50       3730 return unless ref $on_ice_table->{_content};
533 1260         1557 my $num = scalar @{$on_ice_table->{_content}};
  1260         1873  
534 1260   100     3356 $event->{on_ice} ||= [];
535 1260         2458 $event->{on_ice}[$team-1] = [];
536 1260         2502 for (my $i = 0; $i < $num; $i+=2) {
537 7272         17280 my $on_ice_font = $self->get_sub_tree(0, [$i,0,0,0,0], $on_ice_table);
538 7272   50     15379 my $name = $on_ice_font->attr('title') || '';
539 7272         79268 $event->{description} .= " $name";
540 7272         16647 my $on_ice_cell = $self->get_sub_tree(0, [$i,0,0,0,0,0], $on_ice_table);
541 7272 50       13273 next unless defined $on_ice_cell;
542 7272 50       19031 $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         8735 push(@{$event->{on_ice}[$team-1]}, $on_ice_cell);
  7272         20198  
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 827 my $self = shift;
767 648         823 my $play_row = shift;
768              
769 648         1032 my $event = {};
770              
771 648         1054 for my $pp (@EVENT_INDICES) {
772 5072         10615 my $event_cell = $self->get_sub_tree(0, [$pp,0], $play_row);
773 5072         8269 for ($pp) {
774 5072         6603 when ($ID_INDEX) {
775 648 100 33     2392 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     2147 ! $BROKEN_EVENTS{PL}->{$self->{_id}}{$event_cell};
778             }
779 4424         5513 when ($TYPE_INDEX) {
780 632 50       981 die "Bad event row: " . $event_cell if ! $event_cell;
781 632 50       1143 return undef if grep { $event_cell eq $_ } @IGNORED_EVENT_TYPES;
  5688         8504  
782             die "UNKNOWN event $event_cell / $event->{id} " . Dumper($event) . $play_row->dump
783 632 50       945 if ! grep { $event_cell eq $_ } @KNOWN_EVENT_TYPES;
  8848         13162  
784             }
785             }
786 5056         10028 $event->{$event_fields[$pp]} = $event_cell;
787 5056 100 100     10119 if ($pp == $DESCRIPTION_INDEX && $event->{type} eq 'GOAL') {
788 10         47 my $extra_description = $self->get_sub_tree(0, [$pp,2], $play_row);
789 10 50       66 $event->{description} .= ' ' . $extra_description if $extra_description;
790 10 50       45 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     1398 $event->{time} = '5:00' if $event->{type} eq 'PEND' && $event->{time} !~ /^\d+/;
803 632         1023 $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 5 my $self = shift;
832              
833 2         6 my $evx = $BROKEN_EVENTS{PL}->{$self->{_id}};
834 2 50       7 return unless defined $evx;
835 2 50       9 if ($evx->{-1}) {
836 0         0 unshift(@{$self->{events}}, @{$evx->{-1}});
  0         0  
  0         0  
837             }
838 2         4 for my $event (@{$self->{events}}) {
  2         7  
839 632 50       1600 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 911 my $self = shift;
856 632         786 my $event = shift;
857              
858 632 50 33     1851 return 1 if $event && $event->{period} > 11;
859             return 1 if
860             ($event->{type} eq 'PEND' || $event->{type} eq 'PSTR') &&
861 632 50 66     1906 @{$self->{events}} && $self->{events}[-1]{type} eq $event->{type};
  6   66     48  
      66        
862 632         1469 0;
863             }
864              
865             sub fill_event_values ($$) {
866              
867 632     632 1 807 my $self = shift;
868 632         745 my $event = shift;
869 632         1041 $event->{file} = $self->{file};
870 632         1106 $event->{season} = $self->{season};
871 632         1608 $event->{game_id} = $self->{_id};
872 632         1272 $event->{stage} = $self->{stage};
873 632 50 33     1340 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         4 my $gend = 0;
885             do {
886 6         19 my $p = 3;
887 6         20 my $main_table = $self->get_sub_tree(0, [@{$self->{head}}]);
  6         25  
888 6 50       24 $gend = 1 unless $main_table;
889 6         27 while (my $play_row = $self->get_sub_tree(0, [++$p], $main_table)) {
890 704 100 100     1300 next unless ref $play_row && scalar @{$play_row->{_content}} >= @event_fields;
  700         2967  
891 648         1444 my $event = $self->read_event($play_row, $self->{_id});
892 648 100 66     2135 next if ! $event || $self->skip_event($event);
893 632         1419 $self->parse_description($event);
894 632 50 33     1377 next if $event->{type} eq 'CHL' && $event->{team1} eq 'html';
895 632 100       1796 $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       2173 : undef;
899 632         1810 fill_broken($event, $evx);
900 632 50 66     1325 delete $event->{on_ice1} if $event->{on_ice1} && ref $event->{on_ice1} && ref $event->{on_ice1} ne 'ARRAY';
      33        
901 632 50 66     1188 delete $event->{on_ice2} if $event->{on_ice2} && ref $event->{on_ice2} && ref $event->{on_ice2} ne 'ARRAY';
      33        
902 632         1441 $self->fill_event_values($event);
903 632         763 push(@{$self->{events}}, $event);
  632         1180  
904 632 100       2541 last if $self->{events}[-1]{type} eq 'GEND';
905             }
906 6         91 $self->{head}[-1]++;
907 2   66     4 } until ($self->{events}[-1]{type} eq 'GEND' || $gend);
908 2         15 $self->fill_broken_events();
909 2 50       9 $self->add_game_end() unless $self->{events}[-1]{type} eq 'GEND';
910 2 50       13 die "No gend" unless $self->{events}[-1]{type} eq 'GEND';
911             }
912              
913             sub normalize ($$) {
914              
915 2     2 1 12 my $self = shift;
916              
917 2         7 for my $event (@{$self->{events}}) {
  2         10  
918 632         1262 $event->{file} = $self->{file};
919 632 100       968 if ($event->{penalty}) {
920 32 100 66     164 if ($event->{penalty} =~ /\bbench\b/i && $event->{penalty} !~ /leav/i) {
    50          
921 2         34 $event->{player1} = $BENCH_PLAYER_ID;
922 2         18 $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         61 $event->{penalty} = uc ($event->{penalty});
929 32 50       112 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         50 $event->{penalty} =~ s/(game)-(\S)/"$1 - $2"/ie;
  0         0  
938             }
939 632         808 for my $v (qw(penalty miss shot_type stopreason strength)) {
940 3160 100       4974 next unless exists $event->{$v};
941 1088 100       1713 if ($v eq 'stopreason') {
    100          
942 98         308 $event->{$v} = [ split(/\,/, $event->{$v}) ];
943 98         156 for my $ev (@{$event->{$v}}) {
  98         162  
944 112         203 $ev = vocabulary_lookup($v, $ev);
945             }
946             }
947             elsif ($v eq 'penalty') {
948 32         63 $event->{$v} = normalize_penalty($event->{$v});
949             }
950             else {
951 958         1746 $event->{$v} = vocabulary_lookup($v, $event->{$v});
952             }
953             }
954 632 100       1067 if ($event->{assist1}) {
955 10         23 $event->{assists} = [ $event->{assist1} ];
956 10 50       36 push(@{$event->{assists}}, $event->{assist2}) if ($event->{assist2});
  10         23  
957             }
958 632 50 33     1263 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       14 ? $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