{-# LANGUAGE FlexibleInstances, RecordWildCards, ScopedTypeVariables, OverloadedStrings, DeriveGeneric #-}
{-|

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

-}

module Hledger.Reports.MultiBalanceReport (
  MultiBalanceReport(..),
  MultiBalanceReportRow,
  multiBalanceReport,
  multiBalanceReportWith,
  balanceReportFromMultiBalanceReport,
  mbrNegate,
  mbrNormaliseSign,
  multiBalanceReportSpan,
  tableAsText,

  -- -- * Tests
  tests_MultiBalanceReport
)
where

import GHC.Generics (Generic)
import Control.DeepSeq (NFData)
import Data.List
import Data.Maybe
import Data.Ord
import Data.Time.Calendar
import Safe
import Text.Tabular as T
import Text.Tabular.AsciiWide

import Hledger.Data
import Hledger.Query
import Hledger.Utils
import Hledger.Read (mamountp')
import Hledger.Reports.ReportOptions
import Hledger.Reports.BalanceReport


-- | A multi balance report is a balance report with multiple columns,
-- corresponding to consecutive subperiods within the overall report
-- period. It has:
--
-- 1. a list of each column's period (date span)
--
-- 2. a list of rows, each containing:
--
--   * the full account name
--
--   * the leaf account name
--
--   * the account's depth
--
--   * A list of amounts, one for each column. The meaning of the
--     amounts depends on the type of multi balance report, of which
--     there are three: periodic, cumulative and historical (see
--     'BalanceType' and "Hledger.Cli.Commands.Balance").
--
--   * the total of the row's amounts for a periodic report,
--     or zero for cumulative/historical reports (since summing
--     end balances generally doesn't make sense).
--
--   * 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.
--
newtype MultiBalanceReport =
  MultiBalanceReport ([DateSpan]
                     ,[MultiBalanceReportRow]
                     ,MultiBalanceReportTotals
                     )
  deriving ((forall x. MultiBalanceReport -> Rep MultiBalanceReport x)
-> (forall x. Rep MultiBalanceReport x -> MultiBalanceReport)
-> Generic MultiBalanceReport
forall x. Rep MultiBalanceReport x -> MultiBalanceReport
forall x. MultiBalanceReport -> Rep MultiBalanceReport x
forall a.
(forall x. a -> Rep a x) -> (forall x. Rep a x -> a) -> Generic a
$cto :: forall x. Rep MultiBalanceReport x -> MultiBalanceReport
$cfrom :: forall x. MultiBalanceReport -> Rep MultiBalanceReport x
Generic)

type MultiBalanceReportRow    = (AccountName, AccountName, Int, [MixedAmount], MixedAmount, MixedAmount)
type MultiBalanceReportTotals = ([MixedAmount], MixedAmount, MixedAmount) -- (Totals list, sum of totals, average of totals)

instance NFData MultiBalanceReport

instance Show MultiBalanceReport where
    -- use pshow (pretty-show's ppShow) to break long lists onto multiple lines
    -- we add some bogus extra shows here to help it parse the output
    -- and wrap tuples and lists properly
    show :: MultiBalanceReport -> String
show (MultiBalanceReport (spans :: [DateSpan]
spans, items :: [MultiBalanceReportRow]
items, totals :: MultiBalanceReportTotals
totals)) =
        "MultiBalanceReport (ignore extra quotes):\n" String -> ShowS
forall a. [a] -> [a] -> [a]
++ (String, [String], MultiBalanceReportTotals) -> String
forall a. Show a => a -> String
pshow ([DateSpan] -> String
forall a. Show a => a -> String
show [DateSpan]
spans, (MultiBalanceReportRow -> String)
-> [MultiBalanceReportRow] -> [String]
forall a b. (a -> b) -> [a] -> [b]
map MultiBalanceReportRow -> String
forall a. Show a => a -> String
show [MultiBalanceReportRow]
items, MultiBalanceReportTotals
totals)

-- type alias just to remind us which AccountNames might be depth-clipped, below.
type ClippedAccountName = AccountName

-- | 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. Does not support tree-mode boring parent eliding.
-- 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 multiBalanceReport') by the bs/cf/is commands.
multiBalanceReport :: ReportOpts -> Query -> Journal -> MultiBalanceReport
multiBalanceReport :: ReportOpts -> Query -> Journal -> MultiBalanceReport
multiBalanceReport ropts :: ReportOpts
ropts q :: Query
q j :: Journal
j = ReportOpts -> Query -> Journal -> PriceOracle -> MultiBalanceReport
multiBalanceReportWith ReportOpts
ropts Query
q Journal
j (Journal -> PriceOracle
journalPriceOracle Journal
j)

-- | A helper for multiBalanceReport. This one takes an extra argument, a
-- PriceOracle to be used for looking up market prices. 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 :: ReportOpts -> Query -> Journal -> PriceOracle -> MultiBalanceReport
multiBalanceReportWith :: ReportOpts -> Query -> Journal -> PriceOracle -> MultiBalanceReport
multiBalanceReportWith ropts :: ReportOpts
ropts@ReportOpts{..} q :: Query
q j :: Journal
j@Journal{..} priceoracle :: PriceOracle
priceoracle =
  (if Bool
invert_ then MultiBalanceReport -> MultiBalanceReport
mbrNegate else MultiBalanceReport -> MultiBalanceReport
forall a. a -> a
id) (MultiBalanceReport -> MultiBalanceReport)
-> MultiBalanceReport -> MultiBalanceReport
forall a b. (a -> b) -> a -> b
$
  ([DateSpan], [MultiBalanceReportRow], MultiBalanceReportTotals)
-> MultiBalanceReport
MultiBalanceReport ([DateSpan]
colspans, [MultiBalanceReportRow]
mappedsortedrows, MultiBalanceReportTotals
mappedtotalsrow)
    where
      dbg1 :: String -> a -> a
dbg1 s :: String
s = let p :: String
p = "multiBalanceReport" in String -> a -> a
forall a. Show a => String -> a -> a
Hledger.Utils.dbg1 (String
pString -> ShowS
forall a. [a] -> [a] -> [a]
++" "String -> ShowS
forall a. [a] -> [a] -> [a]
++String
s)  -- add prefix in this function's debug output
      -- dbg1 = const id  -- exclude this function from debug output

      ----------------------------------------------------------------------
      -- 1. Queries, report/column dates.

      symq :: Query
symq       = String -> Query -> Query
forall a. Show a => String -> a -> a
dbg1 "symq"   (Query -> Query) -> Query -> Query
forall a b. (a -> b) -> a -> b
$ (Query -> Bool) -> Query -> Query
filterQuery Query -> Bool
queryIsSym (Query -> Query) -> Query -> Query
forall a b. (a -> b) -> a -> b
$ String -> Query -> Query
forall a. Show a => String -> a -> a
dbg1 "requested q" Query
q
      depthq :: Query
depthq     = String -> Query -> Query
forall a. Show a => String -> a -> a
dbg1 "depthq" (Query -> Query) -> Query -> Query
forall a b. (a -> b) -> a -> b
$ (Query -> Bool) -> Query -> Query
filterQuery Query -> Bool
queryIsDepth Query
q
      depth :: Int
depth      = Query -> Int
queryDepth Query
depthq
      depthless :: Query -> Query
depthless  = String -> Query -> Query
forall a. Show a => String -> a -> a
dbg1 "depthless" (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
queryIsDepth)
      datelessq :: Query
datelessq  = String -> Query -> Query
forall a. Show a => String -> a -> a
dbg1 "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
q
      dateqcons :: DateSpan -> Query
dateqcons  = if Bool
date2_ then DateSpan -> Query
Date2 else DateSpan -> Query
Date
      -- The date span specified by -b/-e/-p options and query args if any.
      requestedspan :: DateSpan
requestedspan  = String -> DateSpan -> DateSpan
forall a. Show a => String -> a -> a
dbg1 "requestedspan"  (DateSpan -> DateSpan) -> DateSpan -> DateSpan
forall a b. (a -> b) -> a -> b
$ Bool -> Query -> DateSpan
queryDateSpan Bool
date2_ Query
q
      -- If the requested span is open-ended, close it using the journal's end dates.
      -- This can still be the null (open) span if the journal is empty.
      requestedspan' :: DateSpan
requestedspan' = String -> DateSpan -> DateSpan
forall a. Show a => String -> a -> a
dbg1 "requestedspan'" (DateSpan -> DateSpan) -> DateSpan -> DateSpan
forall a b. (a -> b) -> a -> b
$ DateSpan
requestedspan DateSpan -> DateSpan -> DateSpan
`spanDefaultsFrom` Bool -> Journal -> DateSpan
journalDateSpan Bool
date2_ Journal
j
      -- The list of interval spans enclosing the requested span.
      -- This list can be empty if the journal was empty,
      -- or if hledger-ui has added its special date:-tomorrow to the query
      -- and all txns are in the future.
      intervalspans :: [DateSpan]
intervalspans  = String -> [DateSpan] -> [DateSpan]
forall a. Show a => String -> a -> a
dbg1 "intervalspans"  ([DateSpan] -> [DateSpan]) -> [DateSpan] -> [DateSpan]
forall a b. (a -> b) -> a -> b
$ Interval -> DateSpan -> [DateSpan]
splitSpan Interval
interval_ DateSpan
requestedspan'
      -- The requested span enlarged to enclose a whole number of intervals.
      -- This can be the null span if there were no intervals.
      reportspan :: DateSpan
reportspan     = String -> DateSpan -> DateSpan
forall a. Show a => String -> a -> a
dbg1 "reportspan"     (DateSpan -> DateSpan) -> DateSpan -> DateSpan
forall a b. (a -> b) -> a -> b
$ Maybe Day -> Maybe Day -> DateSpan
DateSpan (Maybe Day -> (DateSpan -> Maybe Day) -> Maybe DateSpan -> Maybe Day
forall b a. b -> (a -> b) -> Maybe a -> b
maybe Maybe Day
forall a. Maybe a
Nothing DateSpan -> Maybe Day
spanStart (Maybe DateSpan -> Maybe Day) -> Maybe DateSpan -> Maybe Day
forall a b. (a -> b) -> a -> b
$ [DateSpan] -> Maybe DateSpan
forall a. [a] -> Maybe a
headMay [DateSpan]
intervalspans)
                                                        (Maybe Day -> (DateSpan -> Maybe Day) -> Maybe DateSpan -> Maybe Day
forall b a. b -> (a -> b) -> Maybe a -> b
maybe Maybe Day
forall a. Maybe a
Nothing DateSpan -> Maybe Day
spanEnd   (Maybe DateSpan -> Maybe Day) -> Maybe DateSpan -> Maybe Day
forall a b. (a -> b) -> a -> b
$ [DateSpan] -> Maybe DateSpan
forall a. [a] -> Maybe a
lastMay [DateSpan]
intervalspans)
      mreportstart :: Maybe Day
mreportstart = DateSpan -> Maybe Day
spanStart DateSpan
reportspan
      -- 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).
      reportq :: Query
reportq   = String -> Query -> Query
forall a. Show a => String -> a -> a
dbg1 "reportq" (Query -> Query) -> Query -> Query
forall a b. (a -> b) -> a -> b
$ Query -> Query
depthless (Query -> Query) -> Query -> Query
forall a b. (a -> b) -> a -> b
$
        if DateSpan
reportspan DateSpan -> DateSpan -> Bool
forall a. Eq a => a -> a -> Bool
== DateSpan
nulldatespan
        then Query
q
        else [Query] -> Query
And [Query
datelessq, Query
reportspandatesq]
          where
            reportspandatesq :: Query
reportspandatesq = String -> Query -> Query
forall a. Show a => String -> a -> a
dbg1 "reportspandatesq" (Query -> Query) -> Query -> Query
forall a b. (a -> b) -> a -> b
$ DateSpan -> Query
dateqcons DateSpan
reportspan
      -- The date spans to be included as report columns.
      [DateSpan]
colspans :: [DateSpan] = String -> [DateSpan] -> [DateSpan]
forall a. Show a => String -> a -> a
dbg1 "colspans" ([DateSpan] -> [DateSpan]) -> [DateSpan] -> [DateSpan]
forall a b. (a -> b) -> a -> b
$ Interval -> DateSpan -> [DateSpan]
splitSpan Interval
interval_ DateSpan
displayspan
        where
          displayspan :: DateSpan
displayspan
            | Bool
empty_    = String -> DateSpan -> DateSpan
forall a. Show a => String -> a -> a
dbg1 "displayspan (-E)" DateSpan
reportspan                              -- all the requested intervals
            | Bool
otherwise = String -> DateSpan -> DateSpan
forall a. Show a => String -> a -> a
dbg1 "displayspan" (DateSpan -> DateSpan) -> DateSpan -> DateSpan
forall a b. (a -> b) -> a -> b
$ DateSpan
requestedspan DateSpan -> DateSpan -> DateSpan
`spanIntersect` DateSpan
matchedspan  -- exclude leading/trailing empty intervals
          matchedspan :: DateSpan
matchedspan = String -> DateSpan -> DateSpan
forall a. Show a => String -> a -> a
dbg1 "matchedspan" (DateSpan -> DateSpan) -> DateSpan -> DateSpan
forall a b. (a -> b) -> a -> b
$ WhichDate -> [Posting] -> DateSpan
postingsDateSpan' (ReportOpts -> WhichDate
whichDateFromOpts ReportOpts
ropts) [Posting]
ps

      -- If doing cost valuation, convert amounts to cost.
      j' :: Journal
j' = ReportOpts -> Journal -> Journal
journalSelectingAmountFromOpts ReportOpts
ropts Journal
j

      ----------------------------------------------------------------------
      -- 2. Calculate starting balances, if needed for -H

      -- Balances at report start date, from all earlier postings which otherwise match the query.
      -- These balances are unvalued except maybe converted to cost.
      [(Text, MixedAmount)]
startbals :: [(AccountName, MixedAmount)] = String -> [(Text, MixedAmount)] -> [(Text, MixedAmount)]
forall a. Show a => String -> a -> a
dbg1 "startbals" ([(Text, MixedAmount)] -> [(Text, MixedAmount)])
-> [(Text, MixedAmount)] -> [(Text, MixedAmount)]
forall a b. (a -> b) -> a -> b
$ ((Text, Text, Int, MixedAmount) -> (Text, MixedAmount))
-> [(Text, Text, Int, MixedAmount)] -> [(Text, MixedAmount)]
forall a b. (a -> b) -> [a] -> [b]
map (\(a :: Text
a,_,_,b :: MixedAmount
b) -> (Text
a,MixedAmount
b)) [(Text, Text, Int, MixedAmount)]
startbalanceitems
        where
          (startbalanceitems :: [(Text, Text, Int, MixedAmount)]
startbalanceitems,_) = String
-> ([(Text, Text, Int, MixedAmount)], MixedAmount)
-> ([(Text, Text, Int, MixedAmount)], MixedAmount)
forall a. Show a => String -> a -> a
dbg1 "starting balance report" (([(Text, Text, Int, MixedAmount)], MixedAmount)
 -> ([(Text, Text, Int, MixedAmount)], MixedAmount))
-> ([(Text, Text, Int, MixedAmount)], MixedAmount)
-> ([(Text, Text, Int, MixedAmount)], MixedAmount)
forall a b. (a -> b) -> a -> b
$ ReportOpts
-> Query
-> Journal
-> ([(Text, Text, Int, MixedAmount)], MixedAmount)
balanceReport ReportOpts
ropts''{value_ :: Maybe ValuationType
value_=Maybe ValuationType
forall a. Maybe a
Nothing, percent_ :: Bool
percent_=Bool
False} Query
startbalq Journal
j'
            where
              ropts' :: ReportOpts
ropts' | ReportOpts -> Bool
tree_ ReportOpts
ropts = ReportOpts
ropts{no_elide_ :: Bool
no_elide_=Bool
True}
                     | Bool
otherwise   = ReportOpts
ropts{accountlistmode_ :: AccountListMode
accountlistmode_=AccountListMode
ALFlat}
              ropts'' :: ReportOpts
ropts'' = ReportOpts
ropts'{period_ :: Period
period_ = Period
precedingperiod}
                where
                  precedingperiod :: Period
precedingperiod = DateSpan -> Period
dateSpanAsPeriod (DateSpan -> Period) -> DateSpan -> Period
forall a b. (a -> b) -> a -> b
$ DateSpan -> DateSpan -> DateSpan
spanIntersect (Maybe Day -> Maybe Day -> DateSpan
DateSpan Maybe Day
forall a. Maybe a
Nothing Maybe Day
mreportstart) (DateSpan -> DateSpan) -> DateSpan -> DateSpan
forall a b. (a -> b) -> a -> b
$ Period -> DateSpan
periodAsDateSpan Period
period_
              -- 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.
              startbalq :: Query
startbalq = String -> Query -> Query
forall a. Show a => String -> a -> a
dbg1 "startbalq" (Query -> Query) -> Query -> Query
forall a b. (a -> b) -> a -> b
$ [Query] -> Query
And [Query
datelessq, DateSpan -> Query
dateqcons DateSpan
precedingspan]
                where
                  precedingspan :: DateSpan
precedingspan = case Maybe Day
mreportstart of
                                  Just d :: Day
d  -> Maybe Day -> Maybe Day -> DateSpan
DateSpan Maybe Day
forall a. Maybe a
Nothing (Day -> Maybe Day
forall a. a -> Maybe a
Just Day
d)
                                  Nothing -> DateSpan
emptydatespan
      -- The matched accounts with a starting balance. All of these should appear
      -- in the report even if they have no postings during the report period.
      startaccts :: [Text]
startaccts = String -> [Text] -> [Text]
forall a. Show a => String -> a -> a
dbg1 "startaccts" ([Text] -> [Text]) -> [Text] -> [Text]
forall a b. (a -> b) -> a -> b
$ ((Text, MixedAmount) -> Text) -> [(Text, MixedAmount)] -> [Text]
forall a b. (a -> b) -> [a] -> [b]
map (Text, MixedAmount) -> Text
forall a b. (a, b) -> a
fst [(Text, MixedAmount)]
startbals
      -- Helpers to look up an account's starting balance.
      startingBalanceFor :: Text -> MixedAmount
startingBalanceFor a :: Text
a = MixedAmount -> Maybe MixedAmount -> MixedAmount
forall a. a -> Maybe a -> a
fromMaybe MixedAmount
nullmixedamt (Maybe MixedAmount -> MixedAmount)
-> Maybe MixedAmount -> MixedAmount
forall a b. (a -> b) -> a -> b
$ Text -> [(Text, MixedAmount)] -> Maybe MixedAmount
forall a b. Eq a => a -> [(a, b)] -> Maybe b
lookup Text
a [(Text, MixedAmount)]
startbals

      ----------------------------------------------------------------------
      -- 3. Gather postings for each column.

      -- Postings matching the query within the report period.
      [Posting]
ps :: [Posting] =
          String -> [Posting] -> [Posting]
forall a. Show a => String -> a -> a
dbg1 "ps" ([Posting] -> [Posting]) -> [Posting] -> [Posting]
forall a b. (a -> b) -> a -> b
$
          Journal -> [Posting]
journalPostings (Journal -> [Posting]) -> Journal -> [Posting]
forall a b. (a -> b) -> a -> b
$
          Query -> Journal -> Journal
filterJournalAmounts Query
symq (Journal -> Journal) -> Journal -> Journal
forall a b. (a -> b) -> a -> b
$      -- remove amount parts excluded by cur:
          Query -> Journal -> Journal
filterJournalPostings Query
reportq (Journal -> Journal) -> Journal -> Journal
forall a b. (a -> b) -> a -> b
$  -- remove postings not matched by (adjusted) query
          Journal
j'

      -- Group postings into their columns, with the column end dates.
      [([Posting], Maybe Day)]
colps :: [([Posting], Maybe Day)] =
          String -> [([Posting], Maybe Day)] -> [([Posting], Maybe Day)]
forall a. Show a => String -> a -> a
dbg1 "colps"
          [((Posting -> Bool) -> [Posting] -> [Posting]
forall a. (a -> Bool) -> [a] -> [a]
filter (WhichDate -> DateSpan -> Posting -> Bool
isPostingInDateSpan' (ReportOpts -> WhichDate
whichDateFromOpts ReportOpts
ropts) DateSpan
s) [Posting]
ps, DateSpan -> Maybe Day
spanEnd DateSpan
s) | DateSpan
s <- [DateSpan]
colspans]

      ----------------------------------------------------------------------
      -- 4. Calculate account balance changes in each column.

      -- In each column, gather the accounts that have postings and their change amount.
      acctChangesFromPostings :: [Posting] -> [(ClippedAccountName, MixedAmount)]
      acctChangesFromPostings :: [Posting] -> [(Text, MixedAmount)]
acctChangesFromPostings ps :: [Posting]
ps = [(Account -> Text
aname Account
a, (if ReportOpts -> Bool
tree_ ReportOpts
ropts then Account -> MixedAmount
aibalance else Account -> MixedAmount
aebalance) Account
a) | Account
a <- [Account]
as]
          where
            as :: [Account]
as = [Account] -> [Account]
depthLimit ([Account] -> [Account]) -> [Account] -> [Account]
forall a b. (a -> b) -> a -> b
$
                 (if ReportOpts -> Bool
tree_ ReportOpts
ropts then [Account] -> [Account]
forall a. a -> a
id else (Account -> Bool) -> [Account] -> [Account]
forall a. (a -> Bool) -> [a] -> [a]
filter ((Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
>0)(Int -> Bool) -> (Account -> Int) -> Account -> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
.Account -> Int
anumpostings)) ([Account] -> [Account]) -> [Account] -> [Account]
forall a b. (a -> b) -> a -> b
$
                 Int -> [Account] -> [Account]
forall a. Int -> [a] -> [a]
drop 1 ([Account] -> [Account]) -> [Account] -> [Account]
forall a b. (a -> b) -> a -> b
$ [Posting] -> [Account]
accountsFromPostings [Posting]
ps
            depthLimit :: [Account] -> [Account]
depthLimit
                | ReportOpts -> Bool
tree_ ReportOpts
ropts = (Account -> Bool) -> [Account] -> [Account]
forall a. (a -> Bool) -> [a] -> [a]
filter ((Query
depthq Query -> Text -> Bool
`matchesAccount`)(Text -> Bool) -> (Account -> Text) -> Account -> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
.Account -> Text
aname) -- exclude deeper balances
                | Bool
otherwise   = Int -> [Account] -> [Account]
clipAccountsAndAggregate Int
depth -- aggregate deeper balances at the depth limit
      [[(Text, MixedAmount)]]
colacctchanges :: [[(ClippedAccountName, MixedAmount)]] =
          String -> [[(Text, MixedAmount)]] -> [[(Text, MixedAmount)]]
forall a. Show a => String -> a -> a
dbg1 "colacctchanges" ([[(Text, MixedAmount)]] -> [[(Text, MixedAmount)]])
-> [[(Text, MixedAmount)]] -> [[(Text, MixedAmount)]]
forall a b. (a -> b) -> a -> b
$ (([Posting], Maybe Day) -> [(Text, MixedAmount)])
-> [([Posting], Maybe Day)] -> [[(Text, MixedAmount)]]
forall a b. (a -> b) -> [a] -> [b]
map ([Posting] -> [(Text, MixedAmount)]
acctChangesFromPostings ([Posting] -> [(Text, MixedAmount)])
-> (([Posting], Maybe Day) -> [Posting])
-> ([Posting], Maybe Day)
-> [(Text, MixedAmount)]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. ([Posting], Maybe Day) -> [Posting]
forall a b. (a, b) -> a
fst) [([Posting], Maybe Day)]
colps

      ----------------------------------------------------------------------
      -- 5. Gather the account balance changes into a regular matrix including the accounts
      -- from all columns (and with -H, accounts with starting balances), adding zeroes where needed.

      -- All account names that will be displayed, possibly depth-clipped.
      [Text]
displayaccts :: [ClippedAccountName] =
          String -> [Text] -> [Text]
forall a. Show a => String -> a -> a
dbg1 "displayaccts" ([Text] -> [Text]) -> [Text] -> [Text]
forall a b. (a -> b) -> a -> b
$
          (if ReportOpts -> Bool
tree_ ReportOpts
ropts then [Text] -> [Text]
expandAccountNames else [Text] -> [Text]
forall a. a -> a
id) ([Text] -> [Text]) -> [Text] -> [Text]
forall a b. (a -> b) -> a -> b
$
          [Text] -> [Text]
forall a. Eq a => [a] -> [a]
nub ([Text] -> [Text]) -> [Text] -> [Text]
forall a b. (a -> b) -> a -> b
$ (Text -> Text) -> [Text] -> [Text]
forall a b. (a -> b) -> [a] -> [b]
map (Int -> Text -> Text
clipOrEllipsifyAccountName Int
depth) ([Text] -> [Text]) -> [Text] -> [Text]
forall a b. (a -> b) -> a -> b
$
          if Bool
empty_ Bool -> Bool -> Bool
|| BalanceType
balancetype_ BalanceType -> BalanceType -> Bool
forall a. Eq a => a -> a -> Bool
== BalanceType
HistoricalBalance
          then [Text] -> [Text]
forall a. Eq a => [a] -> [a]
nub ([Text] -> [Text]) -> [Text] -> [Text]
forall a b. (a -> b) -> a -> b
$ [Text] -> [Text]
forall a. Ord a => [a] -> [a]
sort ([Text] -> [Text]) -> [Text] -> [Text]
forall a b. (a -> b) -> a -> b
$ [Text]
startaccts [Text] -> [Text] -> [Text]
forall a. [a] -> [a] -> [a]
++ [Text]
allpostedaccts
          else [Text]
allpostedaccts
        where
          [Text]
allpostedaccts :: [AccountName] = String -> [Text] -> [Text]
forall a. Show a => String -> a -> a
dbg1 "allpostedaccts" ([Text] -> [Text]) -> [Text] -> [Text]
forall a b. (a -> b) -> a -> b
$ [Text] -> [Text]
forall a. Ord a => [a] -> [a]
sort ([Text] -> [Text]) -> [Text] -> [Text]
forall a b. (a -> b) -> a -> b
$ [Posting] -> [Text]
accountNamesFromPostings [Posting]
ps
      -- Each column's balance changes for each account, adding zeroes where needed.
      [[(Text, MixedAmount)]]
colallacctchanges :: [[(ClippedAccountName, MixedAmount)]] =
          String -> [[(Text, MixedAmount)]] -> [[(Text, MixedAmount)]]
forall a. Show a => String -> a -> a
dbg1 "colallacctchanges"
          [((Text, MixedAmount) -> (Text, MixedAmount) -> Ordering)
-> [(Text, MixedAmount)] -> [(Text, MixedAmount)]
forall a. (a -> a -> Ordering) -> [a] -> [a]
sortBy (((Text, MixedAmount) -> Text)
-> (Text, MixedAmount) -> (Text, MixedAmount) -> Ordering
forall a b. Ord a => (b -> a) -> b -> b -> Ordering
comparing (Text, MixedAmount) -> Text
forall a b. (a, b) -> a
fst) ([(Text, MixedAmount)] -> [(Text, MixedAmount)])
-> [(Text, MixedAmount)] -> [(Text, MixedAmount)]
forall a b. (a -> b) -> a -> b
$
           ((Text, MixedAmount) -> (Text, MixedAmount) -> Bool)
-> [(Text, MixedAmount)]
-> [(Text, MixedAmount)]
-> [(Text, MixedAmount)]
forall a. (a -> a -> Bool) -> [a] -> [a] -> [a]
unionBy (\(a :: Text
a,_) (a' :: Text
a',_) -> Text
a Text -> Text -> Bool
forall a. Eq a => a -> a -> Bool
== Text
a') [(Text, MixedAmount)]
postedacctchanges [(Text, MixedAmount)]
zeroes
           | [(Text, MixedAmount)]
postedacctchanges <- [[(Text, MixedAmount)]]
colacctchanges]
          where zeroes :: [(Text, MixedAmount)]
zeroes = [(Text
a, MixedAmount
nullmixedamt) | Text
a <- [Text]
displayaccts]
      -- Transpose to get each account's balance changes across all columns.
      [(Text, [MixedAmount])]
acctchanges :: [(ClippedAccountName, [MixedAmount])] =
          String -> [(Text, [MixedAmount])] -> [(Text, [MixedAmount])]
forall a. Show a => String -> a -> a
dbg1 "acctchanges"
          [(Text
a, ((Text, MixedAmount) -> MixedAmount)
-> [(Text, MixedAmount)] -> [MixedAmount]
forall a b. (a -> b) -> [a] -> [b]
map (Text, MixedAmount) -> MixedAmount
forall a b. (a, b) -> b
snd [(Text, MixedAmount)]
abs) | abs :: [(Text, MixedAmount)]
abs@((a :: Text
a,_):_) <- [[(Text, MixedAmount)]] -> [[(Text, MixedAmount)]]
forall a. [[a]] -> [[a]]
transpose [[(Text, MixedAmount)]]
colallacctchanges] -- never null, or used when null...

      ----------------------------------------------------------------------
      -- 6. Build the report rows.

      -- One row per account, with account name info, row amounts, row total and row average.
      [MultiBalanceReportRow]
rows :: [MultiBalanceReportRow] =
          String -> [MultiBalanceReportRow] -> [MultiBalanceReportRow]
forall a. Show a => String -> a -> a
dbg1 "rows" ([MultiBalanceReportRow] -> [MultiBalanceReportRow])
-> [MultiBalanceReportRow] -> [MultiBalanceReportRow]
forall a b. (a -> b) -> a -> b
$
          [(Text
a, Text -> Text
accountLeafName Text
a, Text -> Int
accountNameLevel Text
a, [MixedAmount]
valuedrowbals, MixedAmount
rowtot, MixedAmount
rowavg)
           | (a :: Text
a,changes :: [MixedAmount]
changes) <- String -> [(Text, [MixedAmount])] -> [(Text, [MixedAmount])]
forall a. Show a => String -> a -> a
dbg1 "acctchanges" [(Text, [MixedAmount])]
acctchanges
             -- The row amounts to be displayed: per-period changes,
             -- zero-based cumulative totals, or
             -- starting-balance-based historical balances.
           , let rowbals :: [MixedAmount]
rowbals = String -> [MixedAmount] -> [MixedAmount]
forall a. Show a => String -> a -> a
dbg1 "rowbals" ([MixedAmount] -> [MixedAmount]) -> [MixedAmount] -> [MixedAmount]
forall a b. (a -> b) -> a -> b
$ case BalanceType
balancetype_ of
                   PeriodChange      -> [MixedAmount]
changes
                   CumulativeChange  -> Int -> [MixedAmount] -> [MixedAmount]
forall a. Int -> [a] -> [a]
drop 1 ([MixedAmount] -> [MixedAmount]) -> [MixedAmount] -> [MixedAmount]
forall a b. (a -> b) -> a -> b
$ (MixedAmount -> MixedAmount -> MixedAmount)
-> MixedAmount -> [MixedAmount] -> [MixedAmount]
forall b a. (b -> a -> b) -> b -> [a] -> [b]
scanl MixedAmount -> MixedAmount -> MixedAmount
forall a. Num a => a -> a -> a
(+) 0                      [MixedAmount]
changes
                   HistoricalBalance -> Int -> [MixedAmount] -> [MixedAmount]
forall a. Int -> [a] -> [a]
drop 1 ([MixedAmount] -> [MixedAmount]) -> [MixedAmount] -> [MixedAmount]
forall a b. (a -> b) -> a -> b
$ (MixedAmount -> MixedAmount -> MixedAmount)
-> MixedAmount -> [MixedAmount] -> [MixedAmount]
forall b a. (b -> a -> b) -> b -> [a] -> [b]
scanl MixedAmount -> MixedAmount -> MixedAmount
forall a. Num a => a -> a -> a
(+) (Text -> MixedAmount
startingBalanceFor Text
a) [MixedAmount]
changes
             -- We may be converting amounts to value, per hledger_options.m4.md "Effect of --value on reports".
           , let valuedrowbals :: [MixedAmount]
valuedrowbals = String -> [MixedAmount] -> [MixedAmount]
forall a. Show a => String -> a -> a
dbg1 "valuedrowbals" ([MixedAmount] -> [MixedAmount]) -> [MixedAmount] -> [MixedAmount]
forall a b. (a -> b) -> a -> b
$ [Day -> MixedAmount -> MixedAmount
avalue Day
periodlastday MixedAmount
amt | (amt :: MixedAmount
amt,periodlastday :: Day
periodlastday) <- [MixedAmount] -> [Day] -> [(MixedAmount, Day)]
forall a b. [a] -> [b] -> [(a, b)]
zip [MixedAmount]
rowbals [Day]
lastdays]
             -- The total and average for the row.
             -- These are always simply the sum/average of the displayed row amounts.
             -- Total for a cumulative/historical report is always zero.
           , let rowtot :: MixedAmount
rowtot = if BalanceType
balancetype_BalanceType -> BalanceType -> Bool
forall a. Eq a => a -> a -> Bool
==BalanceType
PeriodChange then [MixedAmount] -> MixedAmount
forall (t :: * -> *) a. (Foldable t, Num a) => t a -> a
sum [MixedAmount]
valuedrowbals else 0
           , let rowavg :: MixedAmount
rowavg = [MixedAmount] -> MixedAmount
averageMixedAmounts [MixedAmount]
valuedrowbals
           , Bool
empty_ Bool -> Bool -> Bool
|| Int
depth Int -> Int -> Bool
forall a. Eq a => a -> a -> Bool
== 0 Bool -> Bool -> Bool
|| (MixedAmount -> Bool) -> [MixedAmount] -> Bool
forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Bool
any (Bool -> Bool
not (Bool -> Bool) -> (MixedAmount -> Bool) -> MixedAmount -> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. MixedAmount -> Bool
isZeroMixedAmount) [MixedAmount]
valuedrowbals
           ]
        where
          avalue :: Day -> MixedAmount -> MixedAmount
avalue periodlast :: Day
periodlast =
            (MixedAmount -> MixedAmount)
-> (ValuationType -> MixedAmount -> MixedAmount)
-> Maybe ValuationType
-> MixedAmount
-> MixedAmount
forall b a. b -> (a -> b) -> Maybe a -> b
maybe MixedAmount -> MixedAmount
forall a. a -> a
id (PriceOracle
-> Map Text AmountStyle
-> Day
-> Maybe Day
-> Day
-> Bool
-> ValuationType
-> MixedAmount
-> MixedAmount
mixedAmountApplyValuation PriceOracle
priceoracle Map Text AmountStyle
styles Day
periodlast Maybe Day
mreportlast Day
today Bool
multiperiod) Maybe ValuationType
value_
            where
              -- Some things needed if doing valuation.
              styles :: Map Text AmountStyle
styles = Journal -> Map Text AmountStyle
journalCommodityStyles Journal
j
              mreportlast :: Maybe Day
mreportlast = ReportOpts -> Maybe Day
reportPeriodLastDay ReportOpts
ropts
              today :: Day
today = Day -> Maybe Day -> Day
forall a. a -> Maybe a -> a
fromMaybe (String -> Day
forall a. String -> a
error' "multiBalanceReport: could not pick a valuation date, ReportOpts today_ is unset") Maybe Day
today_  -- XXX shouldn't happen
              multiperiod :: Bool
multiperiod = Interval
interval_ Interval -> Interval -> Bool
forall a. Eq a => a -> a -> Bool
/= Interval
NoInterval
          -- The last day of each column's subperiod.
          lastdays :: [Day]
lastdays =
            (DateSpan -> Day) -> [DateSpan] -> [Day]
forall a b. (a -> b) -> [a] -> [b]
map ((Day -> (Day -> Day) -> Maybe Day -> Day
forall b a. b -> (a -> b) -> Maybe a -> b
maybe
                  (String -> Day
forall a. String -> a
error' "multiBalanceReport: expected all spans to have an end date")  -- XXX should not happen
                  (Year -> Day -> Day
addDays (-1)))
                (Maybe Day -> Day) -> (DateSpan -> Maybe Day) -> DateSpan -> Day
forall b c a. (b -> c) -> (a -> b) -> a -> c
. DateSpan -> Maybe Day
spanEnd) [DateSpan]
colspans

      ----------------------------------------------------------------------
      -- 7. Sort the report rows.

      -- Sort the rows by amount or by account declaration order. This is a bit tricky.
      -- TODO: is it always ok to sort report rows after report has been generated, as a separate step ?
      [MultiBalanceReportRow]
sortedrows :: [MultiBalanceReportRow] =
        String -> [MultiBalanceReportRow] -> [MultiBalanceReportRow]
forall a. Show a => String -> a -> a
dbg1 "sortedrows" ([MultiBalanceReportRow] -> [MultiBalanceReportRow])
-> [MultiBalanceReportRow] -> [MultiBalanceReportRow]
forall a b. (a -> b) -> a -> b
$
        [MultiBalanceReportRow] -> [MultiBalanceReportRow]
forall b c d f.
[(Text, b, c, d, MixedAmount, f)]
-> [(Text, b, c, d, MixedAmount, f)]
sortrows [MultiBalanceReportRow]
rows
        where
          sortrows :: [(Text, b, c, d, MixedAmount, f)]
-> [(Text, b, c, d, MixedAmount, f)]
sortrows
            | Bool
sort_amount_ Bool -> Bool -> Bool
&& AccountListMode
accountlistmode_ AccountListMode -> AccountListMode -> Bool
forall a. Eq a => a -> a -> Bool
== AccountListMode
ALTree = [(Text, b, c, d, MixedAmount, f)]
-> [(Text, b, c, d, MixedAmount, f)]
forall b c d f.
[(Text, b, c, d, MixedAmount, f)]
-> [(Text, b, c, d, MixedAmount, f)]
sortTreeMBRByAmount
            | Bool
sort_amount_                               = [(Text, b, c, d, MixedAmount, f)]
-> [(Text, b, c, d, MixedAmount, f)]
forall a b c d f.
[(a, b, c, d, MixedAmount, f)] -> [(a, b, c, d, MixedAmount, f)]
sortFlatMBRByAmount
            | Bool
otherwise                                  = [(Text, b, c, d, MixedAmount, f)]
-> [(Text, b, c, d, MixedAmount, f)]
forall b c d e f.
[(Text, b, c, d, e, f)] -> [(Text, b, c, d, e, f)]
sortMBRByAccountDeclaration
            where
              -- Sort the report rows, representing a tree of accounts, by row total at each level.
              -- Similar to sortMBRByAccountDeclaration/sortAccountNamesByDeclaration.
              sortTreeMBRByAmount :: [(Text, b, c, d, MixedAmount, f)]
-> [(Text, b, c, d, MixedAmount, f)]
sortTreeMBRByAmount rows :: [(Text, b, c, d, MixedAmount, f)]
rows = [(Text, b, c, d, MixedAmount, f)]
sortedrows
                where
                  anamesandrows :: [(Text, (Text, b, c, d, MixedAmount, f))]
anamesandrows = [((Text, b, c, d, MixedAmount, f) -> Text
forall a b c d e f. (a, b, c, d, e, f) -> a
first6 (Text, b, c, d, MixedAmount, f)
r, (Text, b, c, d, MixedAmount, f)
r) | (Text, b, c, d, MixedAmount, f)
r <- [(Text, b, c, d, MixedAmount, f)]
rows]
                  anames :: [Text]
anames = ((Text, (Text, b, c, d, MixedAmount, f)) -> Text)
-> [(Text, (Text, b, c, d, MixedAmount, f))] -> [Text]
forall a b. (a -> b) -> [a] -> [b]
map (Text, (Text, b, c, d, MixedAmount, f)) -> Text
forall a b. (a, b) -> a
fst [(Text, (Text, b, c, d, MixedAmount, f))]
anamesandrows
                  atotals :: [(Text, MixedAmount)]
atotals = [(Text
a,MixedAmount
tot) | (a :: Text
a,_,_,_,tot :: MixedAmount
tot,_) <- [(Text, b, c, d, MixedAmount, f)]
rows]
                  accounttree :: Account
accounttree = Text -> [Text] -> Account
accountTree "root" [Text]
anames
                  accounttreewithbals :: Account
accounttreewithbals = (Account -> Account) -> Account -> Account
mapAccounts Account -> Account
setibalance Account
accounttree
                    where
                      -- should not happen, but it's dangerous; TODO
                      setibalance :: Account -> Account
setibalance a :: Account
a = Account
a{aibalance :: MixedAmount
aibalance=MixedAmount -> Maybe MixedAmount -> MixedAmount
forall a. a -> Maybe a -> a
fromMaybe (String -> MixedAmount
forall a. HasCallStack => String -> a
error "sortTreeMBRByAmount 1") (Maybe MixedAmount -> MixedAmount)
-> Maybe MixedAmount -> MixedAmount
forall a b. (a -> b) -> a -> b
$ Text -> [(Text, MixedAmount)] -> Maybe MixedAmount
forall a b. Eq a => a -> [(a, b)] -> Maybe b
lookup (Account -> Text
aname Account
a) [(Text, MixedAmount)]
atotals}
                  sortedaccounttree :: Account
sortedaccounttree = NormalSign -> Account -> Account
sortAccountTreeByAmount (NormalSign -> Maybe NormalSign -> NormalSign
forall a. a -> Maybe a -> a
fromMaybe NormalSign
NormallyPositive Maybe NormalSign
normalbalance_) Account
accounttreewithbals
                  sortedanames :: [Text]
sortedanames = (Account -> Text) -> [Account] -> [Text]
forall a b. (a -> b) -> [a] -> [b]
map Account -> Text
aname ([Account] -> [Text]) -> [Account] -> [Text]
forall a b. (a -> b) -> a -> b
$ Int -> [Account] -> [Account]
forall a. Int -> [a] -> [a]
drop 1 ([Account] -> [Account]) -> [Account] -> [Account]
forall a b. (a -> b) -> a -> b
$ Account -> [Account]
flattenAccounts Account
sortedaccounttree
                  sortedrows :: [(Text, b, c, d, MixedAmount, f)]
sortedrows = [Text]
-> [(Text, (Text, b, c, d, MixedAmount, f))]
-> [(Text, b, c, d, MixedAmount, f)]
forall b. [Text] -> [(Text, b)] -> [b]
sortAccountItemsLike [Text]
sortedanames [(Text, (Text, b, c, d, MixedAmount, f))]
anamesandrows

              -- Sort the report rows, representing a flat account list, by row total.
              sortFlatMBRByAmount :: [(a, b, c, d, MixedAmount, f)] -> [(a, b, c, d, MixedAmount, f)]
sortFlatMBRByAmount = ((a, b, c, d, MixedAmount, f)
 -> (a, b, c, d, MixedAmount, f) -> Ordering)
-> [(a, b, c, d, MixedAmount, f)] -> [(a, b, c, d, MixedAmount, f)]
forall a. (a -> a -> Ordering) -> [a] -> [a]
sortBy (((a, b, c, d, MixedAmount, f)
 -> (a, b, c, d, MixedAmount, f) -> Ordering)
-> (a, b, c, d, MixedAmount, f)
-> (a, b, c, d, MixedAmount, f)
-> Ordering
forall a c. (a -> a -> c) -> a -> a -> c
maybeflip (((a, b, c, d, MixedAmount, f)
  -> (a, b, c, d, MixedAmount, f) -> Ordering)
 -> (a, b, c, d, MixedAmount, f)
 -> (a, b, c, d, MixedAmount, f)
 -> Ordering)
-> ((a, b, c, d, MixedAmount, f)
    -> (a, b, c, d, MixedAmount, f) -> Ordering)
-> (a, b, c, d, MixedAmount, f)
-> (a, b, c, d, MixedAmount, f)
-> Ordering
forall a b. (a -> b) -> a -> b
$ ((a, b, c, d, MixedAmount, f) -> MixedAmount)
-> (a, b, c, d, MixedAmount, f)
-> (a, b, c, d, MixedAmount, f)
-> Ordering
forall a b. Ord a => (b -> a) -> b -> b -> Ordering
comparing (MixedAmount -> MixedAmount
normaliseMixedAmountSquashPricesForDisplay (MixedAmount -> MixedAmount)
-> ((a, b, c, d, MixedAmount, f) -> MixedAmount)
-> (a, b, c, d, MixedAmount, f)
-> MixedAmount
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (a, b, c, d, MixedAmount, f) -> MixedAmount
forall a b c d e f. (a, b, c, d, e, f) -> e
fifth6))
                where
                  maybeflip :: (a -> a -> c) -> a -> a -> c
maybeflip = if Maybe NormalSign
normalbalance_ Maybe NormalSign -> Maybe NormalSign -> Bool
forall a. Eq a => a -> a -> Bool
== NormalSign -> Maybe NormalSign
forall a. a -> Maybe a
Just NormalSign
NormallyNegative then (a -> a -> c) -> a -> a -> c
forall a. a -> a
id else (a -> a -> c) -> a -> a -> c
forall a b c. (a -> b -> c) -> b -> a -> c
flip

              -- Sort the report rows by account declaration order then account name.
              sortMBRByAccountDeclaration :: [(Text, b, c, d, e, f)] -> [(Text, b, c, d, e, f)]
sortMBRByAccountDeclaration rows :: [(Text, b, c, d, e, f)]
rows = [(Text, b, c, d, e, f)]
sortedrows
                where
                  anamesandrows :: [(Text, (Text, b, c, d, e, f))]
anamesandrows = [((Text, b, c, d, e, f) -> Text
forall a b c d e f. (a, b, c, d, e, f) -> a
first6 (Text, b, c, d, e, f)
r, (Text, b, c, d, e, f)
r) | (Text, b, c, d, e, f)
r <- [(Text, b, c, d, e, f)]
rows]
                  anames :: [Text]
anames = ((Text, (Text, b, c, d, e, f)) -> Text)
-> [(Text, (Text, b, c, d, e, f))] -> [Text]
forall a b. (a -> b) -> [a] -> [b]
map (Text, (Text, b, c, d, e, f)) -> Text
forall a b. (a, b) -> a
fst [(Text, (Text, b, c, d, e, f))]
anamesandrows
                  sortedanames :: [Text]
sortedanames = Journal -> Bool -> [Text] -> [Text]
sortAccountNamesByDeclaration Journal
j (ReportOpts -> Bool
tree_ ReportOpts
ropts) [Text]
anames
                  sortedrows :: [(Text, b, c, d, e, f)]
sortedrows = [Text]
-> [(Text, (Text, b, c, d, e, f))] -> [(Text, b, c, d, e, f)]
forall b. [Text] -> [(Text, b)] -> [b]
sortAccountItemsLike [Text]
sortedanames [(Text, (Text, b, c, d, e, f))]
anamesandrows

      ----------------------------------------------------------------------
      -- 8. Build the report totals row.

      -- Calculate the column totals. These are always the sum of column amounts.
      highestlevelaccts :: [Text]
highestlevelaccts = [Text
a | Text
a <- [Text]
displayaccts, Bool -> Bool
not (Bool -> Bool) -> Bool -> Bool
forall a b. (a -> b) -> a -> b
$ (Text -> Bool) -> [Text] -> Bool
forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Bool
any (Text -> [Text] -> Bool
forall (t :: * -> *) a. (Foldable t, Eq a) => a -> t a -> Bool
`elem` [Text]
displayaccts) ([Text] -> Bool) -> [Text] -> Bool
forall a b. (a -> b) -> a -> b
$ [Text] -> [Text]
forall a. [a] -> [a]
init ([Text] -> [Text]) -> [Text] -> [Text]
forall a b. (a -> b) -> a -> b
$ Text -> [Text]
expandAccountName Text
a]
      colamts :: [[MixedAmount]]
colamts           = [[MixedAmount]] -> [[MixedAmount]]
forall a. [[a]] -> [[a]]
transpose [[MixedAmount]
bs | (a :: Text
a,_,_,bs :: [MixedAmount]
bs,_,_) <- [MultiBalanceReportRow]
rows, Bool -> Bool
not (ReportOpts -> Bool
tree_ ReportOpts
ropts) Bool -> Bool -> Bool
|| Text
a Text -> [Text] -> Bool
forall (t :: * -> *) a. (Foldable t, Eq a) => a -> t a -> Bool
`elem` [Text]
highestlevelaccts]
      [MixedAmount]
coltotals :: [MixedAmount] =
        String -> [MixedAmount] -> [MixedAmount]
forall a. Show a => String -> a -> a
dbg1 "coltotals" ([MixedAmount] -> [MixedAmount]) -> [MixedAmount] -> [MixedAmount]
forall a b. (a -> b) -> a -> b
$ ([MixedAmount] -> MixedAmount) -> [[MixedAmount]] -> [MixedAmount]
forall a b. (a -> b) -> [a] -> [b]
map [MixedAmount] -> MixedAmount
forall (t :: * -> *) a. (Foldable t, Num a) => t a -> a
sum [[MixedAmount]]
colamts
      -- Calculate the grand total and average. These are always the sum/average
      -- of the column totals.
      [grandtotal :: MixedAmount
grandtotal,grandaverage :: MixedAmount
grandaverage] =
        let amts :: [MixedAmount]
amts = (([MixedAmount] -> MixedAmount) -> MixedAmount)
-> [[MixedAmount] -> MixedAmount] -> [MixedAmount]
forall a b. (a -> b) -> [a] -> [b]
map (([MixedAmount] -> MixedAmount) -> [MixedAmount] -> MixedAmount
forall a b. (a -> b) -> a -> b
$ ([MixedAmount] -> MixedAmount) -> [[MixedAmount]] -> [MixedAmount]
forall a b. (a -> b) -> [a] -> [b]
map [MixedAmount] -> MixedAmount
forall (t :: * -> *) a. (Foldable t, Num a) => t a -> a
sum [[MixedAmount]]
colamts)
              [if BalanceType
balancetype_BalanceType -> BalanceType -> Bool
forall a. Eq a => a -> a -> Bool
==BalanceType
PeriodChange then [MixedAmount] -> MixedAmount
forall (t :: * -> *) a. (Foldable t, Num a) => t a -> a
sum else MixedAmount -> [MixedAmount] -> MixedAmount
forall a b. a -> b -> a
const 0
              ,[MixedAmount] -> MixedAmount
averageMixedAmounts
              ]
        in [MixedAmount]
amts
      -- Totals row.
      MultiBalanceReportTotals
totalsrow :: MultiBalanceReportTotals =
        String -> MultiBalanceReportTotals -> MultiBalanceReportTotals
forall a. Show a => String -> a -> a
dbg1 "totalsrow" ([MixedAmount]
coltotals, MixedAmount
grandtotal, MixedAmount
grandaverage)

      ----------------------------------------------------------------------
      -- 9. Map the report rows to percentages if needed
      -- It is not correct to do this before step 6 due to the total and average columns.
      -- This is not done in step 6, since the report totals are calculated in 8.
      
      -- Perform the divisions to obtain percentages
      [MultiBalanceReportRow]
mappedsortedrows :: [MultiBalanceReportRow] =
        if Bool -> Bool
not Bool
percent_ then [MultiBalanceReportRow]
sortedrows
        else String -> [MultiBalanceReportRow] -> [MultiBalanceReportRow]
forall a. Show a => String -> a -> a
dbg1 "mappedsortedrows"
          [(Text
aname, Text
alname, Int
alevel, (MixedAmount -> MixedAmount -> MixedAmount)
-> [MixedAmount] -> [MixedAmount] -> [MixedAmount]
forall a b c. (a -> b -> c) -> [a] -> [b] -> [c]
zipWith MixedAmount -> MixedAmount -> MixedAmount
perdivide [MixedAmount]
rowvals [MixedAmount]
coltotals, MixedAmount
rowtotal MixedAmount -> MixedAmount -> MixedAmount
`perdivide` MixedAmount
grandtotal, MixedAmount
rowavg MixedAmount -> MixedAmount -> MixedAmount
`perdivide` MixedAmount
grandaverage)
           | (aname :: Text
aname, alname :: Text
alname, alevel :: Int
alevel, rowvals :: [MixedAmount]
rowvals, rowtotal :: MixedAmount
rowtotal, rowavg :: MixedAmount
rowavg) <- [MultiBalanceReportRow]
sortedrows
          ]
      MultiBalanceReportTotals
mappedtotalsrow :: MultiBalanceReportTotals =
        if Bool -> Bool
not Bool
percent_ then MultiBalanceReportTotals
totalsrow
        else String -> MultiBalanceReportTotals -> MultiBalanceReportTotals
forall a. Show a => String -> a -> a
dbg1 "mappedtotalsrow" (
          (MixedAmount -> MixedAmount) -> [MixedAmount] -> [MixedAmount]
forall a b. (a -> b) -> [a] -> [b]
map (\t :: MixedAmount
t -> MixedAmount -> MixedAmount -> MixedAmount
perdivide MixedAmount
t MixedAmount
t) [MixedAmount]
coltotals,
          MixedAmount -> MixedAmount -> MixedAmount
perdivide MixedAmount
grandtotal MixedAmount
grandtotal,
          MixedAmount -> MixedAmount -> MixedAmount
perdivide MixedAmount
grandaverage MixedAmount
grandaverage)

-- | Given a MultiBalanceReport and its normal balance sign,
-- if it is known to be normally negative, convert it to normally positive.
mbrNormaliseSign :: NormalSign -> MultiBalanceReport -> MultiBalanceReport
mbrNormaliseSign :: NormalSign -> MultiBalanceReport -> MultiBalanceReport
mbrNormaliseSign NormallyNegative = MultiBalanceReport -> MultiBalanceReport
mbrNegate
mbrNormaliseSign _ = MultiBalanceReport -> MultiBalanceReport
forall a. a -> a
id

-- | Flip the sign of all amounts in a MultiBalanceReport.
mbrNegate :: MultiBalanceReport -> MultiBalanceReport
mbrNegate (MultiBalanceReport (colspans :: [DateSpan]
colspans, rows :: [MultiBalanceReportRow]
rows, totalsrow :: MultiBalanceReportTotals
totalsrow)) =
  ([DateSpan], [MultiBalanceReportRow], MultiBalanceReportTotals)
-> MultiBalanceReport
MultiBalanceReport ([DateSpan]
colspans, (MultiBalanceReportRow -> MultiBalanceReportRow)
-> [MultiBalanceReportRow] -> [MultiBalanceReportRow]
forall a b. (a -> b) -> [a] -> [b]
map MultiBalanceReportRow -> MultiBalanceReportRow
forall b e f a b c.
(Num b, Num e, Num f) =>
(a, b, c, [b], e, f) -> (a, b, c, [b], e, f)
mbrRowNegate [MultiBalanceReportRow]
rows, MultiBalanceReportTotals -> MultiBalanceReportTotals
forall b b c. (Num b, Num b, Num c) => ([b], b, c) -> ([b], b, c)
mbrTotalsRowNegate MultiBalanceReportTotals
totalsrow)
  where
    mbrRowNegate :: (a, b, c, [b], e, f) -> (a, b, c, [b], e, f)
mbrRowNegate (acct :: a
acct,shortacct :: b
shortacct,indent :: c
indent,amts :: [b]
amts,tot :: e
tot,avg :: f
avg) = (a
acct,b
shortacct,c
indent,(b -> b) -> [b] -> [b]
forall a b. (a -> b) -> [a] -> [b]
map b -> b
forall a. Num a => a -> a
negate [b]
amts,-e
tot,-f
avg)
    mbrTotalsRowNegate :: ([b], b, c) -> ([b], b, c)
mbrTotalsRowNegate (amts :: [b]
amts,tot :: b
tot,avg :: c
avg) = ((b -> b) -> [b] -> [b]
forall a b. (a -> b) -> [a] -> [b]
map b -> b
forall a. Num a => a -> a
negate [b]
amts,-b
tot,-c
avg)

-- | Figure out the overall date span of a multicolumn balance report.
multiBalanceReportSpan :: MultiBalanceReport -> DateSpan
multiBalanceReportSpan :: MultiBalanceReport -> DateSpan
multiBalanceReportSpan (MultiBalanceReport ([], _, _))       = Maybe Day -> Maybe Day -> DateSpan
DateSpan Maybe Day
forall a. Maybe a
Nothing Maybe Day
forall a. Maybe a
Nothing
multiBalanceReportSpan (MultiBalanceReport (colspans :: [DateSpan]
colspans, _, _)) = Maybe Day -> Maybe Day -> DateSpan
DateSpan (DateSpan -> Maybe Day
spanStart (DateSpan -> Maybe Day) -> DateSpan -> Maybe Day
forall a b. (a -> b) -> a -> b
$ [DateSpan] -> DateSpan
forall a. [a] -> a
head [DateSpan]
colspans) (DateSpan -> Maybe Day
spanEnd (DateSpan -> Maybe Day) -> DateSpan -> Maybe Day
forall a b. (a -> b) -> a -> b
$ [DateSpan] -> DateSpan
forall a. [a] -> a
last [DateSpan]
colspans)

-- | Generates a simple non-columnar BalanceReport, but using multiBalanceReport,
-- in order to support --historical. Does not support tree-mode boring parent eliding.
-- If the normalbalance_ option is set, it adjusts the sorting and sign of amounts
-- (see ReportOpts and CompoundBalanceCommand).
balanceReportFromMultiBalanceReport :: ReportOpts -> Query -> Journal -> BalanceReport
balanceReportFromMultiBalanceReport :: ReportOpts
-> Query
-> Journal
-> ([(Text, Text, Int, MixedAmount)], MixedAmount)
balanceReportFromMultiBalanceReport opts :: ReportOpts
opts q :: Query
q j :: Journal
j = ([(Text, Text, Int, MixedAmount)]
rows', MixedAmount
total)
  where
    MultiBalanceReport (_, rows :: [MultiBalanceReportRow]
rows, (totals :: [MixedAmount]
totals, _, _)) = ReportOpts -> Query -> Journal -> MultiBalanceReport
multiBalanceReport ReportOpts
opts Query
q Journal
j
    rows' :: [(Text, Text, Int, MixedAmount)]
rows' = [(Text
a
             ,if ReportOpts -> Bool
flat_ ReportOpts
opts then Text
a else Text
a'   -- BalanceReport expects full account name here with --flat
             ,if ReportOpts -> Bool
tree_ ReportOpts
opts then Int
dInt -> Int -> Int
forall a. Num a => a -> a -> a
-1 else 0  -- BalanceReport uses 0-based account depths
             , MixedAmount -> [MixedAmount] -> MixedAmount
forall a. a -> [a] -> a
headDef MixedAmount
nullmixedamt [MixedAmount]
amts     -- 0 columns is illegal, should not happen, return zeroes if it does
             ) | (a :: Text
a,a' :: Text
a',d :: Int
d, amts :: [MixedAmount]
amts, _, _) <- [MultiBalanceReportRow]
rows]
    total :: MixedAmount
total = MixedAmount -> [MixedAmount] -> MixedAmount
forall a. a -> [a] -> a
headDef MixedAmount
nullmixedamt [MixedAmount]
totals


-- common rendering helper, XXX here for now

tableAsText :: ReportOpts -> (a -> String) -> Table String String a -> String
tableAsText :: ReportOpts -> (a -> String) -> Table String String a -> String
tableAsText (ReportOpts{pretty_tables_ :: ReportOpts -> Bool
pretty_tables_ = Bool
pretty}) showcell :: a -> String
showcell =
  [String] -> String
unlines
  ([String] -> String)
-> (Table String String a -> [String])
-> Table String String a
-> String
forall b c a. (b -> c) -> (a -> b) -> a -> c
. [String] -> [String]
forall a. [[a]] -> [[a]]
trimborder
  ([String] -> [String])
-> (Table String String a -> [String])
-> Table String String a
-> [String]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. String -> [String]
lines
  (String -> [String])
-> (Table String String a -> String)
-> Table String String a
-> [String]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Bool
-> ShowS
-> ShowS
-> (a -> String)
-> Table String String a
-> String
forall rh ch a.
Bool
-> (rh -> String)
-> (ch -> String)
-> (a -> String)
-> Table rh ch a
-> String
render Bool
pretty ShowS
forall a. a -> a
id ShowS
forall a. a -> a
id a -> String
showcell
  (Table String String a -> String)
-> (Table String String a -> Table String String a)
-> Table String String a
-> String
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Table String String a -> Table String String a
forall ch a. Table String ch a -> Table String ch a
align
  where
    trimborder :: [[a]] -> [[a]]
trimborder = Int -> [[a]] -> [[a]]
forall a. Int -> [a] -> [a]
drop 1 ([[a]] -> [[a]]) -> ([[a]] -> [[a]]) -> [[a]] -> [[a]]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. [[a]] -> [[a]]
forall a. [a] -> [a]
init ([[a]] -> [[a]]) -> ([[a]] -> [[a]]) -> [[a]] -> [[a]]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. ([a] -> [a]) -> [[a]] -> [[a]]
forall a b. (a -> b) -> [a] -> [b]
map (Int -> [a] -> [a]
forall a. Int -> [a] -> [a]
drop 1 ([a] -> [a]) -> ([a] -> [a]) -> [a] -> [a]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. [a] -> [a]
forall a. [a] -> [a]
init)
    align :: Table String ch a -> Table String ch a
align (Table l :: Header String
l t :: Header ch
t d :: [[a]]
d) = Header String -> Header ch -> [[a]] -> Table String ch a
forall rh ch a. Header rh -> Header ch -> [[a]] -> Table rh ch a
Table Header String
l' Header ch
t [[a]]
d
      where
        acctswidth :: Int
acctswidth = [Int] -> Int
forall a. Integral a => [a] -> a
maximum' ([Int] -> Int) -> [Int] -> Int
forall a b. (a -> b) -> a -> b
$ (String -> Int) -> [String] -> [Int]
forall a b. (a -> b) -> [a] -> [b]
map String -> Int
strWidth (Header String -> [String]
forall h. Header h -> [h]
headerContents Header String
l)
        l' :: Header String
l'         = Int -> ShowS
padRightWide Int
acctswidth ShowS -> Header String -> Header String
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Header String
l

-- tests

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

  let
    amt0 :: Amount
amt0 = Amount :: Text
-> Quantity -> Bool -> AmountStyle -> Maybe AmountPrice -> Amount
Amount {acommodity :: Text
acommodity="$", aquantity :: Quantity
aquantity=0, aprice :: Maybe AmountPrice
aprice=Maybe AmountPrice
forall a. Maybe a
Nothing, astyle :: AmountStyle
astyle=$WAmountStyle :: Side
-> Bool
-> Int
-> Maybe Char
-> Maybe DigitGroupStyle
-> AmountStyle
AmountStyle {ascommodityside :: Side
ascommodityside = Side
L, ascommodityspaced :: Bool
ascommodityspaced = Bool
False, asprecision :: Int
asprecision = 2, asdecimalpoint :: Maybe Char
asdecimalpoint = Char -> Maybe Char
forall a. a -> Maybe a
Just '.', asdigitgroups :: Maybe DigitGroupStyle
asdigitgroups = Maybe DigitGroupStyle
forall a. Maybe a
Nothing}, aismultiplier :: Bool
aismultiplier=Bool
False}
    (opts :: ReportOpts
opts,journal :: Journal
journal) gives :: (ReportOpts, Journal)
-> ([MultiBalanceReportRow], MixedAmount) -> IO ()
`gives` r :: ([MultiBalanceReportRow], MixedAmount)
r = do
      let (eitems :: [MultiBalanceReportRow]
eitems, etotal :: MixedAmount
etotal) = ([MultiBalanceReportRow], MixedAmount)
r
          (MultiBalanceReport (_, aitems :: [MultiBalanceReportRow]
aitems, atotal :: MultiBalanceReportTotals
atotal)) = ReportOpts -> Query -> Journal -> MultiBalanceReport
multiBalanceReport ReportOpts
opts (Day -> ReportOpts -> Query
queryFromOpts Day
nulldate ReportOpts
opts) Journal
journal
          showw :: (a, b, c, [MixedAmount], MixedAmount, MixedAmount)
-> (a, b, c, [String], String, String)
showw (acct :: a
acct,acct' :: b
acct',indent :: c
indent,lAmt :: [MixedAmount]
lAmt,amt :: MixedAmount
amt,amt' :: MixedAmount
amt') = (a
acct, b
acct', c
indent, (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')
      ((MultiBalanceReportRow
 -> (Text, Text, Int, [String], String, String))
-> [MultiBalanceReportRow]
-> [(Text, Text, Int, [String], String, String)]
forall a b. (a -> b) -> [a] -> [b]
map MultiBalanceReportRow
-> (Text, Text, Int, [String], String, String)
forall a b c.
(a, b, c, [MixedAmount], MixedAmount, MixedAmount)
-> (a, b, c, [String], String, String)
showw [MultiBalanceReportRow]
aitems) [(Text, Text, Int, [String], String, String)]
-> [(Text, Text, Int, [String], String, String)] -> IO ()
forall a. (Eq a, Show a, HasCallStack) => a -> a -> IO ()
@?= ((MultiBalanceReportRow
 -> (Text, Text, Int, [String], String, String))
-> [MultiBalanceReportRow]
-> [(Text, Text, Int, [String], String, String)]
forall a b. (a -> b) -> [a] -> [b]
map MultiBalanceReportRow
-> (Text, Text, Int, [String], String, String)
forall a b c.
(a, b, c, [MixedAmount], MixedAmount, MixedAmount)
-> (a, b, c, [String], String, String)
showw [MultiBalanceReportRow]
eitems)
      ((\(_, b :: MixedAmount
b, _) -> MixedAmount -> String
showMixedAmountDebug MixedAmount
b) MultiBalanceReportTotals
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
tests "multiBalanceReport" [
      String -> IO () -> TestTree
test "null journal"  (IO () -> TestTree) -> IO () -> TestTree
forall a b. (a -> b) -> a -> b
$
      (ReportOpts
defreportopts, Journal
nulljournal) (ReportOpts, Journal)
-> ([MultiBalanceReportRow], MixedAmount) -> IO ()
`gives` ([], [Amount] -> MixedAmount
Mixed [Amount
nullamt])

     ,String -> IO () -> TestTree
test "with -H on a populated period"  (IO () -> TestTree) -> IO () -> TestTree
forall a b. (a -> b) -> a -> b
$
      (ReportOpts
defreportopts{period_ :: Period
period_= Day -> Day -> Period
PeriodBetween (Year -> Int -> Int -> Day
fromGregorian 2008 1 1) (Year -> Int -> Int -> Day
fromGregorian 2008 1 2), balancetype_ :: BalanceType
balancetype_=BalanceType
HistoricalBalance}, Journal
samplejournal) (ReportOpts, Journal)
-> ([MultiBalanceReportRow], MixedAmount) -> IO ()
`gives`
       (
        [
         ("assets:bank:checking", "checking", 3, [String -> MixedAmount
mamountp' "$1.00"] , [Amount] -> MixedAmount
Mixed [Amount
nullamt], [Amount] -> MixedAmount
Mixed [Amount
amt0 {aquantity :: Quantity
aquantity=1}])
        ,("income:salary"       ,"salary"   , 2, [String -> MixedAmount
mamountp' "$-1.00"], [Amount] -> MixedAmount
Mixed [Amount
nullamt], [Amount] -> MixedAmount
Mixed [Amount
amt0 {aquantity :: Quantity
aquantity=(-1)}])
        ],
        [Amount] -> MixedAmount
Mixed [Amount
nullamt])

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

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