File Coverage

blib/lib/CSV/Reader.pm
Criterion Covered Total %
statement 130 162 80.2
branch 60 94 63.8
condition 13 27 48.1
subroutine 13 15 86.6
pod 7 7 100.0
total 223 305 73.1


line stmt bran cond sub pod time code
1             package CSV::Reader;
2 4     4   3564 use strict;
  4         10  
  4         135  
3 4     4   21 use Carp qw(carp croak);
  4         7  
  4         210  
4 4     4   3022 use Text::CSV ();
  4         83853  
  4         100  
5 4     4   2045 use Tie::IxHash ();
  4         18390  
  4         6900  
6             our $VERSION = 1.11;
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             'mutators' => {
50             'postcode' => sub { # if postalcode is Dutch, then make sure it has no spaces and is in uppercase.
51             my $val_ref = shift;
52             my $row_ref = shift;
53             if (defined($$val_ref) && defined($row_ref->{'country'}) && ($row_ref->{'country'} eq 'NL')) {
54             $$val_ref =~ s/\s+//;
55             $$val_ref = uc($$val_ref);
56             }
57             },
58             'has_fiber_internet' => sub { # set a default for an empty (undef) value
59             my $val_ref = shift;
60             $$val_ref //= 0;
61             },
62             },
63             );
64              
65             # Show the field names found in the header row:
66             print 'Field names: ' . join("\n", $reader->fieldNames()) . "\n";
67              
68             # Iterate over the data rows:
69             while (my $row = $reader->nextRow()) {
70             # It's recommended to validate the $row hashref first with something such as Params::Validate.
71             # Now do whatever you want with the (validated) row hashref...
72             require Data::Dumper; local $Data::Dumper::Terse = 1;
73             print Data::Dumper::Dumper($row);
74             }
75              
76             =head1 PUBLIC STATIC METHODS
77              
78             =head2 new($file, %options)
79              
80             Constructor.
81              
82             $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).
83             If a string file name is given, then the file is opened via File::BOM.
84              
85             The following %options are supported:
86              
87             - debug: boolean, if true, then debug messages are emitted using warn().
88             - field_aliases: hashref of case insensitive alias (in file) => real name (as expected in code) pairs.
89             - field_normalizer: callback that receives a field name by reference to normalize (e.g. make lowercase).
90             - include_fields: arrayref of field names to include. If given, then all other field names are excluded.
91             - delimiter: string, default ','
92             - enclosure: string, default '"'
93             - escape: string, default backslash
94             - mutators: hashref of field name => callback($value_ref, $row_ref) pairs.
95              
96             Note: the option field_aliases is processed after the option field_normalizer if given.
97              
98             Note: the callbacks given with the mutators option are called in their key order (which is an unpredictable order unless they're tied with Tie::IxHash).
99              
100             =cut
101              
102             sub new {
103 8     8 1 5711 my $proto = shift;
104 8         16 my $file = shift;
105 8         29 my %options = @_;
106 8         72 my $self = {
107             'h' => undef, # File handle.
108             'own_h' => undef, # Does this class own the file handle.
109             'field_cols' => {}, # Hashref of fieldname => column index pairs.
110             'row' => undef, # Current ReaderRow object.
111             'linenum' => 0, # Data row index.
112             'text_csv' => undef, # The Text::CSV object
113              
114             # Options:
115             'debug' => 0,
116             'delimiter' => ',',
117             'enclosure' => '"',
118             'escape' => '\\',
119             'mutators' => undef,
120             'skip_empty_lines' => 0, # TODO: implement this
121             };
122 8         16 tie(%{$self->{'field_cols'}}, 'Tie::IxHash');
  8         46  
123              
124 8 50 33     163 unless(defined($file) && length($file)) {
125 0         0 croak('Missing $file argument');
126             }
127 8 100       29 if (ref($file)) {
128 3 50 66     14 unless ((ref($file) eq 'GLOB') || UNIVERSAL::isa($file, 'IO::Handle')) {
129 0         0 croak(ref($file) . ' is not a legal file argument type');
130             }
131 3         7 $self->{'h'} = $file;
132 3         5 $self->{'own_h'} = 0;
133             }
134             else {
135 5         8 my $h;
136 5         10 eval {
137 5         1693 require File::BOM;
138             };
139 5 50       38474 my $mode = $@ ? '<' : '<:via(File::BOM)';
140 5 50       23 $options{'debug'} && warn(__PACKAGE__ . "::new file open mode is $mode\n");
141 5 50   3   149 open($h, $mode, $file) || croak('Failed to open "' . $file . '" for reading using mode "' . $mode . '": ' . $!);
  3         21  
  3         7  
  3         23  
142 5         2735 $self->{'h'} = $h;
143 5         15 $self->{'own_h'} = 1;
144             }
145              
146             # Get the options.
147 8         24 my %opt_field_aliases;
148             my $opt_field_normalizer;
149 8         0 my %opt_include_fields;
150 8 50       23 if (%options) {
151 8         28 foreach my $key (keys %options) {
152 25         77 my $value = $options{$key};
153 25 50 33     171 if (($key eq 'debug') || ($key eq 'skip_empty_lines')) {
    100 66        
    100          
    100          
    100          
    100          
    50          
154 0         0 $self->{$key} = $value;
155             }
156             elsif (($key eq 'enclosure') || ($key eq 'escape')) {
157 7 50 33     29 if (!defined($value) || ref($value)) {
158 0         0 croak("The '$key' option must be a string");
159             }
160 7         16 $self->{$key} = $value;
161             }
162             elsif ($key eq 'delimiter') {
163 7 50 33     40 if (!defined($value) || ref($value) || !length($value)) {
      33        
164 0         0 croak("The '$key' option must be a non-empty string");
165             }
166 7         15 $self->{$key} = $value;
167             }
168              
169             elsif ($key eq 'include_fields') {
170 1 50       6 if (ref($value) ne 'ARRAY') {
171 0         0 croak("The '$key' option must be an arrayref");
172             }
173 1         4 %opt_include_fields = map { $_ => undef } @$value;
  2         8  
174             }
175             elsif ($key eq 'field_aliases') {
176 8 50       24 if (ref($value) ne 'HASH') {
177 0         0 croak("The '$key' option must be a hashref");
178             }
179 8         21 %opt_field_aliases = map { lc($_) => $value->{$_} } keys %$value;
  8         45  
180             }
181             elsif ($key eq 'field_normalizer') {
182 1 50       3 if (ref($value) ne 'CODE') {
183 0         0 croak("The '$key' option must be a code ref");
184             }
185 1         3 $opt_field_normalizer = $value;
186             }
187             elsif ($key eq 'mutators') {
188 1 50       4 if (ref($value) ne 'HASH') {
189 0         0 croak("The '$key' option must be a hashref of field name => code ref pairs");
190             }
191 1         4 foreach my $name (keys %$value) {
192 2         3 my $mutator = $options{$key}->{$name};
193 2 50       7 if (defined($mutator)) {
194 2 50       8 unless (ref($mutator) eq 'CODE') {
195 0         0 croak('The mutator for "' . $name . '" must be a CODE ref');
196             }
197             }
198             }
199 1         2 $self->{$key} = $value;
200             }
201             else {
202 0         0 croak("Unknown option '$key'");
203             }
204             }
205             }
206              
207             my $text_csv = $self->{'text_csv'} = Text::CSV->new({
208             'auto_diag' => 1,
209             'binary' => 1,
210             'blank_is_undef' => 1,
211             'empty_is_undef' => 1,
212             'sep_char' => $self->{'delimiter'},
213             'escape_char' => $self->{'escape'},
214 8         79 'quote_char' => $self->{'enclosure'},
215             });
216              
217             # Emulate the original Text::CSV error message format but without the LF and with the caller script/module.
218 8         1666 if (0) {
219             $text_csv->callbacks(
220             'error' => sub {
221 0     0   0 my ($err, $msg, $pos, $recno, $fldno) = @_; # This is dumb because the object itself is not given.
222 0 0       0 if ($err eq '2012') { # EOF
223 0         0 return;
224             }
225             #CSV_XS ERROR: 2021 - EIQ - NL char inside quotes, binary off @ rec 10 pos 51 field 6
226             #die 'error args: ' . Data::Dumper::Dumper(\@_);
227 0         0 local $Carp::CarpInternal{'Text::CSV'} = 1;
228 0         0 local $Carp::CarpInternal{'Text::CSV_PP'} = 1;
229 0         0 local $Carp::CarpInternal{'Text::CSV_XS'} = 1;
230 0         0 carp(ref($text_csv) . " ERROR: $err - $msg \@ rec $recno pos $pos field $fldno");
231 0         0 return;
232             },
233             );
234             }
235              
236             # Read header row.
237 8 50       322 if (my $row = $self->{'text_csv'}->getline($self->{'h'})) {
238             # Get the fieldname => column indices
239 8         2336 for (my $x = 0; $x < @$row; $x++) {
240 64         762 my $name = $row->[$x];
241 64 50       161 unless(defined($name)) {
242 0         0 next;
243             }
244 64         253 $name =~ s/^\s+|\s+$//g;
245 64 50       124 unless(length($name)) {
246 0         0 next;
247             }
248 64 100       106 if ($opt_field_normalizer) {
249 8         16 &$opt_field_normalizer(\$name);
250             }
251 64 50       166 if (%opt_field_aliases) {
252 64         107 my $key = lc($name);
253 64 100       137 if (defined($opt_field_aliases{$key})) {
254 8         28 $name = $opt_field_aliases{$key};
255             }
256             }
257 64 100 100     128 if (%opt_include_fields && !exists($opt_include_fields{$name})) {
258 6         14 next;
259             }
260 58 50       216 if (exists($self->{'field_cols'}->{$name})) {
261 0         0 croak('Duplicate field "' . $name . '" detected');
262             }
263 58         387 $self->{'field_cols'}->{$name} = $x;
264             }
265 8 50       104 unless(%{$self->{'field_cols'}}) {
  8         40  
266 0 0       0 croak(%opt_include_fields ? 'No fields found in header row to include' : 'No fields found in header row');
267             }
268             # If include_fields option was given, reorder keys of field_cols to match it.
269 8 100       98 if (%opt_include_fields) {
270 1         2 my %field_cols;
271             #{$self->{'field_cols'}}
272 1         6 tie(%field_cols, 'Tie::IxHash');
273 1         15 foreach my $key (@{$options{'include_fields'}}) {
  1         3  
274 2 50       25 if (exists($self->{'field_cols'}->{$key})) {
275 2         13 $field_cols{$key} = $self->{'field_cols'}->{$key};
276             }
277             }
278 1         22 $self->{'field_cols'} = \%field_cols;
279             }
280             }
281             else {
282 0         0 croak('No header line found in CSV');
283             }
284              
285             # Check that all the required header fields are present.
286 8 100       21 if (%opt_include_fields) {
287 1         2 my @missing;
288 1         4 foreach my $name (keys %opt_include_fields) {
289 2 50       10 if (!exists($self->{'field_cols'}->{$name})) {
290 0         0 push(@missing, $name);
291             }
292             }
293 1 50       7 if (@missing) {
294 0         0 croak('The following column headers are missing: ' . join(', ', @missing));
295             }
296             }
297              
298              
299              
300 8   33     59 bless($self, ref($proto) || $proto);
301 8         47 return $self;
302             }
303              
304              
305              
306              
307             =head2 DESTROY
308              
309             Closes the private file handle, if any.
310              
311             =cut
312              
313             sub DESTROY {
314 8     8   5014 my $self = shift;
315 8 100       49 if ($self->{'own_h'}) {
316 5         37 close($self->{'h'});
317             }
318             }
319              
320              
321              
322              
323              
324              
325             =head1 PROTECTED OBJECT METHODS
326              
327             =head2 _read()
328              
329             Reads the next CSV data row and sets internal variables.
330              
331             =cut
332              
333             sub _read {
334 36     36   47 my $self = shift;
335 36 100       989 if (my $csv_row = $self->{'text_csv'}->getline($self->{'h'})) {
336 27 50       1168 if ($self->{'debug'}) {
337 0         0 require Data::Dumper;
338 0         0 local $Data::Dumper::Terse = 1;
339 0         0 warn(__PACKAGE__ . '::_read ' . Data::Dumper::Dumper($csv_row));
340             }
341 27         147 tie(my %row, 'Tie::IxHash');
342 27         451 my $field_cols = $self->{'field_cols'}; # name to index map
343 27         71 foreach my $k ($self->fieldNames()) {
344 198         3544 my $i = $field_cols->{$k};
345 198         1172 my $v = $csv_row->[$i];
346 198 100       404 if (defined($v)) {
347 124         362 $v =~ s/^\s+|\s+$//g;
348 124 50       235 unless(length($v)) {
349 0         0 $v = undef;
350             }
351             }
352 198         550 $row{$k} = $v;
353             }
354              
355             # Call mutators if defined
356 27 100       412 if (my $mutators = $self->{'mutators'}) { # name to coderef map
357 3         10 foreach my $k (keys %$mutators) {
358 6 50       102 if (exists($row{$k})) {
359 6 50       30 if (my $mutator = $mutators->{$k}) {
360 6         20 &$mutator(\$row{$k}, \%row);
361             }
362             }
363             }
364             }
365              
366 27         139 $self->{'row'} = \%row;
367 27         63 $self->{'linenum'}++;
368             }
369             else {
370 9         802 $self->{'row'} = undef;
371 9         21 $self->{'linenum'} = 0;
372             }
373             }
374              
375              
376              
377              
378              
379              
380             =head1 PUBLIC OBJECT METHODS
381              
382             =head2 fieldNames()
383              
384             Returns the field names as an array.
385              
386             =cut
387              
388             sub fieldNames {
389 30     30 1 2124 my $self = shift;
390 30         39 return keys(%{$self->{'field_cols'}});
  30         137  
391             }
392              
393              
394              
395              
396              
397             =head2 current()
398              
399             Returns the current row.
400              
401             =cut
402              
403             sub current {
404 0     0 1 0 my $self = shift;
405 0         0 return $self->{'row'};
406             }
407              
408              
409              
410              
411              
412             =head2 linenum()
413              
414             Returns the current row index.
415              
416             =cut
417              
418             sub linenum {
419 9     9 1 192 my $self = shift;
420 9         85 return $self->{'linenum'};
421             }
422              
423              
424              
425              
426              
427             =head2 nextRow()
428              
429             Reads the next row.
430              
431             =cut
432              
433             sub nextRow {
434 36     36 1 18342 my $self = shift;
435 36         89 $self->_read();
436 36         105 return $self->{'row'};
437             }
438              
439              
440              
441              
442              
443             =head2 eof()
444              
445             Returns boolean
446              
447             =cut
448              
449             sub eof {
450 20     20 1 74 my $self = shift;
451 20         60 return $self->{'text_csv'}->eof();
452             }
453              
454              
455              
456              
457             =head2 rewind()
458              
459             Rewinds the file handle.
460              
461             =cut
462              
463             sub rewind {
464 1     1 1 6 my $self = shift;
465 1 50       6 seek($self->{'h'},0,0) || croak('Failed to rewind file handle');
466 1         49 $self->{'text_csv'}->getline($self->{'h'}); # skip the header row
467             }
468              
469              
470              
471             1;
472              
473              
474             __END__