File Coverage

blib/lib/CSV/Reader.pm
Criterion Covered Total %
statement 114 149 76.5
branch 47 80 58.7
condition 13 27 48.1
subroutine 12 15 80.0
pod 7 7 100.0
total 193 278 69.4


line stmt bran cond sub pod time code
1             package CSV::Reader;
2 3     3   2774 use strict;
  3         6  
  3         151  
3 3     3   19 use Carp qw(carp croak);
  3         5  
  3         183  
4 3     3   2340 use Text::CSV ();
  3         62529  
  3         76  
5 3     3   1565 use Tie::IxHash ();
  3         13535  
  3         4764  
6             our $VERSION = 1.10;
7              
8             =head1 NAME
9              
10             CSV::Reader - CSV reader class
11              
12             =head1 DESCRIPTION
13              
14             Simple CSV reader class that uses Text::CSV internally.
15             The CSV files are expected to have a header row of column names.
16             This was designed with the idea of using an iterator interface, but Perl does not support interators (nor interfaces) yet :(
17              
18             =head1 SYNOPSIS
19              
20             use CSV::Reader ();
21             use open OUT => ':locale'; # optional; make perl aware of your terminal's encoding
22              
23             # Create reader from file name:
24             my $reader = new CSV::Reader('/path/to/file.csv');
25              
26             # Create reader from a file handle (GLOB):
27             open(my $h, '<', $filename) || die("Failed to open $filename: $!");
28             # or preferred method that can handle files having a UTF-8 BOM:
29             open(my $h, '<:via(File::BOM)', $filename) || die("Failed to open $filename: $!");
30             my $reader = new CSV::Reader($h);
31              
32             # Create reader from an IO::Handle based object:
33             my $io = IO::File->new(); # subclass of IO::Handle
34             $io->open($filename, '<:via(File::BOM)') || die("Failed to open $filename: $!");
35             my $reader = new CSV::Reader($io);
36              
37             # Create reader with advanced options:
38             my $reader = new CSV::Reader('/path/to/file.csv',
39             'delimiter' => ';',
40             'enclosure' => '',
41             'field_normalizer' => sub {
42             my $nameref = shift;
43             $$nameref = lc($$nameref); # lowercase
44             $$nameref =~ s/\s/_/g; # whitespace to underscore
45             },
46             'field_aliases' => {
47             'postal_code' => 'postcode', # applied after normalization
48             },
49             );
50              
51             # Show the field names found in the header row:
52             print 'Field names: ' . join("\n", $reader->fieldNames()) . "\n";
53              
54             # Iterate over the data rows:
55             while (my $row = $reader->nextRow()) {
56             # It's recommended to validate the $row hashref first with something such as Params::Validate.
57             # Now do whatever you want with the (validated) row hashref...
58             require Data::Dumper; local $Data::Dumper::Terse = 1;
59             print Data::Dumper::Dumper($row);
60             }
61              
62             =head1 PUBLIC STATIC METHODS
63              
64             =head2 new($file, %options)
65              
66             Constructor.
67              
68             $file can be a string file name, an open file handle (GLOB), or an IO::Handle based object (e.g. IO::File or IO::Scalar).
69             If a string file name is given, then the file is opened via File::BOM.
70              
71             The following %options are supported:
72              
73             - debug: boolean, if true, then debug messages are emitted using warn().
74             - field_aliases: hashref of case insensitive alias (in file) => real name (as expected in code) pairs.
75             - field_normalizer: optional callback that receives a field name by reference to normalize (e.g. make lowercase).
76             - include_fields: optional arrayref of field names to include. If given, then all other field names are excluded.
77             - delimiter: string, default ','
78             - enclosure: string, default '"'
79             - escape: string, default backslash
80              
81             Note: the option field_aliases is processed after the option field_normalizer if given.
82              
83             =cut
84              
85             sub new {
86 7     7 1 5071 my $proto = shift;
87 7         13 my $file = shift;
88 7         24 my %options = @_;
89 7         63 my $self = {
90             'h' => undef, # File handle.
91             'own_h' => undef, # Does this class own the file handle.
92             'field_cols' => {}, # Hashref of fieldname => column index pairs.
93             'row' => undef, # Current ReaderRow object.
94             'linenum' => 0, # Data row index.
95             'text_csv' => undef, # The Text::CSV object
96              
97             # Options:
98             'debug' => 0,
99             'delimiter' => ',',
100             'enclosure' => '"',
101             'escape' => '\\',
102             'skip_empty_lines' => 0, # TODO: implement this
103             };
104 7         14 tie(%{$self->{'field_cols'}}, 'Tie::IxHash');
  7         47  
105              
106 7 50 33     142 unless(defined($file) && length($file)) {
107 0         0 croak('Missing $file argument');
108             }
109 7 100       20 if (ref($file)) {
110 3 50 66     20 unless ((ref($file) eq 'GLOB') || UNIVERSAL::isa($file, 'IO::Handle')) {
111 0         0 croak(ref($file) . ' is not a legal file argument type');
112             }
113 3         7 $self->{'h'} = $file;
114 3         6 $self->{'own_h'} = 0;
115             }
116             else {
117 4         6 my $h;
118 4         8 eval {
119 4         1113 require File::BOM;
120             };
121 4 50       25923 my $mode = $@ ? '<' : '<:via(File::BOM)';
122 4 50       18 $options{'debug'} && warn(__PACKAGE__ . "::new file open mode is $mode\n");
123 4 50   2   130 open($h, $mode, $file) || croak('Failed to open "' . $file . '" for reading using mode "' . $mode . '": ' . $!);
  2         17  
  2         4  
  2         17  
124 4         1829 $self->{'h'} = $h;
125 4         12 $self->{'own_h'} = 1;
126             }
127              
128             # Get the options.
129 7         18 my %opt_field_aliases;
130             my $opt_field_normalizer;
131 7         0 my %opt_include_fields;
132 7 50       23 if (%options) {
133 7         23 foreach my $key (keys %options) {
134 20         58 my $value = $options{$key};
135 20 50 33     133 if (($key eq 'debug') || ($key eq 'skip_empty_lines')) {
    100 66        
    100          
    100          
    50          
    0          
136 0         0 $self->{$key} = $value;
137             }
138             elsif (($key eq 'enclosure') || ($key eq 'escape')) {
139 6 50 33     23 if (!defined($value) || ref($value)) {
140 0         0 croak("The '$key' option must be a string");
141             }
142 6         12 $self->{$key} = $value;
143             }
144             elsif ($key eq 'delimiter') {
145 6 50 33     34 if (!defined($value) || ref($value) || !length($value)) {
      33        
146 0         0 croak("The '$key' option must be a non-empty string");
147             }
148 6         13 $self->{$key} = $value;
149             }
150              
151             elsif ($key eq 'include_fields') {
152 1 50       6 if (ref($value) ne 'ARRAY') {
153 0         0 croak("The '$key' option must be an arrayref");
154             }
155 1         4 %opt_include_fields = map { $_ => undef } @$value;
  2         8  
156             }
157             elsif ($key eq 'field_aliases') {
158 7 50       22 if (ref($value) ne 'HASH') {
159 0         0 croak("The '$key' option must be a hashref");
160             }
161 7         17 %opt_field_aliases = map { lc($_) => $value->{$_} } keys %$value;
  7         40  
162             }
163             elsif ($key eq 'field_normalizer') {
164 0 0       0 if (ref($value) ne 'CODE') {
165 0         0 croak("The '$key' option must be a code ref");
166             }
167 0         0 $opt_field_normalizer = $value;
168             }
169             else {
170 0         0 croak("Unknown option '$key'");
171             }
172             }
173             }
174              
175             my $text_csv = $self->{'text_csv'} = Text::CSV->new({
176             'auto_diag' => 1,
177             'binary' => 1,
178             'blank_is_undef' => 1,
179             'empty_is_undef' => 1,
180             'sep_char' => $self->{'delimiter'},
181             'escape_char' => $self->{'escape'},
182 7         75 'quote_char' => $self->{'enclosure'},
183             });
184              
185             # Emulate the original Text::CSV error message format but without the LF and with the caller script/module.
186 7         1434 if (0) {
187             $text_csv->callbacks(
188             'error' => sub {
189 0     0   0 my ($err, $msg, $pos, $recno, $fldno) = @_; # This is dumb because the object itself is not given.
190 0 0       0 if ($err eq '2012') { # EOF
191 0         0 return;
192             }
193             #CSV_XS ERROR: 2021 - EIQ - NL char inside quotes, binary off @ rec 10 pos 51 field 6
194             #die 'error args: ' . Data::Dumper::Dumper(\@_);
195 0         0 local $Carp::CarpInternal{'Text::CSV'} = 1;
196 0         0 local $Carp::CarpInternal{'Text::CSV_PP'} = 1;
197 0         0 local $Carp::CarpInternal{'Text::CSV_XS'} = 1;
198 0         0 carp(ref($text_csv) . " ERROR: $err - $msg \@ rec $recno pos $pos field $fldno");
199 0         0 return;
200             },
201             );
202             }
203              
204             # Read header row.
205 7 50       313 if (my $row = $self->{'text_csv'}->getline($self->{'h'})) {
206             # Get the fieldname => column indices
207 7         2087 for (my $x = 0; $x < @$row; $x++) {
208 56         688 my $name = $row->[$x];
209 56 50       96 unless(defined($name)) {
210 0         0 next;
211             }
212 56         216 $name =~ s/^\s+|\s+$//g;
213 56 50       101 unless(length($name)) {
214 0         0 next;
215             }
216 56 50       86 if ($opt_field_normalizer) {
217 0         0 &$opt_field_normalizer(\$name);
218             }
219 56 50       93 if (%opt_field_aliases) {
220 56         85 my $key = lc($name);
221 56 100       117 if (defined($opt_field_aliases{$key})) {
222 7         12 $name = $opt_field_aliases{$key};
223             }
224             }
225 56 100 100     104 if (%opt_include_fields && !exists($opt_include_fields{$name})) {
226 6         12 next;
227             }
228 50 50       176 if (exists($self->{'field_cols'}->{$name})) {
229 0         0 croak('Duplicate field "' . $name . '" detected');
230             }
231 50         331 $self->{'field_cols'}->{$name} = $x;
232             }
233 7 50       86 unless(%{$self->{'field_cols'}}) {
  7         32  
234 0 0       0 croak(%opt_include_fields ? 'No fields found in header row to include' : 'No fields found in header row');
235             }
236             # If include_fields option was given, reorder keys of field_cols to match it.
237 7 100       84 if (%opt_include_fields) {
238 1         2 my %field_cols;
239             #{$self->{'field_cols'}}
240 1         6 tie(%field_cols, 'Tie::IxHash');
241 1         15 foreach my $key (@{$options{'include_fields'}}) {
  1         4  
242 2 50       24 if (exists($self->{'field_cols'}->{$key})) {
243 2         13 $field_cols{$key} = $self->{'field_cols'}->{$key};
244             }
245             }
246 1         23 $self->{'field_cols'} = \%field_cols;
247             }
248             }
249             else {
250 0         0 croak('No header line found in CSV');
251             }
252              
253             # Check that all the required header fields are present.
254 7 100       17 if (%opt_include_fields) {
255 1         2 my @missing;
256 1         4 foreach my $name (keys %opt_include_fields) {
257 2 50       10 if (!exists($self->{'field_cols'}->{$name})) {
258 0         0 push(@missing, $name);
259             }
260             }
261 1 50       7 if (@missing) {
262 0         0 croak('The following column headers are missing: ' . join(', ', @missing));
263             }
264             }
265 7   33     39 bless($self, ref($proto) || $proto);
266 7         44 return $self;
267             }
268              
269              
270              
271              
272             =head2 DESTROY
273              
274             Closes the private file handle, if any.
275              
276             =cut
277              
278             sub DESTROY {
279 7     7   5046 my $self = shift;
280 7 100       43 if ($self->{'own_h'}) {
281 4         29 close($self->{'h'});
282             }
283             }
284              
285              
286              
287              
288              
289              
290             =head1 PROTECTED OBJECT METHODS
291              
292             =head2 _read()
293              
294             Reads the next CSV data row and sets internal variables.
295              
296             =cut
297              
298             sub _read {
299 32     32   47 my $self = shift;
300 32 100       921 if (my $csv_row = $self->{'text_csv'}->getline($self->{'h'})) {
301 24 50       1095 if ($self->{'debug'}) {
302 0         0 require Data::Dumper;
303 0         0 local $Data::Dumper::Terse = 1;
304 0         0 warn(__PACKAGE__ . '::_read ' . Data::Dumper::Dumper($csv_row));
305             }
306 24         102 tie(my %row, 'Tie::IxHash');
307 24         367 my $field_cols = $self->{'field_cols'}; # name to index map
308 24         61 foreach my $k ($self->fieldNames()) {
309 174         3057 my $i = $field_cols->{$k};
310 174         1024 my $v = $csv_row->[$i];
311 174 100       317 if (defined($v)) {
312 109         321 $v =~ s/^\s+|\s+$//g;
313 109 50       207 unless(length($v)) {
314 0         0 $v = undef;
315             }
316             }
317 174         482 $row{$k} = $v;
318             }
319 24         407 $self->{'row'} = \%row;
320 24         55 $self->{'linenum'}++;
321             }
322             else {
323 8         680 $self->{'row'} = undef;
324 8         19 $self->{'linenum'} = 0;
325             }
326             }
327              
328              
329              
330              
331              
332              
333             =head1 PUBLIC OBJECT METHODS
334              
335             =head2 fieldNames()
336              
337             Returns the field names as an array.
338              
339             =cut
340              
341             sub fieldNames {
342 26     26 1 2166 my $self = shift;
343 26         32 return keys(%{$self->{'field_cols'}});
  26         104  
344             }
345              
346              
347              
348              
349              
350             =head2 current()
351              
352             Returns the current row.
353              
354             =cut
355              
356             sub current {
357 0     0 1 0 my $self = shift;
358 0         0 return $self->{'row'};
359             }
360              
361              
362              
363              
364              
365             =head2 linenum()
366              
367             Returns the current row index.
368              
369             =cut
370              
371             sub linenum {
372 0     0 1 0 my $self = shift;
373 0         0 return $self->{'linenum'};
374             }
375              
376              
377              
378              
379              
380             =head2 nextRow()
381              
382             Reads the next row.
383              
384             =cut
385              
386             sub nextRow {
387 32     32 1 16275 my $self = shift;
388 32         80 $self->_read();
389 32         98 return $self->{'row'};
390             }
391              
392              
393              
394              
395              
396             =head2 eof()
397              
398             Returns boolean
399              
400             =cut
401              
402             sub eof {
403 20     20 1 77 my $self = shift;
404 20         58 return $self->{'text_csv'}->eof();
405             }
406              
407              
408              
409              
410             =head2 rewind()
411              
412             Rewinds the file handle.
413              
414             =cut
415              
416             sub rewind {
417 1     1 1 7 my $self = shift;
418 1 50       6 seek($self->{'h'},0,0) || croak('Failed to rewind file handle');
419 1         49 $self->{'text_csv'}->getline($self->{'h'}); # skip the header row
420             }
421              
422              
423              
424             1;
425              
426              
427             __END__