Search code examples
csvhaskellconduit

Parse CSV file into custom data type with csv-conduit


I'm pretty confused about how to use csv-conduit with custom data types. I'd like to take a row of stock data such as this one:

Date,Open,High,Low,Close,Volume,Adj Close
2017-02-10,2312.27002,2319.22998,2311.100098,2316.100098,3475020000,2316.100098

and parse that into the StockInfo type I have declared in my MWE below. I've gathered from the documentation that I need to make my StockInfo an instance of FromNamedRecord, ToNamedRecord, and CSV ByteString. I believe I have done so for the first two, but I do not understand how to implement the necessary methods for CSV ByteString. Any help would be greatly appreciated.

MWE:

{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings     #-}

module Lib
    ( readStocks
    ) where

import           Data.ByteString
import           Data.Conduit
import           Data.Conduit.Binary
import           Data.Conduit.List           as CL
import           Data.CSV.Conduit
import           Data.CSV.Conduit.Conversion
import           Data.Text                   (Text)
import           Data.Vector
import           System.IO

readStocks :: FilePath -> IO (Vector StockInfo)
readStocks fp = readCSVFile defCSVSettings fp

data StockInfo = StockInfo
  { date     :: !String
  , open     :: !Double
  , high     :: !Double
  , low      :: !Double
  , close    :: !Double
  , volume   :: !Integer
  , adjClose :: !Double
  }

instance FromNamedRecord StockInfo where
  parseNamedRecord m =
    StockInfo <$>
    m .: "Date" <*>
    m .: "Open" <*>
    m .: "High" <*>
    m .: "Low" <*>
    m .: "Close" <*>
    m .: "Volume" <*>
    m .: "Adj Close"

instance ToNamedRecord StockInfo where
  toNamedRecord (StockInfo date open high low close volume adjClose) =
    namedRecord [ "Date" .= date
                , "Open" .= open
                , "High" .= high
                , "Low" .= low
                , "Close" .= close
                , "Volume" .= volume
                , "Adj Close" .= adjClose
                ]

instance CSV ByteString StockInfo where
    -- rowToStr = undefined
    -- intoCSV = undefined
    -- fromCSV = undefined

Solution

  • Here's what I should have done:

    {-# LANGUAGE OverloadedStrings     #-}
    
    module Lib
        ( readStocks
        ) where
    
    import           Data.ByteString
    import           Data.Conduit
    import           Data.Conduit.Binary
    import           Data.CSV.Conduit
    import           Data.CSV.Conduit.Conversion
    import           Data.Text                   (Text)
    import           Data.Vector
    import           System.IO
    
    readStocks :: FilePath -> IO (Vector (Named StockInfo))
    readStocks fp = readCSVFile defCSVSettings fp
    
    data StockInfo = StockInfo
      { date     :: !String
      , open     :: !Double
      , high     :: !Double
      , low      :: !Double
      , close    :: !Double
      , volume   :: !Integer
      , adjClose :: !Double
      } deriving (Show, Eq, Read)
    
    instance FromNamedRecord StockInfo where
      parseNamedRecord m =
        StockInfo <$>
        m .: "Date" <*>
        m .: "Open" <*>
        m .: "High" <*>
        m .: "Low" <*>
        m .: "Close" <*>
        m .: "Volume" <*>
        m .: "Adj Close"
    
    instance ToNamedRecord StockInfo where
      toNamedRecord (StockInfo date open high low close volume adjClose) =
        namedRecord [ "Date" .= date
                    , "Open" .= open
                    , "High" .= high
                    , "Low" .= low
                    , "Close" .= close
                    , "Volume" .= volume
                    , "Adj Close" .= adjClose
                    ]