{-# 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)