packages feed

exchangealgebra-0.5.2.0: src/ExchangeAlgebra/Journal/Exact.hs

{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE TypeFamilies #-}

-- | Checked journal readouts preserve exact residuals across note boundaries.
-- This journal layer uses "ExchangeAlgebra.Algebra.Exact" and the existing
-- journal projections. Start with the algebra accumulator contract, then read
-- the projection section for cancellation within each note. Other readouts
-- aggregate notes, as the existing journal bar does.
--
-- Inputs must be finite and non-negative; all intermediate aggregation units
-- and final outputs must fit the value type. Each scalar is rounded once,
-- without cancellation tolerance, and negative zero becomes positive zero.
-- For Double, MoneyDouble, and NN.Double, scalar bits depend only on the multiset
-- of (note, complete base, side, value), including under accumulator merging.
-- Reassigning notes, compressing, substituting rounded partial sums, enumeration,
-- Show, and Binary are outside that guarantee.
module ExchangeAlgebra.Journal.Exact (
                                     -- * Accumulators
                                     ExactSum(..)
                                     , ExactSumError(..)
                                     , netAccum
                                     , sumExact
                                     -- * Journal readouts
                                     , normExact
                                     , barExact
                                     , balanceMapByExact
                                     , netPairMapByExact
                                     , postFromNetByExact
                                     -- * Projections
                                     , projNetNormExact
                                     , projWithBaseNetNormExact
                                     , projWithNoteBaseNetNormExact
                                     -- * Accounting readouts
                                     , diffRLExact
                                     , balanceExact
                                     , accountBalancesExact
                                     ) where

import qualified Data.Foldable as Foldable
import qualified Data.HashMap.Strict as HashMap
import qualified Data.Map.Strict as Map

import ExchangeAlgebra.Algebra (Alg, HatVal(..))
import qualified ExchangeAlgebra.Algebra.Internal as Internal
import ExchangeAlgebra.Algebra.Base (
                                    AccountTitles
                                    , ExBaseClass
                                    , Hat(..)
                                    , HatBaseClass(..)
                                    , Side
                                    )
import ExchangeAlgebra.Algebra.Exact (
                                     ExactSum(..)
                                     , ExactSumError(..)
                                     , netAccum
                                     , sumExact
                                     )
import qualified ExchangeAlgebra.Algebra.Exact as Exact
import ExchangeAlgebra.Journal (Journal, Note(..), (.|))
import qualified ExchangeAlgebra.Journal as Journal
import ExchangeAlgebra.TrialBalance.Balance (AccountBalance)

-- * Accumulators

-- The scalar accumulator operations are re-exported from the algebra layer.

-- * Journal readouts

-- | Scan stored complete-base pairs, retaining Not and Hat states independently.
-- No note boundary is crossed until the caller explicitly combines its input.
residuals :: (ExactSum n, HatBaseClass b)
          => Alg n b -> Either ExactSumError [(b, Accum n)]
residuals Internal.Zero = Right []
residuals (value Internal.:@ postingBase)
    | isZeroValue value = Right []
    | otherwise = do
        (direction, state) <- netAccumState (addAccum value emptyAccum) emptyAccum
        pure $ case direction of
            EQ -> []
            _ -> [(merge side (base postingBase), state)]
  where
    side
        | isHat postingBase = Hat
        | otherwise = Not
residuals (Internal.Liner pairs _ _ _ _ _) = HashMap.foldlWithKey' finish (Right []) pairs
  where
    add total value
        | isZeroValue value = total
        | otherwise = addAccum value total
    finish result basePart (Internal.Pair hats nots) = do
        previous <- result
        (direction, difference) <- netAccumState
            (Foldable.foldl' add emptyAccum nots) (Foldable.foldl' add emptyAccum hats)
        pure $ case direction of
            EQ -> previous
            GT -> (merge Not basePart, difference) : previous
            LT -> (merge Hat basePart, difference) : previous

-- | Sum every posting across notes and round the gross norm once.
-- Finite non-negative inputs and their combined Hat-plus-Not total must fit the
-- type. Unlike the existing norm, floating summation order does not affect bits.
normExact :: (ExactSum n, HatBaseClass b, Note t)
          => Journal t n b -> Either ExactSumError n
normExact = Exact.normExact . Journal.toAlg

-- | Gather notes into plank, then cancel complete bases exactly.
-- Each finite non-negative base-side total must fit the type. Each residual is
-- rounded once, with no tolerance. Notes are aggregated as in the existing bar;
-- use the projection readouts for cancellation confined to each note.
barExact :: (ExactSum n, HatBaseClass b, Note t)
         => Journal t n b -> Either ExactSumError (Journal t n b)
barExact journal = (.| plank) <$> Exact.barExact (Journal.toAlg journal)

-- | Aggregate selected postings across notes by key and net each key once.
-- Inputs must be finite and non-negative and each key-side total must fit the
-- type. GT means Not wins, LT means Hat wins, and zero keys remain (EQ,0).
-- The magnitude is non-negative, without the old signed sequential summation.
-- Postings whose key is Nothing are not validated or aggregated.
balanceMapByExact :: (ExactSum n, HatBaseClass b, Note t, Ord k)
                  => (BasePart b -> Maybe k) -> Journal t n b
                  -> Either ExactSumError (Map.Map k (Ordering, n))
balanceMapByExact keyOf = Exact.balanceMapByExact keyOf . Journal.toAlg

-- | Gather notes, cancel per complete base, and merge by key and winning side.
-- Finite non-negative inputs, base-side totals, and key-side residual totals
-- must fit the type. Each (Not,Hat) output component is rounded once, without
-- tolerance. Distinct bases with Not 10 and Hat 7 retain (10,7).
netPairMapByExact :: (ExactSum n, HatBaseClass b, Note t, Ord k)
                  => (BasePart b -> Maybe k) -> Journal t n b
                  -> Either ExactSumError (Map.Map k (n, n))
netPairMapByExact keyOf = Exact.netPairMapByExact keyOf . Journal.toAlg

-- | Gather notes, net each base, restore its winning side, select the key,
-- merge residual states, round once per key, then invoke the journal callback.
-- Finite non-negative inputs and all base-side and selected key totals must
-- fit the type. No tolerance or rounded intermediate bar is used. The checks
-- and rounding guarantee end at the callback's argument, not its output.
postFromNetByExact :: (ExactSum n, HatBaseClass b, Note t, Ord k)
                   => (b -> Maybe k) -> (k -> n -> Journal t n b) -> Journal t n b
                   -> Either ExactSumError (Journal t n b)
postFromNetByExact keyOf post journal = do
    remaining <- residuals (Journal.toAlg journal)
    amounts <- traverse roundAccum (Foldable.foldl' collect Map.empty remaining)
    pure (Map.foldlWithKey' (\result key value -> result <> post key value) mempty amounts)
  where
    collect totals (postingBase, state) = case keyOf postingBase of
        Nothing -> totals
        Just key -> Map.insertWith mergeAccum key state totals

-- * Projections

-- | Merge every note's exact base residuals before the only scalar rounding.
-- The caller has already selected notes and bases with the existing projections.
projectedNorm :: (ExactSum n, HatBaseClass b, Note t)
              => Journal t n b -> Either ExactSumError n
projectedNorm journal = do
    total <- HashMap.foldl' collect (Right emptyAccum) (Journal.toMap journal)
    roundAccum total
  where
    collect result algebra = do
        previous <- result
        remaining <- residuals algebra
        pure (Foldable.foldl' (\total (_, state) -> mergeAccum total state) previous remaining)

-- | Project bases with set semantics and cancel within each note.
-- This is 'projWithBaseNetNormExact': finite non-negative selected inputs,
-- base-side totals, and the combined residual total must fit the type. There
-- is one final rounding and no tolerance; repeated queries do not repeat values.
projNetNormExact :: (ExactSum n, HatBaseClass b, Note t)
                 => [b] -> Journal t n b -> Either ExactSumError n
projNetNormExact = projWithBaseNetNormExact

-- | Project bases, net each complete base within each note, merge residual
-- states across notes, and round once. Finite non-negative selected inputs,
-- each base-side total, and the final residual sum must fit the type.
-- Different notes containing Not 10 and Hat 10 yield 20, not zero.
--
-- Unlike the old readout, no note-local scalar is rounded before summation.
-- Equality with a norm of per-note bars holds only as a mathematical operation
-- interpreting every operation at infinite precision. With T = 2^53, residuals
-- T+1 and 1 yield T+2 here, but T if each base is rounded first. Duplicate base
-- queries have set semantics and do not change output bits.
projWithBaseNetNormExact :: (ExactSum n, HatBaseClass b, Note t)
                         => [b] -> Journal t n b -> Either ExactSumError n
projWithBaseNetNormExact bases = projectedNorm . Journal.projWithBase bases

-- | Select notes and bases with the existing wildcard and set semantics, then
-- cancel within each note and round the merged residual states once.
-- Empty notes or plank select all notes; duplicate queries do not duplicate
-- postings. Finite non-negative selected inputs, base-side totals, and the
-- final residual sum must fit the type. No tolerance or intermediate rounding
-- is used, with the same mathematical equality qualification as
-- 'projWithBaseNetNormExact'.
projWithNoteBaseNetNormExact :: (ExactSum n, HatBaseClass b, Note t)
                             => [t] -> [b] -> Journal t n b -> Either ExactSumError n
projWithNoteBaseNetNormExact notes bases =
    projectedNorm . Journal.projWithNoteBase notes bases

-- * Accounting readouts

-- | Net debit and credit across notes and round the absolute difference once.
-- Finite non-negative inputs and both side totals must fit the type. Only
-- exact equality yields Side, without the tolerance of the old diffRL.
-- Structural Side postings contribute no value and are not validated.
diffRLExact :: (ExactSum n, ExBaseClass b, Note t)
            => Journal t n b -> Either ExactSumError (Side, n)
diffRLExact = Exact.diffRLExact . Journal.toAlg

-- | Test exact debit-credit equality across notes with the checks of 'diffRLExact'.
-- This observes exact direction, with no tolerance; its scalar is rounded once.
balanceExact :: (ExactSum n, ExBaseClass b, Note t)
             => Journal t n b -> Either ExactSumError Bool
balanceExact = Exact.balanceExact . Journal.toAlg

-- | Aggregate by account title across notes and net each account exactly.
-- Finite non-negative inputs and account-side totals must fit the type. Each
-- magnitude is rounded once, replacing sequential sums without a tolerance.
-- Exact zero accounts remain NoBalance, as in the algebra account readout.
-- Structural Side postings contribute no value and are not validated.
accountBalancesExact :: (ExactSum n, ExBaseClass b, Note t)
                     => Journal t n b
                     -> Either ExactSumError (Map.Map AccountTitles (AccountBalance n))
accountBalancesExact = Exact.accountBalancesExact . Journal.toAlg