pilu/src/CSV.hs

44 lines
1.5 KiB
Haskell

{-# LANGUAGE FlexibleInstances #-}
module CSV (
Row(..)
, parse
) where
import Control.Applicative ((<|>))
import Data.Map (Map)
import qualified Data.Map as Map (fromList, keys)
import Text.Parsec.Pos (newPos)
import Text.ParserCombinators.Parsec ((<?>), Parser, ParseError, char, endBy, many, noneOf, sepBy, string, try)
import Text.ParserCombinators.Parsec.Error (Message(..), newErrorMessage)
import qualified Text.ParserCombinators.Parsec as Parsec (parse)
class Row a where
fromRow :: Map String String -> Either Message a
instance Row [String] where
fromRow = return . Map.keys
csvFile :: Char -> Parser [[String]]
csvFile separator = line `endBy` eol
where
line = cell `sepBy` (char separator)
cell = quoted <|> many (noneOf $ separator:"\r\n")
eol = try (string "\r\n") <|> try (string "\r") <|> try (string "\n") <?> "end-of-line"
quoted :: Parser String
quoted = char '"' *> many quotedChar <* char '"' <?> "quote ending the cell"
where
quotedChar = noneOf "\"" <|> try (string "\"\"" *> return '"')
parse :: Row a => String -> Char -> String -> Either ParseError [a]
parse name separator input = do
lines <- Parsec.parse (csvFile separator) name input
case lines of
[] -> Right []
(header:rows) ->
let assocs = Map.fromList . zipWith (,) header <$> rows in
sequence $ zipWith parseRow [1..] assocs
where
parseRow n row = either (errorAt n) return $ fromRow row
errorAt n message = Left $ newErrorMessage message (newPos name n 0)