packages feed

currycarbon-0.6.0.0: src/Currycarbon/TSV.hs

{-# LANGUAGE OverloadedStrings #-}

module Currycarbon.TSV where

import           Currycarbon.ParserHelpers
import           Currycarbon.Types
import           Currycarbon.Utils

import           Control.Applicative       (empty)
import           Control.Exception         (throwIO)
import qualified Data.ByteString.Char8     as B8
import qualified Data.ByteString.Lazy      as BL
import           Data.Char                 (ord)
import qualified Data.Csv                  as Csv
import qualified Data.HashMap.Strict       as HM
import           Data.Maybe                (catMaybes)
import qualified Data.Vector               as V
import qualified Text.Parsec               as P
import qualified Text.Parsec.String        as P

-- | Turn 'TSV' to 'NamedCalExpr's for further processing (see 'evalNamedCalExpr')
tsv2NamedCalExprs :: CombinationStrategy -> TSV -> [Either CurrycarbonException NamedCalExpr]
tsv2NamedCalExprs combStrat (TSV _ _ rows) = V.toList $ V.map (tsvRow2NamedCalExprs combStrat) rows

tsvRow2NamedCalExprs :: CombinationStrategy -> TSVRow -> Either CurrycarbonException NamedCalExpr
-- C14 age with n dates and a labcode
tsvRow2NamedCalExprs combStrat
    (TSVRow i (Just (ListColumn lcs)) (Just (ListColumn bps)) (Just (ListColumn errs)) _ _) |
    (length lcs == length bps) && (length bps == length errs) =
    Right $ NamedCalExpr i $ foldC14 combStrat $ zip3 lcs bps errs
-- C14 age with n dates and no labcode
tsvRow2NamedCalExprs combStrat
    (TSVRow i _ (Just (ListColumn bps)) (Just (ListColumn errs)) _ _) |
    length bps == length errs =
    Right $ NamedCalExpr i $ foldC14 combStrat $ zip3 (repeat "") bps errs
-- contextual age
tsvRow2NamedCalExprs _ (TSVRow i _ _ _ (Just start) (Just stop)) =
   NamedCalExpr i . WindowBCAD <$> makeTimeWindowBCAD "" start stop
-- error case if nothing fits
tsvRow2NamedCalExprs _ (TSVRow i _ _ _ _ _) =
    Left $ CurrycarbonTSV2CalExprException i

-- | A data type for the strategy to combine multiple C14 dates.
data CombinationStrategy =
     CombSum -- ^ Form a normalised sum of the 'CalPDF's.
   | CombProduct -- ^ Form a normalised product of the 'CalPDF's.

instance Show CombinationStrategy where
    show CombSum     = "Sum"
    show CombProduct = "Product"

readCombinationStrategy :: String -> Either String CombinationStrategy
readCombinationStrategy s =
    case P.runParser parseCombinationStrategy () s s of
        Left err -> Left $ showParsecErrOneLine err
        Right x  -> Right x

parseCombinationStrategy :: P.Parser CombinationStrategy
parseCombinationStrategy =  do
    x <- P.many P.anyChar
    case x of
        "Sum"     -> pure CombSum
        "Product" -> pure CombProduct
        _         -> fail "must be either Sum or Product"

foldC14 :: CombinationStrategy -> [(String, Word, Word)] -> CalExpr
foldC14 CombSum xs     = foldl1 SumCal     $ map (\(lc,bp,err) -> UnCalDate $ UncalC14 lc bp err) xs
foldC14 CombProduct xs = foldl1 ProductCal $ map (\(lc,bp,err) -> UnCalDate $ UncalC14 lc bp err) xs

-- | Read calibration expressions from a .tsv file. The file should have the following columns:
--
-- @
-- Date_ID	Date_C14_Labnr	Date_C14_Uncal_BP	Date_C14_Uncal_BP_Err	Date_BC_AD_Start	Date_BC_AD_Stop
-- Sample1				-200	100
-- Sample2	TEST-1	1000	30
-- Sample3	TEST-2;TEST-3	3000;3200	30;50
-- @
--
-- Subsets like <Date_ID> + <Date_C14_Uncal_BP> + <Date_C14_Uncal_BP_Err> or
-- <Date_ID> + <Date_BC_AD_Start> + <Date_BC_AD_Stop> are also sufficient.
-- The structure is inspired by the Poseidon .janno file.
--
readTSV :: FilePath -> IO TSV
readTSV path = do
    bs <- BL.readFile path
    case Csv.decodeByNameWith decodingOptions bs of
      Left s               -> throwIO $ CurrycarbonTSVParsingException s
      Right (header, rows) -> return (TSV path header rows)

decodingOptions :: Csv.DecodeOptions
decodingOptions = Csv.defaultDecodeOptions { Csv.decDelimiter = fromIntegral (ord '\t') }

-- | A data type for tab-separated (.tsv) files
data TSV = TSV {
      _tsvFile   :: FilePath
    , _tsvHeader :: Csv.Header
    , _tsvRows   :: V.Vector TSVRow
    }

-- | A data type to represent the relevant fields of one row in a .tsv file
data TSVRow = TSVRow {
    -- | Identifier of the expression
      _tsvRowID                :: String
    -- | Lab codes of radiocarbon dates
    , _tsvRowDateC14Labnr      :: Maybe (ListColumn String)
    -- | C14 ages in years BP
    , _tsvRowDateC14UncalBP    :: Maybe (ListColumn Word)
    -- | C14 standard deviations (one sigma in years)
    , _tsvRowDateC14UncalBPErr :: Maybe (ListColumn Word)
    -- | Start of time window in years BC
    , _tsvRowDateBCADStart     :: Maybe Int
    -- | End of time window in years BC
    , _tsvRowDateBCADStop      :: Maybe Int
    -- for any other columns
    -- , _tsvRowAllColumns        :: Csv.NamedRecord
    }
    deriving Show

instance Csv.FromNamedRecord TSVRow where
    parseNamedRecord m = do
        i          <- filterLookupMulti m ["Date_ID", "Poseidon_ID"]
        labnr      <- filterLookupOptional m "Date_C14_Labnr"
        uncalBP    <- filterLookupOptional m "Date_C14_Uncal_BP"
        uncalBPErr <- filterLookupOptional m "Date_C14_Uncal_BP_Err"
        start      <- filterLookupOptional m "Date_BC_AD_Start"
        stop       <- filterLookupOptional m "Date_BC_AD_Stop"
        pure $ TSVRow {
              _tsvRowID                = i
            , _tsvRowDateC14Labnr      = labnr
            , _tsvRowDateC14UncalBP    = uncalBP
            , _tsvRowDateC14UncalBPErr = uncalBPErr
            , _tsvRowDateBCADStart     = start
            , _tsvRowDateBCADStop      = stop
            -- for any other columns
            -- , _tsvRowAllColumns        = m
            }

-- lookup column by name
filterLookup :: Csv.FromField a => Csv.NamedRecord -> B8.ByteString -> Csv.Parser a
filterLookup m name = maybe empty Csv.parseField $ cleanInput $ HM.lookup name m

-- lookup optional column by name
filterLookupOptional :: Csv.FromField a => Csv.NamedRecord -> B8.ByteString -> Csv.Parser (Maybe a)
filterLookupOptional m name = maybe (pure Nothing) (\bs -> Just <$> Csv.parseField bs) $
                              cleanInput $ HM.lookup name m

-- lookup column by multiple different names and keep the first match
filterLookupMulti :: Csv.FromField a => Csv.NamedRecord -> [B8.ByteString] -> Csv.Parser a
filterLookupMulti m names =
    maybe empty Csv.parseField $ cleanInput $ lookupMulti names
    where
        lookupMulti :: [B8.ByteString] -> Maybe B8.ByteString
        lookupMulti ns =
            let vals = map (`HM.lookup` m) ns
            in case catMaybes vals of
                []    -> Nothing
                (x:_) -> Just x

cleanInput :: Maybe B8.ByteString -> Maybe B8.ByteString
cleanInput Nothing           = Nothing
cleanInput (Just rawInputBS) = transNA rawInputBS
    where
        transNA ".."  = Nothing
        transNA ""    = Nothing
        transNA "n/a" = Nothing
        transNA x     = Just x

newtype ListColumn a = ListColumn {getListColumn :: [a]}
    deriving (Eq, Ord, Show)
instance (Csv.FromField a) => Csv.FromField (ListColumn a) where
    parseField x = fmap ListColumn . mapM Csv.parseField $ B8.splitWith (==';') x