File Coverage

blib/lib/Sport/Analytics/NHL/Report/ES.pm
Criterion Covered Total %
statement 116 170 68.2
branch 28 44 63.6
condition 22 44 50.0
subroutine 14 16 87.5
pod 6 6 100.0
total 186 280 66.4


line stmt bran cond sub pod time code
1             package Sport::Analytics::NHL::Report::ES;
2              
3 19     19   76898 use v5.10.1;
  19         81  
4 19     19   95 use strict;
  19         42  
  19         430  
5 19     19   96 use warnings FATAL => 'all';
  19         35  
  19         637  
6 19     19   102 use experimental qw(smartmatch);
  19         46  
  19         141  
7              
8 19     19   1356 use parent 'Sport::Analytics::NHL::Report';
  19         287  
  19         123  
9              
10 19     19   1108 use Sport::Analytics::NHL::Config;
  19         63  
  19         3606  
11 19     19   143 use Sport::Analytics::NHL::Errors;
  19         46  
  19         3113  
12 19     19   133 use Sport::Analytics::NHL::Util;
  19         56  
  19         1265  
13              
14 19     19   130 use Storable qw(dclone);
  19         40  
  19         803  
15 19     19   112 use Data::Dumper;
  19         38  
  19         33873  
16              
17             =head1 NAME
18              
19             Sport::Analytics::NHL::Report::ES - Class for the NHL HTML ES report.
20              
21             =head1 SYNOPSYS
22              
23             Class for the NHL HTML ES report. Should not be constructed directly, but via Sport::Analytics::NHL::Report (q.v.)
24             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.
25              
26             =head1 METHODS
27              
28             =over 2
29              
30             =item C
31              
32             Get the headers of the player tables in the new HTML.
33              
34             Arguments: HTML element with the team table
35             Returns: the array of headers.
36              
37             =item C
38              
39             Get the headers of the player tables in the old HTML.
40              
41             Arguments: HTML element with the team table
42             Returns: the array of headers.
43              
44             =item C
45              
46             Cleaning up and standardizing the parsed data.
47              
48             Arguments: none
49             Returns: void. Everything is in the $self.
50              
51             =item C
52              
53             Parse the ES html tree into a boxscore object
54              
55             Arguments: none
56             Returns: void. Everything is in the $self.
57              
58             =item C
59              
60             Parse a team's table in the new HTML.
61              
62             Arguments: HTML element with the team table
63             Returns: the team hashref.
64              
65             =item C
66              
67             Parse a team's table in the old HTML.
68              
69             Arguments: HTML element with the team table
70             Returns: the team hashref.
71              
72             =back
73              
74             =cut
75              
76             my %NORMAL_FIELDS = (
77             S => {
78             G => 'goals', 'A' => 'assists', P => 'points', FW => 'faceOffWins',
79             PN => 'penalties', FL => 'faceOffLosses', 'F%' => 'faceOffPct',
80             TOI => 'timeOnIce', AVG => 'averageTimeShift', S => 'shots', MS => 'misses',
81             '+/-' => 'plusMinus', 'A/B' => 'attemptsBlocked', HT => 'hits',
82             GV => 'giveaways', TK => 'takeaways', BS => 'blocked', SHF => 'shifts',
83             TOIPP => 'powerPlayTimeOnIce', TOISH => 'shortHandedTimeOnIce', TOITOT => 'timeOnIce',
84             PP => 'powerPlayTimeOnIce', SH => 'shortHandedTimeOnIce', EV => 'evenTimeOnIce',
85             TOIEV => 'evenTimeOnIce', POS => 'position', 'No.' => 'number', TOI => 'timeOnIce',
86             PIM => 'penaltyMinutes', TOISHF => 'shifts', TOIAVG => 'averageIceTime', SHOTT => 'shots',
87             },
88             );
89             my %LIVE_FIELDS = (
90             S => [qw(shortHandedGoals shortHandedAssists powerPlayGoals powerPlayAssists evenStrengthGoals evenStrengthAssists)],
91             G => [qw(pim goals assists)]
92             );
93             my %FRENCH = (
94             'TOIDN/SH' => 'TOISH',
95             'TOI' => '14:19',
96             'TOIFÃ/EV' => 'TOIEV',
97             'TOIMOY' => 'TOIAVG',
98             'LB' => 'BS',
99             'MG' => 'FW',
100             'TOIAN/PP' => 'TOIPP',
101             'LANC.' => 'S',
102             'PP' => 'TK',
103             'B' => 'G',
104             'LR' => 'MS',
105             'M%' => 'F%',
106             'PUN' => 'PN',
107             'MP' => 'FL',
108             'MO' => 'FL',
109             'TOIPR' => 'TOISHF',
110             'R' => 'GV',
111             'MIN' => 'PIM',
112             'TENT/BL' => 'A/B',
113             'MÃ' => 'HT'
114             );
115              
116             sub get_old_headers ($$;$) {
117              
118 0     0 1 0 my $self = shift;
119 0         0 my $team_summary = shift;
120              
121 0         0 my $header_row = $self->get_sub_tree(0, [ 0 ], $team_summary);
122 0         0 my $headers_num = scalar @{$header_row->{_content}};
  0         0  
123 0         0 my @headers;
124              
125 0         0 for my $h (0..$headers_num-1) {
126 0         0 for ($h) {
127 0         0 when (0) {
128 0         0 $headers[$h] = 'No.';
129             }
130 0         0 when (1) {
131 0         0 $headers[$h] = 'POS';
132             }
133 0         0 when (2) {
134 0         0 $headers[$h] = 'name';
135             }
136 0         0 default {
137 0   0     0 $headers[$h] =
138             $self->get_sub_tree(0, [$h,0,1], $header_row) ||
139             $self->get_sub_tree(0, [$h,0,0,1], $header_row) ||
140             '';
141 0 0       0 if (ref $headers[$h]) {
142 0   0     0 $headers[$h] =
143             $self->get_sub_tree(0, [$h,0,2], $header_row) ||
144             $self->get_sub_tree(0, [$h,0,0,2], $header_row);
145             }
146             }
147             }
148             }
149 0         0 my $shots_row = $self->get_sub_tree(0, [1], $team_summary);
150 0         0 my $h = 0;
151 0         0 my $shot_offset;
152 0         0 for my $header (@headers) {
153 0 0       0 unless ($header) {
154 0         0 $shot_offset = $h;
155 0         0 last;
156             }
157 0         0 $h++;
158             }
159 0         0 for my $h (2..$#{$shots_row->{_content}}) {
  0         0  
160 0         0 my $subshot = $self->get_sub_tree(0, [$h,0,0,0], $shots_row);
161 0 0       0 last unless $subshot;
162 0         0 splice(@headers, $shot_offset-1+$h, 0, "SHOT" . $subshot);
163             }
164 0         0 splice(@headers, $shot_offset, 1);
165 0         0 @headers = grep { /\S/ } @headers;
  0         0  
166 0         0 @headers;
167             }
168              
169             sub get_new_headers ($$) {
170              
171 2     2 1 5 my $self = shift;
172 2         4 my $team_summary = shift;
173              
174 2         9 my $header_row = $self->get_sub_tree(0, [ 0,0,0 ], $team_summary);
175 2         4 my $headers_num = scalar @{$header_row->{_content}};
  2         6  
176 2         4 my @headers;
177              
178 2         7 for my $h (0..$headers_num-1) {
179 38         54 for ($h) {
180 38         48 when (0) {
181 2         8 $headers[$h] = 'name';
182             }
183 36         41 default {
184 36   100     82 $headers[$h] =
185             $self->get_sub_tree(0, [$h,0,], $header_row) ||
186             '';
187             }
188             }
189             }
190 2         18 unshift(@headers, qw(No. POS));
191 2         8 my $shots_row = $self->get_sub_tree(0, [0,0,1], $team_summary);
192              
193 2         16 for my $h (0..$#{$shots_row->{_content}}) {
  2         13  
194 14         30 my $subcol = $self->get_sub_tree(0, [$h,0], $shots_row);
195 14 100       38 splice(@headers, 10+$h, 0, "TOI$subcol") if $subcol;
196             }
197 2         5 splice(@headers, 9, 1);
198 2         10 @headers = grep { /\S/ } @headers;
  52         96  
199 2         11 for my $header (@headers) {
200 50 50       82 $header = $FRENCH{$header} if $FRENCH{$header};
201 50 50       73 $header = 'TOIEV' if $header =~ /TOIF.*EV/;
202 50 0 66     94 $header = 'HT' if $header =~ /^M./ && $header ne 'MS' && $header ne 'M%' && $header ne 'MO' && $header ne 'MG';
      33        
      33        
      0        
203             }
204 2         12 @headers;
205             }
206              
207             sub parse_old_team_summary ($$) {
208              
209 0     0 1 0 my $self = shift;
210 0         0 my $team_summary = shift;
211              
212 0         0 my $roster = [];
213              
214 0         0 my @headers = $self->get_old_headers($team_summary);
215 0         0 my $g = 2;
216 0         0 while (my $player_row = $self->get_sub_tree(0, [$g], $team_summary)) {
217 0         0 my $player = {};
218 0         0 for my $h (0..$#headers) {
219 0         0 $player->{$headers[$h]} = $self->get_sub_tree(0, [$h,0,0], $player_row);
220             }
221 0         0 push(@{$roster}, $player) if
222 0 0 0     0 $player->{name} && $player->{name} !~ /TOTALS/ && $player->{name} !~ /team penalty/i;
      0        
223 0         0 $g++;
224             }
225 0         0 $roster;
226             }
227              
228             sub parse_new_team_summary ($$) {
229              
230 2     2 1 3 my $self = shift;
231 2         5 my $team_summary = shift;
232              
233 2         9 my @headers = $self->get_new_headers($team_summary);
234 2         5 my @rosters;
235 2         3 my $g = 2;
236 2         6 my $roster = [];
237 2         7 while (my $player_row = $self->get_sub_tree(0, [0,0,$g], $team_summary)) {
238 96         153 my $player = {};
239 96         166 for my $h (0..$#headers) {
240 2400         4643 $player->{$headers[$h]} = $self->get_sub_tree(0, [$h,0], $player_row);
241             }
242 80         141 push(@{$roster}, $player) if
243 96 100 66     511 $player->{'No.'} && $player->{'No.'} =~ /\S/ && $player->{'No.'} !~ /\D/;
      100        
244 96 100 100     249 if ($player->{'No.'} && $player->{'No.'} =~ /TOTALS/) {
245 4         7 push(@rosters, $roster);
246 4         8 $roster = [];
247             }
248 96         294 $g++;
249             }
250 2         12 @rosters;
251             }
252              
253             sub parse ($$) {
254              
255 2     2 1 5 my $self = shift;
256              
257 2         4 my $body_size = scalar @{$self->{html}{_content}};
  2         6  
258 2 50       7 if ($self->{old}) {
259 0         0 my $away_summary = $self->get_sub_tree(0, [3,(0)x$#{$self->{head}}]);
  0         0  
260 0         0 $self->{teams}[0]{roster} = $self->parse_old_team_summary($away_summary);
261 0         0 my $home_summary = $self->get_sub_tree(0, [5,(0)x$#{$self->{head}}]);
  0         0  
262 0         0 $self->{teams}[1]{roster} = $self->parse_old_team_summary($home_summary);
263             }
264             else {
265 2         11 my $summary = $self->get_sub_tree(0, [$body_size/2,7]);
266 2         18 my @rosters = $self->parse_new_team_summary($summary);
267 2         640 $self->{teams}[0]{roster} = dclone $rosters[0];
268 2         681 $self->{teams}[1]{roster} = dclone $rosters[1];
269             }
270             }
271              
272             sub normalize ($$) {
273              
274 2     2 1 7 my $self = shift;
275              
276 2         5 for my $team (@{$self->{teams}}) {
  2         10  
277 4         7 for my $player (@{$team->{roster}}) {
  4         13  
278 80         138 $player->{'No.'} =~ s/\D//g;
279 80 100       142 if ($player->{POS} eq 'G') {
280 8         28 delete @{$player}{qw(BS +/- SHF FW FL F% A/B)};
  8         25  
281             }
282 80         99 for my $field (keys %{$player}) {
  80         289  
283 1952 50 66     3007 $player->{$field} ||= $field eq 'TOIAVG' ? '0:00' : 0;
284 1952         2734 $player->{$field} =~ s/^\s+//;
285 1952         2591 $player->{$field} =~ s/\s+$//;
286 1952         2284 $player->{$field} =~ s/\;/:/g;
287 1952 100 100     4877 if ($player->{$field} =~ /:/) {
    100          
288 360         1054 $player->{$field} =~ s/^(\d+):(\d+)$/$1*60+$2/e;
  360         958  
289             }
290             elsif ($field ne 'name' && $field ne 'POS') {
291 1432 100       2773 $player->{$field} = 0 if $player->{$field} !~ /\d/;
292 1432         1911 $player->{$field} += 0;
293             }
294 1952 100       4987 $player->{$field} += 0 if $player->{$field} =~ /^\-?\d+$/;
295             }
296 80 50       444 $player->{name} = "$2 $1" if $player->{name} =~ /^(\S.*\S)\,\s+(\S.*)$/;
297 80         128 $player->{start} = 2;
298 80         112 $player->{status} = 'X';
299 80         120 for my $field (qw(G A PIM)) {
300 240   100     544 $player->{$field} ||= 0E0;
301             }
302 80         98 for my $field (keys %{$NORMAL_FIELDS{S}}) {
  80         371  
303             $player->{$NORMAL_FIELDS{S}->{$field}} = delete $player->{$field}
304 2480 100       4565 if exists $player->{$field};
305             }
306 80 100       231 my $pos = $player->{position} eq 'G' ? 'G' : 'S';
307 80         94 for my $field (@{$LIVE_FIELDS{$pos}}) {
  80         138  
308 456   50     1094 $player->{$field} ||= 0E0;
309             }
310 80 100       162 if ($player->{position} ne 'G') {
311 72         114 $player->{faceoffTaken} = $player->{faceOffWins} + $player->{faceOffLosses};
312 72 50       179 $player->{timeOnIce} = $player->{evenTimeOnIce} + $player->{powerPlayTimeOnIce} + $player->{shortHandedTimeOnIce} if defined $player->{evenTimeOnIce};
313             }
314             }
315             }
316             }
317              
318             1;
319              
320             =head1 AUTHOR
321              
322             More Hockey Stats, C<< >>
323              
324             =head1 BUGS
325              
326             Please report any bugs or feature requests to C, or through
327             the web interface at L. I will be notified, and then you'll
328             automatically be notified of progress on your bug as I make changes.
329              
330              
331             =head1 SUPPORT
332              
333             You can find documentation for this module with the perldoc command.
334              
335             perldoc Sport::Analytics::NHL::Report::ES
336              
337             You can also look for information at:
338              
339             =over 4
340              
341             =item * RT: CPAN's request tracker (report bugs here)
342              
343             L
344              
345             =item * AnnoCPAN: Annotated CPAN documentation
346              
347             L
348              
349             =item * CPAN Ratings
350              
351             L
352              
353             =item * Search CPAN
354              
355             L
356              
357             =back