#!/usr/bin/perl -w
###
# $Id: //depotit/tools/source/Acme-CSV-2.00/lib/Acme/CSV.pm#1 $
#
# Distributed under the GNU Lesser General Public License v2.1.  See the
# accompanying lgpl.txt file for the license text; if the file was missing
# you may always obtain a copy from http://www.fsf.org/.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
#
# Copyright (c)2001 Christopher Rath <Christopher@Rath.ca> and
#    Mark Mielke <Mark@Mielke.cc>
###

###
# POD starts.
#

=head1 NAME

Acme::CSV - a module providing manipulation routines for comma separated value (CSV) records

=head1 SYNOPSIS

    use Acme::CSV;

    %fieldLayout = CSVinit($firstRecord);
    if (! CSVvalidate(%CSVfields, I<field list>)) {
        die "Fields missing."};
    }
    while (<INPUT>) { 
        @record = CSVsplit($_);
        I<process the records>;
    }

=head1 LICENSE

Copyright (c)2001 Christopher Rath <C<Christopher@Rath.ca>> and Mark Mielke
<C<Mark@Mielke.cc>>.

Distributed under the GNU Lesser General Public License v2.1.  See the
accompanying C<lgpl.txt> file for the license text; if the file was missing you
may always obtain a copy from C<http://www.fsf.org/>.

=head1 WARRANTY

This program is distributed in the hope that it will be useful, but B<without
any warranty>; without even the implied warranty of B<merchantability> or
B<fitness for a particular purpose>.  See the GNU General Public License for
more details.

=head1 DESCRIPTION

This module defines some functions for reading comma separated value (CSV)
files (e.g., Remedy-ARS generated files).  The module also allows some a
delimiter other than a comma to be used, or a selection of delimiters to be
used (e.g., comma and semicolon).

The B<CSVinit(>I<$firstRecord>B<)> function is used to parse the initial record
of a CSV file; which by definition will contain the field names for each field
in the CSV records which follow in the file.  This initial record is itself a
CSV record.

The B<CSVvalidate(>I<\%output_from_CSVinit>B<,> <I<field_list>>B<)> function
is used to verify that the incoming file contains the full list of fields
specified in the I<field_list> provided to the function.

The B<CSVsplit(>I<$rawRecord>B<)> function splits a line, passed as
I<$rawRecord> (a CSV record), and returns it split into fields; sort-of like
B<split()>.

The B<$CSV::Delimiters> variable can be set to specify an alternate, or set of
alternate, field delimiters.  In general, it should be specified as a B<local>.

The B<CSVjoin(>I<@fields_to_join>B<)> function takes an array of fields and
turns them into a comma separated value record.  If B<$CSV::Delimiters> has
been used to specify multiple delimiters then B<CSVjoin()> will use the first
delimiter of the set for creation of CSV records.

=head1 USAGE

A sample application (Demo_App) of this module has been provided in order to
demonstrate how it can be used.  Some additional usage notes follow.

=head2 CSV::Delimiters

    local $CSV::Delimiters = ",;";
    %fieldLayout = CSVinit($firstRecord);
    @Fields = CSVsplit($aRawRecord);

This will cause either of 'B<,>' or 'B<;>' to be valid delimiters, even if
mixed.  B<local()> is used to temporarily override the value, instead of
permanently overriding the value.

B<CSVjoin()> will prefer the first character in the Delimiters string, or the old
behaviour of a ',' if the Delimiters string is not defined.

If you peek at the code, it actually implements the split core twice.  The
first with B<$CSV::Delimiters>, and the second with 'B<,>' hard-coded.  This is
to maintain efficiency for code that does not make use of a dynamic set of
delimiter characters.

=head2 CSVinit()

B<CSVinit()> is used to initialize the data structures for further CSV parsing.
The function scans the input record passed to it.  If it contains data, then
that data is used to define the field titles.  If no data is passed then some
default column data is used instead.  The function returns an associative array
containing field names and the corresponding field number.

=head3 Parameters

[1] $rawRecord - Record defining field layouts.

=head3 Returns

Returns an associative array of field layout.

=head2 CSVvalidate()

B<CSVvalidate()> is used to validate a set of field names.  A list of fields is
validated against the associative array previously built by a call to
CSVinit().  Returns true or false.

=head3 Parameters

[1] \%fields - the fields as built by CSVinit(). (reference)

[?] $...     - a list of field names to check.

=head3 Returns

Returns 0 if any single field is not found in %fields; otherwise 1.

=head2 CSVjoin()

B<CSVjoin()> is used to join CSV data.

=head3 Parameters

Context 1:

    [1] \@fields  - An array of fields to join into a CSV.
    [2] "minimum"|"quoteall" - Defaults to "minimum", define 
            whether fields must be quoted when not req'd.

Context 2:

    [?] $... - An array of fields to join into 
            a CSV.

=head3 Returns

Returns a string which may be "un"join'ed using CSVsplit().
except in the case of a newline contained within a field)

=head2 CSVsplit()

B<CSVsplit> is used to split CSV data, just like Perl's own split() does.

=head3 Parameters
    [1] $rawRecord - the record to split.

=head3 Returns

Returns an array of values split out of $rawRecord.

=head1 TESTING

Put the following lines into a file to test the comma-based parsing of this
module:

    #!/usr/bin/perl -w

    use CSV;

    while (<DATA>) {
        print join(":", CSVsplit($_)), "\n";
    }

    __DATA__
    "One from FAQ will do","sec""ond"
    "But, not","3rd""","or","""fifth"
    "don't","forget","fourth"",""","and"  ,  """,""sixth".
    "Of course, it doesn't solve",everything,8,N,1
    "Is there a new emacs   perl-mode?"
    "Tom","tom@fiction.org"
    Empty fields needed,,"and "wanted

    Some,"",boundary  ",cases"",  ,too
    Grok this\, Spok!
    "Didn't you notice\! in it \{the spec\}"
    "Multi-
    line",test

This will produce the following output:

    One from FAQ will do:sec"ond
    But, not:3rd":or:"fifth
    don't:forget:fourth",":"and"  :  """:""sixth".
    Of course, it doesn't solve:everything:8:N:1
    Is there a new emacs   perl-mode?
    Tom:tom@fiction.org
    Empty fields needed::"and "wanted

    Some::boundary  ":cases"":  :too
    Grok this\: Spok!
    Didn't you notice\! in it \{the spec\}
    "Multi-
    line":test

=head1 CSV SPECIFICATION

This section attempts to define CSV records and files in a fairly rigorous
fashion.  The point behind this is to make this module usable without having to
read and understand the source code.

=head2 CSV Records

The basic idea behind a CSV record is this: literal field values are
delimited by commas.  The immediate complication that arises is, of
course, ``What should be done when a comma must appear within a
field?''  Within the bounds of current practice, there are two
immediate solutions to this complication:

=over 4

=item 1.

Use a predefined escape character to tag commas which appear within fields.

=item 2.

Allow quotation marks to enclose a field and _protect_ a comma appearing within
a field.

=back

As with all work-arounds, these "immediate solutions" have complications of
their own (these secondary complications are numbered the same as their primary
counterparts):

=over 4

=item 1.

Escape characters must themselves be escaped in order to appear as a value
within a field (e.g., if a literal comma is expressed as ``\,'', then a literal
backslash must appear as ``\\'').

=item 2.

Quotation marks must somehow be protected if they are to appear as a literal
character within a field.

=back

Given that the essence of CSV files is simplicity, I have decided to reject
I<all> escape and escaped characters with the exception of quoation marks
appearing within quotation marks.  That is, the case of the escaped comma has
been rejected from this specification.

Within the context of Perl, the L<string(3)> library and the UNIX shells, an
additional level of complexity is added to this equation when we begin to ask,
``What is the meaning or significance of whitespace within a CSV record?''  The
meaning of whitespace is a key technical detail which must be accounted for in
both the specification and its implementation; otherwise, everyone's
implementation will produce semi-random results based upon that implementors
opinion regarding whitespace.

=head2 Semi-Formal CSV Record Specification

This specification uses the syntax described in S<Appendix A> of the first
edition of O'Reilly's Programming Perl book (i.e., the S<Perl 4> camel book).

    CSV_RECORD ::= (* FIELD DELIM *) FIELD REC_SEP

    FIELD ::= QUOTED_TEXT | TEXT

    DELIM ::= `,'

    REC_SEP ::= `\n'

    TEXT ::= LIT_STR | ["] LIT_STR [^"] | [^"] LIT_STR ["]

    LIT_STR ::= (* LITERAL_CHAR *)

    LITERAL_CHAR ::= NOT_COMMA_NL

    NOT_COMMA_NL ::= [^,\n]

    QUOTED_TEXT ::= ["] (* NOT_A_QUOTE *) ["]

    NOT_A_QUOTE ::= [^"] | ESCAPED_QUOTE

    ESCAPED_QUOTE ::= `""'

=head2 Notes

This specification does not grant any special status to whitespace characters.
This means that B<I<all whitespace is part of some field value.>>

The B<TEXT> non-terminal is attempting to express the cases where quotation
marks exist but do not completely encapsulate the field value; in cases like
this, the quotation marks should be treated as literal characters making up
part of the field value.

One ambiguity exist in this specification that I have been unable to properly
express.  The case of a field with the value ,abc""de,.  Should the double
quotation marks be treated as an escaped quotation mark or as two quotation
marks?  I believe that occurences of "" should be treated as escaped quotation
marks I<only> within a quoted string.

The B<LITERAL_CHAR> non-terminal exists partially as a place-holder.  Escaped
characters may be easily accomadated by this specification at a later date by
OR-ing them to the right side of B<LITERAL_CHAR>.

Some of the non-terminals exist solely as documentation/reading aids.  The
B<NOT_A_COMMA_NL> is one example of this case; its name helps express the
meaning of the regex (which should assist other readers of this document to
detect errors in the specification).

The B<ESCAPED_QUOTE> non-terminal includes the PASCAL-like case of ``""'' and
excludes the more traditional UNIX ``\\"''.  This is not my preference; I have
included it here because I know there exists at least one commercial tool that
produces CSV records containing the PASCAL-like construct and not the UNIX-like
one.

=head1 AUTHORS

Christopher Rath (C<christopher@rath.ca>) wrote the CSV specification and
everything in the module except the essential snippet of code that actually
does the work :).

Mark Mielke (C<mark@mielke.cc>) took the specification and wrote the essential
piece of code that actually breaks the CSV records into its constituent fields.
He also took the initial .pl version and .pm'ed it (this only makes sense,
since this module is only usable in perl5).

=head1 BUGS

=head2 Not Thread-Safe

This module, and hence B<CSVinit()>/B<CSVsplit()>, is not thread-safe or
re-entrant.

=head2 Fields Spanning Lines

This module currently fails in one of the test-cases, although the test output
listed herein, above, has been constructed to show the actual output of this
module, as opposed to the correct output:

    "Multi-
    line",test

This is due to the fact that perl is reading one line at a time with:

    while (<DATA>) { ... }

So the first line is read ("Multi-) and evaluated. The _second_ time around the
loop the second line (line",test) is read and evaluated.  There is no
workaround available, this is simply a limitation of the module.

=head1 VERSION

The RCS identifier for this module is
S<$Id: //depotit/tools/source/Acme-CSV-2.00/lib/Acme/CSV.pm#1 $>.

=cut

#
# End of POD.
###

package Acme::CSV;
require 5.000;
require Exporter;
$VERSION = 2.00;

@ISA = qw(Exporter);
@EXPORT = qw(CSVinit CSVvalidate CSVjoin CSVsplit);

use strict;

$CSV::Delimiters = undef;

###
# Setup Variables.
#
# CSVDefaultColumnMapping - this contains a dummy record; just in case the
#                           user calls us with nothing.
#
my(%CSVDefaultColumnMapping) = ("" => -1);

###
# CSVinit() - initialize data structures for further CSV parsing.
#
# This function scans the input record passed to it.  If it contains data, then
# that data is used to define the field titles.  If no data is passed then some
# default column data is used instead.  The function returns an associative
# array containing field names and the corresponding field number.
#
# Parameters;
#        [1] $rawRecord - Record defining field layouts.
#
# Returns:
#        Associative array of field layout.
###
sub CSVinit
{
    my @fields = CSVsplit(@_);
    my %columns;
    my $i = 0;

    if ($#fields >= 0) {
        # Build associative array with field -> column_number mapping.
        map { $columns{$_} = $i++ } @fields;
        %columns;
    } else {
        %CSVDefaultColumnMapping;
    }
}


### 
# CSVvalidate() - validate the field names passed in.
# 
# A list of fields is validated against the associative array previously built
# by a call to CSVinit().  Returns true or false.
# 
# Parameters: 
#       [1] \%fields - the fields as built by CSVinit(). (reference)
#        [?] $...     - a list of field names to check.
# 
# Returns: 
#       0 if any single field is not found in %fields; otherwise 1.
### 
sub CSVvalidate
{
    my $fields = shift;                # pass by reference!
    my $field;

    foreach $field (@_) {
        exists $fields->{$field} || return 0;
    }

    1;
}


###
# CSVjoin() - join CSV data.
#
# Parameters:
#    Context 1:
#          [1] \@fields  - An array of fields to join into a CSV.
#          [2] "minimum"|"quoteall" - Defaults to "minimum", defines
#                          whether fields must be quoted when not req'd.
#    Context 2:
#          [?] $...      - An array of fields to join into a CSV.
#
# Returns:
#        returns a string which may be "un"join'ed using CSVsplit().
#           (except in the case of a newline contained within a field)
###
sub CSVjoin
{
    my($fields, $mode) = ref($_[0]) ? @_ : \@_;
    my($quoteall, @quotedfields, $quotedfield);
    local $_;

    $mode = 'minimum' if !defined($mode);
    $quoteall = ($mode eq 'quoteall') ? 1 : 0;

    foreach (@$fields) {
        if ($quoteall || /[\",\n]/) {
            ($quotedfield = $_) =~ s/\"/""/g;
            push(@quotedfields, qq{"$quotedfield"});
        } else {
            push(@quotedfields, $_);
        }
    }

    join((defined($CSV::Delimiters) ? substr($CSV::Delimiters,0,1) : ','),
         @quotedfields);
}


###
# CSVsplit() - split CSV data, just like Perl's own split() does.
#
# See description of record layout at top of this module.
#
# Parameters:
#        [1] $rawRecord - the record to split.
#
# Returns:
#        An array of values split out of $rawRecord.
###
sub CSVsplit
{
    local $_ = $_[0];
    my(@fields, $t);

    # According to the specs, a new-line is a field terminator not a field
    # delimiter. This only becomes an issue at the end of a string where
    # a new-line implies end of field, not a field delimiter. By stripping
    # the last optional new-line we allow all remaining new-lines to be
    # considered field delimiters.
    chomp;

    ###
    # Only build the array if at least one field exists. (if at least one
    # character exists, by definition, at least one field exists)
    #
    if (length($_) > 0) {
        ###
        # Break the record into fields.
        # Field Delimiter == /[,\n]/   (See comment above: According...)
        my $expecting;
        if (defined $CSV::Delimiters) {
            while (m`(?=.)((?:"((?:""|[^"]+)*)")?([^$CSV::Delimiters]*))([$CSV::Delimiters]?)`sg) {
                $expecting = $4;
                if (defined($2) && !length($3)) {
                    ($t = $2) =~ s/""/\"/g;
                    push(@fields, $t);
                } else {
                    push(@fields, $1);
                }
            }
        } else {
            while (m`(?=.)((?:"((?:""|[^"]+)*)")?([^,]*))(,?)`sg) {
                $expecting = $4;
                if (defined($2) && !length($3)) {
                    ($t = $2) =~ s/""/\"/g;
                    push(@fields, $t);
                } else {
                    push(@fields, $1);
                }
            }
        }
        push(@fields, '') if $expecting;
    }

    @fields;
}

1;

###
# End of package.
###

# Local Variables:
#       fill-column: 79
# End:
