File Coverage

blib/lib/Flat/Profile.pm
Criterion Covered Total %
statement 112 120 93.3
branch 54 76 71.0
condition 14 21 66.6
subroutine 8 8 100.0
pod 3 3 100.0
total 191 228 83.7


line stmt bran cond sub pod time code
1             package Flat::Profile;
2              
3 12     12   1819408 use strict;
  12         25  
  12         455  
4 12     12   66 use warnings;
  12         23  
  12         1299  
5              
6             our $VERSION = '0.02';
7              
8 12     12   83 use Carp qw(croak);
  12         20  
  12         713  
9              
10 12     12   5947 use Flat::Profile::Iterator;
  12         60  
  12         17655  
11              
12             sub new {
13 11     11 1 2450672 my ($class, %opts) = @_;
14              
15 11         90 my $self = bless {
16             _opts => { %opts },
17             }, $class;
18              
19 11         50 return $self;
20             }
21              
22             sub profile_file {
23 8     8 1 6641 my ($self, %args) = @_;
24              
25 8 50       52 if (!exists $args{path}) {
26 0         0 croak "profile_file() requires named argument: path";
27             }
28              
29 8         24 my $path = $args{path};
30              
31 8 100       48 my $delimiter = exists $args{delimiter} ? $args{delimiter} : ",";
32 8 50 33     47 if ($delimiter ne "," && $delimiter ne "\t") {
33 0         0 croak "profile_file() delimiter must be ',' or \"\\t\"";
34             }
35              
36 8 50       36 my $has_header = $args{has_header} ? 1 : 0;
37              
38 8 50       39 my $encoding = exists $args{encoding} ? $args{encoding} : "UTF-8";
39              
40 8 100       31 my $example_cap = exists $args{example_cap} ? $args{example_cap} : 10;
41 8 50 33     155 if ($example_cap !~ /^\d+$/ || $example_cap < 0) {
42 0         0 croak "profile_file() example_cap must be an integer >= 0";
43             }
44              
45 8 100       47 my $null_empty = exists $args{null_empty} ? ($args{null_empty} ? 1 : 0) : 1;
    100          
46              
47 8 100       39 my $null_tokens = exists $args{null_tokens} ? $args{null_tokens} : [];
48 8 50       42 if (ref($null_tokens) ne 'ARRAY') {
49 0         0 croak "profile_file() null_tokens must be an arrayref";
50             }
51              
52 8         27 my %null_token_map;
53 8         36 for my $tok (@{$null_tokens}) {
  8         36  
54 1 50       6 if (!defined $tok) {
55 0         0 croak "profile_file() null_tokens must not contain undef";
56             }
57 1         5 $null_token_map{$tok} = 1;
58             }
59              
60 8 50       1072 open my $fh, "<:encoding($encoding)", $path
61             or croak "Failed to open '$path' for reading: $!";
62              
63 8         750 my $it = Flat::Profile::Iterator->new(
64             fh => $fh,
65             delimiter => $delimiter,
66             has_header => $has_header,
67             );
68              
69 8         39 my $generated_at = _format_utc_timestamp();
70              
71             my %report = (
72             report_version => 1,
73              
74             generated_at => $generated_at,
75             perl_version => $],
76             module_version => $VERSION,
77              
78             path => $path,
79             delimiter => $delimiter,
80             encoding => $encoding,
81             has_header => $has_header ? 1 : 0,
82             null_empty => $null_empty ? 1 : 0,
83 8 50       55 null_tokens => [ @{$null_tokens} ],
  8 100       191  
84             header => undef,
85              
86             rows => 0,
87             columns => [],
88              
89             expected_width => undef,
90             max_observed_width => 0,
91             ragged => {
92             short_rows => 0,
93             long_rows => 0,
94             short_examples => [],
95             long_examples => [],
96             example_cap => 10,
97             },
98             );
99              
100 8         21 my $ragged_example_cap = 10;
101 8         19 my $header_captured = 0;
102              
103 8         60 while (my $row = $it->next_row) {
104 20         46 $report{rows}++;
105              
106 20 100 66     124 if ($has_header && !$header_captured) {
107 8         39 $report{header} = $it->get_Header;
108 8         15 $header_captured = 1;
109              
110 8 50       36 if (defined $report{header}) {
111 8         19 $report{expected_width} = scalar @{$report{header}};
  8         25  
112             }
113             }
114              
115 20         34 my $width = scalar @{$row};
  20         50  
116 20 100       62 if ($width > $report{max_observed_width}) {
117 9         20 $report{max_observed_width} = $width;
118             }
119              
120 20 50       69 if (!defined $report{expected_width}) {
121 0         0 $report{expected_width} = $width;
122             }
123              
124 20 50       52 if (defined $report{expected_width}) {
125 20 100       91 if ($width < $report{expected_width}) {
    100          
126 1         3 $report{ragged}{short_rows}++;
127              
128 1 50       2 if (@{$report{ragged}{short_examples}} < $ragged_example_cap) {
  1         16  
129 1         6 push @{$report{ragged}{short_examples}}, {
130             row_number => $report{rows},
131 1         2 width => $width,
132             };
133             }
134             }
135             elsif ($width > $report{expected_width}) {
136 1         26 $report{ragged}{long_rows}++;
137              
138 1 50       3 if (@{$report{ragged}{long_examples}} < $ragged_example_cap) {
  1         5  
139 1         5 push @{$report{ragged}{long_examples}}, {
140             row_number => $report{rows},
141 1         2 width => $width,
142             };
143             }
144             }
145             }
146              
147 20         36 my $num_cols = $width;
148              
149 20         59 for (my $i = 0; $i < $num_cols; $i++) {
150 28         59 my $value = $row->[$i];
151              
152 28         50 my $col = $report{columns}->[$i];
153 28 100       67 if (!defined $col) {
154 12         110 $col = {
155             index => $i,
156             count_values => 0,
157             count_null => 0,
158             count_nonnull => 0,
159             min_length => undef,
160             max_length => undef,
161             sample_values => [],
162             _sample_seen => {},
163             };
164 12         34 $report{columns}->[$i] = $col;
165             }
166              
167 28         49 $col->{count_values}++;
168              
169             my $is_null =
170             !defined $value
171             || ($null_empty && defined $value && $value eq '')
172 28   100     335 || (defined $value && $null_token_map{$value});
173              
174 28 100       121 if ($is_null) {
175 3         6 $col->{count_null}++;
176 3         22 next;
177             }
178              
179 25         58 $col->{count_nonnull}++;
180              
181 25         47 my $len = length($value);
182              
183 25 100 100     142 if (!defined $col->{min_length} || $len < $col->{min_length}) {
184 13         29 $col->{min_length} = $len;
185             }
186 25 100 100     111 if (!defined $col->{max_length} || $len > $col->{max_length}) {
187 14         26 $col->{max_length} = $len;
188             }
189              
190 25 50       69 if ($example_cap > 0) {
191 25 100       48 if (@{$col->{sample_values}} < $example_cap) {
  25         93  
192 23 50       57 if (!$col->{_sample_seen}{$value}) {
193 23         39 push @{$col->{sample_values}}, $value;
  23         69  
194 23         140 $col->{_sample_seen}{$value} = 1;
195             }
196             }
197             }
198             }
199             }
200              
201 8         40 for my $col (@{$report{columns}}) {
  8         27  
202 12 50       63 next if !defined $col;
203 12         55 delete $col->{_sample_seen};
204             }
205              
206 8         40 $report{ragged}{example_cap} = $ragged_example_cap;
207              
208 8         337 return \%report;
209             }
210              
211             sub iter_rows {
212 4     4 1 50 my ($self, %args) = @_;
213              
214 4 50       23 if (!exists $args{path}) {
215 0         0 croak "iter_rows() requires named argument: path";
216             }
217              
218 4         12 my $path = $args{path};
219              
220 4 50       22 my $delimiter = exists $args{delimiter} ? $args{delimiter} : ",";
221 4 50 33     24 if ($delimiter ne "," && $delimiter ne "\t") {
222 0         0 croak "iter_rows() delimiter must be ',' or \"\\t\"";
223             }
224              
225 4 100       15 my $has_header = $args{has_header} ? 1 : 0;
226              
227 4 50       17 my $encoding = exists $args{encoding} ? $args{encoding} : "UTF-8";
228              
229 4 50       185 open my $fh, "<:encoding($encoding)", $path
230             or croak "Failed to open '$path' for reading: $!";
231              
232 4         380 return Flat::Profile::Iterator->new(
233             fh => $fh,
234             delimiter => $delimiter,
235             has_header => $has_header,
236             );
237             }
238              
239             sub _format_utc_timestamp {
240 8     8   57 my @t = gmtime(time());
241 8         27 my $year = $t[5] + 1900;
242 8         23 my $mon = $t[4] + 1;
243 8         15 my $day = $t[3];
244 8         17 my $hour = $t[2];
245 8         17 my $min = $t[1];
246 8         50 my $sec = $t[0];
247              
248 8         80 return sprintf("%04d-%02d-%02dT%02d:%02d:%02dZ", $year, $mon, $day, $hour, $min, $sec);
249             }
250              
251             1;
252              
253             __END__