packages feed

exchangealgebra-0.6.0.0: src/ExchangeAlgebra/Simulation/Network/Csv.hs

{-# LANGUAGE OverloadedStrings #-}
{- | Parse fixed-schema network CSV files in the Simulation layer.
This adapter uses the network constructors and the shared input CSV splitter.
Import "ExchangeAlgebra.Simulation.Network" for network values.
-}
module ExchangeAlgebra.Simulation.Network.Csv
    ( parseEdgeCsv, parseCoefCsv, readEdgeCsv, readCoefCsv ) where

import qualified Data.Text as T
import Data.Text (Text)
import qualified Data.Text.IO as TIO
import ExchangeAlgebra.IO.Input.Csv (splitTrim)
import ExchangeAlgebra.Simulation.Network

-- * CSV (fixed schema, minimal self-contained parser)
------------------------------------------------------------------
--
-- A deliberately tiny CSV reader: comma-separated, no quoting, blank lines and
-- lines whose first non-space character is @#@ are skipped, surrounding
-- whitespace on each field is trimmed. The first non-skipped line must be the
-- header. Avoids a @cassava@ dependency for these fixed schemas. Line splitting
-- is shared with "ExchangeAlgebra.Convert.Csv" through 'splitTrim'.

-- | Parse an edge CSV with header @from,to@ into @(from, to)@ pairs.
--
-- >>> parseEdgeCsv (T.pack "from,to\na,b\nb,c\n")
-- Right [("a","b"),("b","c")]
parseEdgeCsv :: Text -> Either Text [(Text, Text)]
parseEdgeCsv txt =
    case dataRows ["from", "to"] txt of
      Left e     -> Left e
      Right rows -> traverse row rows
  where
    row [a, b] = Right (a, b)
    row r      = Left ("edge row expected 2 fields, got " <> T.pack (show (length r)))

-- | Parse a coefficient CSV with header @from,to,coef@ into
-- @(from, to, coef)@ triples (coefficient read as 'Double').
--
-- >>> parseCoefCsv (T.pack "from,to,coef\na,b,0.5\n")
-- Right [("a","b",0.5)]
parseCoefCsv :: Text -> Either Text [(Text, Text, Double)]
parseCoefCsv txt =
    case dataRows ["from", "to", "coef"] txt of
      Left e     -> Left e
      Right rows -> traverse row rows
  where
    row [a, b, c] = case reads (T.unpack c) of
        [(d, "")] -> Right (a, b, d)
        _         -> Left ("coef field not a number: " <> T.pack (show c))
    row r         = Left ("coef row expected 3 fields, got " <> T.pack (show (length r)))

-- | Read an edge CSV file into a t'TradeNetwork'. Combines parse and validation
-- errors into the @Left@ string.
readEdgeCsv :: FilePath -> IO (Either Text (TradeNetwork Text))
readEdgeCsv fp = do
    txt <- TIO.readFile fp
    pure $ case parseEdgeCsv txt of
      Left e    -> Left e
      Right es  -> either (Left . T.pack . show) Right (networkFromTable es)

-- | Read a coefficient CSV file into a pair of a t'TradeNetwork' and
-- t'InputCoefficients'. Combines parse and validation errors into the @Left@ string.
readCoefCsv :: FilePath -> IO (Either Text (TradeNetwork Text, InputCoefficients Text Double))
readCoefCsv fp = do
    txt <- TIO.readFile fp
    pure $ case parseCoefCsv txt of
      Left e       -> Left e
      Right trips  -> either (Left . T.pack . show) Right (coefficientsFromTable trips)

-- | Split CSV text into trimmed data-field rows, after checking the header.
dataRows :: [Text] -> Text -> Either Text [[Text]]
dataRows expectedHeader txt =
    case keptLines of
      []           -> Left "empty CSV (no header)"
      (h : body)
        | splitTrim h == expectedHeader -> Right (map splitTrim body)
        | otherwise -> Left ("unexpected header: " <> T.pack (show (splitTrim h))
                              <> ", expected " <> T.pack (show expectedHeader))
  where
    keptLines = filter keep (T.lines txt)
    keep l =
        let s = T.strip l
        in not (T.null s) && not ("#" `T.isPrefixOf` s)

------------------------------------------------------------------