small circular points
[MojoFacets.git] / lib / MojoFacets / Import / CSV.pm
index ae8a518..e8fa196 100644 (file)
@@ -5,73 +5,74 @@ use strict;
 
 use base 'Mojo::Base';
 
-use File::Slurp;
+use Text::CSV;
 use Data::Dump qw(dump);
-use Encode;
-
-__PACKAGE__->attr('path');
-__PACKAGE__->attr('full_path'); # FIXME remove full_path
-
-my $null = ''; # FIXME undef?
-
-sub _split_line {
-       my ( $delimiter, $line ) = @_;
-       my @v;
-       while ( $line ) {
-               if ( $line =~ s/^"([^"]+)"\Q$delimiter\E?// ) {
-                       push @v, $1;
-               } elsif ( $line =~ s/^([^\Q$delimiter\E]+)\Q$delimiter\E?// ) {
-                       push @v, $1;
-               } elsif ( $line =~ s/^\Q$delimiter\E// ) {
-                       push @v, $null;
-               } else {
-                       die "can't parse [$line]\n";
-               }
-       }
 
-       return @v;
-}
+__PACKAGE__->attr('full_path');
+
+sub ext { '\.[ct]sv$' };
 
 sub data {
        my $self = shift;
 
-       my $path = $self->full_path || $self->path;
+       my $path = $self->full_path;
 
-       my $data = read_file $path, { binmode => ':raw' }; # FIXME configurable!
        my $encoding = 'utf-8';
-       if ( $path =~ m/\.(\w+).csv/i ) {
+       if ( $path =~ m/\.([\w\-]+).[ct]sv/i ) {
                $encoding = $1;
        }
-       warn "decoding ", length($data), " bytes using $encoding\n";
-       $data = decode($encoding, $data);
 
-       my @lines = split(/\r?\n/, $data);
-       $data = { items => [] };
+       my $data = { items => [] };
+       my @header;
 
-       my $delimiter = ',';
+       open my $fh, "<:encoding($encoding)", $path or die "$path: $!";
+       my $first = <$fh>;
+       my $possible_delimiters;
+       while ( $first =~ s/(\W)// ) {
+               $possible_delimiters->{$1}++;
+       }
+       warn "# possible_delimiters = ",dump($possible_delimiters);
+       seek $fh,0,0; # rewind for Text::CSV
+
+       my @sep_by_usage = sort { $possible_delimiters->{$b} <=> $possible_delimiters->{$a} } keys %$possible_delimiters;
+       my $sep_char = shift @sep_by_usage;
+       while ( $sep_char =~ m/^\s$/ ) {
+               last if $sep_char eq "\t" && $path =~ m/\.tsv$/i;
+               warn "## skip whitespace separator ",dump($sep_char);
+               $sep_char = shift @sep_by_usage;
+       }
 
-       if ( $lines[0] !~ /;/ && $lines[1] =~ /;/ ) {
-               shift @lines; # FIXME ship non-header line
-               $delimiter = ';';
+       while ( $sep_char =~ m/^\"$/ ) {
+               warn "## skip quote separator ",dump($sep_char);
+               $sep_char = shift @sep_by_usage;
        }
 
-       warn "$path ", $#lines + 1, " lines encoding: $encoding delimiter:",dump($delimiter);
+       if ( $sep_char !~ m/,/ && $possible_delimiters->{','} && $path =~ m/\.csv/i ) {
+               $sep_char = ',';
+               warn "## csv file detected so prefer , as separator";
+       }
 
-       my $header_line = shift @lines;
+       warn "sep_char = [$sep_char] for $path\n";
 
-       my @header = _split_line( $delimiter, $header_line );
-       warn "# header ",dump( @header );
+       my $csv = Text::CSV->new ( { binary => 1, eol => $/, sep_char => $sep_char } )
+               or die "Cannot use CSV: ".Text::CSV->error_diag ();
 
-       while ( my $line = shift @lines ) {
-               chomp $line;
-               my @v = _split_line($delimiter, $line);
+       while ( my $row = $csv->getline( $fh ) ) {
+               if ( ! @header ) {
+                       @header = @$row;
+                       $header[0] =~ s/^#// if $path =~ m/\.tsv/i; # remove hash from 1st column
+                       next;
+               }
                my $item;
-               foreach my $i ( 0 .. $#v ) {
-                       $item->{ $header[$i] || "f_$i" } = [ $v[$i] ];
+               foreach my $i ( 0 .. $#{$row} ) {
+                       $item->{ $header[$i] || "f_$i" } = [ $row->[$i] ];
                }
                push @{ $data->{items} }, $item;
        }
 
+       $csv->eof or $csv->error_diag();
+       close $fh;
+
        $data->{header} = [ @header ];
        
        return $data;