File Coverage

blib/lib/App/LogStats.pm
Criterion Covered Total %
statement 229 234 97.8
branch 85 94 90.4
condition 20 30 66.6
subroutine 30 31 96.7
pod 1 1 100.0
total 365 390 93.5


line stmt bran cond sub pod time code
1             package App::LogStats;
2 9     9   556712 use strict;
  9         23  
  9         449  
3 9     9   53 use warnings;
  9         17  
  9         260  
4 9     9   48 use File::Spec;
  9         22  
  9         306  
5 9     9   12576 use Getopt::Long qw/GetOptionsFromArray/;
  9         146665  
  9         59  
6 9     9   17600 use IO::Interactive::Tiny;
  9         89  
  9         596  
7              
8             our $VERSION = '0.091';
9              
10             our $DEFAULT_RCFILE_NAME = '.statsrc';
11              
12             use Class::Accessor::Lite (
13 9         78 new => 1,
14             rw => [qw/
15             result
16             config
17             /],
18 9     9   7825 );
  9         18633  
19              
20             our @RESULT_LIST = (qw/
21             count sum _line_
22             average median mode _line_
23             max min range variance stddev
24             /);
25             our %MORE_RESULT = (
26             median => 1,
27             mode => 1,
28             variance => 1,
29             stddev => 1,
30             );
31              
32             our @DRAW_TABLE = (
33             [' ',' ','-',' '],
34             [' ',' ',' '],
35             [' ',' ','-',' '],
36             [' ',' ',' '],
37             [' ',' ','-',' '],
38             [' ',' ','-',' '],
39             );
40              
41             sub run {
42 26     26 1 55730 my $self = shift;
43 26         109 $self->_prepare(\@_)->_main->_finalize;
44             }
45              
46             sub _prepare {
47 26     26   59 my ($self, $argv) = @_;
48              
49 26         84 my $config = $self->_read_rc( $self->_rc_file($argv) );
50 26         110 $self->_merge_opt($config, $argv);
51              
52 24         103 $self->config($config);
53              
54 24         245 $self;
55             }
56              
57             sub _rc_file {
58 26     26   41 my ($self, $argv) = @_;
59              
60 26         39 my $rc = 0;
61 26         41 for my $opt (@{$argv}) {
  26         74  
62 52 50       157 if ($opt =~ m!--rc=([^\s]+)!) {
63 0         0 return $1;
64             }
65 52 100       123 return $opt if $rc == 1;
66 51 100       153 $rc = 1 if $opt eq '--rc';
67             }
68 25         112 return $DEFAULT_RCFILE_NAME;
69             }
70              
71             sub _read_rc {
72 26     26   51 my ($self, $rc_file) = @_;
73              
74 26         37 my %config;
75              
76 26         94 for my $dir ('/etc/', $ENV{STATSRC_DIR}, $ENV{HOME}, '.') {
77 104 100       225 next unless $dir;
78 78         765 my $file = File::Spec->catfile($dir, $rc_file);
79 78 100       1172 next unless -e $file;
80 1         9 $self->_parse_rc($file => \%config);
81             }
82              
83 26         77 return \%config;
84             }
85              
86             sub _parse_rc {
87 1     1   7 my ($self, $file, $config) = @_;
88              
89 1         56 open my $fh, '<', $file;
90 1         28 while (<$fh>) {
91 2         9 chomp;
92 2 50       36 next if /\A\s*\Z/sm;
93 2 50       23 if (/\A(\w+):\s*(.+)\Z/sm) {
94 2         12 my ($key, $value) = ($1, $2);
95 2 100       11 if ($key eq 'file') {
96 1         3 push @{$config->{$key}}, $value;
  1         12  
97             }
98             else {
99 1         17 $config->{$key} = $value;
100             }
101             }
102             }
103 1         28 close $fh;
104             }
105              
106             sub _merge_opt {
107 26     26   48 my ($self, $config, $argv) = @_;
108              
109 26         131 Getopt::Long::Configure('bundling');
110             GetOptionsFromArray(
111             $argv,
112             'file=s@' => \$config->{file},
113             'd|delimiter=s' => \$config->{delimiter},
114             'f|fields=s' => \$config->{fields},
115             't|through' => \$config->{through},
116             'di|digit=i' => \$config->{digit},
117             's|strict' => \$config->{strict},
118             'no-comma' => \$config->{no_comma},
119             'tsv' => \$config->{tsv},
120             'csv' => \$config->{csv},
121             'more' => \$config->{more},
122             'cr' => \$config->{cr},
123             'crlf' => \$config->{crlf},
124             'rc=s' => \$config->{rc},
125             'h|help' => sub {
126 1     1   1190 $self->_show_usage(1);
127             },
128             'v|version' => sub {
129 0     0   0 print "stats v$App::LogStats::VERSION\n";
130 0         0 exit 1;
131             },
132 26 100       1354 ) or $self->_show_usage(2);
133              
134 24         25385 push @{$config->{file}}, @{$argv};
  24         60  
  24         56  
135              
136 24         89 $self->_validate_config($config);
137             }
138              
139             sub _show_usage {
140 3     3   1362 my ($self, $exitval) = @_;
141              
142 3         27 require Pod::Usage;
143 3         23 Pod::Usage::pod2usage($exitval);
144             }
145              
146             sub _validate_config {
147 24     24   50 my ($self, $config) = @_;
148              
149 24 100 66     96 if (!$config->{digit} || $config->{digit} !~ m!^\d+$!) {
150 23         35 $config->{digit} = 2;
151             }
152              
153 24 100       79 $config->{delimiter} = "\t" unless defined $config->{delimiter};
154              
155 24 100       61 if ($config->{fields}) {
156 9         66 for my $f ( split ',', $config->{fields} ) {
157 19         63 $config->{field}->{$f} = 1;
158             }
159 9         35 delete $config->{fields};
160             }
161             else {
162 15         64 $config->{field}->{1} = 1;
163             }
164             }
165              
166             sub _main {
167 24     24   45 my $self = shift;
168              
169 24         43 my $r = +{};
170              
171 24 50       117 if ( ! IO::Interactive::Tiny::is_interactive(*STDIN) ) {
  24 100       142  
172              
173 0         0 while ( my $line = ) {
174 0         0 $self->_loop(\$line => $r);
175             }
176              
177             }
178             elsif ( scalar @{ $self->config->{file} } ) {
179              
180 22         150 for my $file (@{$self->config->{file}}) {
  22         60  
181 22 100       1027 open my $fh, '<', $file or die "$file: No such file";
182 21         460 while ( my $line = <$fh> ) {
183 157         1309 $self->_loop(\$line => $r);
184             }
185 21         554 close $fh;
186             }
187              
188             }
189              
190 23         94 $self->_after_calc($r);
191              
192 23         213 $self->result($r);
193 23         172 $self;
194             }
195              
196             sub _loop {
197 157     157   221 my ($self, $line_ref, $r) = @_;
198              
199 157         196 my $line = $$line_ref;
200              
201 157 100       373 print $line if $self->config->{through};
202 157         1020 chomp $line;
203 157 100       319 return unless $line;
204 154         375 $self->_calc_line($r, [ split $self->config->{delimiter}, $line ]);
205             }
206              
207             sub _calc_line {
208 154     154   1044 my ($self, $r, $elements) = @_;
209              
210 154         336 my $strict = $self->config->{strict};
211 154         596 my $i = 0;
212 154         170 for my $element (@{$elements}) {
  154         285  
213 198         422 $i++;
214 198 100       452 next unless $self->config->{field}{$i};
215 188 50 66     1845 if ( (!$strict && $element =~ m!\d!)
      33        
      66        
216             || ($strict && $element =~ m!^(\d+\.?(:?\d+)?)$!) ) {
217 188         695 my ($num) = ($element =~ m!^(\d+\.?(:?\d+)?)!);
218 188   50     408 $num ||= 0; # FIXME
219 188         314 $r->{$i}{count}++;
220 188         369 $r->{$i}{sum} += $num;
221 188 50 66     935 $r->{$i}{max} = $num
222             if !defined $r->{$i}{max} || $num > $r->{$i}{max};
223 188 100 66     793 $r->{$i}{min} = $num
224             if !defined $r->{$i}{min} || $num < $r->{$i}{min};
225 188 100       455 push @{$r->{$i}{list}}, $num if $self->config->{more};
  26         290  
226             }
227             }
228             }
229              
230             sub _after_calc {
231 23     23   43 my ($self, $r) = @_;
232              
233 23         29 for my $i (keys %{$r}) {
  23         77  
234 29 50       107 next unless $r->{$i}{count};
235 29   100     112 $r->{show_result} ||= 1;
236 29         103 $r->{$i}{average} = $r->{$i}{sum} / $r->{$i}{count};
237 29         91 $r->{$i}{range} = $r->{$i}{max} - $r->{$i}{min};
238 29 100       73 if ($self->config->{more}) {
239 4         31 $r->{$i}{median} = $self->_calc_median($i, $r);
240 4         14 $r->{$i}{mode} = $self->_calc_mode($i, $r);
241 4         23 $r->{$i}{variance} = $self->_calc_variance($i, $r);
242 4         16 $r->{$i}{stddev} = $self->_calc_stddev($i, $r);
243             }
244             }
245             }
246              
247             sub _calc_median {
248 14     14   3429 my ($self, $i, $r) = @_;
249              
250 14         29 my $list = $r->{$i}{list};
251              
252 14 50       40 return unless ref $list eq 'ARRAY';
253 14 100       18 return $list->[0] unless @{$list} > 1;
  14         42  
254 12         17 @{$list} = sort { $a <=> $b } @{$list};
  12         40  
  66         78  
  12         42  
255 12         16 my $element_count = scalar(@{$list});
  12         20  
256 12 100       32 return $list->[ $#{$list} / 2 ] if $element_count & 1;
  6         28  
257 6         13 my $mid = $element_count / 2;
258 6         25 return ( $list->[ $mid - 1 ] + $list->[ $mid ] ) / 2;
259             }
260              
261             sub _calc_mode {
262 12     12   6744 my ($self, $i, $r) = @_;
263              
264 12         24 my $list = $r->{$i}{list};
265              
266 12 50       39 return unless ref $list eq 'ARRAY';
267 12 100       14 return $list->[0] unless @{$list} > 1;
  12         36  
268 10         15 my %hash;
269 10         12 $hash{$_}++ for @{$list};
  10         70  
270 10         42 my $max_val = ( sort { $b <=> $a } values %hash )[0];
  56         71  
271 10         66 for my $key (keys %hash) {
272 41 100       162 delete $hash{$key} unless $hash{$key} == $max_val;
273             }
274 10         47 return _calc_average([keys %hash]);
275             }
276              
277             sub _calc_variance {
278 18     18   48 my ($self, $i, $r) = @_;
279              
280 18         35 my $list = $r->{$i}{list};
281              
282 18 100       20 return 0 unless @{$list} > 1;
  18         51  
283 16         38 my $average = $r->{$i}{average};
284 16         33 return _calc_sum([ map { ($_ - $average) ** 2 } @{$list} ]) / $#{$list};
  62         194  
  16         35  
  16         50  
285             }
286              
287             sub _calc_stddev {
288 12     12   41 my ($self, $i, $r) = @_;
289              
290 12 100       17 return 0 unless @{$r->{$i}{list}} > 1;
  12         49  
291 10 100       42 my $variance = defined $r->{$i}{variance}
292             ? $r->{$i}{variance} : $self->_calc_variance($i, $r);
293 10         52 return sqrt($variance);
294             }
295              
296             sub _calc_average {
297 26     26   7277 my $list = shift;
298              
299 26         39 my $sum = 0;
300 26         35 for my $i (@{$list}) {
  26         51  
301 77         121 $sum += $i;
302             }
303 26         41 return $sum / scalar(@{$list});
  26         135  
304             }
305              
306             sub _calc_sum {
307 16     16   26 my $list = shift;
308              
309 16         22 my $sum = 0;
310 16         21 $sum += $_ for (@{$list});
  16         76  
311              
312 16         35 return $sum;
313             }
314              
315             sub _finalize {
316 23     23   41 my $self = shift;
317              
318 23 100       56 return unless $self->result->{show_result};
319              
320 21         106 my $output_lines;
321 21 100       54 if ($self->config->{tsv}) {
    100          
322 2         13 $output_lines = $self->_view_delimited_line("\t");
323             }
324             elsif ($self->config->{csv}) {
325 2         25 $output_lines = $self->_view_delimited_line(',');
326             }
327             else {
328 17         180 $output_lines = $self->_view_table;
329             }
330              
331 21 100       54750 my $lf = $self->config->{cr} ? "\r" : $self->config->{crlf} ? "\r\n" : "\n";
    100          
332              
333 21         1652 print $lf;
334 21         44 for my $line ( @{$output_lines} ) {
  21         53  
335 244         2445 print $line, $lf;
336             }
337             }
338              
339             sub _view_delimited_line {
340 4     4   7 my ($self, $delimiter) = @_;
341              
342 4         7 my @fields = sort keys %{$self->config->{field}};
  4         10  
343 4         29 my @output;
344 4         9 push @output, join($delimiter, '', map { $self->_quote($_) } @fields);
  6         21  
345 4         24 for my $col (@RESULT_LIST) {
346 48 100 66     175 next if !$self->config->{more} && $MORE_RESULT{$col};
347 32 100       272 next if $col eq '_line_';
348 24         43 my @rows = ( $self->_quote($col) );
349 24         84 for my $i (@fields) {
350 36         111 push @rows, $self->_quote( $self->_facing($self->result->{$i}{$col}) );
351             }
352 24         122 push @output, join($delimiter, @rows);
353             }
354 4         34 return \@output;
355             }
356              
357             sub _view_table {
358 17     17   22 my $self = shift;
359              
360 17         26 my @fields = sort keys %{$self->config->{field}};
  17         42  
361              
362 17         2502 require Text::ASCIITable;
363 17         293119 my $t = Text::ASCIITable->new;
364 17         728 $t->setCols('', @fields);
365 17         993 for my $col (@RESULT_LIST) {
366 204 100 100     19018 next if !$self->config->{more} && $MORE_RESULT{$col};
367 148 100       1166 if ($col eq '_line_') {
368 34         101 $t->addRowLine;
369 34         432 next;
370             }
371 114         269 my @rows;
372 114         157 for my $i (@fields) {
373 166         404 push @rows, $self->_facing($self->result->{$i}{$col});
374             }
375 114         326 $t->addRow($col, @rows);
376             }
377 17         562 return [ split( "\n", $t->draw(@DRAW_TABLE) ) ];
378             }
379              
380             sub _quote {
381 66     66   86 my ($self, $value, $quote) = @_;
382              
383 66 100       138 return $value unless $self->config->{csv};
384 33   50     207 $quote ||= '"';
385 33         104 return "$quote$value$quote";
386             }
387              
388             sub _facing {
389 202     202   948 my ($self, $value) = @_;
390              
391 202 100       412 return '-' unless defined $value;
392              
393 190 100       870 if ($value =~ m!\.!) {
394 25         72 $value = sprintf("%.". $self->config->{digit}. 'f', $value);
395             }
396              
397 190 100       718 unless ($self->config->{no_comma}) {
398 158         1029 my ($n, $d) = split /\./, $value;
399 158         446 while ( $n =~ s!(.*\d)(\d\d\d)!$1,$2! ){};
400 158 100       1387 $value = $d ? "$n\.$d" : $n;
401             }
402              
403 190         625 return $value;
404             }
405              
406             1;
407              
408             __END__