diff options
| author | Murilo Ijanc <[email protected]> | 2026-07-26 14:40:05 -0300 |
|---|---|---|
| committer | Murilo Ijanc <[email protected]> | 2026-07-26 14:40:05 -0300 |
| commit | 0bc578d6d334c7f1ccaf329cd61bb8bce76b7f59 (patch) | |
| tree | 86f73108f0432fd8330edcaab8b8ae7018322dee /lib/Cepis/CSV.pm | |
| download | perl-6661cb35162c38099b86f248b938f5e4d4d11515.tar.gz | |
Initial importv0.1.0
Diffstat (limited to 'lib/Cepis/CSV.pm')
| -rw-r--r-- | lib/Cepis/CSV.pm | 165 |
1 files changed, 165 insertions, 0 deletions
diff --git a/lib/Cepis/CSV.pm b/lib/Cepis/CSV.pm new file mode 100644 index 0000000..6fabc0c --- /dev/null +++ b/lib/Cepis/CSV.pm @@ -0,0 +1,165 @@ +package Cepis::CSV; + +use strict; +use warnings; + +our $VERSION = '0.1.0'; + +sub parse_rows { + my ($text) = @_; + + return [] unless defined $text && length $text; + $text =~ s/\A\x{FEFF}//; + + my @rows; + my @row; + my $field = ''; + my $quoted = 0; + my $started = 0; + my $length = length $text; + my $i = 0; + + while ( $i < $length ) { + my $char = substr( $text, $i, 1 ); + + if ($quoted) { + if ( $char eq '"' ) { + if ( $i + 1 < $length && substr( $text, $i + 1, 1 ) eq '"' ) { + $field .= '"'; + $i += 2; + next; + } + $quoted = 0; + $i++; + next; + } + $field .= $char; + $i++; + next; + } + + if ( $char eq '"' && $field eq '' ) { + $quoted = 1; + $started = 1; + $i++; + next; + } + + if ( $char eq ',' ) { + push @row, $field; + $field = ''; + $started = 1; + $i++; + next; + } + + if ( $char eq "\r" || $char eq "\n" ) { + $i++; + $i++ + if $char eq "\r" + && $i < $length + && substr( $text, $i, 1 ) eq "\n"; + if ( $started || @row ) { + push @row, $field; + push @rows, [@row]; + } + @row = (); + $field = ''; + $started = 0; + next; + } + + $field .= $char; + $started = 1; + $i++; + } + + if ( $started || @row ) { + push @row, $field; + push @rows, [@row]; + } + + return \@rows; +} + +sub parse_hashes { + my ($text) = @_; + + my $rows = parse_rows($text); + return ( [], [] ) unless @$rows; + + my $header = shift @$rows; + my @out; + for my $row (@$rows) { + my %record; + for my $index ( 0 .. $#$header ) { + my $value = $index <= $#$row ? $row->[$index] : ''; + $record{ $header->[$index] } = defined $value ? $value : ''; + } + push @out, \%record; + } + + return ( $header, \@out ); +} + +1; + +__END__ + +=encoding UTF-8 + +=head1 NAME + +Cepis::CSV - a small RFC 4180 reader + +=head1 SYNOPSIS + + use Cepis::CSV; + + my ( $header, $records ) = Cepis::CSV::parse_hashes($text); + print $records->[0]{localidade}; + +=head1 DESCRIPTION + +Perl ships no CSV parser in its core, and this distribution has no non-core +dependencies, so it carries its own. It handles what RFC 4180 requires and what +the C<GET /csv> endpoint actually emits: quoted fields, embedded commas, embedded +newlines, C<""> as an escaped quote, C<CRLF> or C<LF> line endings, and a leading +byte order mark. + +The input is a character string, not bytes — decode the response body before +calling in. + +This module is internal to L<Cepis>. It is documented because it is tested +separately, not because it is meant to be a general-purpose CSV library: it has +no writer, no configurable separator and no streaming interface. + +=head1 FUNCTIONS + +=head2 parse_rows + + my $rows = Cepis::CSV::parse_rows($text); + +Returns an arrayref of arrayrefs, one per record, fields in order. Blank lines +are skipped, so a trailing newline does not produce a phantom final record. + +=head2 parse_hashes + + my ( $header, $records ) = Cepis::CSV::parse_hashes($text); + +Treats the first record as a header row and returns it as an arrayref, plus an +arrayref of hashrefs keyed by column name. A record with fewer fields than the +header gets empty strings for the ones it lacks; fields beyond the header are +dropped. + +Returns two empty arrayrefs for empty input. + +=head1 SEE ALSO + +L<Cepis::Client> + +=head1 LICENCE + +ISC. Copyright (c) 2026 Murilo Ijanc E<lt>[email protected]<gt>. + +=cut |