{-# 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.HashSet qualified as HS
import Data.List (sortOn)
import Data.List.NonEmpty (NonEmpty((:|)))
import Data.Map qualified as M
import Data.Maybe (fromMaybe, isJust)
import Data.Ord (Down(..))
import Data.Semigroup (sconcat)
import Data.These (these)
import Data.Time.Calendar (Day(..), 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, Maybe DayPartition
colspans) = String
-> (DateSpan, Maybe DayPartition) -> (DateSpan, Maybe DayPartition)
forall a. Show a => String -> a -> a
dbg5 String
"multiBalanceReportWith reportSpan" ((DateSpan, Maybe DayPartition) -> (DateSpan, Maybe DayPartition))
-> (DateSpan, Maybe DayPartition) -> (DateSpan, Maybe DayPartition)
forall a b. (a -> b) -> a -> b
$ Journal -> ReportSpec -> (DateSpan, Maybe DayPartition)
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
-> Maybe DayPartition
-> [Posting]
-> Account BalanceData
generateMultiBalanceAccount ReportSpec
rspec Journal
j PriceOracle
priceoracle Maybe DayPartition
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
-> Maybe DayPartition -> Account BalanceData -> MultiBalanceReport
generateMultiBalanceReport (ReportSpec -> ReportOpts
_rsReportOpts ReportSpec
rspec) Maybe DayPartition
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, Maybe DayPartition
colspans) = String
-> (DateSpan, Maybe DayPartition) -> (DateSpan, Maybe DayPartition)
forall a. Show a => String -> a -> a
dbg5 String
"compoundBalanceReportWith reportSpan" ((DateSpan, Maybe DayPartition) -> (DateSpan, Maybe DayPartition))
-> (DateSpan, Maybe DayPartition) -> (DateSpan, Maybe DayPartition)
forall a b. (a -> b) -> a -> b
$ Journal -> ReportSpec -> (DateSpan, Maybe DayPartition)
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
-> Maybe DayPartition -> Account BalanceData -> MultiBalanceReport
generateMultiBalanceReport ReportOpts
ropts Maybe DayPartition
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
-> Maybe DayPartition
-> [Posting]
-> Account BalanceData
generateMultiBalanceAccount ReportSpec
rspecsub Journal
j PriceOracle
priceoracle Maybe DayPartition
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
"" (Maybe DayPartition -> [DateSpan]
maybeDayPartitionToDateSpans Maybe DayPartition
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 -> Maybe DayPartition -> [Posting] -> Account BalanceData
generateMultiBalanceAccount :: ReportSpec
-> Journal
-> PriceOracle
-> Maybe DayPartition
-> [Posting]
-> Account BalanceData
generateMultiBalanceAccount rspec :: ReportSpec
rspec@ReportSpec{_rsReportOpts :: ReportSpec -> ReportOpts
_rsReportOpts=ReportOpts
ropts} Journal
j PriceOracle
priceoracle Maybe DayPartition
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
-> Maybe DayPartition
-> [Posting]
-> Account BalanceData
calculateReportAccount ReportSpec
rspec Journal
j PriceOracle
priceoracle Maybe DayPartition
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 -> Maybe DayPartition -> [Posting] -> Account BalanceData
calculateReportAccount :: ReportSpec
-> Journal
-> PriceOracle
-> Maybe DayPartition
-> [Posting]
-> Account BalanceData
calculateReportAccount ReportSpec
_ Journal
_ PriceOracle
_ Maybe DayPartition
Nothing [Posting]
_ =
    CommoditySymbol -> PeriodData BalanceData -> Account BalanceData
forall a. CommoditySymbol -> PeriodData a -> Account a
accountFromBalances CommoditySymbol
"root" (PeriodData BalanceData -> Account BalanceData)
-> PeriodData BalanceData -> Account BalanceData
forall a b. (a -> b) -> a -> b
$ BalanceData -> [(Day, BalanceData)] -> PeriodData BalanceData
forall a. a -> [(Day, a)] -> PeriodData a
periodDataFromList BalanceData
forall a. Monoid a => a
mempty [(Day
nulldate, BalanceData
forall a. Monoid a => a
mempty)]
calculateReportAccount rspec :: ReportSpec
rspec@ReportSpec{_rsReportOpts :: ReportSpec -> ReportOpts
_rsReportOpts=ReportOpts
ropts} Journal
j PriceOracle
priceoracle (Just DayPartition
colspans) [Posting]
ps =
    (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
-> DayPartition
-> PeriodData BalanceData
-> PeriodData BalanceData
periodDataValuation ReportOpts
ropts Journal
j PriceOracle
priceoracle DayPartition
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 (BalanceData
-> PeriodData Day
-> PeriodData BalanceData
-> PeriodData BalanceData
forall a b. a -> PeriodData b -> PeriodData a -> PeriodData a
padPeriodData BalanceData
forall a. Monoid a => a
mempty (DayPartition -> PeriodData Day
dayPartitionToPeriodData DayPartition
colspans)) (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 = (Maybe Day, Day) -> Maybe Day
forall a b. (a, b) -> a
fst ((Maybe Day, Day) -> Maybe Day) -> (Maybe Day, Day) -> Maybe Day
forall a b. (a -> b) -> a -> b
$ Day -> DayPartition -> (Maybe Day, Day)
dayPartitionFind (Posting -> Day
getPostingDate Posting
p) DayPartition
colspans
    getPostingDate :: Posting -> Day
getPostingDate = WhichDate -> Posting -> Day
postingDateOrDate2 (ReportOpts -> WhichDate
whichDate (ReportSpec -> ReportOpts
_rsReportOpts ReportSpec
rspec))

-- | The valuation function to use for the chosen report options.
periodDataValuation :: ReportOpts -> Journal -> PriceOracle -> DayPartition
                    -> PeriodData BalanceData -> PeriodData BalanceData
periodDataValuation :: ReportOpts
-> Journal
-> PriceOracle
-> DayPartition
-> PeriodData BalanceData
-> PeriodData BalanceData
periodDataValuation ReportOpts
ropts Journal
j PriceOracle
priceoracle DayPartition
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 (DayPartition -> PeriodData Day
dayPartitionToPeriodData DayPartition
colspans)
  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

-- | 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 = Int
d Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
> Int
qdepth                                       -- Throw out anything too deep
        allZeros :: Bool
allZeros = (BalanceData -> MixedAmount) -> Map Day BalanceData -> Bool
forall {t :: * -> *} {a}.
Foldable t =>
(a -> MixedAmount) -> t a -> Bool
isZeroRow BalanceData -> MixedAmount
balance Map Day 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 :: Map Day BalanceData
amts = PeriodData BalanceData -> Map Day BalanceData
forall a. PeriodData a -> Map Day a
pdperiods (PeriodData BalanceData -> Map Day BalanceData)
-> PeriodData BalanceData -> Map Day BalanceData
forall a b. (a -> b) -> a -> b
$ Account BalanceData -> PeriodData BalanceData
forall a. Account a -> PeriodData a
adata Account BalanceData
acct
        d :: Int
d = CommoditySymbol -> Int
accountNameLevel (CommoditySymbol -> Int) -> CommoditySymbol -> Int
forall a b. (a -> b) -> a -> b
$ Account BalanceData -> CommoditySymbol
forall a. Account a -> CommoditySymbol
aname Account BalanceData
acct

        qdepth :: Int
qdepth = Int -> Maybe Int -> Int
forall a. a -> Maybe a -> a
fromMaybe Int
forall a. Bounded a => a
maxBound (Maybe Int -> Int)
-> (CommoditySymbol -> Maybe Int) -> CommoditySymbol -> Int
forall b c a. (b -> c) -> (a -> b) -> a -> c
. DepthSpec -> CommoditySymbol -> Maybe Int
getAccountNameClippedDepth DepthSpec
depthspec (CommoditySymbol -> Int) -> CommoditySymbol -> Int
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 | Int
d Int -> Int -> Bool
forall a. Eq a => a -> a -> Bool
== Int
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] -> Int
forall a. [a] -> Int
forall (t :: * -> *) a. Foldable t => t a -> Int
length [Account a]
interestingSubs Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
< Int
minimumSubs
        droppedAccount :: Bool
droppedAccount = CommoditySymbol -> Int
accountNameLevel (Account a -> CommoditySymbol
forall a. Account a -> CommoditySymbol
aname Account a
acct) Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
<= ReportOpts -> Int
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 :: Int
minimumSubs = if ReportOpts -> Bool
no_elide_ ReportOpts
ropts then Int
1 else Int
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) -> Map Day BalanceData -> Bool
forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Bool
any ((Int
0<) (Int -> Bool) -> (BalanceData -> Int) -> BalanceData -> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. BalanceData -> Int
bdnumpostings) (Map Day BalanceData -> Bool)
-> (Account BalanceData -> Map Day BalanceData)
-> Account BalanceData
-> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. PeriodData BalanceData -> Map Day BalanceData
forall a. PeriodData a -> Map Day a
pdperiods (PeriodData BalanceData -> Map Day BalanceData)
-> (Account BalanceData -> PeriodData BalanceData)
-> Account BalanceData
-> Map Day 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 Int -> [(Regexp, Int)] -> DepthSpec
DepthSpec (Int -> Maybe Int
forall a. a -> Maybe a
Just Int
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 -> Maybe DayPartition -> Account BalanceData -> MultiBalanceReport
generateMultiBalanceReport :: ReportOpts
-> Maybe DayPartition -> Account BalanceData -> MultiBalanceReport
generateMultiBalanceReport ReportOpts
ropts Maybe DayPartition
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
-> Maybe DayPartition
-> Account BalanceData
-> MultiBalanceReport
forall c b.
Show c =>
(forall a.
 ReportOpts
 -> (BalanceData -> MixedAmount)
 -> a
 -> Account b
 -> PeriodicReportRow a c)
-> (b -> MixedAmount)
-> (c -> MixedAmount)
-> ReportOpts
-> Maybe DayPartition
-> 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 Maybe DayPartition
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 -> Maybe DayPartition -> 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
-> Maybe DayPartition
-> 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 Maybe DayPartition
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 (Maybe DayPartition -> [DateSpan]
maybeDayPartitionToDateSpans Maybe DayPartition
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
. Map Day MixedAmount -> MixedAmount
forall {a}. Map a MixedAmount -> MixedAmount
sortKey (Map Day MixedAmount -> MixedAmount)
-> (Account b -> Map Day MixedAmount) -> Account b -> MixedAmount
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (b -> MixedAmount) -> Map Day b -> Map Day MixedAmount
forall a b. (a -> b) -> Map Day a -> Map Day b
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
fmap b -> MixedAmount
treeAmt (Map Day b -> Map Day MixedAmount)
-> (Account b -> Map Day b) -> Account b -> Map Day MixedAmount
forall b c a. (b -> c) -> (a -> b) -> a -> c
. PeriodData b -> Map Day b
forall a. PeriodData a -> Map Day a
pdperiods (PeriodData b -> Map Day b)
-> (Account b -> PeriodData b) -> Account b -> Map Day b
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Account b -> PeriodData b
forall a. Account a -> PeriodData a
adata
        sortKey :: Map a MixedAmount -> MixedAmount
sortKey = case ReportOpts -> BalanceAccumulation
balanceaccum_ ReportOpts
ropts of
          BalanceAccumulation
PerPeriod -> Map a MixedAmount -> MixedAmount
forall (t :: * -> *). Foldable t => t MixedAmount -> MixedAmount
maSum
          BalanceAccumulation
_         -> MixedAmount
-> ((a, MixedAmount) -> MixedAmount)
-> Maybe (a, MixedAmount)
-> MixedAmount
forall b a. b -> (a -> b) -> Maybe a -> b
maybe MixedAmount
nullmixedamt (a, MixedAmount) -> MixedAmount
forall a b. (a, b) -> b
snd (Maybe (a, MixedAmount) -> MixedAmount)
-> (Map a MixedAmount -> Maybe (a, MixedAmount))
-> Map a MixedAmount
-> MixedAmount
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Map a MixedAmount -> Maybe (a, MixedAmount)
forall k a. Map k a -> Maybe (k, a)
M.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
-> Int -> Int -> Account b -> [PeriodicReportRow DisplayName c]
mkRows Bool
True (-ReportOpts -> Int
drop_ ReportOpts
ropts) Int
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
-> Int -> Int -> Account b -> [PeriodicReportRow DisplayName c]
mkRows Bool
isRoot Int
d Int
boringParents Account b
acct
        -- Account is boring and has no interesting children at any depth, so we stop
        | Account b -> Bool
forall a. Account a -> Bool
allBoring 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         = Int -> Int -> [PeriodicReportRow DisplayName c]
buildSubrows Int
d Int
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
&& Int
d Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
< Int
0          = Int -> Int -> [PeriodicReportRow DisplayName c]
buildSubrows (Int
d Int -> Int -> Int
forall a. Num a => a -> a -> a
+ Int
1) Int
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 = Int -> Int -> [PeriodicReportRow DisplayName c]
buildSubrows Int
d (Int
boringParents Int -> Int -> Int
forall a. Num a => a -> a -> a
+ Int
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]
: Int -> Int -> [PeriodicReportRow DisplayName c]
buildSubrows (Int
d Int -> Int -> Int
forall a. Num a => a -> a -> a
+ Int
1) Int
0
      where
        displayname :: DisplayName
displayname = Int -> Int -> CommoditySymbol -> DisplayName
displayedName Int
d Int
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 :: Int -> Int -> [PeriodicReportRow DisplayName c]
buildSubrows Int
i Int
b = (Account b -> [PeriodicReportRow DisplayName c])
-> [Account b] -> [PeriodicReportRow DisplayName c]
forall (t :: * -> *) a b. Foldable t => (a -> [b]) -> t a -> [b]
concatMap (Bool
-> Int -> Int -> Account b -> [PeriodicReportRow DisplayName c]
mkRows Bool
False Int
i Int
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)
    allBoring :: Account a -> Bool
allBoring Account a
a = Account a -> Bool
forall a. Account a -> Bool
aboring Account a
a Bool -> Bool -> Bool
&& (Account a -> Bool) -> [Account a] -> Bool
forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Bool
all Account a -> Bool
allBoring (Account a -> [Account a]
forall a. Account a -> [Account a]
asubs Account a
a)
    balance :: BalanceData -> MixedAmount
balance = case ReportOpts -> AccountListMode
accountlistmode_ ReportOpts
ropts of
        AccountListMode
ALTree -> BalanceData -> MixedAmount
bdincludingsubs
        AccountListMode
ALFlat -> BalanceData -> MixedAmount
bdexcludingsubs

    displayedName :: Int -> Int -> CommoditySymbol -> DisplayName
displayedName Int
d Int
boringParents CommoditySymbol
name
        | Int
d Int -> Int -> Bool
forall a. Eq a => a -> a -> Bool
== Int
0 Bool -> Bool -> Bool
&& CommoditySymbol
name CommoditySymbol -> CommoditySymbol -> Bool
forall a. Eq a => a -> a -> Bool
== CommoditySymbol
"root" = CommoditySymbol -> CommoditySymbol -> Int -> DisplayName
DisplayName CommoditySymbol
"..." CommoditySymbol
"..." Int
0
        | Bool
otherwise = case ReportOpts -> AccountListMode
accountlistmode_ ReportOpts
ropts of
            AccountListMode
ALTree -> CommoditySymbol -> CommoditySymbol -> Int -> DisplayName
DisplayName CommoditySymbol
name CommoditySymbol
leaf (Int -> DisplayName) -> Int -> DisplayName
forall a b. (a -> b) -> a -> b
$ Int -> Int -> Int
forall a. Ord a => a -> a -> a
max Int
0 Int
d
            AccountListMode
ALFlat -> CommoditySymbol -> CommoditySymbol -> Int -> DisplayName
DisplayName CommoditySymbol
name CommoditySymbol
droppedName Int
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
. Int -> [CommoditySymbol] -> [CommoditySymbol]
forall a. Int -> [a] -> [a]
take (Int
boringParents Int -> Int -> Int
forall a. Num a => a -> a -> a
+ Int
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 = Int -> CommoditySymbol -> CommoditySymbol
accountNameDrop (ReportOpts -> Int
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
-> (Map Day MixedAmount -> (MixedAmount, MixedAmount))
-> ReportOpts
-> (BalanceData -> MixedAmount)
-> a
-> Account BalanceData
-> PeriodicReportRow a MixedAmount
forall c b a.
c
-> (Map Day c -> (c, c))
-> ReportOpts
-> (b -> c)
-> a
-> Account b
-> PeriodicReportRow a c
makePeriodicReportRow MixedAmount
nullmixedamt Map Day 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 -> (M.Map Day c -> (c, c))
                      -> ReportOpts -> (b -> c)
                      -> a -> Account b -> PeriodicReportRow a c
makePeriodicReportRow :: forall c b a.
c
-> (Map Day c -> (c, c))
-> ReportOpts
-> (b -> c)
-> a
-> Account b
-> PeriodicReportRow a c
makePeriodicReportRow c
nullEntry Map Day 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 (Map Day c -> [c]
forall a. Map Day a -> [a]
forall (t :: * -> *) a. Foldable t => t a -> [a]
toList Map Day c
rowbals) c
rowtotal c
avg
  where
    rowbals :: Map Day c
rowbals = (b -> c) -> Map Day b -> Map Day c
forall a b. (a -> b) -> Map Day a -> Map Day b
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
fmap b -> c
balance (Map Day b -> Map Day c)
-> (PeriodData b -> Map Day b) -> PeriodData b -> Map Day c
forall b c a. (b -> c) -> (a -> b) -> a -> c
. PeriodData b -> Map Day b
forall a. PeriodData a -> Map Day a
pdperiods (PeriodData b -> Map Day c) -> PeriodData b -> Map Day 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) = Map Day c -> (c, c)
totalAndAverage Map Day 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 -> ((Day, c) -> c) -> Maybe (Day, c) -> c
forall b a. b -> (a -> b) -> Maybe a -> b
maybe c
nullEntry (Day, c) -> c
forall a b. (a, b) -> b
snd (Maybe (Day, c) -> c) -> Maybe (Day, c) -> c
forall a b. (a -> b) -> a -> b
$ Map Day c -> Maybe (Day, c)
forall k a. Map k a -> Maybe (k, a)
M.lookupMax Map Day 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, Int, [String], String,
    String)
showw (PeriodicReportRow DisplayName
a [MixedAmount]
lAmt MixedAmount
amt MixedAmount
amt')
              = (DisplayName -> CommoditySymbol
displayFull DisplayName
a, DisplayName -> CommoditySymbol
displayName DisplayName
a, DisplayName -> Int
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, Int, [String], String,
     String))
-> [PeriodicReportRow DisplayName MixedAmount]
-> [(CommoditySymbol, CommoditySymbol, Int, [String], String,
     String)]
forall a b. (a -> b) -> [a] -> [b]
map PeriodicReportRow DisplayName MixedAmount
-> (CommoditySymbol, CommoditySymbol, Int, [String], String,
    String)
showw [PeriodicReportRow DisplayName MixedAmount]
aitems) [(CommoditySymbol, CommoditySymbol, Int, [String], String, String)]
-> [(CommoditySymbol, CommoditySymbol, Int, [String], String,
     String)]
-> IO ()
forall a. (Eq a, Show a, HasCallStack) => a -> a -> IO ()
@?= ((PeriodicReportRow DisplayName MixedAmount
 -> (CommoditySymbol, CommoditySymbol, Int, [String], String,
     String))
-> [PeriodicReportRow DisplayName MixedAmount]
-> [(CommoditySymbol, CommoditySymbol, Int, [String], String,
     String)]
forall a b. (a -> b) -> [a] -> [b]
map PeriodicReportRow DisplayName MixedAmount
-> (CommoditySymbol, CommoditySymbol, Int, [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)
    ]
 ]