packages feed

dataframe-csv-th-1.4.0.0: src/DataFrame/TH/CSV.hs

{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-}

-- | Splices that derive column bindings from a CSV file.
module DataFrame.TH.CSV (
    declareColumnsFromCsvFile,
    declareColumnsFromCsvWithOpts,
    deriveSchemaValuesFromCsvFile,
    deriveSchemaValuesFromCsvWithOpts,
) where

import qualified Data.ByteString as BS

import Control.Monad.IO.Class (liftIO)
import Data.Maybe (fromMaybe)
import Language.Haskell.TH
import System.IO (IOMode (ReadMode), withFile)

import qualified DataFrame.IO.CSV as CSV
import DataFrame.TH.Records (declareColumns, declareSchemaValues)

-- | Splice an @Expr T@ binding for every column of a CSV file.
declareColumnsFromCsvFile :: String -> DecsQ
declareColumnsFromCsvFile path = do
    df <-
        liftIO
            ( CSV.readSeparated
                (CSV.defaultReadOptions{CSV.numRowsToRead = Just 100})
                path
            )
    declareColumns df

-- | Like 'declareColumnsFromCsvFile' but with custom 'CSV.ReadOptions'.
declareColumnsFromCsvWithOpts :: CSV.ReadOptions -> String -> DecsQ
declareColumnsFromCsvWithOpts opts path = do
    df <- liftIO (CSV.readSeparated opts path)
    declareColumns df

deriveSchemaValuesFromCsvFile :: String -> String -> DecsQ
deriveSchemaValuesFromCsvFile =
    deriveSchemaValuesFromCsvWithOpts
        CSV.defaultReadOptions{CSV.numRowsToRead = Just 100}

deriveSchemaValuesFromCsvWithOpts ::
    CSV.ReadOptions -> String -> String -> DecsQ
deriveSchemaValuesFromCsvWithOpts opts prefix path = do
    df <- liftIO $ do
        sample <- readCsvSample sampleWindowBytes path
        CSV.decodeSeparatedStrict opts sample
    declareSchemaValues prefix df

sampleWindowBytes :: Int
sampleWindowBytes = 1024 * 1024

readCsvSample :: Int -> FilePath -> IO BS.ByteString
readCsvSample n path = do
    chunk <- withFile path ReadMode (\h -> BS.hGet h (n + 1))
    let stripBom b = fromMaybe b (BS.stripPrefix "\xEF\xBB\xBF" b)
    pure . stripBom $
        if BS.length chunk <= n
            then chunk
            else
                let window = BS.take n chunk
                 in maybe window (\i -> BS.take (i + 1) window) $
                        BS.elemIndexEnd 0x0a window