44 lines
1.5 KiB
Haskell
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)
|