packages feed

chart-cli-0.1.0.0: src/Parser.hs

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