# to_CSV
# Takes an array of elements, and returns one line of CSV code.
# e.g. to_CSV('ab', 'cd', 'e,f', 'h"i') -> ('ab,cd,"e,f","h""i"')
sub to_CSV {
    my(@row) = @_;
    my($cell);
    my($line) = '';
    foreach $cell (@row) {
        if ($cell =~ /"/ || $cell =~ /,/) {
            $cell =~ s/"/""/g;
            $cell = "\"$cell\"";
        }
        $line .= $cell.',';
    }
    $line =~ s/,$//; # remove trailing comma

    $line;
}


# from_CSV
# Takes one line of CSV code, and returns an array of elements.
# e.g. from_CSV('ab,"cd","e,f","h""i"') -> ('ab', 'cd', 'e,f', 'h"i')
sub from_CSV {
    my($line) = @_;
    $line =~ s/[\n\r]+$//;
    my(@row) = ();
    my($quotes) = 0;
    my($value) = '';
    my(@tokens) = tokenise ($line, ',"');
    foreach $token (@tokens) {
        if ($token eq ',') {
            if ($quotes == 0 || $quotes == 2) {
                push(@row, $value);
                $value = '';
                $quotes = 0;
            } elsif ($quotes == 1) {
                $value = $value.$token;
            }
        } elsif ($token eq '"') {
            if ($quotes == 0) {
                if ($value) {
                    # Quotes in an unquoted value...
                    # This is an illegal CSV line; feel free to raise an error here.
                    if ($value =~ /^\s+$/) {
                        # Let's be nice, and strip leading spaces before a quoted string.
                        $value = '';
                        $quotes = 1;
                    } else {
                        # Let's be nice, and quietly add the rogue quote to the value.
                        $value = $value.$token;
                    }
                } else {
                    $quotes = 1;
                }
            } elsif ($quotes == 1) {
                $quotes = 2;
            } elsif ($quotes == 2) {
                $value = $value.$token;
                $quotes = 1;
            }
        } else {
            $value = $value.$token;
        }
    }
    if ($quotes == 1) {
        # Quotes didn't end...
        # This is an illegal CSV line; feel free to raise and error here.
    }
    push(@row, $value);

    @row;
}


# tokenise(subject, dividers)
# Break the first string into pieces based on the chars of the second string.
# Returns a list of the words and the dividing chars.
# e.g. tokenise('Hello World', ' o') -> ('Hell', 'o', ' ', 'W', 'o', 'rld')
sub tokenise {
    ($#_ == 1) || die "Expecting two arguments!  Got $#_ instead.\n";
    my($raw, $chars) = @_;
    my($firstIndex);
    my($thisIndex);
    @tokens = ();

    while (length($raw) > 0) {
        $firstIndex = length($raw)+1;
        foreach $char (split(//, $chars)) {
            $thisIndex = index($raw, $char);
            if (0 <= $thisIndex && $thisIndex < $firstIndex) {
                $firstIndex = $thisIndex;
            }
        }
        if ($firstIndex > length($raw)) {
            push(@tokens, $raw);
            $raw = "";
        } elsif ($firstIndex == 0) {
            push(@tokens, substr($raw, 0, 1));
            $raw = substr($raw, 1);
        } else {
            push(@tokens, substr($raw, 0, $firstIndex));
            push(@tokens, substr($raw, $firstIndex, 1));
            $raw = substr($raw, $firstIndex+1);
        }
    }

    @tokens;
}
