use Text::CSV;
use Params::Validate qw(:all);
use Rose::Object::MakeMethods::Generic scalar => [ qw(
- file encoding sep_char quote_char header header_acc class numberformat
- dateformat _io _csv _objects _parsed _data
+ file encoding sep_char quote_char escape_char header header_acc class
+ numberformat dateformat _io _csv _objects _parsed _data _errors
) ];
my %params = validate(@_, {
sep_char => { default => ';' },
quote_char => { default => '"' },
+ escape_char => { default => '"' },
header => { type => ARRAYREF, optional => 1 },
header_acc => { type => HASHREF, optional => 1 },
file => 1,
$self->_io(IO::File->new);
$self->_csv(Text::CSV->new({
binary => 1,
- sep_char => $self->sep_char,
- quote_char => $self->quote_char,
+ sep_char => $self->sep_char,
+ quote_char => $self->quote_char,
+ escape_char => $self->escape_char,
}));
+ $self->_errors([]);
return $self;
}
return wantarray ? @{ $self->_objects } : $self->_objects;
}
+sub errors {
+ @{ $_[0]->_errors }
+}
+
# private stuff
sub _open_file {
sub _parse_data {
my ($self, %params) = @_;
- my @data;
+ my (@data, @errors);
$self->_csv->column_names(@{ $self->header });
- push @data, $self->_csv->getline_hr($self->_io)
- while !$self->_csv->eof;
+ while (1) {
+ my $row = $self->_csv->getline($self->_io);
+ last if $self->_csv->eof;
+
+ if ($row) {
+ my %hr;
+ @hr{@{ $self->header }} = @$row;
+ push @data, \%hr;
+ } else {
+ push @errors, [
+ $self->_csv->error_input,
+ $self->_csv->error_diag,
+ $self->_io->input_line_number,
+ ];
+ }
+ }
$self->_data(\@data);
+ $self->_errors(\@errors);
+
+ return if @errors;
+ return \@data;
}
sub _encode_layer {
__END__
+=encoding utf-8
+
=head1 NAME
SL::Helper::Csv - take care of csv file uploads
)
my $status = $csv->parse;
- my @hrefs = $csv->get_data;
+ my $hrefs = $csv->get_data;
my @objects = $scv->get_objects;
=head1 DESCRIPTION
Parse the data into objects and return those.
+This method will return list or arrayref depending on context.
+
=item C<get_data>
Returns an arrayref of the raw lines as hashrefs.
+=item C<errors>
+
+Return all errors that came up druing parsing. See error handling for detailed
+information.
+
+=back
+
+=head1 PARAMS
+
+=over 4
+
=item C<file>
The file which contents are to be read. Can be a name of a physical file or a
=item C<quote_char>
+=item C<escape_char>
+
Same as in L<Text::CSV>
=item C<header> \@FIELDS
=back
-=head1 BUGS
+=head1 ERROR HANDLING
+
+After parsing a file all errors will be accumulated into C<errors>.
+
+Each entry is an arrayref with the following structure:
+
+ [
+ offending raw input,
+ Text::CSV error code if present,
+ Text::CSV error diagnostics if present,
+ position in line,
+ estimated line in file,
+ ]
+
+Note that the last entry can be off, but will give an estimate.
+
+=head1 CAVEATS
+
+=over 4
+
+=item *
+
+sep_char, quote_char, and escape_char are passed to Text::CSV on creation.
+Changing them later has no effect currently.
+
+=item *
+
+Encoding errors are not dealt with properly.
+
+=item *
+
+Errors are not gathered.
+
+=back
=head1 AUTHOR
+Sven Schöling E<lt>s.schoeling@linet-services.deE<gt>
+
=cut
my $csv;
$csv = SL::Helper::Csv->new(
- file => \"Kaffee;\n",
+ file => \"Kaffee\n",
header => [ 'description' ],
);
);
is $csv->parse, undef, 'broken csv header won\'t get parsed';
+######
+
+$csv = SL::Helper::Csv->new(
+ file => \<<EOL,
+description;partnumber;sellprice;lastcost_as_number;
+"Kaf"fee";;0.12;1,221.52
+Beer;1123245;0.12;1.5234
+EOL
+ numberformat => '1,000.00',
+ class => 'SL::DB::Part',
+);
+is $csv->parse, undef, 'broken csv content won\'t get parsed';
+is_deeply $csv->errors, [ '"Kaf"fee";;0.12;1,221.52'."\n", 2023, 'EIQ - QUO character not allowed', 5, 2 ], 'error';
done_testing();
# vim: ft=perl