packages feed

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

{-# LANGUAGE FlexibleContexts #-}

{- |
Module      : ExchangeAlgebra.Accounting.TrialBalance.Balance
Description : Account-balance representation and netting for trial balances,
              presentation, and consolidation.

Account balances retain their debit or credit direction while keeping scalar
values non-negative. All pair-valued functions in this module use debit first
and credit second.
-}
module ExchangeAlgebra.Accounting.TrialBalance.Balance
    ( AccountBalance(..)
    , balancePair
    , addPair
    , netPair
    , combineBalances
    , balanceFor
    , balanceSide
    , balanceAmount
    , accountBalances
    ) where

import qualified ExchangeAlgebra.Accounting.TrialBalance.Accum as Accum

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

import           ExchangeAlgebra.Algebra.Core.Representation (Alg)
import           ExchangeAlgebra.Algebra.Value.Class (HatVal(..))
import           ExchangeAlgebra.Accounting.Account.Classification (Side(..))
import           ExchangeAlgebra.Accounting.Account.Title (AccountTitles)
import           ExchangeAlgebra.Accounting.Exchange
                     ( ExBaseClass )

-- | Net balance direction remains structural; values stay non-negative.
data AccountBalance v
  = NoBalance
  | DebitBalance v
  | CreditBalance v
  deriving (Show, Eq)

-- | Convert a balance to a pair ordered as debit, then credit.
balancePair :: Num v => AccountBalance v -> (v, v)
balancePair NoBalance = (0, 0)
balancePair (DebitBalance value) = (value, 0)
balancePair (CreditBalance value) = (0, value)

-- | Add two debit-credit pairs componentwise.
addPair :: Num v => (v, v) -> (v, v) -> (v, v)
addPair (leftDebit, leftCredit) (rightDebit, rightCredit) =
    (leftDebit + rightDebit, leftCredit + rightCredit)

-- | Net a debit-credit pair into one structural balance.
netPair :: (Ord v, Num v) => (v, v) -> AccountBalance v
netPair (debit, credit)
    | debit == credit = NoBalance
    | debit > credit = DebitBalance (debit - credit)
    | otherwise = CreditBalance (credit - debit)

-- | Combine two balances using the common netting rule.
combineBalances
    :: (Ord v, Num v)
    => AccountBalance v -> AccountBalance v -> AccountBalance v
combineBalances left right = netPair
    (addPair (balancePair left) (balancePair right))

-- | Look up an account balance, treating an absent account as balanced.
balanceFor :: AccountTitles -> Map AccountTitles (AccountBalance v)
           -> AccountBalance v
balanceFor title = M.findWithDefault NoBalance title

-- | Recover the structural side of a balance.
balanceSide :: AccountBalance v -> Side
balanceSide NoBalance = Side
balanceSide (DebitBalance _) = Debit
balanceSide (CreditBalance _) = Credit

-- | Recover the non-negative scalar amount of a balance.
balanceAmount :: Num v => AccountBalance v -> v
balanceAmount NoBalance = 0
balanceAmount (DebitBalance value) = value
balanceAmount (CreditBalance value) = value

-- | Net the exact posting sum of each account and round its magnitude once.
-- The direction is determined before rounding, without a tolerance.
accountBalances
    :: (HatVal v, ExBaseClass b)
    => Alg v b
    -> Map AccountTitles (AccountBalance v)
accountBalances = M.map finish . Accum.accountAccums
  where
    finish total = case Accum.readAccountAccum Credit total of
        (Side, _) -> NoBalance
        (Debit, value) -> DebitBalance value
        (Credit, value) -> CreditBalance value