summaryrefslogtreecommitdiffstats
path: root/lib/Cepis/CSV.pm
diff options
context:
space:
mode:
authorMurilo Ijanc <[email protected]>2026-07-26 14:40:05 -0300
committerMurilo Ijanc <[email protected]>2026-07-26 14:40:05 -0300
commit0bc578d6d334c7f1ccaf329cd61bb8bce76b7f59 (patch)
tree86f73108f0432fd8330edcaab8b8ae7018322dee /lib/Cepis/CSV.pm
downloadperl-0bc578d6d334c7f1ccaf329cd61bb8bce76b7f59.tar.gz
Initial importv0.1.0
Diffstat (limited to 'lib/Cepis/CSV.pm')
-rw-r--r--lib/Cepis/CSV.pm165
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