{-# LANGUAGE OverloadedStrings #-} module Parser where import Control.Monad import Data.Colour.Names import qualified Data.Text as T import qualified Data.Text.IO as TIO import qualified Data.Attoparsec.Text as A import Data.Dates import System.IO.Unsafe (unsafePerformIO) import Types now :: DateTime now = unsafePerformIO getCurrentDateTime parseText :: ParseOptions -> T.Text -> T.Text -> Either String ChartData parseText opts title text = do let lines = T.lines text splitLine line = T.splitOn (poSeparator opts) line inputColsCount = length (splitLine firstLine) parseLine :: Int -> T.Text -> Either String [Value] parseLine lineNo line = let items = splitLine line in if length items == inputColsCount then zipWithM (parseValue lineNo) [1..] items else Left $ "Line " ++ show lineNo ++ ": number of columns is not equal to number of columns in the first line." parseValue lineNo colNo s = case A.parseOnly (A.double <* A.endOfInput) s of Right value -> Right $ Number value Left numberErr -> case parseDateTime now (T.unpack s) of Right dt -> Right $ Date $ toLocalTime dt Left dateErr -> Left $ "Line " ++ show lineNo ++ ", column " ++ show colNo ++ ":\n" ++ "Can't parse `" ++ T.unpack s ++ "` as a number: " ++ numberErr ++ ";\nCan't parse it as a date/time value: " ++ show dateErr firstLine = head lines defHeader n = "Column " <> T.pack (show n) dataLines = if poHeader opts then tail lines else lines inputValues <- zipWithM parseLine [1..] dataLines let values = if poIndex opts then zipWith (:) (map Index [1..]) inputValues else inputValues colsCount = if poIndex opts then inputColsCount + 1 else inputColsCount columnNames = if poHeader opts then splitLine firstLine else map defHeader [0 .. colsCount - 1] columns = map NumberColumn columnNames return $ ChartData { chtTitle = title , chtColumns = columns , chtBackground = white , chtForeground = black , chtLegend = True , chtValues = values } parseFile :: ParseOptions -> Maybe FilePath -> IO ChartData parseFile opts mbPath = do text <- case mbPath of Nothing -> TIO.getContents Just path -> TIO.readFile path let title = case mbPath of Nothing -> "stdin" Just path -> T.pack path case parseText opts title text of Right chart -> return chart Left err -> fail err