{-# LANGUAGE CPP                 #-}
{-# LANGUAGE FlexibleInstances   #-}
{-# LANGUAGE OverloadedStrings   #-}
{-# LANGUAGE NamedFieldPuns      #-}
{-# LANGUAGE RankNTypes          #-}
{-# LANGUAGE RecordWildCards     #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections       #-}
{-|

Multi-column balance reports, used by the balance command.

-}

module Hledger.Reports.MultiBalanceReport (
  MultiBalanceReport,
  MultiBalanceReportRow,

  multiBalanceReport,
  multiBalanceReportWith,

  compoundBalanceReport,
  compoundBalanceReportWith,

  -- * Helper functions
  makeReportQuery,
  getPostings,
  generateMultiBalanceAccount,
  generatePeriodicReport,
  makePeriodicReportRow,

  -- -- * Tests
  tests_MultiBalanceReport
)
where

#if !MIN_VERSION_base(4,18,0)
import Control.Applicative (liftA2)
#endif
import Control.Monad (guard)
import Data.Foldable (toList)
import Data.List (sortOn)
import Data.List.NonEmpty (NonEmpty((:|)))
import qualified Data.HashSet as HS
import qualified Data.IntMap.Strict as IM
import qualified Data.IntSet as IS
import Data.Maybe (fromMaybe, isJust, mapMaybe)
import Data.Ord (Down(..))
import Data.Semigroup (sconcat)
import Data.These (these)
import Data.Time.Calendar (Day(..), addDays, fromGregorian)
import Data.Traversable (mapAccumL)

import Hledger.Data
import Hledger.Query
import Hledger.Utils
import Hledger.Reports.ReportOptions
import Hledger.Reports.ReportTypes


-- | A multi balance report is a kind of periodic report, where the amounts
-- correspond to balance changes or ending balances in a given period. It has:
--
-- 1. a list of each column's period (date span)
--
-- 2. a list of rows, each containing:
--
--   * the full account name, display name, and display depth
--
--   * A list of amounts, one for each column.
--
--   * the total of the row's amounts for a periodic report
--
--   * the average of the row's amounts
--
-- 3. the column totals, and the overall grand total (or zero for
-- cumulative/historical reports) and grand average.

type MultiBalanceReport    = PeriodicReport    DisplayName MixedAmount
type MultiBalanceReportRow = PeriodicReportRow DisplayName MixedAmount


-- | Generate a multicolumn balance report for the matched accounts,
-- showing the change of balance, accumulated balance, or historical balance
-- in each of the specified periods. If the normalbalance_ option is set, it
-- adjusts the sorting and sign of amounts (see ReportOpts and
-- CompoundBalanceCommand). hledger's most powerful and useful report, used
-- by the balance command (in multiperiod mode) and (via compoundBalanceReport)
-- by the bs/cf/is commands.
multiBalanceReport :: ReportSpec -> Journal -> MultiBalanceReport
multiBalanceReport :: ReportSpec -> Journal -> MultiBalanceReport
multiBalanceReport ReportSpec
rspec Journal
j = ReportSpec -> Journal -> PriceOracle -> MultiBalanceReport
multiBalanceReportWith ReportSpec
rspec Journal
j (Bool -> Journal -> PriceOracle
journalPriceOracle Bool
infer Journal
j)
  where infer :: Bool
infer = ReportOpts -> Bool
infer_prices_ (ReportOpts -> Bool) -> ReportOpts -> Bool
forall a b. (a -> b) -> a -> b
$ ReportSpec -> ReportOpts
_rsReportOpts ReportSpec
rspec

-- | A helper for multiBalanceReport. This one takes some extra arguments,
-- a 'PriceOracle' to be used for looking up market prices, and a set of
-- 'AccountName's which should not be elided. Commands which run multiple
-- reports (bs etc.) can generate the price oracle just once for efficiency,
-- passing it to each report by calling this function directly.
multiBalanceReportWith :: ReportSpec -> Journal -> PriceOracle -> MultiBalanceReport
multiBalanceReportWith :: ReportSpec -> Journal -> PriceOracle -> MultiBalanceReport
multiBalanceReportWith ReportSpec
rspec' Journal
j PriceOracle
priceoracle = MultiBalanceReport
report
  where
    -- Queries, report/column dates.
    (DateSpan
reportspan, [DateSpan]
colspans) = String -> (DateSpan, [DateSpan]) -> (DateSpan, [DateSpan])
forall a. Show a => String -> a -> a
dbg5 String
"multiBalanceReportWith reportSpan" ((DateSpan, [DateSpan]) -> (DateSpan, [DateSpan]))
-> (DateSpan, [DateSpan]) -> (DateSpan, [DateSpan])
forall a b. (a -> b) -> a -> b
$ Journal -> ReportSpec -> (DateSpan, [DateSpan])
reportSpan Journal
j ReportSpec
rspec'
    rspec :: ReportSpec
rspec = String -> ReportSpec -> ReportSpec
forall a. Show a => String -> a -> a
dbg3 String
"multiBalanceReportWith rspec" (ReportSpec -> ReportSpec) -> ReportSpec -> ReportSpec
forall a b. (a -> b) -> a -> b
$ ReportSpec -> DateSpan -> ReportSpec
makeReportQuery ReportSpec
rspec' DateSpan
reportspan
    -- force evaluation order to show price lookup after date spans in debug output (XXX not working)
    -- priceoracle = reportspan `seq` priceoracle0

    -- Get postings
    ps :: [Posting]
ps = String -> [Posting] -> [Posting]
forall a. Show a => String -> a -> a
dbg5 String
"multiBalanceReportWith ps" ([Posting] -> [Posting]) -> [Posting] -> [Posting]
forall a b. (a -> b) -> a -> b
$ ReportSpec -> Journal -> PriceOracle -> DateSpan -> [Posting]
getPostings ReportSpec
rspec Journal
j PriceOracle
priceoracle DateSpan
reportspan

    -- Process changes into normal, cumulative, or historical amounts, plus value them and mark which are uninteresting
    acct :: Account BalanceData
acct = String -> Account BalanceData -> Account BalanceData
forall a. Show a => String -> a -> a
dbg5 String
"multiBalanceReportWith acct" (Account BalanceData -> Account BalanceData)
-> Account BalanceData -> Account BalanceData
forall a b. (a -> b) -> a -> b
$ ReportSpec
-> Journal
-> PriceOracle
-> [DateSpan]
-> [Posting]
-> Account BalanceData
generateMultiBalanceAccount ReportSpec
rspec Journal
j PriceOracle
priceoracle [DateSpan]
colspans [Posting]
ps

    -- Generate and postprocess the report, negating balances and taking percentages if needed
    report :: MultiBalanceReport
report = String -> MultiBalanceReport -> MultiBalanceReport
forall a. Show a => String -> a -> a
dbg4 String
"multiBalanceReportWith report" (MultiBalanceReport -> MultiBalanceReport)
-> MultiBalanceReport -> MultiBalanceReport
forall a b. (a -> b) -> a -> b
$ ReportOpts
-> [DateSpan] -> Account BalanceData -> MultiBalanceReport
generateMultiBalanceReport (ReportSpec -> ReportOpts
_rsReportOpts ReportSpec
rspec) [DateSpan]
colspans Account BalanceData
acct

-- | Generate a compound balance report from a list of CBCSubreportSpec. This
-- shares postings between the subreports.
compoundBalanceReport :: ReportSpec -> Journal -> [CBCSubreportSpec a]
                      -> CompoundPeriodicReport a MixedAmount
compoundBalanceReport :: forall a.
ReportSpec
-> Journal
-> [CBCSubreportSpec a]
-> CompoundPeriodicReport a MixedAmount
compoundBalanceReport ReportSpec
rspec Journal
j = ReportSpec
-> Journal
-> PriceOracle
-> [CBCSubreportSpec a]
-> CompoundPeriodicReport a MixedAmount
forall a.
ReportSpec
-> Journal
-> PriceOracle
-> [CBCSubreportSpec a]
-> CompoundPeriodicReport a MixedAmount
compoundBalanceReportWith ReportSpec
rspec Journal
j (Bool -> Journal -> PriceOracle
journalPriceOracle Bool
infer Journal
j)
  where infer :: Bool
infer = ReportOpts -> Bool
infer_prices_ (ReportOpts -> Bool) -> ReportOpts -> Bool
forall a b. (a -> b) -> a -> b
$ ReportSpec -> ReportOpts
_rsReportOpts ReportSpec
rspec

-- | A helper for compoundBalanceReport, similar to multiBalanceReportWith.
compoundBalanceReportWith :: ReportSpec -> Journal -> PriceOracle
                          -> [CBCSubreportSpec a]
                          -> CompoundPeriodicReport a MixedAmount
compoundBalanceReportWith :: forall a.
ReportSpec
-> Journal
-> PriceOracle
-> [CBCSubreportSpec a]
-> CompoundPeriodicReport a MixedAmount
compoundBalanceReportWith ReportSpec
rspec' Journal
j PriceOracle
priceoracle [CBCSubreportSpec a]
subreportspecs = CompoundPeriodicReport a MixedAmount
cbr
  where
    -- Queries, report/column dates.
    (DateSpan
reportspan, [DateSpan]
colspans) = String -> (DateSpan, [DateSpan]) -> (DateSpan, [DateSpan])
forall a. Show a => String -> a -> a
dbg5 String
"compoundBalanceReportWith reportSpan" ((DateSpan, [DateSpan]) -> (DateSpan, [DateSpan]))
-> (DateSpan, [DateSpan]) -> (DateSpan, [DateSpan])
forall a b. (a -> b) -> a -> b
$ Journal -> ReportSpec -> (DateSpan, [DateSpan])
reportSpan Journal
j ReportSpec
rspec'
    rspec :: ReportSpec
rspec = String -> ReportSpec -> ReportSpec
forall a. Show a => String -> a -> a
dbg3 String
"compoundBalanceReportWith rspec" (ReportSpec -> ReportSpec) -> ReportSpec -> ReportSpec
forall a b. (a -> b) -> a -> b
$ ReportSpec -> DateSpan -> ReportSpec
makeReportQuery ReportSpec
rspec' DateSpan
reportspan

    -- Get postings
    ps :: [Posting]
ps = String -> [Posting] -> [Posting]
forall a. Show a => String -> a -> a
dbg5 String
"compoundBalanceReportWith ps" ([Posting] -> [Posting]) -> [Posting] -> [Posting]
forall a b. (a -> b) -> a -> b
$ ReportSpec -> Journal -> PriceOracle -> DateSpan -> [Posting]
getPostings ReportSpec
rspec Journal
j PriceOracle
priceoracle DateSpan
reportspan

    subreports :: [(CommoditySymbol, PeriodicReport a MixedAmount, Bool)]
subreports = (CBCSubreportSpec a
 -> (CommoditySymbol, PeriodicReport a MixedAmount, Bool))
-> [CBCSubreportSpec a]
-> [(CommoditySymbol, PeriodicReport a MixedAmount, Bool)]
forall a b. (a -> b) -> [a] -> [b]
map CBCSubreportSpec a
-> (CommoditySymbol, PeriodicReport a MixedAmount, Bool)
forall {a}.
CBCSubreportSpec a
-> (CommoditySymbol, PeriodicReport a MixedAmount, Bool)
generateSubreport [CBCSubreportSpec a]
subreportspecs
      where
        generateSubreport :: CBCSubreportSpec a
-> (CommoditySymbol, PeriodicReport a MixedAmount, Bool)
generateSubreport CBCSubreportSpec{Bool
CommoditySymbol
Query
ReportOpts -> ReportOpts
MultiBalanceReport -> PeriodicReport a MixedAmount
cbcsubreporttitle :: CommoditySymbol
cbcsubreportquery :: Query
cbcsubreportoptions :: ReportOpts -> ReportOpts
cbcsubreporttransform :: MultiBalanceReport -> PeriodicReport a MixedAmount
cbcsubreportincreasestotal :: Bool
cbcsubreportincreasestotal :: forall a. CBCSubreportSpec a -> Bool
cbcsubreporttransform :: forall a.
CBCSubreportSpec a
-> MultiBalanceReport -> PeriodicReport a MixedAmount
cbcsubreportoptions :: forall a. CBCSubreportSpec a -> ReportOpts -> ReportOpts
cbcsubreportquery :: forall a. CBCSubreportSpec a -> Query
cbcsubreporttitle :: forall a. CBCSubreportSpec a -> CommoditySymbol
..} =
            ( CommoditySymbol
cbcsubreporttitle
            -- Postprocess the report, negating balances and taking percentages if needed
            , MultiBalanceReport -> PeriodicReport a MixedAmount
cbcsubreporttransform (MultiBalanceReport -> PeriodicReport a MixedAmount)
-> MultiBalanceReport -> PeriodicReport a MixedAmount
forall a b. (a -> b) -> a -> b
$ ReportOpts
-> [DateSpan] -> Account BalanceData -> MultiBalanceReport
generateMultiBalanceReport ReportOpts
ropts [DateSpan]
colspans Account BalanceData
acct
            , Bool
cbcsubreportincreasestotal
            )
          where
            ropts :: ReportOpts
ropts = ReportOpts -> ReportOpts
cbcsubreportoptions (ReportOpts -> ReportOpts) -> ReportOpts -> ReportOpts
forall a b. (a -> b) -> a -> b
$ ReportSpec -> ReportOpts
_rsReportOpts ReportSpec
rspec
            -- Add a restriction to this subreport to the report query.
            -- XXX in non-thorough way, consider updateReportSpec ?
            rspecsub :: ReportSpec
rspecsub = ReportSpec
rspec{_rsReportOpts=ropts, _rsQuery=And [cbcsubreportquery, _rsQuery rspec]}
            -- Match and postings for the subreport
            subreportps :: [Posting]
subreportps = (Posting -> Bool) -> [Posting] -> [Posting]
forall a. (a -> Bool) -> [a] -> [a]
filter ((CommoditySymbol -> Maybe AccountType) -> Query -> Posting -> Bool
matchesPostingExtra (Journal -> CommoditySymbol -> Maybe AccountType
journalAccountType Journal
j) Query
cbcsubreportquery) [Posting]
ps
            -- Account representing this subreport
            acct :: Account BalanceData
acct = ReportSpec
-> Journal
-> PriceOracle
-> [DateSpan]
-> [Posting]
-> Account BalanceData
generateMultiBalanceAccount ReportSpec
rspecsub Journal
j PriceOracle
priceoracle [DateSpan]
colspans [Posting]
subreportps

    -- Sum the subreport totals by column. Handle these cases:
    -- - no subreports
    -- - empty subreports, having no subtotals (#588)
    -- - subreports with a shorter subtotals row than the others
    overalltotals :: PeriodicReportRow () MixedAmount
overalltotals = case [(CommoditySymbol, PeriodicReport a MixedAmount, Bool)]
subreports of
        []     -> ()
-> [MixedAmount]
-> MixedAmount
-> MixedAmount
-> PeriodicReportRow () MixedAmount
forall a b. a -> [b] -> b -> b -> PeriodicReportRow a b
PeriodicReportRow () [] MixedAmount
nullmixedamt MixedAmount
nullmixedamt
        ((CommoditySymbol, PeriodicReport a MixedAmount, Bool)
r:[(CommoditySymbol, PeriodicReport a MixedAmount, Bool)]
rs) -> NonEmpty (PeriodicReportRow () MixedAmount)
-> PeriodicReportRow () MixedAmount
forall a. Semigroup a => NonEmpty a -> a
sconcat (NonEmpty (PeriodicReportRow () MixedAmount)
 -> PeriodicReportRow () MixedAmount)
-> NonEmpty (PeriodicReportRow () MixedAmount)
-> PeriodicReportRow () MixedAmount
forall a b. (a -> b) -> a -> b
$ ((CommoditySymbol, PeriodicReport a MixedAmount, Bool)
 -> PeriodicReportRow () MixedAmount)
-> NonEmpty (CommoditySymbol, PeriodicReport a MixedAmount, Bool)
-> NonEmpty (PeriodicReportRow () MixedAmount)
forall a b. (a -> b) -> NonEmpty a -> NonEmpty b
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
fmap (CommoditySymbol, PeriodicReport a MixedAmount, Bool)
-> PeriodicReportRow () MixedAmount
forall {a} {a}.
(a, PeriodicReport a MixedAmount, Bool)
-> PeriodicReportRow () MixedAmount
subreportTotal ((CommoditySymbol, PeriodicReport a MixedAmount, Bool)
r(CommoditySymbol, PeriodicReport a MixedAmount, Bool)
-> [(CommoditySymbol, PeriodicReport a MixedAmount, Bool)]
-> NonEmpty (CommoditySymbol, PeriodicReport a MixedAmount, Bool)
forall a. a -> [a] -> NonEmpty a
:|[(CommoditySymbol, PeriodicReport a MixedAmount, Bool)]
rs)
      where
        subreportTotal :: (a, PeriodicReport a MixedAmount, Bool)
-> PeriodicReportRow () MixedAmount
subreportTotal (a
_, PeriodicReport a MixedAmount
sr, Bool
increasestotal) =
            (if Bool
increasestotal then PeriodicReportRow () MixedAmount
-> PeriodicReportRow () MixedAmount
forall a. a -> a
id else (MixedAmount -> MixedAmount)
-> PeriodicReportRow () MixedAmount
-> PeriodicReportRow () MixedAmount
forall a b.
(a -> b) -> PeriodicReportRow () a -> PeriodicReportRow () b
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
fmap MixedAmount -> MixedAmount
maNegate) (PeriodicReportRow () MixedAmount
 -> PeriodicReportRow () MixedAmount)
-> PeriodicReportRow () MixedAmount
-> PeriodicReportRow () MixedAmount
forall a b. (a -> b) -> a -> b
$ PeriodicReport a MixedAmount -> PeriodicReportRow () MixedAmount
forall a b. PeriodicReport a b -> PeriodicReportRow () b
prTotals PeriodicReport a MixedAmount
sr

    cbr :: CompoundPeriodicReport a MixedAmount
cbr = CommoditySymbol
-> [DateSpan]
-> [(CommoditySymbol, PeriodicReport a MixedAmount, Bool)]
-> PeriodicReportRow () MixedAmount
-> CompoundPeriodicReport a MixedAmount
forall a b.
CommoditySymbol
-> [DateSpan]
-> [(CommoditySymbol, PeriodicReport a b, Bool)]
-> PeriodicReportRow () b
-> CompoundPeriodicReport a b
CompoundPeriodicReport CommoditySymbol
"" [DateSpan]
colspans [(CommoditySymbol, PeriodicReport a MixedAmount, Bool)]
subreports PeriodicReportRow () MixedAmount
overalltotals


-- | Remove any date queries and insert queries from the report span.
-- The user's query expanded to the report span
-- if there is one (otherwise any date queries are left as-is, which
-- handles the hledger-ui+future txns case above).
makeReportQuery :: ReportSpec -> DateSpan -> ReportSpec
makeReportQuery :: ReportSpec -> DateSpan -> ReportSpec
makeReportQuery ReportSpec
rspec DateSpan
reportspan
    | DateSpan
reportspan DateSpan -> DateSpan -> Bool
forall a. Eq a => a -> a -> Bool
== DateSpan
nulldatespan = ReportSpec
rspec
    | Bool
otherwise = ReportSpec
rspec{_rsQuery=query}
  where
    query :: Query
query            = Query -> Query
simplifyQuery (Query -> Query) -> Query -> Query
forall a b. (a -> b) -> a -> b
$ [Query] -> Query
And [Query -> Query
dateless (Query -> Query) -> Query -> Query
forall a b. (a -> b) -> a -> b
$ ReportSpec -> Query
_rsQuery ReportSpec
rspec, Query
reportspandatesq]
    reportspandatesq :: Query
reportspandatesq = String -> Query -> Query
forall a. Show a => String -> a -> a
dbg3 String
"makeReportQuery reportspandatesq" (Query -> Query) -> Query -> Query
forall a b. (a -> b) -> a -> b
$ DateSpan -> Query
dateqcons DateSpan
reportspan
    dateless :: Query -> Query
dateless         = String -> Query -> Query
forall a. Show a => String -> a -> a
dbg3 String
"makeReportQuery dateless" (Query -> Query) -> (Query -> Query) -> Query -> Query
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (Query -> Bool) -> Query -> Query
filterQuery (Bool -> Bool
not (Bool -> Bool) -> (Query -> Bool) -> Query -> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Query -> Bool
queryIsDateOrDate2)
    dateqcons :: DateSpan -> Query
dateqcons        = if ReportOpts -> Bool
date2_ (ReportSpec -> ReportOpts
_rsReportOpts ReportSpec
rspec) then DateSpan -> Query
Date2 else DateSpan -> Query
Date

-- | Gather postings matching the query within the report period.
getPostings :: ReportSpec -> Journal -> PriceOracle -> DateSpan -> [Posting]
getPostings :: ReportSpec -> Journal -> PriceOracle -> DateSpan -> [Posting]
getPostings rspec :: ReportSpec
rspec@ReportSpec{_rsQuery :: ReportSpec -> Query
_rsQuery=Query
query, _rsReportOpts :: ReportSpec -> ReportOpts
_rsReportOpts=ReportOpts
ropts} Journal
j PriceOracle
priceoracle DateSpan
reportspan =
    [Posting] -> [Posting]
setPostingsCount
    ([Posting] -> [Posting])
-> (Journal -> [Posting]) -> Journal -> [Posting]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Journal -> [Posting]
journalPostings
    (Journal -> [Posting]) -> Journal -> [Posting]
forall a b. (a -> b) -> a -> b
$ ReportSpec -> Journal -> PriceOracle -> Journal
journalValueAndFilterPostingsWith ReportSpec
rspec' Journal
j PriceOracle
priceoracle
  where
    -- If doing --count, set all posting amounts to "1".
    setPostingsCount :: [Posting] -> [Posting]
setPostingsCount = case ReportOpts -> BalanceCalculation
balancecalc_ ReportOpts
ropts of
        BalanceCalculation
CalcPostingsCount -> (Posting -> Posting) -> [Posting] -> [Posting]
forall a b. (a -> b) -> [a] -> [b]
map ((MixedAmount -> MixedAmount) -> Posting -> Posting
postingTransformAmount (MixedAmount -> MixedAmount -> MixedAmount
forall a b. a -> b -> a
const (MixedAmount -> MixedAmount -> MixedAmount)
-> MixedAmount -> MixedAmount -> MixedAmount
forall a b. (a -> b) -> a -> b
$ [Amount] -> MixedAmount
forall (t :: * -> *). Foldable t => t Amount -> MixedAmount
mixed [Quantity -> Amount
num Quantity
1]))
        BalanceCalculation
_                 -> [Posting] -> [Posting]
forall a. a -> a
id

    rspec' :: ReportSpec
rspec' = ReportSpec
rspec{_rsQuery=fullreportq,_rsReportOpts=ropts'}
    -- If we're re-valuing every period, we need to have the unvalued start
    -- balance, so we can do it ourselves later.
    ropts' :: ReportOpts
ropts' = if Maybe (Maybe CommoditySymbol) -> Bool
forall a. Maybe a -> Bool
isJust (ReportOpts -> Maybe (Maybe CommoditySymbol)
valuationAfterSum ReportOpts
ropts)
        then ReportOpts
ropts{period_=dateSpanAsPeriod fullreportspan, value_=Nothing, conversionop_=Just NoConversionOp}  -- If we're valuing after the sum, don't do it now
        else ReportOpts
ropts{period_=dateSpanAsPeriod fullreportspan}

    -- q projected back before the report start date.
    -- When there's no report start date, in case there are future txns (the hledger-ui case above),
    -- we use emptydatespan to make sure they aren't counted as starting balance.
    fullreportq :: Query
fullreportq = String -> Query -> Query
forall a. Show a => String -> a -> a
dbg3 String
"getPostings fullreportq" (Query -> Query) -> Query -> Query
forall a b. (a -> b) -> a -> b
$ [Query] -> Query
And [Query
datelessq, Query
fullreportspanq]
    datelessq :: Query
datelessq   = String -> Query -> Query
forall a. Show a => String -> a -> a
dbg3 String
"getPostings datelessq" (Query -> Query) -> Query -> Query
forall a b. (a -> b) -> a -> b
$ (Query -> Bool) -> Query -> Query
filterQuery (Bool -> Bool
not (Bool -> Bool) -> (Query -> Bool) -> Query -> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Query -> Bool
queryIsDateOrDate2) Query
depthlessq

    -- The user's query with no depth limit, and expanded to the report span
    -- if there is one (otherwise any date queries are left as-is, which
    -- handles the hledger-ui+future txns case above).
    depthlessq :: Query
depthlessq = String -> Query -> Query
forall a. Show a => String -> a -> a
dbg3 String
"getPostings depthlessq" (Query -> Query) -> Query -> Query
forall a b. (a -> b) -> a -> b
$ (Query -> Bool) -> Query -> Query
filterQuery (Bool -> Bool
not (Bool -> Bool) -> (Query -> Bool) -> Query -> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Query -> Bool
queryIsDepth) Query
query

    fullreportspan :: DateSpan
fullreportspan  = if ReportOpts -> Bool
requiresHistorical ReportOpts
ropts then Maybe EFDay -> Maybe EFDay -> DateSpan
DateSpan Maybe EFDay
forall a. Maybe a
Nothing (Day -> EFDay
Exact (Day -> EFDay) -> Maybe Day -> Maybe EFDay
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> DateSpan -> Maybe Day
spanEnd DateSpan
reportspan) else DateSpan
reportspan
    fullreportspanq :: Query
fullreportspanq = (if ReportOpts -> Bool
date2_ ReportOpts
ropts then DateSpan -> Query
Date2 else DateSpan -> Query
Date) (DateSpan -> Query) -> DateSpan -> Query
forall a b. (a -> b) -> a -> b
$ case DateSpan
fullreportspan of
        DateSpan Maybe EFDay
Nothing Maybe EFDay
Nothing -> DateSpan
emptydatespan
        DateSpan
a -> DateSpan
a

-- | Generate the 'Account' for the requested multi-balance report from a list
-- of 'Posting's.
generateMultiBalanceAccount :: ReportSpec -> Journal -> PriceOracle -> [DateSpan] -> [Posting] -> Account BalanceData
generateMultiBalanceAccount :: ReportSpec
-> Journal
-> PriceOracle
-> [DateSpan]
-> [Posting]
-> Account BalanceData
generateMultiBalanceAccount rspec :: ReportSpec
rspec@ReportSpec{_rsReportOpts :: ReportSpec -> ReportOpts
_rsReportOpts=ReportOpts
ropts} Journal
j PriceOracle
priceoracle [DateSpan]
colspans =
    -- Add declared accounts if called with --declared and --empty
    (if (ReportOpts -> Bool
declared_ ReportOpts
ropts Bool -> Bool -> Bool
&& ReportOpts -> Bool
empty_ ReportOpts
ropts) then ReportSpec -> Journal -> Account BalanceData -> Account BalanceData
forall a.
Monoid a =>
ReportSpec -> Journal -> Account a -> Account a
addDeclaredAccounts ReportSpec
rspec Journal
j else Account BalanceData -> Account BalanceData
forall a. a -> a
id)
    -- Negate amounts if applicable
    (Account BalanceData -> Account BalanceData)
-> ([Posting] -> Account BalanceData)
-> [Posting]
-> Account BalanceData
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (if ReportOpts -> Bool
invert_ ReportOpts
ropts then (BalanceData -> BalanceData)
-> Account BalanceData -> Account BalanceData
forall a b. (a -> b) -> Account a -> Account b
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
fmap ((MixedAmount -> MixedAmount) -> BalanceData -> BalanceData
mapBalanceData MixedAmount -> MixedAmount
maNegate) else Account BalanceData -> Account BalanceData
forall a. a -> a
id)
    -- Mark which accounts are boring and which are interesting
    (Account BalanceData -> Account BalanceData)
-> ([Posting] -> Account BalanceData)
-> [Posting]
-> Account BalanceData
forall b c a. (b -> c) -> (a -> b) -> a -> c
. ReportSpec -> Account BalanceData -> Account BalanceData
markAccountBoring ReportSpec
rspec
    -- Set account declaration info (for sorting purposes)
    (Account BalanceData -> Account BalanceData)
-> ([Posting] -> Account BalanceData)
-> [Posting]
-> Account BalanceData
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (Account BalanceData -> Account BalanceData)
-> Account BalanceData -> Account BalanceData
forall a. (Account a -> Account a) -> Account a -> Account a
mapAccounts (Journal -> Account BalanceData -> Account BalanceData
forall a. Journal -> Account a -> Account a
accountSetDeclarationInfo Journal
j)
    -- Process changes into normal, cumulative, or historical amounts, plus value them
    (Account BalanceData -> Account BalanceData)
-> ([Posting] -> Account BalanceData)
-> [Posting]
-> Account BalanceData
forall b c a. (b -> c) -> (a -> b) -> a -> c
. ReportSpec
-> Journal
-> PriceOracle
-> [DateSpan]
-> [Posting]
-> Account BalanceData
calculateReportAccount ReportSpec
rspec Journal
j PriceOracle
priceoracle [DateSpan]
colspans
    -- Clip account names
    ([Posting] -> Account BalanceData)
-> ([Posting] -> [Posting]) -> [Posting] -> Account BalanceData
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (Posting -> Posting) -> [Posting] -> [Posting]
forall a b. (a -> b) -> [a] -> [b]
map Posting -> Posting
clipPosting
  where
    -- Clip postings to the requested depth according to the query
    clipPosting :: Posting -> Posting
clipPosting Posting
p = Posting
p{paccount = clipOrEllipsifyAccountName depthSpec $ paccount p}
    depthSpec :: DepthSpec
depthSpec = String -> DepthSpec -> DepthSpec
forall a. Show a => String -> a -> a
dbg3 String
"generateMultiBalanceAccount depthSpec"
              (DepthSpec -> DepthSpec)
-> (Query -> DepthSpec) -> Query -> DepthSpec
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Query -> DepthSpec
queryDepth (Query -> DepthSpec) -> (Query -> Query) -> Query -> DepthSpec
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (Query -> Bool) -> Query -> Query
filterQuery Query -> Bool
queryIsDepth (Query -> DepthSpec) -> Query -> DepthSpec
forall a b. (a -> b) -> a -> b
$ ReportSpec -> Query
_rsQuery ReportSpec
rspec

-- | Add declared accounts to the account tree.
addDeclaredAccounts :: Monoid a => ReportSpec -> Journal -> Account a -> Account a
addDeclaredAccounts :: forall a.
Monoid a =>
ReportSpec -> Journal -> Account a -> Account a
addDeclaredAccounts ReportSpec
rspec Journal
j Account a
acct =
    (a -> a) -> (a -> a) -> (a -> a -> a) -> These a a -> a
forall a c b.
(a -> c) -> (b -> c) -> (a -> b -> c) -> These a b -> c
these a -> a
forall a. a -> a
id a -> a
forall a. a -> a
id a -> a -> a
forall a b. a -> b -> a
const (These a a -> a) -> Account (These a a) -> Account a
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Account a -> Account a -> Account (These a a)
forall a b. Account a -> Account b -> Account (These a b)
mergeAccounts Account a
acct Account a
declaredTree
  where
    declaredTree :: Account a
declaredTree =
        (Account a -> Account a) -> Account a -> Account a
forall a. (Account a -> Account a) -> Account a -> Account a
mapAccounts (\Account a
a -> Account a
a{aboring = not $ aname a `HS.member` HS.fromList declaredAccounts}) (Account a -> Account a) -> Account a -> Account a
forall a b. (a -> b) -> a -> b
$
          CommoditySymbol -> PeriodData a -> [CommoditySymbol] -> Account a
forall a.
CommoditySymbol -> PeriodData a -> [CommoditySymbol] -> Account a
accountTreeFromBalanceAndNames CommoditySymbol
"root" (a
forall a. Monoid a => a
mempty a -> PeriodData a -> PeriodData a
forall a b. a -> PeriodData b -> PeriodData a
forall (f :: * -> *) a b. Functor f => a -> f b -> f a
<$ Account a -> PeriodData a
forall a. Account a -> PeriodData a
adata Account a
acct) [CommoditySymbol]
declaredAccounts

    -- With --declared, add the query-matching declared accounts (as dummy postings
    -- so they are processed like the rest).
    declaredAccounts :: [CommoditySymbol]
declaredAccounts =
      (CommoditySymbol -> CommoditySymbol)
-> [CommoditySymbol] -> [CommoditySymbol]
forall a b. (a -> b) -> [a] -> [b]
map (DepthSpec -> CommoditySymbol -> CommoditySymbol
clipOrEllipsifyAccountName DepthSpec
depthSpec) ([CommoditySymbol] -> [CommoditySymbol])
-> ([CommoditySymbol] -> [CommoditySymbol])
-> [CommoditySymbol]
-> [CommoditySymbol]
forall b c a. (b -> c) -> (a -> b) -> a -> c
.
      (CommoditySymbol -> Bool) -> [CommoditySymbol] -> [CommoditySymbol]
forall a. (a -> Bool) -> [a] -> [a]
filter ((CommoditySymbol -> Maybe AccountType)
-> (CommoditySymbol -> [Tag]) -> Query -> CommoditySymbol -> Bool
matchesAccountExtra (Journal -> CommoditySymbol -> Maybe AccountType
journalAccountType Journal
j) (Journal -> CommoditySymbol -> [Tag]
journalAccountTags Journal
j) Query
accttypetagsq) ([CommoditySymbol] -> [CommoditySymbol])
-> [CommoditySymbol] -> [CommoditySymbol]
forall a b. (a -> b) -> a -> b
$
      Journal -> [CommoditySymbol]
journalAccountNamesDeclared Journal
j

    accttypetagsq :: Query
accttypetagsq  = String -> Query -> Query
forall a. Show a => String -> a -> a
dbg3 String
"addDeclaredAccounts accttypetagsq" (Query -> Query) -> (Query -> Query) -> Query -> Query
forall b c a. (b -> c) -> (a -> b) -> a -> c
.
      (Query -> Bool) -> Query -> Query
filterQueryOrNotQuery (\Query
q -> Query -> Bool
queryIsAcct Query
q Bool -> Bool -> Bool
|| Query -> Bool
queryIsType Query
q Bool -> Bool -> Bool
|| Query -> Bool
queryIsTag Query
q) (Query -> Query) -> Query -> Query
forall a b. (a -> b) -> a -> b
$
      ReportSpec -> Query
_rsQuery ReportSpec
rspec

    depthSpec :: DepthSpec
depthSpec = Query -> DepthSpec
queryDepth (Query -> DepthSpec) -> (Query -> Query) -> Query -> DepthSpec
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (Query -> Bool) -> Query -> Query
filterQuery Query -> Bool
queryIsDepth (Query -> DepthSpec) -> Query -> DepthSpec
forall a b. (a -> b) -> a -> b
$ ReportSpec -> Query
_rsQuery ReportSpec
rspec


-- | Gather the account balance changes into a regular matrix, then
-- accumulate and value amounts, as specified by the report options.
-- Makes sure all report columns have an entry.
calculateReportAccount :: ReportSpec -> Journal -> PriceOracle -> [DateSpan] -> [Posting] -> Account BalanceData
calculateReportAccount :: ReportSpec
-> Journal
-> PriceOracle
-> [DateSpan]
-> [Posting]
-> Account BalanceData
calculateReportAccount rspec :: ReportSpec
rspec@ReportSpec{_rsReportOpts :: ReportSpec -> ReportOpts
_rsReportOpts=ReportOpts
ropts} Journal
j PriceOracle
priceoracle [DateSpan]
colspans [Posting]
ps =  -- PARTIAL:
    (PeriodData BalanceData -> PeriodData BalanceData)
-> Account BalanceData -> Account BalanceData
forall a. (PeriodData a -> PeriodData a) -> Account a -> Account a
mapPeriodData PeriodData BalanceData -> PeriodData BalanceData
rowbals Account BalanceData
changesAcct
  where
    -- The valued row amounts to be displayed: per-period changes,
    -- zero-based cumulative totals, or
    -- starting-balance-based historical balances.
    rowbals :: PeriodData BalanceData -> PeriodData BalanceData
    rowbals :: PeriodData BalanceData -> PeriodData BalanceData
rowbals PeriodData BalanceData
unvaluedChanges = case ReportOpts -> BalanceAccumulation
balanceaccum_ ReportOpts
ropts of
        BalanceAccumulation
PerPeriod  -> PeriodData BalanceData
changes
        BalanceAccumulation
Cumulative -> PeriodData BalanceData
cumulative
        BalanceAccumulation
Historical -> PeriodData BalanceData
historical
      where
        -- changes to report on: usually just the valued changes themselves, but use the
        -- differences in the valued historical amount for CalcValueChange and CalcGain.
        changes :: PeriodData BalanceData
changes = case ReportOpts -> BalanceCalculation
balancecalc_ ReportOpts
ropts of
            BalanceCalculation
CalcChange        -> PeriodData BalanceData -> PeriodData BalanceData
avalue PeriodData BalanceData
unvaluedChanges
            BalanceCalculation
CalcBudget        -> PeriodData BalanceData -> PeriodData BalanceData
avalue PeriodData BalanceData
unvaluedChanges
            BalanceCalculation
CalcValueChange   -> PeriodData BalanceData -> PeriodData BalanceData
forall (t :: * -> *).
Traversable t =>
t BalanceData -> t BalanceData
periodChanges PeriodData BalanceData
historical
            BalanceCalculation
CalcGain          -> PeriodData BalanceData -> PeriodData BalanceData
forall (t :: * -> *).
Traversable t =>
t BalanceData -> t BalanceData
periodChanges PeriodData BalanceData
historical
            BalanceCalculation
CalcPostingsCount -> PeriodData BalanceData -> PeriodData BalanceData
avalue PeriodData BalanceData
unvaluedChanges
        -- the historical balance is the valued cumulative sum of all unvalued changes
        historical :: PeriodData BalanceData
historical = PeriodData BalanceData -> PeriodData BalanceData
avalue (PeriodData BalanceData -> PeriodData BalanceData)
-> PeriodData BalanceData -> PeriodData BalanceData
forall a b. (a -> b) -> a -> b
$ PeriodData BalanceData -> PeriodData BalanceData
forall (t :: * -> *).
Traversable t =>
t BalanceData -> t BalanceData
cumulativeSum PeriodData BalanceData
unvaluedChanges
        -- since this is a cumulative sum of valued amounts, it should not be valued again
        cumulative :: PeriodData BalanceData
cumulative = PeriodData BalanceData -> PeriodData BalanceData
forall (t :: * -> *).
Traversable t =>
t BalanceData -> t BalanceData
cumulativeSum PeriodData BalanceData
changes{pdpre = mempty}
        avalue :: PeriodData BalanceData -> PeriodData BalanceData
avalue = ReportOpts
-> Journal
-> PriceOracle
-> [DateSpan]
-> PeriodData BalanceData
-> PeriodData BalanceData
periodDataValuation ReportOpts
ropts Journal
j PriceOracle
priceoracle [DateSpan]
colspans

    changesAcct :: Account BalanceData
changesAcct = (Account BalanceData -> String)
-> Account BalanceData -> Account BalanceData
forall a. (a -> String) -> a -> a
dbg5With (\Account BalanceData
x -> String
"calculateReportAccount changesAcct\n" String -> String -> String
forall a. [a] -> [a] -> [a]
++ Account BalanceData -> String
forall a. Show a => Account a -> String
showAccounts Account BalanceData
x) (Account BalanceData -> Account BalanceData)
-> (Account BalanceData -> Account BalanceData)
-> Account BalanceData
-> Account BalanceData
forall b c a. (b -> c) -> (a -> b) -> a -> c
.
        (PeriodData BalanceData -> PeriodData BalanceData)
-> Account BalanceData -> Account BalanceData
forall a. (PeriodData a -> PeriodData a) -> Account a -> Account a
mapPeriodData (IntSet -> PeriodData BalanceData -> PeriodData BalanceData
forall a. Monoid a => IntSet -> PeriodData a -> PeriodData a
padPeriodData IntSet
intervalStarts) (Account BalanceData -> Account BalanceData)
-> Account BalanceData -> Account BalanceData
forall a b. (a -> b) -> a -> b
$
        (Posting -> Maybe Day) -> [Posting] -> Account BalanceData
accountFromPostings Posting -> Maybe Day
getIntervalStartDate [Posting]
ps

    getIntervalStartDate :: Posting -> Maybe Day
getIntervalStartDate Posting
p = Key -> Day
intToDay (Key -> Day) -> Maybe Key -> Maybe Day
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Key -> IntSet -> Maybe Key
IS.lookupLE (Day -> Key
dayToInt (Day -> Key) -> Day -> Key
forall a b. (a -> b) -> a -> b
$ Posting -> Day
getPostingDate Posting
p) IntSet
intervalStarts
    getPostingDate :: Posting -> Day
getPostingDate = WhichDate -> Posting -> Day
postingDateOrDate2 (ReportOpts -> WhichDate
whichDate (ReportSpec -> ReportOpts
_rsReportOpts ReportSpec
rspec))

    intervalStarts :: IntSet
intervalStarts = [Key] -> IntSet
IS.fromList ([Key] -> IntSet) -> ([Day] -> [Key]) -> [Day] -> IntSet
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (Day -> Key) -> [Day] -> [Key]
forall a b. (a -> b) -> [a] -> [b]
map Day -> Key
dayToInt ([Day] -> IntSet) -> [Day] -> IntSet
forall a b. (a -> b) -> a -> b
$ case (DateSpan -> Maybe Day) -> [DateSpan] -> [Day]
forall a b. (a -> Maybe b) -> [a] -> [b]
mapMaybe DateSpan -> Maybe Day
spanStart [DateSpan]
colspans of
      [] -> [Day
nulldate]  -- Deal with the case of the empty journal
      [Day]
xs -> [Day]
xs
    dayToInt :: Day -> Key
dayToInt = Integer -> Key
forall a. Num a => Integer -> a
fromInteger (Integer -> Key) -> (Day -> Integer) -> Day -> Key
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Day -> Integer
toModifiedJulianDay
    intToDay :: Key -> Day
intToDay = Integer -> Day
ModifiedJulianDay (Integer -> Day) -> (Key -> Integer) -> Key -> Day
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Key -> Integer
forall a. Integral a => a -> Integer
toInteger

-- | The valuation function to use for the chosen report options.
-- This can call error in various situations.
periodDataValuation :: ReportOpts -> Journal -> PriceOracle -> [DateSpan]
                    -> PeriodData BalanceData -> PeriodData BalanceData
periodDataValuation :: ReportOpts
-> Journal
-> PriceOracle
-> [DateSpan]
-> PeriodData BalanceData
-> PeriodData BalanceData
periodDataValuation ReportOpts
ropts Journal
j PriceOracle
priceoracle [DateSpan]
colspans =
    (Day -> BalanceData -> BalanceData)
-> PeriodData Day
-> PeriodData BalanceData
-> PeriodData BalanceData
forall a b c.
(a -> b -> c) -> PeriodData a -> PeriodData b -> PeriodData c
opPeriodData Day -> BalanceData -> BalanceData
valueBalanceData PeriodData Day
balanceDataPeriodEnds
  where
    valueBalanceData :: Day -> BalanceData -> BalanceData
    valueBalanceData :: Day -> BalanceData -> BalanceData
valueBalanceData Day
d = (MixedAmount -> MixedAmount) -> BalanceData -> BalanceData
mapBalanceData (Day -> MixedAmount -> MixedAmount
valueMixedAmount Day
d)

    valueMixedAmount :: Day -> MixedAmount -> MixedAmount
    valueMixedAmount :: Day -> MixedAmount -> MixedAmount
valueMixedAmount = ReportOpts
-> Journal -> PriceOracle -> Day -> MixedAmount -> MixedAmount
mixedAmountApplyValuationAfterSumFromOptsWith ReportOpts
ropts Journal
j PriceOracle
priceoracle

    balanceDataPeriodEnds :: PeriodData Day
    balanceDataPeriodEnds :: PeriodData Day
balanceDataPeriodEnds = String -> PeriodData Day -> PeriodData Day
forall a. Show a => String -> a -> a
dbg5 String
"balanceDataPeriodEnds" (PeriodData Day -> PeriodData Day)
-> PeriodData Day -> PeriodData Day
forall a b. (a -> b) -> a -> b
$ case [DateSpan]
colspans of  -- FIXME: Change colspans to nonempty list
        [DateSpan Maybe EFDay
Nothing Maybe EFDay
Nothing] -> Day -> [(Day, Day)] -> PeriodData Day
forall a. a -> [(Day, a)] -> PeriodData a
periodDataFromList Day
nulldate [(Day
nulldate, Day
nulldate)]  -- Empty journal
        DateSpan
h:[DateSpan]
ds                       -> Day -> [(Day, Day)] -> PeriodData Day
forall a. a -> [(Day, a)] -> PeriodData a
periodDataFromList ((Maybe Day, Maybe Day) -> Day
forall {b}. (Maybe Day, b) -> Day
makeJustFst ((Maybe Day, Maybe Day) -> Day) -> (Maybe Day, Maybe Day) -> Day
forall a b. (a -> b) -> a -> b
$ DateSpan -> (Maybe Day, Maybe Day)
boundaries DateSpan
h) ([(Day, Day)] -> PeriodData Day) -> [(Day, Day)] -> PeriodData Day
forall a b. (a -> b) -> a -> b
$ (DateSpan -> (Day, Day)) -> [DateSpan] -> [(Day, Day)]
forall a b. (a -> b) -> [a] -> [b]
map ((Maybe Day, Maybe Day) -> (Day, Day)
forall {a}. (Maybe a, Maybe Day) -> (a, Day)
makeJust ((Maybe Day, Maybe Day) -> (Day, Day))
-> (DateSpan -> (Maybe Day, Maybe Day)) -> DateSpan -> (Day, Day)
forall b c a. (b -> c) -> (a -> b) -> a -> c
. DateSpan -> (Maybe Day, Maybe Day)
boundaries) (DateSpan
hDateSpan -> [DateSpan] -> [DateSpan]
forall a. a -> [a] -> [a]
:[DateSpan]
ds)
        []                         -> String -> PeriodData Day
forall a. String -> a
error' String
"balanceDataPeriodEnds: Shouldn't have empty colspans"  -- PARTIAL: Shouldn't occur
      where
        boundaries :: DateSpan -> (Maybe Day, Maybe Day)
boundaries DateSpan
spn = (DateSpan -> Maybe Day
spanStart DateSpan
spn, DateSpan -> Maybe Day
spanEnd DateSpan
spn)

        makeJust :: (Maybe a, Maybe Day) -> (a, Day)
makeJust (Just a
x, Just Day
y) = (a
x, Integer -> Day -> Day
addDays (-Integer
1) Day
y)
        makeJust (Maybe a, Maybe Day)
_    = String -> (a, Day)
forall a. String -> a
error' String
"balanceDataPeriodEnds: expected all non-initial spans to have start and end dates"
        makeJustFst :: (Maybe Day, b) -> Day
makeJustFst (Just Day
x, b
_) = Integer -> Day -> Day
addDays (-Integer
1) Day
x
        makeJustFst (Maybe Day, b)
_ = String -> Day
forall a. String -> a
error' String
"balanceDataPeriodEnds: expected initial span to have an end date"

-- | Mark which nodes of an 'Account' are boring, and so should be omitted from reports.
markAccountBoring :: ReportSpec -> Account BalanceData -> Account BalanceData
markAccountBoring :: ReportSpec -> Account BalanceData -> Account BalanceData
markAccountBoring ReportSpec{_rsQuery :: ReportSpec -> Query
_rsQuery=Query
query,_rsReportOpts :: ReportSpec -> ReportOpts
_rsReportOpts=ReportOpts
ropts}
    -- If depth 0, all accounts except the top-level account are boring
    | Bool
qdepthIsZero = Bool -> Account BalanceData -> Account BalanceData
forall {a}. Bool -> Account a -> Account a
markBoring Bool
False (Account BalanceData -> Account BalanceData)
-> (Account BalanceData -> Account BalanceData)
-> Account BalanceData
-> Account BalanceData
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (Account BalanceData -> Account BalanceData)
-> Account BalanceData -> Account BalanceData
forall a. (Account a -> Account a) -> Account a -> Account a
mapAccounts (Bool -> Account BalanceData -> Account BalanceData
forall {a}. Bool -> Account a -> Account a
markBoring Bool
True)
    -- Otherwise the top level account is boring, and subaccounts are boring if
    -- they are both boring in and of themselves and are boring parents
    | Bool
otherwise    = Bool -> Account BalanceData -> Account BalanceData
forall {a}. Bool -> Account a -> Account a
markBoring Bool
True (Account BalanceData -> Account BalanceData)
-> (Account BalanceData -> Account BalanceData)
-> Account BalanceData
-> Account BalanceData
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (Account BalanceData -> Account BalanceData)
-> Account BalanceData -> Account BalanceData
forall a. (Account a -> Account a) -> Account a -> Account a
mapAccounts ((Account BalanceData -> Bool)
-> Account BalanceData -> Account BalanceData
forall {a}. (Account a -> Bool) -> Account a -> Account a
markBoringBy ((Bool -> Bool -> Bool)
-> (Account BalanceData -> Bool)
-> (Account BalanceData -> Bool)
-> Account BalanceData
-> Bool
forall a b c.
(a -> b -> c)
-> (Account BalanceData -> a)
-> (Account BalanceData -> b)
-> Account BalanceData
-> c
forall (f :: * -> *) a b c.
Applicative f =>
(a -> b -> c) -> f a -> f b -> f c
liftA2 Bool -> Bool -> Bool
(&&) Account BalanceData -> Bool
isBoring Account BalanceData -> Bool
forall a. Account a -> Bool
isBoringParent))
  where
    -- Accounts boring on their own
    isBoring :: Account BalanceData -> Bool
    isBoring :: Account BalanceData -> Bool
isBoring Account BalanceData
acct = Bool
tooDeep Bool -> Bool -> Bool
|| Bool
allZeros
      where
        tooDeep :: Bool
tooDeep = Key
d Key -> Key -> Bool
forall a. Ord a => a -> a -> Bool
> Key
qdepth                                       -- Throw out anything too deep
        allZeros :: Bool
allZeros = (BalanceData -> MixedAmount) -> IntMap BalanceData -> Bool
forall {t :: * -> *} {a}.
Foldable t =>
(a -> MixedAmount) -> t a -> Bool
isZeroRow BalanceData -> MixedAmount
balance IntMap BalanceData
amts Bool -> Bool -> Bool
&& Bool -> Bool
not Bool
keepEmptyAccount  -- Throw away everything with a zero balance in the row, unless..
        keepEmptyAccount :: Bool
keepEmptyAccount = ReportOpts -> Bool
empty_ ReportOpts
ropts Bool -> Bool -> Bool
&& Account BalanceData -> Bool
keepWhenEmpty Account BalanceData
acct      -- We are keeping empty rows and this row meets the criteria

        amts :: IntMap BalanceData
amts = PeriodData BalanceData -> IntMap BalanceData
forall a. PeriodData a -> IntMap a
pdperiods (PeriodData BalanceData -> IntMap BalanceData)
-> PeriodData BalanceData -> IntMap BalanceData
forall a b. (a -> b) -> a -> b
$ Account BalanceData -> PeriodData BalanceData
forall a. Account a -> PeriodData a
adata Account BalanceData
acct
        d :: Key
d = CommoditySymbol -> Key
accountNameLevel (CommoditySymbol -> Key) -> CommoditySymbol -> Key
forall a b. (a -> b) -> a -> b
$ Account BalanceData -> CommoditySymbol
forall a. Account a -> CommoditySymbol
aname Account BalanceData
acct

        qdepth :: Key
qdepth = Key -> Maybe Key -> Key
forall a. a -> Maybe a -> a
fromMaybe Key
forall a. Bounded a => a
maxBound (Maybe Key -> Key)
-> (CommoditySymbol -> Maybe Key) -> CommoditySymbol -> Key
forall b c a. (b -> c) -> (a -> b) -> a -> c
. DepthSpec -> CommoditySymbol -> Maybe Key
getAccountNameClippedDepth DepthSpec
depthspec (CommoditySymbol -> Key) -> CommoditySymbol -> Key
forall a b. (a -> b) -> a -> b
$ Account BalanceData -> CommoditySymbol
forall a. Account a -> CommoditySymbol
aname Account BalanceData
acct
        balance :: BalanceData -> MixedAmount
balance = MixedAmount -> MixedAmount
maybeStripPrices (MixedAmount -> MixedAmount)
-> (BalanceData -> MixedAmount) -> BalanceData -> MixedAmount
forall b c a. (b -> c) -> (a -> b) -> a -> c
. case ReportOpts -> AccountListMode
accountlistmode_ ReportOpts
ropts of
            AccountListMode
ALTree | Key
d Key -> Key -> Bool
forall a. Eq a => a -> a -> Bool
== Key
qdepth -> BalanceData -> MixedAmount
bdincludingsubs
            AccountListMode
_                    -> BalanceData -> MixedAmount
bdexcludingsubs

    -- Accounts which don't have enough interesting subaccounts
    isBoringParent :: Account a -> Bool
    isBoringParent :: forall a. Account a -> Bool
isBoringParent Account a
acct = case ReportOpts -> AccountListMode
accountlistmode_ ReportOpts
ropts of
        AccountListMode
ALTree -> Bool
notEnoughSubs Bool -> Bool -> Bool
|| Bool
droppedAccount
        AccountListMode
ALFlat -> Bool
True
      where
        notEnoughSubs :: Bool
notEnoughSubs = [Account a] -> Key
forall a. [a] -> Key
forall (t :: * -> *) a. Foldable t => t a -> Key
length [Account a]
interestingSubs Key -> Key -> Bool
forall a. Ord a => a -> a -> Bool
< Key
minimumSubs
        droppedAccount :: Bool
droppedAccount = CommoditySymbol -> Key
accountNameLevel (Account a -> CommoditySymbol
forall a. Account a -> CommoditySymbol
aname Account a
acct) Key -> Key -> Bool
forall a. Ord a => a -> a -> Bool
<= ReportOpts -> Key
drop_ ReportOpts
ropts
        interestingSubs :: [Account a]
interestingSubs = (Account a -> Bool) -> [Account a] -> [Account a]
forall a. (a -> Bool) -> [a] -> [a]
filter ((Account a -> Bool) -> Account a -> Bool
forall a. (Account a -> Bool) -> Account a -> Bool
anyAccounts (Bool -> Bool
not (Bool -> Bool) -> (Account a -> Bool) -> Account a -> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Account a -> Bool
forall a. Account a -> Bool
aboring)) ([Account a] -> [Account a]) -> [Account a] -> [Account a]
forall a b. (a -> b) -> a -> b
$ Account a -> [Account a]
forall a. Account a -> [Account a]
asubs Account a
acct
        minimumSubs :: Key
minimumSubs = if ReportOpts -> Bool
no_elide_ ReportOpts
ropts then Key
1 else Key
2

    isZeroRow :: (a -> MixedAmount) -> t a -> Bool
isZeroRow a -> MixedAmount
balance = (a -> Bool) -> t a -> Bool
forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Bool
all (MixedAmount -> Bool
mixedAmountLooksZero (MixedAmount -> Bool) -> (a -> MixedAmount) -> a -> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. a -> MixedAmount
balance)
    keepWhenEmpty :: Account BalanceData -> Bool
keepWhenEmpty = case ReportOpts -> AccountListMode
accountlistmode_ ReportOpts
ropts of
        AccountListMode
ALFlat -> (BalanceData -> Bool) -> IntMap BalanceData -> Bool
forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Bool
any ((Key
0Key -> Key -> Bool
forall a. Ord a => a -> a -> Bool
<) (Key -> Bool) -> (BalanceData -> Key) -> BalanceData -> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. BalanceData -> Key
bdnumpostings) (IntMap BalanceData -> Bool)
-> (Account BalanceData -> IntMap BalanceData)
-> Account BalanceData
-> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. PeriodData BalanceData -> IntMap BalanceData
forall a. PeriodData a -> IntMap a
pdperiods (PeriodData BalanceData -> IntMap BalanceData)
-> (Account BalanceData -> PeriodData BalanceData)
-> Account BalanceData
-> IntMap BalanceData
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Account BalanceData -> PeriodData BalanceData
forall a. Account a -> PeriodData a
adata  -- Keep all accounts that have postings in flat mode
        AccountListMode
ALTree -> [Account BalanceData] -> Bool
forall a. [a] -> Bool
forall (t :: * -> *) a. Foldable t => t a -> Bool
null ([Account BalanceData] -> Bool)
-> (Account BalanceData -> [Account BalanceData])
-> Account BalanceData
-> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Account BalanceData -> [Account BalanceData]
forall a. Account a -> [Account a]
asubs                                    -- Keep only empty leaves in tree mode
    maybeStripPrices :: MixedAmount -> MixedAmount
maybeStripPrices = if ReportOpts -> Maybe ConversionOp
conversionop_ ReportOpts
ropts Maybe ConversionOp -> Maybe ConversionOp -> Bool
forall a. Eq a => a -> a -> Bool
== ConversionOp -> Maybe ConversionOp
forall a. a -> Maybe a
Just ConversionOp
NoConversionOp then MixedAmount -> MixedAmount
forall a. a -> a
id else MixedAmount -> MixedAmount
mixedAmountStripCosts

    qdepthIsZero :: Bool
qdepthIsZero = DepthSpec
depthspec DepthSpec -> DepthSpec -> Bool
forall a. Eq a => a -> a -> Bool
== Maybe Key -> [(Regexp, Key)] -> DepthSpec
DepthSpec (Key -> Maybe Key
forall a. a -> Maybe a
Just Key
0) []
    depthspec :: DepthSpec
depthspec = Query -> DepthSpec
queryDepth Query
query

    markBoring :: Bool -> Account a -> Account a
markBoring   Bool
v Account a
a = Account a
a{aboring = v}
    markBoringBy :: (Account a -> Bool) -> Account a -> Account a
markBoringBy Account a -> Bool
f Account a
a = Account a
a{aboring = f a}


-- | Build a report row.
--
-- Calculate the column totals. These are always the sum of column amounts.
generateMultiBalanceReport :: ReportOpts -> [DateSpan] -> Account BalanceData -> MultiBalanceReport
generateMultiBalanceReport :: ReportOpts
-> [DateSpan] -> Account BalanceData -> MultiBalanceReport
generateMultiBalanceReport ReportOpts
ropts [DateSpan]
colspans =
    ReportOpts -> MultiBalanceReport -> MultiBalanceReport
reportPercent ReportOpts
ropts (MultiBalanceReport -> MultiBalanceReport)
-> (Account BalanceData -> MultiBalanceReport)
-> Account BalanceData
-> MultiBalanceReport
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (forall a.
 ReportOpts
 -> (BalanceData -> MixedAmount)
 -> a
 -> Account BalanceData
 -> PeriodicReportRow a MixedAmount)
-> (BalanceData -> MixedAmount)
-> (MixedAmount -> MixedAmount)
-> ReportOpts
-> [DateSpan]
-> Account BalanceData
-> MultiBalanceReport
forall c b.
Show c =>
(forall a.
 ReportOpts
 -> (BalanceData -> MixedAmount)
 -> a
 -> Account b
 -> PeriodicReportRow a c)
-> (b -> MixedAmount)
-> (c -> MixedAmount)
-> ReportOpts
-> [DateSpan]
-> Account b
-> PeriodicReport DisplayName c
generatePeriodicReport ReportOpts
-> (BalanceData -> MixedAmount)
-> a
-> Account BalanceData
-> PeriodicReportRow a MixedAmount
forall a.
ReportOpts
-> (BalanceData -> MixedAmount)
-> a
-> Account BalanceData
-> PeriodicReportRow a MixedAmount
makeMultiBalanceReportRow BalanceData -> MixedAmount
bdincludingsubs MixedAmount -> MixedAmount
forall a. a -> a
id ReportOpts
ropts [DateSpan]
colspans

-- | Lay out a set of postings grouped by date span into a regular matrix with rows
-- given by AccountName and columns by DateSpan, then generate a MultiBalanceReport
-- from the columns.
generatePeriodicReport :: Show c =>
    (forall a. ReportOpts -> (BalanceData -> MixedAmount) -> a -> Account b -> PeriodicReportRow a c)
    -> (b -> MixedAmount) -> (c -> MixedAmount)
    -> ReportOpts -> [DateSpan] -> Account b -> PeriodicReport DisplayName c
generatePeriodicReport :: forall c b.
Show c =>
(forall a.
 ReportOpts
 -> (BalanceData -> MixedAmount)
 -> a
 -> Account b
 -> PeriodicReportRow a c)
-> (b -> MixedAmount)
-> (c -> MixedAmount)
-> ReportOpts
-> [DateSpan]
-> Account b
-> PeriodicReport DisplayName c
generatePeriodicReport forall a.
ReportOpts
-> (BalanceData -> MixedAmount)
-> a
-> Account b
-> PeriodicReportRow a c
makeRow b -> MixedAmount
treeAmt c -> MixedAmount
flatAmt ReportOpts
ropts [DateSpan]
colspans Account b
acct =
    [DateSpan]
-> [PeriodicReportRow DisplayName c]
-> PeriodicReportRow () c
-> PeriodicReport DisplayName c
forall a b.
[DateSpan]
-> [PeriodicReportRow a b]
-> PeriodicReportRow () b
-> PeriodicReport a b
PeriodicReport [DateSpan]
colspans (Account b -> [PeriodicReportRow DisplayName c]
buildAndSort Account b
acct) PeriodicReportRow () c
totalsrow
  where
    -- Build report rows and sort them
    buildAndSort :: Account b -> [PeriodicReportRow DisplayName c]
buildAndSort = String
-> [PeriodicReportRow DisplayName c]
-> [PeriodicReportRow DisplayName c]
forall a. Show a => String -> a -> a
dbg5 String
"generatePeriodicReport buildAndSort" ([PeriodicReportRow DisplayName c]
 -> [PeriodicReportRow DisplayName c])
-> (Account b -> [PeriodicReportRow DisplayName c])
-> Account b
-> [PeriodicReportRow DisplayName c]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. case ReportOpts -> AccountListMode
accountlistmode_ ReportOpts
ropts of
        AccountListMode
ALTree | ReportOpts -> Bool
sort_amount_ ReportOpts
ropts -> Account b -> [PeriodicReportRow DisplayName c]
buildRows (Account b -> [PeriodicReportRow DisplayName c])
-> (Account b -> Account b)
-> Account b
-> [PeriodicReportRow DisplayName c]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Account b -> Account b
sortTreeByAmount
        AccountListMode
ALFlat | ReportOpts -> Bool
sort_amount_ ReportOpts
ropts -> [PeriodicReportRow DisplayName c]
-> [PeriodicReportRow DisplayName c]
sortFlatByAmount ([PeriodicReportRow DisplayName c]
 -> [PeriodicReportRow DisplayName c])
-> (Account b -> [PeriodicReportRow DisplayName c])
-> Account b
-> [PeriodicReportRow DisplayName c]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Account b -> [PeriodicReportRow DisplayName c]
buildRows
        AccountListMode
_                           -> Account b -> [PeriodicReportRow DisplayName c]
buildRows (Account b -> [PeriodicReportRow DisplayName c])
-> (Account b -> Account b)
-> Account b
-> [PeriodicReportRow DisplayName c]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Account b -> Account b
forall a. Account a -> Account a
sortAccountTreeByDeclaration

    buildRows :: Account b -> [PeriodicReportRow DisplayName c]
buildRows = (ReportOpts
 -> (BalanceData -> MixedAmount)
 -> DisplayName
 -> Account b
 -> PeriodicReportRow DisplayName c)
-> ReportOpts -> Account b -> [PeriodicReportRow DisplayName c]
forall b c.
(ReportOpts
 -> (BalanceData -> MixedAmount)
 -> DisplayName
 -> Account b
 -> PeriodicReportRow DisplayName c)
-> ReportOpts -> Account b -> [PeriodicReportRow DisplayName c]
buildReportRows ReportOpts
-> (BalanceData -> MixedAmount)
-> DisplayName
-> Account b
-> PeriodicReportRow DisplayName c
forall a.
ReportOpts
-> (BalanceData -> MixedAmount)
-> a
-> Account b
-> PeriodicReportRow a c
makeRow ReportOpts
ropts

    -- Calculate column totals from the inclusive balances of the root account
    totalsrow :: PeriodicReportRow () c
totalsrow = String -> PeriodicReportRow () c -> PeriodicReportRow () c
forall a. Show a => String -> a -> a
dbg5 String
"generatePeriodicReport totalsrow" (PeriodicReportRow () c -> PeriodicReportRow () c)
-> PeriodicReportRow () c -> PeriodicReportRow () c
forall a b. (a -> b) -> a -> b
$ ReportOpts
-> (BalanceData -> MixedAmount)
-> ()
-> Account b
-> PeriodicReportRow () c
forall a.
ReportOpts
-> (BalanceData -> MixedAmount)
-> a
-> Account b
-> PeriodicReportRow a c
makeRow ReportOpts
ropts BalanceData -> MixedAmount
bdincludingsubs () Account b
acct

    sortTreeByAmount :: Account b -> Account b
sortTreeByAmount = case NormalSign -> Maybe NormalSign -> NormalSign
forall a. a -> Maybe a -> a
fromMaybe NormalSign
NormallyPositive (Maybe NormalSign -> NormalSign) -> Maybe NormalSign -> NormalSign
forall a b. (a -> b) -> a -> b
$ ReportOpts -> Maybe NormalSign
normalbalance_ ReportOpts
ropts of
        NormalSign
NormallyPositive -> (Account b -> (Down MixedAmount, CommoditySymbol))
-> Account b -> Account b
forall b a. Ord b => (Account a -> b) -> Account a -> Account a
sortAccountTreeOn (\Account b
r -> (MixedAmount -> Down MixedAmount
forall a. a -> Down a
Down (MixedAmount -> Down MixedAmount)
-> MixedAmount -> Down MixedAmount
forall a b. (a -> b) -> a -> b
$ Account b -> MixedAmount
amt Account b
r, Account b -> CommoditySymbol
forall a. Account a -> CommoditySymbol
aname Account b
r))
        NormalSign
NormallyNegative -> (Account b -> (MixedAmount, CommoditySymbol))
-> Account b -> Account b
forall b a. Ord b => (Account a -> b) -> Account a -> Account a
sortAccountTreeOn (\Account b
r -> (Account b -> MixedAmount
amt Account b
r, Account b -> CommoditySymbol
forall a. Account a -> CommoditySymbol
aname Account b
r))
      where
        amt :: Account b -> MixedAmount
amt = MixedAmount -> MixedAmount
mixedAmountStripCosts (MixedAmount -> MixedAmount)
-> (Account b -> MixedAmount) -> Account b -> MixedAmount
forall b c a. (b -> c) -> (a -> b) -> a -> c
. IntMap MixedAmount -> MixedAmount
sortKey (IntMap MixedAmount -> MixedAmount)
-> (Account b -> IntMap MixedAmount) -> Account b -> MixedAmount
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (b -> MixedAmount) -> IntMap b -> IntMap MixedAmount
forall a b. (a -> b) -> IntMap a -> IntMap b
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
fmap b -> MixedAmount
treeAmt (IntMap b -> IntMap MixedAmount)
-> (Account b -> IntMap b) -> Account b -> IntMap MixedAmount
forall b c a. (b -> c) -> (a -> b) -> a -> c
. PeriodData b -> IntMap b
forall a. PeriodData a -> IntMap a
pdperiods (PeriodData b -> IntMap b)
-> (Account b -> PeriodData b) -> Account b -> IntMap b
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Account b -> PeriodData b
forall a. Account a -> PeriodData a
adata
        sortKey :: IntMap MixedAmount -> MixedAmount
sortKey = case ReportOpts -> BalanceAccumulation
balanceaccum_ ReportOpts
ropts of
          BalanceAccumulation
PerPeriod -> IntMap MixedAmount -> MixedAmount
forall (t :: * -> *). Foldable t => t MixedAmount -> MixedAmount
maSum
          BalanceAccumulation
_         -> MixedAmount
-> ((Key, MixedAmount) -> MixedAmount)
-> Maybe (Key, MixedAmount)
-> MixedAmount
forall b a. b -> (a -> b) -> Maybe a -> b
maybe MixedAmount
nullmixedamt (Key, MixedAmount) -> MixedAmount
forall a b. (a, b) -> b
snd (Maybe (Key, MixedAmount) -> MixedAmount)
-> (IntMap MixedAmount -> Maybe (Key, MixedAmount))
-> IntMap MixedAmount
-> MixedAmount
forall b c a. (b -> c) -> (a -> b) -> a -> c
. IntMap MixedAmount -> Maybe (Key, MixedAmount)
forall a. IntMap a -> Maybe (Key, a)
IM.lookupMax

    sortFlatByAmount :: [PeriodicReportRow DisplayName c]
-> [PeriodicReportRow DisplayName c]
sortFlatByAmount = case NormalSign -> Maybe NormalSign -> NormalSign
forall a. a -> Maybe a -> a
fromMaybe NormalSign
NormallyPositive (Maybe NormalSign -> NormalSign) -> Maybe NormalSign -> NormalSign
forall a b. (a -> b) -> a -> b
$ ReportOpts -> Maybe NormalSign
normalbalance_ ReportOpts
ropts of
        NormalSign
NormallyPositive -> (PeriodicReportRow DisplayName c
 -> (Down MixedAmount, CommoditySymbol))
-> [PeriodicReportRow DisplayName c]
-> [PeriodicReportRow DisplayName c]
forall b a. Ord b => (a -> b) -> [a] -> [a]
sortOn (\PeriodicReportRow DisplayName c
r -> (MixedAmount -> Down MixedAmount
forall a. a -> Down a
Down (MixedAmount -> Down MixedAmount)
-> MixedAmount -> Down MixedAmount
forall a b. (a -> b) -> a -> b
$ PeriodicReportRow DisplayName c -> MixedAmount
forall {a}. PeriodicReportRow a c -> MixedAmount
amt PeriodicReportRow DisplayName c
r, PeriodicReportRow DisplayName c -> CommoditySymbol
forall a. PeriodicReportRow DisplayName a -> CommoditySymbol
prrFullName PeriodicReportRow DisplayName c
r))
        NormalSign
NormallyNegative -> (PeriodicReportRow DisplayName c -> (MixedAmount, CommoditySymbol))
-> [PeriodicReportRow DisplayName c]
-> [PeriodicReportRow DisplayName c]
forall b a. Ord b => (a -> b) -> [a] -> [a]
sortOn (\PeriodicReportRow DisplayName c
r -> (PeriodicReportRow DisplayName c -> MixedAmount
forall {a}. PeriodicReportRow a c -> MixedAmount
amt PeriodicReportRow DisplayName c
r, PeriodicReportRow DisplayName c -> CommoditySymbol
forall a. PeriodicReportRow DisplayName a -> CommoditySymbol
prrFullName PeriodicReportRow DisplayName c
r))
      where amt :: PeriodicReportRow a c -> MixedAmount
amt = MixedAmount -> MixedAmount
mixedAmountStripCosts (MixedAmount -> MixedAmount)
-> (PeriodicReportRow a c -> MixedAmount)
-> PeriodicReportRow a c
-> MixedAmount
forall b c a. (b -> c) -> (a -> b) -> a -> c
. c -> MixedAmount
flatAmt (c -> MixedAmount)
-> (PeriodicReportRow a c -> c)
-> PeriodicReportRow a c
-> MixedAmount
forall b c a. (b -> c) -> (a -> b) -> a -> c
. PeriodicReportRow a c -> c
forall a b. PeriodicReportRow a b -> b
prrTotal

-- | Build the report rows.
-- One row per account, with account name info, row amounts, row total and row average.
-- Rows are sorted according to the order in the 'Account' tree.
buildReportRows :: forall b c.
                (ReportOpts -> (BalanceData -> MixedAmount) -> DisplayName -> Account b -> PeriodicReportRow DisplayName c)
                -> ReportOpts -> Account b -> [PeriodicReportRow DisplayName c]
buildReportRows :: forall b c.
(ReportOpts
 -> (BalanceData -> MixedAmount)
 -> DisplayName
 -> Account b
 -> PeriodicReportRow DisplayName c)
-> ReportOpts -> Account b -> [PeriodicReportRow DisplayName c]
buildReportRows ReportOpts
-> (BalanceData -> MixedAmount)
-> DisplayName
-> Account b
-> PeriodicReportRow DisplayName c
makeRow ReportOpts
ropts = Bool
-> Key -> Key -> Account b -> [PeriodicReportRow DisplayName c]
mkRows Bool
True (-ReportOpts -> Key
drop_ ReportOpts
ropts) Key
0
  where
    -- Build the row for an account at a given depth with some number of boring parents
    mkRows :: Bool -> Int -> Int -> Account b -> [PeriodicReportRow DisplayName c]
    mkRows :: Bool
-> Key -> Key -> Account b -> [PeriodicReportRow DisplayName c]
mkRows Bool
isRoot Key
d Key
boringParents Account b
acct
        -- Account is a boring root account, and should be bypassed entirely
        | Account b -> Bool
forall a. Account a -> Bool
aboring Account b
acct Bool -> Bool -> Bool
&& Bool
isRoot         = Key -> Key -> [PeriodicReportRow DisplayName c]
buildSubrows Key
d Key
0
        -- Account is boring and has been dropped, so should be skipped and move up the hierarchy
        | Account b -> Bool
forall a. Account a -> Bool
aboring Account b
acct Bool -> Bool -> Bool
&& Key
d Key -> Key -> Bool
forall a. Ord a => a -> a -> Bool
< Key
0          = Key -> Key -> [PeriodicReportRow DisplayName c]
buildSubrows (Key
d Key -> Key -> Key
forall a. Num a => a -> a -> a
+ Key
1) Key
0
        -- Account is boring, and we can omit boring parents, so we should omit but keep track
        | Account b -> Bool
forall a. Account a -> Bool
aboring Account b
acct Bool -> Bool -> Bool
&& Bool
canOmitParents = Key -> Key -> [PeriodicReportRow DisplayName c]
buildSubrows Key
d (Key
boringParents Key -> Key -> Key
forall a. Num a => a -> a -> a
+ Key
1)
        -- Account is not boring or otherwise should be displayed.
        | Bool
otherwise = ReportOpts
-> (BalanceData -> MixedAmount)
-> DisplayName
-> Account b
-> PeriodicReportRow DisplayName c
makeRow ReportOpts
ropts BalanceData -> MixedAmount
balance DisplayName
displayname Account b
acct PeriodicReportRow DisplayName c
-> [PeriodicReportRow DisplayName c]
-> [PeriodicReportRow DisplayName c]
forall a. a -> [a] -> [a]
: Key -> Key -> [PeriodicReportRow DisplayName c]
buildSubrows (Key
d Key -> Key -> Key
forall a. Num a => a -> a -> a
+ Key
1) Key
0
      where
        displayname :: DisplayName
displayname = Key -> Key -> CommoditySymbol -> DisplayName
displayedName Key
d Key
boringParents (CommoditySymbol -> DisplayName) -> CommoditySymbol -> DisplayName
forall a b. (a -> b) -> a -> b
$ Account b -> CommoditySymbol
forall a. Account a -> CommoditySymbol
aname Account b
acct
        buildSubrows :: Key -> Key -> [PeriodicReportRow DisplayName c]
buildSubrows Key
i Key
b = (Account b -> [PeriodicReportRow DisplayName c])
-> [Account b] -> [PeriodicReportRow DisplayName c]
forall (t :: * -> *) a b. Foldable t => (a -> [b]) -> t a -> [b]
concatMap (Bool
-> Key -> Key -> Account b -> [PeriodicReportRow DisplayName c]
mkRows Bool
False Key
i Key
b) ([Account b] -> [PeriodicReportRow DisplayName c])
-> [Account b] -> [PeriodicReportRow DisplayName c]
forall a b. (a -> b) -> a -> b
$ Account b -> [Account b]
forall a. Account a -> [Account a]
asubs Account b
acct

    canOmitParents :: Bool
canOmitParents = ReportOpts -> Bool
flat_ ReportOpts
ropts Bool -> Bool -> Bool
|| Bool -> Bool
not (ReportOpts -> Bool
no_elide_ ReportOpts
ropts)
    balance :: BalanceData -> MixedAmount
balance = case ReportOpts -> AccountListMode
accountlistmode_ ReportOpts
ropts of
        AccountListMode
ALTree -> BalanceData -> MixedAmount
bdincludingsubs
        AccountListMode
ALFlat -> BalanceData -> MixedAmount
bdexcludingsubs

    displayedName :: Key -> Key -> CommoditySymbol -> DisplayName
displayedName Key
d Key
boringParents CommoditySymbol
name
        | Key
d Key -> Key -> Bool
forall a. Eq a => a -> a -> Bool
== Key
0 Bool -> Bool -> Bool
&& CommoditySymbol
name CommoditySymbol -> CommoditySymbol -> Bool
forall a. Eq a => a -> a -> Bool
== CommoditySymbol
"root" = CommoditySymbol -> CommoditySymbol -> Key -> DisplayName
DisplayName CommoditySymbol
"..." CommoditySymbol
"..." Key
0
        | Bool
otherwise = case ReportOpts -> AccountListMode
accountlistmode_ ReportOpts
ropts of
            AccountListMode
ALTree -> CommoditySymbol -> CommoditySymbol -> Key -> DisplayName
DisplayName CommoditySymbol
name CommoditySymbol
leaf (Key -> DisplayName) -> Key -> DisplayName
forall a b. (a -> b) -> a -> b
$ Key -> Key -> Key
forall a. Ord a => a -> a -> a
max Key
0 Key
d
            AccountListMode
ALFlat -> CommoditySymbol -> CommoditySymbol -> Key -> DisplayName
DisplayName CommoditySymbol
name CommoditySymbol
droppedName Key
0
      where
        leaf :: CommoditySymbol
leaf = [CommoditySymbol] -> CommoditySymbol
accountNameFromComponents
               ([CommoditySymbol] -> CommoditySymbol)
-> ([CommoditySymbol] -> [CommoditySymbol])
-> [CommoditySymbol]
-> CommoditySymbol
forall b c a. (b -> c) -> (a -> b) -> a -> c
. [CommoditySymbol] -> [CommoditySymbol]
forall a. [a] -> [a]
reverse ([CommoditySymbol] -> [CommoditySymbol])
-> ([CommoditySymbol] -> [CommoditySymbol])
-> [CommoditySymbol]
-> [CommoditySymbol]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Key -> [CommoditySymbol] -> [CommoditySymbol]
forall a. Key -> [a] -> [a]
take (Key
boringParents Key -> Key -> Key
forall a. Num a => a -> a -> a
+ Key
1) ([CommoditySymbol] -> [CommoditySymbol])
-> ([CommoditySymbol] -> [CommoditySymbol])
-> [CommoditySymbol]
-> [CommoditySymbol]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. [CommoditySymbol] -> [CommoditySymbol]
forall a. [a] -> [a]
reverse
               ([CommoditySymbol] -> CommoditySymbol)
-> [CommoditySymbol] -> CommoditySymbol
forall a b. (a -> b) -> a -> b
$ CommoditySymbol -> [CommoditySymbol]
accountNameComponents CommoditySymbol
droppedName
        droppedName :: CommoditySymbol
droppedName = Key -> CommoditySymbol -> CommoditySymbol
accountNameDrop (ReportOpts -> Key
drop_ ReportOpts
ropts) CommoditySymbol
name


-- | Build a report row.
--
-- Calculate the column totals. These are always the sum of column amounts.
makeMultiBalanceReportRow :: ReportOpts -> (BalanceData -> MixedAmount)
                          -> a -> Account BalanceData -> PeriodicReportRow a MixedAmount
makeMultiBalanceReportRow :: forall a.
ReportOpts
-> (BalanceData -> MixedAmount)
-> a
-> Account BalanceData
-> PeriodicReportRow a MixedAmount
makeMultiBalanceReportRow = MixedAmount
-> (IntMap MixedAmount -> (MixedAmount, MixedAmount))
-> ReportOpts
-> (BalanceData -> MixedAmount)
-> a
-> Account BalanceData
-> PeriodicReportRow a MixedAmount
forall c b a.
c
-> (IntMap c -> (c, c))
-> ReportOpts
-> (b -> c)
-> a
-> Account b
-> PeriodicReportRow a c
makePeriodicReportRow MixedAmount
nullmixedamt IntMap MixedAmount -> (MixedAmount, MixedAmount)
forall (f :: * -> *).
Foldable f =>
f MixedAmount -> (MixedAmount, MixedAmount)
sumAndAverageMixedAmounts

-- | Build a report row.
--
-- Calculate the column totals. These are always the sum of column amounts.
makePeriodicReportRow :: c -> (IM.IntMap c -> (c, c))
                      -> ReportOpts -> (b -> c)
                      -> a -> Account b -> PeriodicReportRow a c
makePeriodicReportRow :: forall c b a.
c
-> (IntMap c -> (c, c))
-> ReportOpts
-> (b -> c)
-> a
-> Account b
-> PeriodicReportRow a c
makePeriodicReportRow c
nullEntry IntMap c -> (c, c)
totalAndAverage ReportOpts
ropts b -> c
balance a
name Account b
acct =
    a -> [c] -> c -> c -> PeriodicReportRow a c
forall a b. a -> [b] -> b -> b -> PeriodicReportRow a b
PeriodicReportRow a
name (IntMap c -> [c]
forall a. IntMap a -> [a]
forall (t :: * -> *) a. Foldable t => t a -> [a]
toList IntMap c
rowbals) c
rowtotal c
avg
  where
    rowbals :: IntMap c
rowbals = (b -> c) -> IntMap b -> IntMap c
forall a b. (a -> b) -> IntMap a -> IntMap b
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
fmap b -> c
balance (IntMap b -> IntMap c)
-> (PeriodData b -> IntMap b) -> PeriodData b -> IntMap c
forall b c a. (b -> c) -> (a -> b) -> a -> c
. PeriodData b -> IntMap b
forall a. PeriodData a -> IntMap a
pdperiods (PeriodData b -> IntMap c) -> PeriodData b -> IntMap c
forall a b. (a -> b) -> a -> b
$ Account b -> PeriodData b
forall a. Account a -> PeriodData a
adata Account b
acct
    (c
total, c
avg) = IntMap c -> (c, c)
totalAndAverage IntMap c
rowbals
    -- Total for a cumulative/historical report is always the last column.
    rowtotal :: c
rowtotal = case ReportOpts -> BalanceAccumulation
balanceaccum_ ReportOpts
ropts of
        BalanceAccumulation
PerPeriod -> c
total
        BalanceAccumulation
_         -> c -> ((Key, c) -> c) -> Maybe (Key, c) -> c
forall b a. b -> (a -> b) -> Maybe a -> b
maybe c
nullEntry (Key, c) -> c
forall a b. (a, b) -> b
snd (Maybe (Key, c) -> c) -> Maybe (Key, c) -> c
forall a b. (a -> b) -> a -> b
$ IntMap c -> Maybe (Key, c)
forall a. IntMap a -> Maybe (Key, a)
IM.lookupMax IntMap c
rowbals

-- | Map the report rows to percentages if needed
reportPercent :: ReportOpts -> MultiBalanceReport -> MultiBalanceReport
reportPercent :: ReportOpts -> MultiBalanceReport -> MultiBalanceReport
reportPercent ReportOpts
ropts report :: MultiBalanceReport
report@(PeriodicReport [DateSpan]
spans [PeriodicReportRow DisplayName MixedAmount]
rows PeriodicReportRow () MixedAmount
totalrow)
  | ReportOpts -> Bool
percent_ ReportOpts
ropts = [DateSpan]
-> [PeriodicReportRow DisplayName MixedAmount]
-> PeriodicReportRow () MixedAmount
-> MultiBalanceReport
forall a b.
[DateSpan]
-> [PeriodicReportRow a b]
-> PeriodicReportRow () b
-> PeriodicReport a b
PeriodicReport [DateSpan]
spans ((PeriodicReportRow DisplayName MixedAmount
 -> PeriodicReportRow DisplayName MixedAmount)
-> [PeriodicReportRow DisplayName MixedAmount]
-> [PeriodicReportRow DisplayName MixedAmount]
forall a b. (a -> b) -> [a] -> [b]
map PeriodicReportRow DisplayName MixedAmount
-> PeriodicReportRow DisplayName MixedAmount
forall {a}.
PeriodicReportRow a MixedAmount -> PeriodicReportRow a MixedAmount
percentRow [PeriodicReportRow DisplayName MixedAmount]
rows) (PeriodicReportRow () MixedAmount
-> PeriodicReportRow () MixedAmount
forall {a}.
PeriodicReportRow a MixedAmount -> PeriodicReportRow a MixedAmount
percentRow PeriodicReportRow () MixedAmount
totalrow)
  | Bool
otherwise      = MultiBalanceReport
report
  where
    percentRow :: PeriodicReportRow a MixedAmount -> PeriodicReportRow a MixedAmount
percentRow (PeriodicReportRow a
name [MixedAmount]
rowvals MixedAmount
rowtotal MixedAmount
rowavg) =
      a
-> [MixedAmount]
-> MixedAmount
-> MixedAmount
-> PeriodicReportRow a MixedAmount
forall a b. a -> [b] -> b -> b -> PeriodicReportRow a b
PeriodicReportRow a
name
        ((MixedAmount -> MixedAmount -> MixedAmount)
-> [MixedAmount] -> [MixedAmount] -> [MixedAmount]
forall a b c. (a -> b -> c) -> [a] -> [b] -> [c]
zipWith MixedAmount -> MixedAmount -> MixedAmount
perdivide [MixedAmount]
rowvals ([MixedAmount] -> [MixedAmount]) -> [MixedAmount] -> [MixedAmount]
forall a b. (a -> b) -> a -> b
$ PeriodicReportRow () MixedAmount -> [MixedAmount]
forall a b. PeriodicReportRow a b -> [b]
prrAmounts PeriodicReportRow () MixedAmount
totalrow)
        (MixedAmount -> MixedAmount -> MixedAmount
perdivide MixedAmount
rowtotal (MixedAmount -> MixedAmount) -> MixedAmount -> MixedAmount
forall a b. (a -> b) -> a -> b
$ PeriodicReportRow () MixedAmount -> MixedAmount
forall a b. PeriodicReportRow a b -> b
prrTotal PeriodicReportRow () MixedAmount
totalrow)
        (MixedAmount -> MixedAmount -> MixedAmount
perdivide MixedAmount
rowavg (MixedAmount -> MixedAmount) -> MixedAmount -> MixedAmount
forall a b. (a -> b) -> a -> b
$ PeriodicReportRow () MixedAmount -> MixedAmount
forall a b. PeriodicReportRow a b -> b
prrAverage PeriodicReportRow () MixedAmount
totalrow)

-- | A helper: what percentage is the second mixed amount of the first ?
-- Keeps the sign of the first amount.
-- Uses unifyMixedAmount to unify each argument and then divides them.
-- Both amounts should be in the same, single commodity.
-- This can call error if the arguments are not right.
perdivide :: MixedAmount -> MixedAmount -> MixedAmount
perdivide :: MixedAmount -> MixedAmount -> MixedAmount
perdivide MixedAmount
a MixedAmount
b = MixedAmount -> Maybe MixedAmount -> MixedAmount
forall a. a -> Maybe a -> a
fromMaybe (String -> MixedAmount
forall a. String -> a
error' String
errmsg) (Maybe MixedAmount -> MixedAmount)
-> Maybe MixedAmount -> MixedAmount
forall a b. (a -> b) -> a -> b
$ do  -- PARTIAL:
    Amount
a' <- MixedAmount -> Maybe Amount
unifyMixedAmount MixedAmount
a
    Amount
b' <- MixedAmount -> Maybe Amount
unifyMixedAmount MixedAmount
b
    Bool -> Maybe ()
forall (f :: * -> *). Alternative f => Bool -> f ()
guard (Bool -> Maybe ()) -> Bool -> Maybe ()
forall a b. (a -> b) -> a -> b
$ Amount -> Bool
amountIsZero Amount
a' Bool -> Bool -> Bool
|| Amount -> Bool
amountIsZero Amount
b' Bool -> Bool -> Bool
|| Amount -> CommoditySymbol
acommodity Amount
a' CommoditySymbol -> CommoditySymbol -> Bool
forall a. Eq a => a -> a -> Bool
== Amount -> CommoditySymbol
acommodity Amount
b'
    MixedAmount -> Maybe MixedAmount
forall a. a -> Maybe a
forall (m :: * -> *) a. Monad m => a -> m a
return (MixedAmount -> Maybe MixedAmount)
-> MixedAmount -> Maybe MixedAmount
forall a b. (a -> b) -> a -> b
$ [Amount] -> MixedAmount
forall (t :: * -> *). Foldable t => t Amount -> MixedAmount
mixed [Quantity -> Amount
per (Quantity -> Amount) -> Quantity -> Amount
forall a b. (a -> b) -> a -> b
$ if Amount -> Quantity
aquantity Amount
b' Quantity -> Quantity -> Bool
forall a. Eq a => a -> a -> Bool
== Quantity
0 then Quantity
0 else Amount -> Quantity
aquantity Amount
a' Quantity -> Quantity -> Quantity
forall a. Fractional a => a -> a -> a
/ Quantity -> Quantity
forall a. Num a => a -> a
abs (Amount -> Quantity
aquantity Amount
b') Quantity -> Quantity -> Quantity
forall a. Num a => a -> a -> a
* Quantity
100]
  where errmsg :: String
errmsg = String
"Cannot calculate percentages if accounts have different commodities (Hint: Try --cost, -V or similar flags.)"

-- | Calculate a cumulative sum from a list of period changes.
cumulativeSum :: Traversable t => t BalanceData -> t BalanceData
cumulativeSum :: forall (t :: * -> *).
Traversable t =>
t BalanceData -> t BalanceData
cumulativeSum = (BalanceData, t BalanceData) -> t BalanceData
forall a b. (a, b) -> b
snd ((BalanceData, t BalanceData) -> t BalanceData)
-> (t BalanceData -> (BalanceData, t BalanceData))
-> t BalanceData
-> t BalanceData
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (BalanceData -> BalanceData -> (BalanceData, BalanceData))
-> BalanceData -> t BalanceData -> (BalanceData, t BalanceData)
forall (t :: * -> *) s a b.
Traversable t =>
(s -> a -> (s, b)) -> s -> t a -> (s, t b)
mapAccumL (\BalanceData
prev BalanceData
new -> let z :: BalanceData
z = BalanceData
prev BalanceData -> BalanceData -> BalanceData
forall a. Semigroup a => a -> a -> a
<> BalanceData
new in (BalanceData
z, BalanceData
z)) BalanceData
forall a. Monoid a => a
mempty

-- | Extract period changes from a cumulative list.
periodChanges :: Traversable t => t BalanceData -> t BalanceData
periodChanges :: forall (t :: * -> *).
Traversable t =>
t BalanceData -> t BalanceData
periodChanges = (BalanceData, t BalanceData) -> t BalanceData
forall a b. (a, b) -> b
snd ((BalanceData, t BalanceData) -> t BalanceData)
-> (t BalanceData -> (BalanceData, t BalanceData))
-> t BalanceData
-> t BalanceData
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (BalanceData -> BalanceData -> (BalanceData, BalanceData))
-> BalanceData -> t BalanceData -> (BalanceData, t BalanceData)
forall (t :: * -> *) s a b.
Traversable t =>
(s -> a -> (s, b)) -> s -> t a -> (s, t b)
mapAccumL (\BalanceData
prev BalanceData
new -> (BalanceData
new, (MixedAmount -> MixedAmount -> MixedAmount)
-> BalanceData -> BalanceData -> BalanceData
opBalanceData MixedAmount -> MixedAmount -> MixedAmount
maMinus BalanceData
new BalanceData
prev)) BalanceData
forall a. Monoid a => a
mempty

-- tests

tests_MultiBalanceReport :: TestTree
tests_MultiBalanceReport = String -> [TestTree] -> TestTree
testGroup String
"MultiBalanceReport" [

  let
    amt0 :: Amount
amt0 = Amount {acommodity :: CommoditySymbol
acommodity=CommoditySymbol
"$", aquantity :: Quantity
aquantity=Quantity
0, acost :: Maybe AmountCost
acost=Maybe AmountCost
forall a. Maybe a
Nothing,
      astyle :: AmountStyle
astyle=AmountStyle {ascommodityside :: Side
ascommodityside = Side
L, ascommodityspaced :: Bool
ascommodityspaced = Bool
False, asdigitgroups :: Maybe DigitGroupStyle
asdigitgroups = Maybe DigitGroupStyle
forall a. Maybe a
Nothing,
      asdecimalmark :: Maybe Char
asdecimalmark = Char -> Maybe Char
forall a. a -> Maybe a
Just Char
'.', asprecision :: AmountPrecision
asprecision = Word8 -> AmountPrecision
Precision Word8
2, asrounding :: Rounding
asrounding = Rounding
NoRounding}}
    (ReportSpec
rspec,Journal
journal) gives :: (ReportSpec, Journal)
-> ([PeriodicReportRow DisplayName MixedAmount], MixedAmount)
-> IO ()
`gives` ([PeriodicReportRow DisplayName MixedAmount], MixedAmount)
r = do
      let rspec' :: ReportSpec
rspec' = ReportSpec
rspec{_rsQuery=And [queryFromFlags $ _rsReportOpts rspec, _rsQuery rspec]}
          ([PeriodicReportRow DisplayName MixedAmount]
eitems, MixedAmount
etotal) = ([PeriodicReportRow DisplayName MixedAmount], MixedAmount)
r
          (PeriodicReport [DateSpan]
_ [PeriodicReportRow DisplayName MixedAmount]
aitems PeriodicReportRow () MixedAmount
atotal) = ReportSpec -> Journal -> MultiBalanceReport
multiBalanceReport ReportSpec
rspec' Journal
journal
          showw :: PeriodicReportRow DisplayName MixedAmount
-> (CommoditySymbol, CommoditySymbol, Key, [String], String,
    String)
showw (PeriodicReportRow DisplayName
a [MixedAmount]
lAmt MixedAmount
amt MixedAmount
amt')
              = (DisplayName -> CommoditySymbol
displayFull DisplayName
a, DisplayName -> CommoditySymbol
displayName DisplayName
a, DisplayName -> Key
displayIndent DisplayName
a, (MixedAmount -> String) -> [MixedAmount] -> [String]
forall a b. (a -> b) -> [a] -> [b]
map MixedAmount -> String
showMixedAmountDebug [MixedAmount]
lAmt, MixedAmount -> String
showMixedAmountDebug MixedAmount
amt, MixedAmount -> String
showMixedAmountDebug MixedAmount
amt')
      ((PeriodicReportRow DisplayName MixedAmount
 -> (CommoditySymbol, CommoditySymbol, Key, [String], String,
     String))
-> [PeriodicReportRow DisplayName MixedAmount]
-> [(CommoditySymbol, CommoditySymbol, Key, [String], String,
     String)]
forall a b. (a -> b) -> [a] -> [b]
map PeriodicReportRow DisplayName MixedAmount
-> (CommoditySymbol, CommoditySymbol, Key, [String], String,
    String)
showw [PeriodicReportRow DisplayName MixedAmount]
aitems) [(CommoditySymbol, CommoditySymbol, Key, [String], String, String)]
-> [(CommoditySymbol, CommoditySymbol, Key, [String], String,
     String)]
-> IO ()
forall a. (Eq a, Show a, HasCallStack) => a -> a -> IO ()
@?= ((PeriodicReportRow DisplayName MixedAmount
 -> (CommoditySymbol, CommoditySymbol, Key, [String], String,
     String))
-> [PeriodicReportRow DisplayName MixedAmount]
-> [(CommoditySymbol, CommoditySymbol, Key, [String], String,
     String)]
forall a b. (a -> b) -> [a] -> [b]
map PeriodicReportRow DisplayName MixedAmount
-> (CommoditySymbol, CommoditySymbol, Key, [String], String,
    String)
showw [PeriodicReportRow DisplayName MixedAmount]
eitems)
      MixedAmount -> String
showMixedAmountDebug (PeriodicReportRow () MixedAmount -> MixedAmount
forall a b. PeriodicReportRow a b -> b
prrTotal PeriodicReportRow () MixedAmount
atotal) String -> String -> IO ()
forall a. (Eq a, Show a, HasCallStack) => a -> a -> IO ()
@?= MixedAmount -> String
showMixedAmountDebug MixedAmount
etotal -- we only check the sum of the totals
  in
   String -> [TestTree] -> TestTree
testGroup String
"multiBalanceReport" [
      String -> IO () -> TestTree
testCase String
"null journal"  (IO () -> TestTree) -> IO () -> TestTree
forall a b. (a -> b) -> a -> b
$
      (ReportSpec
defreportspec, Journal
nulljournal) (ReportSpec, Journal)
-> ([PeriodicReportRow DisplayName MixedAmount], MixedAmount)
-> IO ()
`gives` ([], MixedAmount
nullmixedamt)

     ,String -> IO () -> TestTree
testCase String
"with -H on a populated period"  (IO () -> TestTree) -> IO () -> TestTree
forall a b. (a -> b) -> a -> b
$
      (ReportSpec
defreportspec{_rsReportOpts=defreportopts{period_= PeriodBetween (fromGregorian 2008 1 1) (fromGregorian 2008 1 2), balanceaccum_=Historical}}, Journal
samplejournal) (ReportSpec, Journal)
-> ([PeriodicReportRow DisplayName MixedAmount], MixedAmount)
-> IO ()
`gives`
       (
        [ DisplayName
-> [MixedAmount]
-> MixedAmount
-> MixedAmount
-> PeriodicReportRow DisplayName MixedAmount
forall a b. a -> [b] -> b -> b -> PeriodicReportRow a b
PeriodicReportRow (CommoditySymbol -> DisplayName
flatDisplayName CommoditySymbol
"assets:bank:checking") [Amount -> MixedAmount
mixedAmount (Amount -> MixedAmount) -> Amount -> MixedAmount
forall a b. (a -> b) -> a -> b
$ Quantity -> Amount
usd Quantity
1]    (Amount -> MixedAmount
mixedAmount (Amount -> MixedAmount) -> Amount -> MixedAmount
forall a b. (a -> b) -> a -> b
$ Quantity -> Amount
usd Quantity
1)    (Amount -> MixedAmount
mixedAmount Amount
amt0{aquantity=1})
        , DisplayName
-> [MixedAmount]
-> MixedAmount
-> MixedAmount
-> PeriodicReportRow DisplayName MixedAmount
forall a b. a -> [b] -> b -> b -> PeriodicReportRow a b
PeriodicReportRow (CommoditySymbol -> DisplayName
flatDisplayName CommoditySymbol
"income:salary")        [Amount -> MixedAmount
mixedAmount (Amount -> MixedAmount) -> Amount -> MixedAmount
forall a b. (a -> b) -> a -> b
$ Quantity -> Amount
usd (-Quantity
1)] (Amount -> MixedAmount
mixedAmount (Amount -> MixedAmount) -> Amount -> MixedAmount
forall a b. (a -> b) -> a -> b
$ Quantity -> Amount
usd (-Quantity
1)) (Amount -> MixedAmount
mixedAmount Amount
amt0{aquantity=(-1)})
        ],
        Amount -> MixedAmount
mixedAmount (Amount -> MixedAmount) -> Amount -> MixedAmount
forall a b. (a -> b) -> a -> b
$ Quantity -> Amount
usd Quantity
0)

     -- ,testCase "a valid history on an empty period"  $
     --  (defreportopts{period_= PeriodBetween (fromGregorian 2008 1 2) (fromGregorian 2008 1 3), balanceaccum_=Historical}, samplejournal) `gives`
     --   (
     --    [
     --     ("assets:bank:checking","checking",3, [mamountp' "$1.00"], mamountp' "$1.00",mixedAmount amt0 {aquantity=1})
     --    ,("income:salary","salary",2, [mamountp' "$-1.00"], mamountp' "$-1.00",mixedAmount amt0 {aquantity=(-1)})
     --    ],
     --    mixedAmount usd0)

     -- ,testCase "a valid history on an empty period (more complex)"  $
     --  (defreportopts{period_= PeriodBetween (fromGregorian 2009 1 1) (fromGregorian 2009 1 2), balanceaccum_=Historical}, samplejournal) `gives`
     --   (
     --    [
     --    ("assets:bank:checking","checking",3, [mamountp' "$1.00"], mamountp' "$1.00",mixedAmount amt0 {aquantity=1})
     --    ,("assets:bank:saving","saving",3, [mamountp' "$1.00"], mamountp' "$1.00",mixedAmount amt0 {aquantity=1})
     --    ,("assets:cash","cash",2, [mamountp' "$-2.00"], mamountp' "$-2.00",mixedAmount amt0 {aquantity=(-2)})
     --    ,("expenses:food","food",2, [mamountp' "$1.00"], mamountp' "$1.00",mixedAmount amt0 {aquantity=(1)})
     --    ,("expenses:supplies","supplies",2, [mamountp' "$1.00"], mamountp' "$1.00",mixedAmount amt0 {aquantity=(1)})
     --    ,("income:gifts","gifts",2, [mamountp' "$-1.00"], mamountp' "$-1.00",mixedAmount amt0 {aquantity=(-1)})
     --    ,("income:salary","salary",2, [mamountp' "$-1.00"], mamountp' "$-1.00",mixedAmount amt0 {aquantity=(-1)})
     --    ],
     --    mixedAmount usd0)
    ]
 ]