#!/usr/bin/perl

# Copyright (C) 2010-2012 John J. Chew, III <jjchew@math.utoronto.ca>
# All Rights Reserved

package TSH::Utility::CSV;

use strict;
use warnings;

use Exporter;

our (@ISA) = 'Exporter';
our (@EXPORT_OK) = qw();

=pod

=head1 NAME

TSH::Utility::CSV - utilities for parsing and constructing CSV data

=head1 SYNOPSIS

  my $rowsp = TSH::Utility::CSV::Decode($csv, { 'quiet' => 1 });

  my $csv = TSH::Utility::CSV::Encode(\@rows, { 'quiet' => 1 });

=head1 ABSTRACT

This library contains code for parsing and constructing CSV data in memory.

=cut

sub Decode ($$);
sub Encode ($;$);

=head1 DESCRIPTION

=over 4

=cut
 
=item my (@rows) = Decode $csv, \%options;

Decode CSV data in C<$csv>) and return a reference to a list of
rows, each row being represented as a reference to a list of its
values.

Options include:

debug: emit verbose debug messages

quiet: if true, emit no warnings about bad input data

=cut

sub Decode ($$) {
  local ($_) = shift;
  my $options = shift;
  my @rows;
  my $row = [''];
  while (length($_)) {
    if (s/^(?:\015\012|\015|\012)//) {
      warn "DEBUG: end of line" if $options->{'debug'};
      push(@rows, \@$row);
      $row = [''];
      }
    elsif (s/^"([^"]*)"//) {
      warn "DEBUG: quoted cell" if $options->{'debug'};
      my $cell = $1;
      while (s/^("[^"]*)"//) { # "" => "
	$cell .= $1;
        }
      if (s/^("[^,\015\012]*)//) {
	warn "CSV error (1): missing close quote at: $1\n" unless $options->{'quiet'};
	$cell .= $1;
	}
      warn "DEBUG: cell = $cell" if $options->{'debug'};
      $row->[-1] .= $cell;
      }
    elsif (s/^"([^,\015\012]*)//) {
      my $cell = $1;
      warn "DEBUG: bad quoted cell = $cell" if $options->{'debug'};
      warn "CSV error (2): missing close quote at: $cell\n" unless $options->{'quiet'};
      $row->[-1] .= $cell;
      }
    elsif (s/^,//) {
      warn "DEBUG: end of cell" if $options->{'debug'};
      push(@$row, '');
      }
    elsif (s/^([^,"\015\012]*)//) { # can match empty string, must be last
      my $cell = $1;
      warn "DEBUG: unquoted cell = $cell" if $options->{'debug'};
      if (/^"/) {
        warn "CSV error (3): quote found in unquoted cell after: $cell\n" unless $options->{'quiet'};
	if (s/^([^,\012\015]*)//) { $cell .= $1; }
	}
      $row->[-1] .= $cell;
      warn "DEBUG: row = @$row" if $options->{'debug'};
      }
    else {
      die "CSV panic: don't know what to do with: $_";
      }
    }
  if (@$row > 1 || length($row->[0])) { push(@rows, \@$row); }
  return \@rows;
  }

=item my ($csv) = Encode \@rows, \%options;

Encode data in list of lists C<@rows>) and return a string containing its
CSV representation.

Options include:

(none)

=cut

sub Encode ($;$) {
  my $rowsp = shift;
  my $options = shift || {};
  my $csv = '';
  for my $rowp (@$rowsp) {
    $csv .= join(',',
      map { my $cell = $_; $cell =~ s/"/""/g; qq("$cell"); }
      @$rowp
      ) . "\n";
    }
  return $csv;
  }

=back

=cut

=head1 BUGS

None known.

=cut

1;
