File Coverage

blib/lib/Sport/Analytics/NHL/Report/PL.pm
Criterion Covered Total %
statement 251 549 45.7
branch 94 284 33.1
condition 48 139 34.5
subroutine 25 36 69.4
pod 21 21 100.0
total 439 1029 42.6


line stmt bran cond sub pod time code
1             package Sport::Analytics::NHL::Report::PL;
2              
3 20     20   73286 use v5.10.1;
  20         79  
4 20     20   103 use strict;
  20         42  
  20         439  
5 20     20   107 no strict 'refs';
  20         44  
  20         584  
6 20     20   100 use warnings FATAL => 'all';
  20         54  
  20         709  
7 20     20   95 use experimental qw(smartmatch);
  20         43  
  20         129  
8              
9 20     20   1583 use parent 'Sport::Analytics::NHL::Report';
  20         280  
  20         147  
10 20     20   1299 use warnings FATAL => 'all';
  20         45  
  20         660  
11 20     20   105 use experimental qw(smartmatch);
  20         39  
  20         74  
12              
13 20     20   923 use Storable qw(dclone);
  20         36  
  20         930  
14              
15 20     20   127 use Sport::Analytics::NHL::Config;
  20         44  
  20         3769  
16 20     20   128 use Sport::Analytics::NHL::Errors;
  20         48  
  20         3657  
17 20     20   129 use Sport::Analytics::NHL::Tools;
  20         138  
  20         3044  
18 20     20   169 use Sport::Analytics::NHL::Util;
  20         97  
  20         1214  
19              
20 20     20   144 use Data::Dumper;
  20         40  
  20         1158  
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 20     20   110 use parent qw(Sport::Analytics::NHL::Report Exporter);
  20         34  
  20         85  
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 64     64 1 145 my $self = shift;
276 64         114 my $event = shift;
277              
278 64         105 my $desc;
279 64         95 my $use_servedby = 0;
280 64         427 ($event->{team1}, $desc) = ($event->{description} =~ /^\s*(\S\S\S)\s+(\S.*)/);
281 64 50       187 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 64 50       218 if ($desc =~ /^\#(\s+)/) {
289 0         0 $desc =~ s/^\#(\s+)/\#00 UNKNOWN /;
290 0         0 $use_servedby = 1;
291             }
292 64         425 $desc =~ s/^(\#\d+)(\D.*?)\s+(?:PS\-)?([A-Z][a-z])/"$1 $3"/e;
  60         332  
293             }
294 64         371 ($event->{player1}, $desc) = ($desc =~ /^\#?(\d+|TEAM|\s)\s*(\S.*)/i);
295 64         559 ($event->{penalty}, $event->{length}, $desc) = ($desc =~ /^([A-Z][a-z].*\S)\((\d+) min\)(.*)/);
296 64 50       169 die "Bad description $event->{id}/$event->{description}" unless defined $desc;
297 64 100       295 if ($desc =~ /Drawn.By: (\S\S\S) #(\d+)/i) {
298 60         168 $event->{team2} = $1; $event->{player2} = $2;
  60         148  
299             }
300             else {
301 4         12 $event->{novictim} = 1;
302             }
303 64 100       199 $event->{servedby} = $1 if $desc =~ /Served.By: #(\d+)/;
304 64 50       268 $event->{player1} = delete $event->{servedby} if $use_servedby;
305 64 100       306 $event->{location} = $1 if $desc =~ /(\w\w\w). Zone/;
306 64 50       472 $event->{misconduct} = 1 if $event->{description} =~ /(misconduct|unsportsmanlike)/i;
307 64   50     159 $event->{player1} ||= '';
308 64 50 33     343 if (! $event->{player1} && $event->{servedby}) {
    100          
    50          
309 0         0 $event->{player1} = delete $event->{servedby};
310             }
311             elsif ($event->{player1} =~ /team/i) {
312 4         14 $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 64   100     199 $event->{location} ||= 'Unk';
319 64 50 66     212 delete $event->{servedby} if $event->{servedby} && $event->{servedby} =~ /^80/;
320 64         142 $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 1264     1264 1 1500 my $self = shift;
424 1264         1432 my $event = shift;
425              
426 1264         2666 $event->{description} =~ tr/ / /;
427 1264         2127 my $evx = $BROKEN_EVENTS{PL}->{$self->{_id}};
428 1264 0 33     2347 $event->{description} = $evx->{$event->{id}}{description} if defined $evx->{$event->{id}} && $evx->{$event->{id}}{description};
429              
430 1264 100       2334 return $self->parse_penalty($event) if $event->{type} eq 'PENL';
431              
432 1200         3354 my @items = split(/\,/, $event->{description});
433 1200         1832 for my $item (@items) {
434 3008         7179 $item =~ s/^\s+//;
435 3008         6442 $item =~ s/\s+$//;
436             }
437 1200 50       2304 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 1200 100       2044 if ($event->{type} ne 'FAC') {
444 940 100       2945 if ($items[-1] =~ /^(\d+) ft./) {
445 400         1335 $event->{distance} = $1;
446 400         611 pop @items;
447             }
448 940 100       4201 if ($items[-1] =~ /^($VALID_ZONES)\. Zone/) {
449 728         2291 $event->{location} = $1;
450 728         971 pop @items;
451             }
452 940 100       3396 if ($items[-1] =~ /$VALID_MISSES/) {
453 128         285 $event->{miss} = $items[-1];
454 128         184 pop @items;
455             }
456 940 100       4271 if ($items[-1] =~ /^$VALID_SHOTS$/) {
457 524         1092 $event->{shot_type} = $items[-1];
458 524         664 pop @items;
459             }
460 940         2483 $items[0] =~ s/ (ONGOAL|TAKEAWAY|GIVEAWAY) \-//g;
461 940         2747 $items[0] =~ s/ (\d+) /" #$1 "/ge;
  0         0  
462             }
463             else {
464 260 50       2461 $event->{location} = $1 if $event->{description} =~ /($VALID_ZONES)\. Zone/;
465             }
466 1200         1832 my $t = 1;
467 1200         4430 while ($items[0] =~ /(\S\S\S) \#(\d+)/gc) {
468 1500         4211 $event->{"team$t"} = $1;
469 1500         3605 $event->{"player$t"} = $2;
470 1500         3770 $t++;
471             }
472 1200 50       2458 $event->{penaltyshot} = 1 if $event->{description} =~ /Penalty Shot/;
473 1200 100 50     3222 $event->{shot_type} ||= 'Unknown' if $event->{type} =~ /^(GOAL|MISS|SHOT|BLOCK)$/;
474 1200 100 50     2293 $event->{miss} ||= 'Unknown' if $event->{type} eq 'MISS';
475             $event->{location} = $event->{type} =~ /(GOAL|SHOT|MISS|BLOCK)/ ? 'Off' : 'Def'
476 1200 50       2559 if ! $event->{location};
    100          
477 1200         2091 for ($event->{type}) {
478 1200         2269 when ('GOAL') {
479 20 50       136 if ($event->{description} =~ /Assists: #(\d+) .* #(\d+)/) {
    0          
480 20         65 $event->{assist1} = $1;
481 20         102 $event->{assist2} = $2;
482             }
483             elsif ($event->{description} =~ /Assist: #(\d+)/) {
484 0         0 $event->{assist1} = $1;
485             }
486             }
487 1180         2963 when ([qw(PEND GEND PSTR)]) {
488 16         83 $event->{description} =~ /time: (\d+:\d+)/;
489 16         137 $event->{timestamp} = $1;
490             }
491 1164         2263 when ('STOP') {
492 196         747 $event->{description} =~ /^\s*(\S.*\S)\s*$/;
493 196         926 $event->{stopreason} = $1;
494             }
495 968         1360 when ('FAC') {
496 260         778 $event->{description} =~ /(\S\S\S) won/;
497 260 50       658 return undef unless $1;
498 260         634 $event->{winning_team} = $1;
499 260 100       959 if ($event->{winning_team} ne $event->{team1}) {
500 128         320 my $x = $event->{player2};
501 128         232 $event->{player2} = $event->{player1};
502 128         198 $event->{player1} = $x;
503 128         482 $x = $event->{team2};
504 128         193 $event->{team2} = $event->{team1};
505 128         504 $event->{team1} = $x;
506             }
507             }
508 708         1645 when ('BLOCK') {
509 124         259 my $x = $event->{player2};
510 124         240 $event->{player2} = $event->{player1};
511 124         222 $event->{player1} = $x;
512 124         272 $x = $event->{team2};
513 124         210 $event->{team2} = $event->{team1};
514 124         474 $event->{team1} = $x;
515             }
516             }
517             }
518              
519             sub parse_on_ice ($$) {
520              
521 1260     1260 1 1550 my $self = shift;
522 1260         1473 my $event = shift;
523              
524 1260         1818 for my $team (1,2) {
525 2520         5651 my $on_ice = delete $event->{"on_ice$team"};
526 2520 50       4926 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 2520         6018 my $on_ice_table = $self->get_sub_tree(0, [0], $on_ice);
532 2520 50       6025 return unless ref $on_ice_table->{_content};
533 2520         2891 my $num = scalar @{$on_ice_table->{_content}};
  2520         3567  
534 2520   100     6542 $event->{on_ice} ||= [];
535 2520         4590 $event->{on_ice}[$team-1] = [];
536 2520         4673 for (my $i = 0; $i < $num; $i+=2) {
537 14544         33435 my $on_ice_font = $self->get_sub_tree(0, [$i,0,0,0,0], $on_ice_table);
538 14544   50     30940 my $name = $on_ice_font->attr('title') || '';
539 14544         145292 $event->{description} .= " $name";
540 14544         31753 my $on_ice_cell = $self->get_sub_tree(0, [$i,0,0,0,0,0], $on_ice_table);
541 14544 50       25413 next unless defined $on_ice_cell;
542 14544 50       38050 $on_ice_cell =
543             $self->get_sub_tree(0, [$i,0,0,1,0,0], $on_ice_table) if $on_ice_cell !~ /^\d+$/;
544 14544         16531 push(@{$event->{on_ice}[$team-1]}, $on_ice_cell);
  14544         40305  
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 1296     1296 1 1720 my $self = shift;
767 1296         1473 my $play_row = shift;
768              
769 1296         1917 my $event = {};
770              
771 1296         2096 for my $pp (@EVENT_INDICES) {
772 10144         20403 my $event_cell = $self->get_sub_tree(0, [$pp,0], $play_row);
773 10144         15493 for ($pp) {
774 10144         12399 when ($ID_INDEX) {
775 1296 100 33     4390 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 1264 50 33     4555 ! $BROKEN_EVENTS{PL}->{$self->{_id}}{$event_cell};
778             }
779 8848         10720 when ($TYPE_INDEX) {
780 1264 50       1981 die "Bad event row: " . $event_cell if ! $event_cell;
781 1264 50       2188 return undef if grep { $event_cell eq $_ } @IGNORED_EVENT_TYPES;
  11376         16666  
782             die "UNKNOWN event $event_cell / $event->{id} " . Dumper($event) . $play_row->dump
783 1264 50       1783 if ! grep { $event_cell eq $_ } @KNOWN_EVENT_TYPES;
  17696         24146  
784             }
785             }
786 10112         19794 $event->{$event_fields[$pp]} = $event_cell;
787 10112 100 100     19268 if ($pp == $DESCRIPTION_INDEX && $event->{type} eq 'GOAL') {
788 20         70 my $extra_description = $self->get_sub_tree(0, [$pp,2], $play_row);
789 20 50       104 $event->{description} .= ' ' . $extra_description if $extra_description;
790 20 50       84 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 1264 50 66     2541 $event->{time} = '5:00' if $event->{type} eq 'PEND' && $event->{time} !~ /^\d+/;
803 1264         1827 $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 4     4 1 12 my $self = shift;
832              
833 4         17 my $evx = $BROKEN_EVENTS{PL}->{$self->{_id}};
834 4 50       38 return unless defined $evx;
835 4 50       25 if ($evx->{-1}) {
836 0         0 unshift(@{$self->{events}}, @{$evx->{-1}});
  0         0  
  0         0  
837             }
838 4         16 for my $event (@{$self->{events}}) {
  4         22  
839 1264 50       2994 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 1264     1264 1 1568 my $self = shift;
856 1264         1432 my $event = shift;
857              
858 1264 0 33     2806 return 1 if $BROKEN_EVENTS{$self->{_id}} && defined $BROKEN_EVENTS{$self->{_id}}->{$event->{id}} && $BROKEN_EVENTS{$self->{_id}}->{$event->{id}} == 0;
      33        
859 1264 50 33     3806 return 1 if $event && $event->{period} > 11;
860             return 1 if
861             ($event->{type} eq 'PEND' || $event->{type} eq 'PSTR') &&
862 1264 50 66     3617 @{$self->{events}} && $self->{events}[-1]{type} eq $event->{type};
  12   66     108  
      66        
863 1264         2797 0;
864             }
865              
866             sub fill_event_values ($$) {
867              
868 1264     1264 1 1488 my $self = shift;
869 1264         1406 my $event = shift;
870 1264         2207 $event->{file} = $self->{file};
871 1264         2063 $event->{season} = $self->{season};
872 1264         2915 $event->{game_id} = $self->{_id};
873 1264         2466 $event->{stage} = $self->{stage};
874 1264 50 33     2806 if ($event->{period} == 5 && $self->{stage} == $REGULAR) {
875 0         0 $event->{so} = 1;
876 0         0 $event->{penaltyshot} = 1;
877             }
878             }
879              
880             sub read_playbyplay ($) {
881              
882 4     4 1 9 my $self = shift;
883              
884 4         11 $self->{events} = [];
885 4         10 my $gend = 0;
886             do {
887 12         29 my $p = 3;
888 12         27 my $main_table = $self->get_sub_tree(0, [@{$self->{head}}]);
  12         52  
889 12 50       39 $gend = 1 unless $main_table;
890 12         37 while (my $play_row = $self->get_sub_tree(0, [++$p], $main_table)) {
891 1408 100 100     2674 next unless ref $play_row && scalar @{$play_row->{_content}} >= @event_fields;
  1400         4291  
892 1296         2721 my $event = $self->read_event($play_row, $self->{_id});
893 1296 100 66     3425 next if ! $event || $self->skip_event($event);
894 1264         2945 $self->parse_description($event);
895 1264 50 33     2753 next if $event->{type} eq 'CHL' && $event->{team1} eq 'html';
896 1264 100       3640 $self->parse_on_ice($event) unless $event->{type} eq 'GEND';
897             my $evx = $BROKEN_EVENTS{PL}->{$self->{_id}}
898             ? $BROKEN_EVENTS{PL}->{$self->{_id}}{$event->{id}}
899 1264 50       4343 : undef;
900 1264         3587 fill_broken($event, $evx);
901 1264 50 66     2399 delete $event->{on_ice1} if $event->{on_ice1} && ref $event->{on_ice1} && ref $event->{on_ice1} ne 'ARRAY';
      33        
902 1264 50 66     2091 delete $event->{on_ice2} if $event->{on_ice2} && ref $event->{on_ice2} && ref $event->{on_ice2} ne 'ARRAY';
      33        
903 1264         2938 $self->fill_event_values($event);
904 1264         1409 push(@{$self->{events}}, $event);
  1264         2373  
905 1264 100       5068 last if $self->{events}[-1]{type} eq 'GEND';
906             }
907 12         93 $self->{head}[-1]++;
908 4   66     7 } until ($self->{events}[-1]{type} eq 'GEND' || $gend);
909 4         32 $self->fill_broken_events();
910             }
911              
912             sub normalize ($$) {
913              
914 4     4 1 11 my $self = shift;
915              
916 4         11 for my $event (@{$self->{events}}) {
  4         20  
917 1264         1934 $event->{file} = $self->{file};
918 1264 100       1716 if ($event->{penalty}) {
919 64 100 66     322 if ($event->{penalty} =~ /\bbench\b/i && $event->{penalty} !~ /leav/i) {
    50          
920 4         13 $event->{player1} = $BENCH_PLAYER_ID;
921 4         22 $event->{penalty} =~ s/\s*\-\s+bench//i;
922             }
923             elsif ($event->{penalty} =~ /(.*\w)\W*\bcoach\b/i) {
924 0         0 $event->{player1} = $COACH_PLAYER_ID;
925 0         0 $event->{penalty} = $1;
926             }
927 64         108 $event->{penalty} = uc ($event->{penalty});
928 64 50       212 if ($event->{penalty} =~ /(.*)\s+\(MAJ\)/i) {
    50          
929 0         0 $event->{penalty} = $1;
930 0         0 $event->{severity} = 'major';
931             }
932             elsif ($event->{penalty} =~ /(.*)\s+\(10 MIN\)/i) {
933 0         0 $event->{penalty} = $1;
934 0         0 $event->{severity} = 'misconduct';
935             }
936 64         106 $event->{penalty} =~ s/(game)-(\S)/"$1 - $2"/ie;
  0         0  
937             }
938 1264         1430 for my $v (qw(penalty miss shot_type stopreason strength)) {
939 6320 100       9016 next unless exists $event->{$v};
940 2176 100       3094 if ($v eq 'stopreason') {
    100          
941 196         501 $event->{$v} = [ split(/\,/, $event->{$v}) ];
942 196         235 for my $ev (@{$event->{$v}}) {
  196         314  
943 224         352 $ev = vocabulary_lookup($v, $ev);
944             }
945             }
946             elsif ($v eq 'penalty') {
947 64         127 $event->{$v} = normalize_penalty($event->{$v});
948             }
949             else {
950 1916         3020 $event->{$v} = vocabulary_lookup($v, $event->{$v});
951             }
952             }
953 1264 100       1875 if ($event->{assist1}) {
954 20         56 $event->{assists} = [ $event->{assist1} ];
955 20 50       47 push(@{$event->{assists}}, $event->{assist2}) if ($event->{assist2});
  20         47  
956             }
957 1264 50 33     2205 if ($event->{period} == 5 && $self->{stage} == $REGULAR) {
958 0         0 $event->{so} = 1;
959 0         0 $self->{so} = 1;
960             }
961             }
962             }
963              
964             sub parse ($) {
965              
966 4     4 1 10 my $self = shift;
967              
968             $self->{old}
969 4 50       25 ? $self->read_playbyplay_old()
970             : $self->read_playbyplay();
971             }
972              
973             1;
974              
975             =head1 AUTHOR
976              
977             More Hockey Stats, C<< >>
978              
979             =head1 BUGS
980              
981             Please report any bugs or feature requests to C, or through
982             the web interface at L. I will be notified, and then you'll
983             automatically be notified of progress on your bug as I make changes.
984              
985              
986             =head1 SUPPORT
987              
988             You can find documentation for this module with the perldoc command.
989              
990             perldoc Sport::Analytics::NHL::Report::PL
991              
992             You can also look for information at:
993              
994             =over 4
995              
996             =item * RT: CPAN's request tracker (report bugs here)
997              
998             L
999              
1000             =item * AnnoCPAN: Annotated CPAN documentation
1001              
1002             L
1003              
1004             =item * CPAN Ratings
1005              
1006             L
1007              
1008             =item * Search CPAN
1009              
1010             L
1011              
1012             =back