    {- arch-tag: CSV and TSV utilities
    Copyright (c) 2005-2011 John Goerzen <jgoerzen@complete.org>

    All rights reserved.

    For license and copyright information, see the file LICENSE
    -}

    {- |
       Module     : Data.CSV
       Copyright  : Copyright (C) 2005-2011 John Goerzen
       License    : BSD3

       Maintainer : John Goerzen <jgoerzen@complete.org>
       Stability  : provisional
       Portability: portable

    Haskell Parsec parsers for comma-separated value (CSV) files.

    Written by John Goerzen, jgoerzen\@complete.org
    -}

    module Data.CSV (csvFile, genCsvFile) where
    import Text.ParserCombinators.Parsec
    import Data.List (intersperse)

    eol :: forall st. GenParser Char st String
    eol = (try $ string "\n\r") <|> (try $ string "\r\n") <|> string "\n" <|>
          string "\r" <?> "End of line"

    cell :: GenParser Char st String
    cell = quotedcell <|> many (noneOf ",\n\r")

    quotedchar :: GenParser Char st Char
    quotedchar = noneOf "\""
                 <|> (try $ do string "\"\""
                               return '"'
                     )
    quotedcell :: CharParser st String
    quotedcell = do char '"'
                    content <- many quotedchar
                    char '"'
                    return content

    line :: GenParser Char st [String]
    line = sepBy cell (char ',')

    {- | Parse a Comma-Separated Value (CSV) file.  The return value is a list of
    lines; each line is a list of cells; and each cell is a String.

    Please note that CSV files may have a different number of cells on each line.
    Also, it is impossible to distinguish a CSV line that has a call with no data
    from a CSV line that has no cells.

    Here are some examples:

    >Input (literal strings)          Parses As (Haskell String syntax)
    >-------------------------------- ---------------------------------
    >1,2,3                            [["1", "2", "3"]]
    >
    >l1                               [["l1"], ["l2"]]
    >l2
    >
    > (empty line)                    [[""]]
    >
    >NQ,"Quoted"                      [["NQ", "Quoted"]]
    >
    >NQ,"Embedded""Quote"             [["NQ", "Embedded\"Quote"]]

    To parse a String, you might use:

    >import Text.ParserCombinators.Parsec
    >import Data.String.CSV
    >....
    >parse csvFile "" mystring

    To parse a file, you might instead use:

    >do result <- parseFromFile csvFile "/path/to/file"

    Please note that the result of parsing will be of type
    (Either ParseError [[String]]).  A Left result indicates an error.
    For more details, see the Parsec information.
    -}

    csvFile :: CharParser st [[String]]
    csvFile = endBy line eol

    {- | Generate CSV data for a file.  The resulting string can be
    written out to disk directly. -}
    genCsvFile :: [[String]] -> String
    genCsvFile inp =
        unlines . map csvline $ inp
        where csvline :: [String] -> String
              csvline l = concat . intersperse "," . map csvcells $ l
              csvcells :: String -> String
              csvcells "" = ""
              csvcells c = '"' : convcell c ++ "\""
              convcell :: String -> String
              convcell c = concatMap convchar c
              convchar '"' = "\"\""
              convchar x = [x]