packages feed

goal-core-0.20: Goal/Core/Project.hs

{-# LANGUAGE OverloadedStrings #-}
-- | This module provides functions for incorporating Goal into a
-- data-processing project. In particular, this module provides tools for
-- managing CSV files, and connecting them with gnuplot scripts for plotting.
-- CSV management is powered by @cassava@.
module Goal.Core.Project
    (
    -- * CSV
      goalImport
    , goalImportNamed
    , goalExport
    , goalExportLines
    , goalExportNamed
    , goalExportNamedLines
    -- ** CSV Instances
    , goalCSVParser
    , goalCSVNamer
    , goalCSVOrder
    -- * Util
    , runGnuplot
    , runGnuplotWithVariables
    ) where


--- Imports ---


-- Unqualified --

import System.Process
import System.Directory
import Data.Csv
import Data.Char
import GHC.Generics

-- Qualified --

import qualified Data.Vector as V
import qualified Data.ByteString.Lazy as BS
import qualified Data.ByteString as BSI


--- Experiments ---


--- Import/Export ---


-- | Runs @gnuplot@ on the given @.gpi@, passing it a @load_path@ variable to
-- help it find Goal-generated csvs.
runGnuplot
    :: FilePath -- ^ Gnuplot loadpath
    -> String -- ^ Gnuplot script
    -> IO ()
runGnuplot ldpth gpipth = do
    let cmd = concat [ "gnuplot ", " -e \"load_path='", ldpth, "'\" ",gpipth,".gpi" ]
    putStrLn $ "Running Command: " ++ cmd
    callCommand cmd

-- | Runs @gnuplot@ on the given @.gpi@, passing it a @load_path@ variable to
-- help it find Goal-generated csvs, and a list of variables.
runGnuplotWithVariables
    :: FilePath -- ^ Gnuplot loadpath
    -> String -- ^ Gnuplot script
    -> [(String,String)] -- ^ Arguments
    -> IO ()
runGnuplotWithVariables ldpth gpipth args = do
    let cmd = concat $ [ "gnuplot ", " -e \"load_path='", ldpth, "'" ]
            ++ (mapArgs <$> args) ++ [ "\" ",gpipth,".gpi" ]
    putStrLn $ "Running Command: " ++ cmd
    callCommand cmd
        where mapArgs (nm,val) = concat ["; ",nm,"='",val,"'"]


-- | Load the given CSV file. The @.csv@ extension is automatically added.
goalImport
    :: FromRecord r
    => FilePath
    -> IO (Either String [r]) -- ^ CSVs
goalImport flpth = do
    bstrm <- decode NoHeader <$> BS.readFile (flpth ++ ".csv")
    case bstrm of
      Right as -> return . Right $ V.toList as
      Left str -> return $ Left str

-- | Load the given CSV file with headers. The @.csv@ extension is automatically added.
goalImportNamed
    :: FromNamedRecord r
    => FilePath
    -> IO (Either String [r]) -- ^ CSVs
goalImportNamed flpth = do
    bstrm <- decodeByName <$> BS.readFile (flpth ++ ".csv")
    case bstrm of
      Right as -> return . Right . V.toList $ snd as
      Left str -> return $ Left str

filePather :: FilePath -> FilePath -> IO FilePath
filePather ldpth flnm = do
    createDirectoryIfMissing True ldpth
    return $ concat [ldpth,"/",flnm,".csv"]

-- | Export the given CSVs to a file in the given directory. The @.csv@
-- extension is automatically added to the file name.
goalExport
    :: ToRecord r
    => FilePath -- load_path
    -> String -- File Name
    -> [r] -- ^ CSVs
    -> IO ()
goalExport ldpth flnm csvs = do
    flpth <- filePather ldpth flnm
    BS.writeFile flpth $ encode csvs

-- | Export the given list of CSVs to a file in the given directory, seperating
-- each set of CSVs by a single line. This causes gnuplot to the read CSV as a
-- collection of line segments. The @.csv@ extension is automatically added to
-- the file name.
goalExportLines
    :: ToRecord r
    => FilePath
    -> FilePath
    -> [[r]] -- ^ CSVss
    -> IO ()
goalExportLines ldpth flnm csvss = do
    flpth <- filePather ldpth flnm
    BS.writeFile flpth . BS.tail . BS.tail . BS.concat $ BS.append "\r\n" . encode <$> csvss

-- | Export the named CSVs to a file in the given directory, adding a header to
-- the @.csv@ file.
goalExportNamed
    :: (ToNamedRecord r, DefaultOrdered r)
    => FilePath
    -> FilePath
    -> [r] -- ^ CSVs
    -> IO ()
goalExportNamed ldpth flnm csvs = do
    flpth <- filePather ldpth flnm
    BS.writeFile flpth $ encodeDefaultOrderedByName csvs

-- | Export the given list of named CSVs to a file, breaking it into a set of
-- line segments (with headers).
goalExportNamedLines
    :: (ToNamedRecord r, DefaultOrdered r)
    => FilePath
    -> FilePath
    -> [[r]] -- ^ CSVss
    -> IO ()
goalExportNamedLines ldpth flnm csvss = do
    flpth <- filePather ldpth flnm
    BS.writeFile flpth . BS.concat $ BS.append "\r\n" . encodeDefaultOrderedByName <$> csvss


--- Util ---


deCamelCaseLoop :: String -> String
deCamelCaseLoop "" = ""
deCamelCaseLoop (c:wrds) =
    let (wrd,wrds') = span isLower wrds
     in (c:wrd) ++ ' ' : deCamelCaseLoop wrds'

deCamelCase :: String -> String
deCamelCase (c:wrds) = init $ deCamelCaseLoop (toUpper c : wrds)
deCamelCase "" = error "How is deCamelCase being run on an empty string?"

deCamelCaseCSV :: Options
deCamelCaseCSV = defaultOptions { fieldLabelModifier = deCamelCase }

-- | A generic @.csv@ parser which reorganizes a header name in camel case into
-- "human readable" text. Useful for instantiating 'FromNamedRecord'.
goalCSVParser :: (Generic a, GFromNamedRecord (Rep a)) => NamedRecord -> Parser a
goalCSVParser = genericParseNamedRecord deCamelCaseCSV

-- | A generic @.csv@ namer which reorganizes a header name in camel case into
-- "human readable" text. Useful for instantiating 'ToNamedRecord'.
goalCSVNamer
    :: (Generic a, GToRecord (Rep a) (BSI.ByteString, BSI.ByteString)) => a -> NamedRecord
goalCSVNamer = genericToNamedRecord deCamelCaseCSV

-- | A generic @.csv@ order which reorganizes a header name in camel case into
-- "human readable" text. Useful for instantiating 'DefaultOrdered'.
goalCSVOrder :: (Generic a, GToNamedRecordHeader (Rep a)) => a -> Header
goalCSVOrder = genericHeaderOrder deCamelCaseCSV