packages feed

exchangealgebra-0.6.0.0: src/ExchangeAlgebra/Accounting/TrialBalance/Accum.hs

{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE FlexibleContexts #-}

-- | Internal exact account sums for trial-balance validation and reporting.
-- The Accounting layer shares the posting fold and directional readout here;
-- the public balance representation remains in TrialBalance.Balance.
module ExchangeAlgebra.Accounting.TrialBalance.Accum
    ( structureErrors
    , accountAccums
    , insertAccountAccum
    , readAccountAccum
    ) where

import qualified Data.Map.Strict as M
import           Data.Map.Strict (Map)

import           ExchangeAlgebra.Algebra.Core.Representation (Alg, pattern (:@), foldEntries)
import           ExchangeAlgebra.Algebra.Value.Class (HatVal(..), isErrorValue)
import           ExchangeAlgebra.Accounting.Account.Classification (Side(..))
import           ExchangeAlgebra.Accounting.Account.Title (AccountTitles(..))
import           ExchangeAlgebra.Accounting.Exchange
                     ( ExBaseClass(getAccountTitle, whichSide) )

import           ExchangeAlgebra.Algebra.Base.Representation (Hat(..), HatBase(..))

-- | Inspect structure without determining posting sides. Lists retain the
-- validation fold's reverse traversal order, including a standalone zero term.
-- The result contains invalid values with titles, wildcard accounts, and
-- wildcard sides, respectively.
structureErrors
    :: HatVal v
    => Alg v (HatBase AccountTitles)
    -> ([(AccountTitles, v)], [AccountTitles], [AccountTitles])
structureErrors alg = case alg of
    value :@ base -> collect ([], [], []) value base
    _ -> foldEntries collect ([], [], []) alg
  where
    collect (invalids, accounts, sides) value (side :< title) =
        ( [(title, value) | isErrorValue value] ++ invalids
        , [title | title == AccountTitle] ++ accounts
        , [title | side == HatNot] ++ sides
        )

-- | Add one posting to its account's exact debit-minus-credit sum.
insertAccountAccum
    :: (HatVal v, ExBaseClass b)
    => Map AccountTitles (Accum v) -> v -> b -> Map AccountTitles (Accum v)
insertAccountAccum totals value base = M.insertWith mergeAccum (getAccountTitle base)
    (case whichSide base of
        Debit -> addAccum value emptyAccum
        Credit -> negateAccum (addAccum value emptyAccum)
        Side -> emptyAccum) totals

-- | Retain each account's exact signed sum until its consumer reads it.
accountAccums
    :: (HatVal v, ExBaseClass b)
    => Alg v b -> Map AccountTitles (Accum v)
accountAccums = foldEntries insertAccountAccum M.empty

-- | Determine direction before rounding a non-negative magnitude once.
-- A NaN uses the caller's reporting convention for its direction.
readAccountAccum :: HatVal v => Side -> Accum v -> (Side, v)
readAccountAccum nanSide total = case signAccum total of
    Just EQ -> (Side, zeroValue)
    Just GT -> (Debit, roundAccum total)
    Just LT -> (Credit, roundAccum (negateAccum total))
    Nothing -> (nanSide, roundAccum total)