comma-1.1.0: src/Text/Comma.hs
{- |
Module : Text.Comma
Description : CSV Parser & Producer
Copyright : (c) 2017 Daniel Lovasko
License : BSD2
Maintainer : Daniel Lovasko <daniel.lovasko@gmail.com>
Stability : stable
Portability : portable
-}
{-# LANGUAGE OverloadedStrings #-}
module Text.Comma
( comma
, uncomma
) where
import Control.Applicative
import Data.List
import qualified Data.Attoparsec.Text as A
import qualified Data.Text as T
-- | Parse a field of a record.
field :: A.Parser T.Text -- ^ parser
field = fmap T.concat quoted <|> normal A.<?> "field"
where
normal = A.takeWhile (A.notInClass "\n,\"") A.<?> "normal field"
quoted = A.char '"' *> many between <* A.char '"' A.<?> "quoted field"
between = A.takeWhile1 (/= '"') <|> (A.string "\"\"" *> pure "\"")
-- | Parse a block of text into a CSV table.
comma :: T.Text -- ^ CSV text
-> Either String [[T.Text]] -- ^ error | table
comma text = A.parseOnly table fixEnd
where
table = A.sepBy1 record (A.char '\n') A.<?> "table"
record = A.sepBy1 field (A.char ',') A.<?> "record"
fixEnd = maybe text id (T.stripSuffix "\n" text)
-- | Render a table of texts into a valid CSV output.
uncomma :: [[T.Text]] -- ^ table
-> T.Text -- ^ CSV text
uncomma = T.unlines . map (T.concat . intersperse "," . map conv)
where
isQuoted = T.any (`elem` ['"', '\n', ','])
enquote x = T.concat ["\"", x, "\""]
conv f
| isQuoted f = enquote (T.replace "\"" "\"\"" f)
| otherwise = f