From f21bf53610981926f45abccc78d647246ab2682b Mon Sep 17 00:00:00 2001 From: Stephen Morgan Date: Fri, 12 Jun 2020 10:39:11 +1000 Subject: [PATCH 01/24] lib: multiBalanceReport: Break startingBalances into separate function. --- .../Hledger/Reports/MultiBalanceReport.hs | 65 ++++++++++++------- 1 file changed, 40 insertions(+), 25 deletions(-) diff --git a/hledger-lib/Hledger/Reports/MultiBalanceReport.hs b/hledger-lib/Hledger/Reports/MultiBalanceReport.hs index a2883a319..bfcd1077d 100644 --- a/hledger-lib/Hledger/Reports/MultiBalanceReport.hs +++ b/hledger-lib/Hledger/Reports/MultiBalanceReport.hs @@ -1,4 +1,7 @@ -{-# LANGUAGE FlexibleInstances, RecordWildCards, ScopedTypeVariables, OverloadedStrings, DeriveGeneric #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE ScopedTypeVariables #-} {-| Multi-column balance reports, used by the balance command. @@ -89,12 +92,6 @@ multiBalanceReportWith ropts@ReportOpts{..} q j priceoracle = (if invert_ then prNegate else id) $ PeriodicReport colspans mappedsortedrows mappedtotalsrow where - -- add a prefix to this function's debug output - dbg s = let p = "multiBalanceReport" in Hledger.Utils.dbg3 (p++" "++s) - dbg' s = let p = "multiBalanceReport" in Hledger.Utils.dbg4 (p++" "++s) - dbg'' s = let p = "multiBalanceReport" in Hledger.Utils.dbg5 (p++" "++s) - -- dbg = const id -- exclude this function from debug output - ---------------------------------------------------------------------- -- 1. Queries, report/column dates. @@ -118,7 +115,6 @@ multiBalanceReportWith ropts@ReportOpts{..} q j priceoracle = -- This can be the null span if there were no intervals. reportspan = dbg "reportspan" $ DateSpan (maybe Nothing spanStart $ headMay intervalspans) (maybe Nothing spanEnd $ lastMay intervalspans) - mreportstart = spanStart 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). @@ -144,23 +140,8 @@ multiBalanceReportWith ropts@ReportOpts{..} q j priceoracle = -- Balances at report start date, from all earlier postings which otherwise match the query. -- These balances are unvalued except maybe converted to cost. - startbals :: [(AccountName, MixedAmount)] = dbg' "startbals" $ map (\(a,_,_,b) -> (a,b)) startbalanceitems - where - (startbalanceitems,_) = dbg'' "starting balance report" $ balanceReport ropts''{value_=Nothing, percent_=False} startbalq j' - where - ropts' | tree_ ropts = ropts{no_elide_=True} - | otherwise = ropts{accountlistmode_=ALFlat} - ropts'' = ropts'{period_ = precedingperiod} - where - precedingperiod = dateSpanAsPeriod $ spanIntersect (DateSpan Nothing mreportstart) $ periodAsDateSpan 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 = dbg'' "startbalq" $ And [datelessq, dateqcons precedingspan] - where - precedingspan = case mreportstart of - Just d -> DateSpan Nothing (Just d) - Nothing -> emptydatespan + startbals :: [(AccountName, MixedAmount)] = dbg' "startbals" $ + startingBalances ropts q j reportspan -- 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 = dbg'' "startaccts" $ map fst startbals @@ -360,6 +341,34 @@ multiBalanceReportWith ropts@ReportOpts{..} q j priceoracle = (perdivide grandaverage grandaverage) | otherwise = totalsrow + +-- | 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. +startingBalances :: ReportOpts -> Query -> Journal -> DateSpan -> [(AccountName, MixedAmount)] +startingBalances ropts q j reportspan = map (\(a,_,_,b) -> (a,b)) startbalanceitems + where + (startbalanceitems,_) = dbg'' "starting balance report" $ + balanceReport ropts''{value_=Nothing, percent_=False} startbalq j + + -- 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 = dbg'' "startbalq" $ And [datelessq, precedingspanq] + datelessq = dbg "datelessq" $ filterQuery (not . queryIsDateOrDate2) q + + ropts' | tree_ ropts = ropts{no_elide_=True} + | otherwise = ropts{accountlistmode_=ALFlat} + ropts'' = ropts'{period_ = precedingperiod} + + precedingperiod = dateSpanAsPeriod . spanIntersect precedingspan . + periodAsDateSpan $ period_ ropts + precedingspan = DateSpan Nothing $ spanStart reportspan + precedingspanq = (if date2_ ropts then Date2 else Date) $ case precedingspan of + DateSpan Nothing Nothing -> emptydatespan + a -> a + -- | 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 @@ -376,6 +385,12 @@ balanceReportFromMultiBalanceReport opts q j = (rows', total) ) | PeriodicReportRow a d amts _ _ <- rows] total = headDef nullmixedamt totals +-- Local debug helper +-- add a prefix to this function's debug output +dbg s = let p = "multiBalanceReport" in Hledger.Utils.dbg3 (p++" "++s) +dbg' s = let p = "multiBalanceReport" in Hledger.Utils.dbg4 (p++" "++s) +dbg'' s = let p = "multiBalanceReport" in Hledger.Utils.dbg5 (p++" "++s) +-- dbg = const id -- exclude this function from debug output -- common rendering helper, XXX here for now From 44dcd613e89cb5d1ee243813d4b01743ae0e1023 Mon Sep 17 00:00:00 2001 From: Stephen Morgan Date: Fri, 12 Jun 2020 11:07:19 +1000 Subject: [PATCH 02/24] lib: multiBalanceReport: Break getPostings and makeReportQuery into separate functions. --- .../Hledger/Reports/MultiBalanceReport.hs | 65 +++++++++++-------- 1 file changed, 37 insertions(+), 28 deletions(-) diff --git a/hledger-lib/Hledger/Reports/MultiBalanceReport.hs b/hledger-lib/Hledger/Reports/MultiBalanceReport.hs index bfcd1077d..b915b3d32 100644 --- a/hledger-lib/Hledger/Reports/MultiBalanceReport.hs +++ b/hledger-lib/Hledger/Reports/MultiBalanceReport.hs @@ -95,12 +95,8 @@ multiBalanceReportWith ropts@ReportOpts{..} q j priceoracle = ---------------------------------------------------------------------- -- 1. Queries, report/column dates. - symq = dbg "symq" $ filterQuery queryIsSym $ dbg "requested q" q depthq = dbg "depthq" $ filterQuery queryIsDepth q depth = queryDepth depthq - depthless = dbg "depthless" . filterQuery (not . queryIsDepth) - datelessq = dbg "datelessq" $ filterQuery (not . queryIsDateOrDate2) q - dateqcons = if date2_ then Date2 else Date -- The date span specified by -b/-e/-p options and query args if any. requestedspan = dbg "requestedspan" $ queryDateSpan date2_ q -- If the requested span is open-ended, close it using the journal's end dates. @@ -118,12 +114,8 @@ multiBalanceReportWith ropts@ReportOpts{..} q j priceoracle = -- 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 = dbg "reportq" $ depthless $ - if reportspan == nulldatespan - then q - else And [datelessq, reportspandatesq] - where - reportspandatesq = dbg "reportspandatesq" $ dateqcons reportspan + reportq = dbg "reportq" $ makeReportQuery ropts reportspan q + -- The date spans to be included as report columns. colspans :: [DateSpan] = dbg "colspans" $ splitSpan interval_ displayspan where @@ -135,13 +127,9 @@ multiBalanceReportWith ropts@ReportOpts{..} q j priceoracle = -- If doing cost valuation, convert amounts to cost. j' = journalSelectingAmountFromOpts ropts 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. - startbals :: [(AccountName, MixedAmount)] = dbg' "startbals" $ - startingBalances ropts q j reportspan + -- The matched accounts with a starting balance. All of these shold appear + -- in the report, even if they have no postings during the report period. + startbals = dbg' "startbals" $ startingBalances ropts q j' reportspan -- 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 = dbg'' "startaccts" $ map fst startbals @@ -152,17 +140,7 @@ multiBalanceReportWith ropts@ReportOpts{..} q j priceoracle = -- 3. Gather postings for each column. -- Postings matching the query within the report period. - ps :: [(Posting, Day)] = - dbg'' "ps" $ - map postingWithDate $ - journalPostings $ - filterJournalAmounts symq $ -- remove amount parts excluded by cur: - filterJournalPostings reportq $ -- remove postings not matched by (adjusted) query - j' - where - postingWithDate p = case whichDateFromOpts ropts of - PrimaryDate -> (p, postingDate p) - SecondaryDate -> (p, postingDate2 p) + ps :: [(Posting, Day)] = dbg'' "ps" $ getPostings ropts reportq j' -- Group postings into their columns, with the column end dates. colps :: [([Posting], Maybe Day)] = @@ -369,6 +347,37 @@ startingBalances ropts q j reportspan = map (\(a,_,_,b) -> (a,b)) startbalanceit DateSpan Nothing Nothing -> emptydatespan a -> a + +-- | Gather postings matching the query within the report period. +getPostings :: ReportOpts -> Query -> Journal -> [(Posting, Day)] +getPostings ropts q = + map (\p -> (p, date p)) . + journalPostings . + filterJournalAmounts symq . -- remove amount parts excluded by cur: + filterJournalPostings reportq -- remove postings not matched by (adjusted) query + where + symq = dbg "symq" . filterQuery queryIsSym $ dbg1 "requested q" q + -- 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 = dbg "reportq" $ depthless q + depthless = dbg "depthless" . filterQuery (not . queryIsDepth) + + date = case whichDateFromOpts ropts of + PrimaryDate -> postingDate + SecondaryDate -> postingDate2 + +-- | Remove any date queries and insert queries from the report span +makeReportQuery :: ReportOpts -> DateSpan -> Query -> Query +makeReportQuery ropts reportspan q + | reportspan == nulldatespan = depthlessq + | otherwise = And [dateless depthlessq, reportspandatesq] + where + depthlessq = dbg1 "depthless" $ filterQuery (not . queryIsDepth) q + reportspandatesq = dbg1 "reportspandatesq" $ dateqcons reportspan + dateless = dbg1 "dateless" . filterQuery (not . queryIsDateOrDate2) + dateqcons = if date2_ ropts then Date2 else Date + -- | 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 From fc6a30b2343551f7dbd0fe40ad376dc645335e76 Mon Sep 17 00:00:00 2001 From: Stephen Morgan Date: Fri, 12 Jun 2020 11:28:21 +1000 Subject: [PATCH 03/24] lib: multiBalanceReport: Break calculateColSpans into a separate function. --- .../Hledger/Reports/MultiBalanceReport.hs | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/hledger-lib/Hledger/Reports/MultiBalanceReport.hs b/hledger-lib/Hledger/Reports/MultiBalanceReport.hs index b915b3d32..888d86c62 100644 --- a/hledger-lib/Hledger/Reports/MultiBalanceReport.hs +++ b/hledger-lib/Hledger/Reports/MultiBalanceReport.hs @@ -116,14 +116,6 @@ multiBalanceReportWith ropts@ReportOpts{..} q j priceoracle = -- handles the hledger-ui+future txns case above). reportq = dbg "reportq" $ makeReportQuery ropts reportspan q - -- The date spans to be included as report columns. - colspans :: [DateSpan] = dbg "colspans" $ splitSpan interval_ displayspan - where - displayspan - | empty_ = dbg "displayspan (-E)" reportspan -- all the requested intervals - | otherwise = dbg "displayspan" $ requestedspan `spanIntersect` matchedspan -- exclude leading/trailing empty intervals - matchedspan = dbg "matchedspan" . daysSpan $ map snd ps - -- If doing cost valuation, convert amounts to cost. j' = journalSelectingAmountFromOpts ropts j @@ -141,6 +133,10 @@ multiBalanceReportWith ropts@ReportOpts{..} q j priceoracle = -- Postings matching the query within the report period. ps :: [(Posting, Day)] = dbg'' "ps" $ getPostings ropts reportq j' + days = map snd ps + + -- The date spans to be included as report columns. + colspans = dbg "colspans" $ calculateColSpans ropts reportspan days -- Group postings into their columns, with the column end dates. colps :: [([Posting], Maybe Day)] = @@ -367,7 +363,7 @@ getPostings ropts q = PrimaryDate -> postingDate SecondaryDate -> postingDate2 --- | Remove any date queries and insert queries from the report span +-- | Remove any date queries and insert queries from the report span. makeReportQuery :: ReportOpts -> DateSpan -> Query -> Query makeReportQuery ropts reportspan q | reportspan == nulldatespan = depthlessq @@ -378,6 +374,17 @@ makeReportQuery ropts reportspan q dateless = dbg1 "dateless" . filterQuery (not . queryIsDateOrDate2) dateqcons = if date2_ ropts then Date2 else Date +-- | Calculate the DateSpans to be used for the columns of the report. +calculateColSpans :: ReportOpts -> DateSpan -> [Day] -> [DateSpan] +calculateColSpans ropts reportspan days = + splitSpan (interval_ ropts) displayspan + where + displayspan + | empty_ ropts = dbg "displayspan (-E)" reportspan -- all the requested intervals + | otherwise = dbg "displayspan" $ reportspan `spanIntersect` matchedspan -- exclude leading/trailing empty intervals + matchedspan = dbg "matchedspan" $ daysSpan days + + -- | 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 From a72c4f285b9c0f368b7be7dcf37c00812d80adb3 Mon Sep 17 00:00:00 2001 From: Stephen Morgan Date: Fri, 12 Jun 2020 11:33:51 +1000 Subject: [PATCH 04/24] lib: multiBalanceReport: Break calculateColumns into a separate function. --- .../Hledger/Reports/MultiBalanceReport.hs | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/hledger-lib/Hledger/Reports/MultiBalanceReport.hs b/hledger-lib/Hledger/Reports/MultiBalanceReport.hs index 888d86c62..7e6576739 100644 --- a/hledger-lib/Hledger/Reports/MultiBalanceReport.hs +++ b/hledger-lib/Hledger/Reports/MultiBalanceReport.hs @@ -24,6 +24,7 @@ where import Data.List import Data.List.Extra (nubSort) +import Data.Map (Map) import qualified Data.Map as M import Data.Maybe import Data.Ord @@ -138,14 +139,8 @@ multiBalanceReportWith ropts@ReportOpts{..} q j priceoracle = -- The date spans to be included as report columns. colspans = dbg "colspans" $ calculateColSpans ropts reportspan days - -- Group postings into their columns, with the column end dates. - colps :: [([Posting], Maybe Day)] = - dbg'' "colps" - [ (posts, end) | (DateSpan _ end, posts) <- M.toList colMap ] - where - colMap = foldr addPosting emptyMap ps - addPosting (p, d) = maybe id (M.adjust (p:)) $ latestSpanContaining colspans d - emptyMap = M.fromList . zip colspans $ repeat [] + -- Group postings into their columns. + colps = dbg'' "colps" $ calculateColumns colspans ps ---------------------------------------------------------------------- -- 4. Calculate account balance changes in each column. @@ -161,7 +156,7 @@ multiBalanceReportWith ropts@ReportOpts{..} q j priceoracle = | tree_ ropts = filter ((depthq `matchesAccount`).aname) -- exclude deeper balances | otherwise = clipAccountsAndAggregate depth -- aggregate deeper balances at the depth limit colacctchanges :: [[(ClippedAccountName, MixedAmount)]] = - dbg'' "colacctchanges" $ map (acctChangesFromPostings . fst) colps + dbg'' "colacctchanges" $ map (acctChangesFromPostings . snd) $ M.toList colps ---------------------------------------------------------------------- -- 5. Gather the account balance changes into a regular matrix including the accounts @@ -384,6 +379,13 @@ calculateColSpans ropts reportspan days = | otherwise = dbg "displayspan" $ reportspan `spanIntersect` matchedspan -- exclude leading/trailing empty intervals matchedspan = dbg "matchedspan" $ daysSpan days +-- | Group postings into their columns. +calculateColumns :: [DateSpan] -> [(Posting, Day)] -> Map DateSpan [Posting] +calculateColumns colspans = foldr addPosting emptyMap + where + addPosting (p, d) = maybe id (M.adjust (p:)) $ latestSpanContaining colspans d + emptyMap = M.fromList . zip colspans $ repeat [] + -- | Generates a simple non-columnar BalanceReport, but using multiBalanceReport, -- in order to support --historical. Does not support tree-mode boring parent eliding. From 0dcfddd20156cd173184aedafd7c0f90973d276f Mon Sep 17 00:00:00 2001 From: Stephen Morgan Date: Fri, 12 Jun 2020 12:23:57 +1000 Subject: [PATCH 05/24] lib: multiBalanceReport: Break calculateAccountChanges and acctChangesFromPostings separate functions. --- .../Hledger/Reports/MultiBalanceReport.hs | 90 ++++++++++++------- hledger-lib/hledger-lib.cabal | 7 +- hledger-lib/package.yaml | 1 + 3 files changed, 62 insertions(+), 36 deletions(-) diff --git a/hledger-lib/Hledger/Reports/MultiBalanceReport.hs b/hledger-lib/Hledger/Reports/MultiBalanceReport.hs index 7e6576739..33ac5b215 100644 --- a/hledger-lib/Hledger/Reports/MultiBalanceReport.hs +++ b/hledger-lib/Hledger/Reports/MultiBalanceReport.hs @@ -22,8 +22,11 @@ module Hledger.Reports.MultiBalanceReport ( ) where +import Data.Foldable (toList) import Data.List import Data.List.Extra (nubSort) +import Data.HashMap.Strict (HashMap) +import qualified Data.HashMap.Strict as HM import Data.Map (Map) import qualified Data.Map as M import Data.Maybe @@ -125,12 +128,9 @@ multiBalanceReportWith ropts@ReportOpts{..} q j priceoracle = startbals = dbg' "startbals" $ startingBalances ropts q j' reportspan -- 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 = dbg'' "startaccts" $ map fst startbals + startaccts = dbg'' "startaccts" $ HM.keys startbals -- Helpers to look up an account's starting balance. - startingBalanceFor a = fromMaybe nullmixedamt $ lookup a startbals - - ---------------------------------------------------------------------- - -- 3. Gather postings for each column. + startingBalanceFor a = HM.lookupDefault nullmixedamt a startbals -- Postings matching the query within the report period. ps :: [(Posting, Day)] = dbg'' "ps" $ getPostings ropts reportq j' @@ -142,21 +142,8 @@ multiBalanceReportWith ropts@ReportOpts{..} q j priceoracle = -- Group postings into their columns. colps = dbg'' "colps" $ calculateColumns colspans ps - ---------------------------------------------------------------------- - -- 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 ps = [(aname a, (if tree_ ropts then aibalance else aebalance) a) | a <- as] - where - as = depthLimit $ - (if tree_ ropts then id else filter ((>0).anumpostings)) $ - drop 1 $ accountsFromPostings ps - depthLimit - | tree_ ropts = filter ((depthq `matchesAccount`).aname) -- exclude deeper balances - | otherwise = clipAccountsAndAggregate depth -- aggregate deeper balances at the depth limit - colacctchanges :: [[(ClippedAccountName, MixedAmount)]] = - dbg'' "colacctchanges" $ map (acctChangesFromPostings . snd) $ M.toList colps + -- Each account's balance changes across all columns. + acctchanges = dbg'' "acctchanges" $ calculateAccountChanges ropts q startbals colps ---------------------------------------------------------------------- -- 5. Gather the account balance changes into a regular matrix including the accounts @@ -173,16 +160,6 @@ multiBalanceReportWith ropts@ReportOpts{..} q j priceoracle = where allpostedaccts :: [AccountName] = dbg'' "allpostedaccts" . sort . accountNamesFromPostings $ map fst ps - -- Each column's balance changes for each account, adding zeroes where needed. - colallacctchanges :: [[(ClippedAccountName, MixedAmount)]] = - dbg'' "colallacctchanges" - [ sortOn fst $ unionBy (\(a,_) (a',_) -> a == a') postedacctchanges zeroes - | postedacctchanges <- colacctchanges ] - where zeroes = [(a, nullmixedamt) | a <- displayaccts] - -- Transpose to get each account's balance changes across all columns. - acctchanges :: [(ClippedAccountName, [MixedAmount])] = - dbg'' "acctchanges" - [(a, map snd abs) | abs@((a,_):_) <- transpose colallacctchanges] -- never null, or used when null... ---------------------------------------------------------------------- -- 6. Build the report rows. @@ -191,7 +168,8 @@ multiBalanceReportWith ropts@ReportOpts{..} q j priceoracle = rows :: [MultiBalanceReportRow] = dbg'' "rows" $ [ PeriodicReportRow a (accountNameLevel a) valuedrowbals rowtot rowavg - | (a,changes) <- dbg'' "acctchanges" acctchanges + | (a,changesMap) <- HM.toList acctchanges + , let changes = toList changesMap -- The row amounts to be displayed: per-period changes, -- zero-based cumulative totals, or -- starting-balance-based historical balances. @@ -315,8 +293,8 @@ multiBalanceReportWith ropts@ReportOpts{..} q j priceoracle = -- -- Balances at report start date, from all earlier postings which otherwise match the query. -- These balances are unvalued except maybe converted to cost. -startingBalances :: ReportOpts -> Query -> Journal -> DateSpan -> [(AccountName, MixedAmount)] -startingBalances ropts q j reportspan = map (\(a,_,_,b) -> (a,b)) startbalanceitems +startingBalances :: ReportOpts -> Query -> Journal -> DateSpan -> HashMap AccountName MixedAmount +startingBalances ropts q j reportspan = HM.fromList $ map (\(a,_,_,b) -> (a,b)) startbalanceitems where (startbalanceitems,_) = dbg'' "starting balance report" $ balanceReport ropts''{value_=Nothing, percent_=False} startbalq j @@ -386,6 +364,37 @@ calculateColumns colspans = foldr addPosting emptyMap addPosting (p, d) = maybe id (M.adjust (p:)) $ latestSpanContaining colspans d emptyMap = M.fromList . zip colspans $ repeat [] +-- | Calculate account balance changes in each column. +-- +-- In each column, gather the accounts that have postings and their change amount. +acctChangesFromPostings :: ReportOpts -> Query -> [Posting] -> HashMap ClippedAccountName MixedAmount +acctChangesFromPostings ropts q ps = + HM.fromList [(aname a, (if tree_ ropts then aibalance else aebalance) a) | a <- as] + where + as = depthLimit $ + (if tree_ ropts then id else filter ((>0).anumpostings)) $ + drop 1 $ accountsFromPostings ps + depthLimit + | tree_ ropts = filter ((depthq `matchesAccount`) . aname) -- exclude deeper balances + | otherwise = clipAccountsAndAggregate $ queryDepth depthq -- aggregate deeper balances at the depth limit + depthq = dbg1 "depthq" $ filterQuery queryIsDepth q + +-- | Gather the account balance changes into a regular matrix including the accounts +-- from all columns +calculateAccountChanges :: ReportOpts -> Query + -> HashMap ClippedAccountName MixedAmount + -> Map DateSpan [Posting] + -> HashMap ClippedAccountName (Map DateSpan MixedAmount) +calculateAccountChanges ropts q startbals colps = acctchanges + where + -- Transpose to get each account's balance changes across all columns. + acctchanges = transposeMap colacctchanges <> (zeros <$ startbals) + + colacctchanges :: Map DateSpan (HashMap ClippedAccountName MixedAmount) = + dbg'' "colacctchanges" $ fmap (acctChangesFromPostings ropts q) colps + + zeros = nullmixedamt <$ colacctchanges + -- | Generates a simple non-columnar BalanceReport, but using multiBalanceReport, -- in order to support --historical. Does not support tree-mode boring parent eliding. @@ -403,6 +412,20 @@ balanceReportFromMultiBalanceReport opts q j = (rows', total) ) | PeriodicReportRow a d amts _ _ <- rows] total = headDef nullmixedamt totals + +-- | Transpose a Map of HashMaps to a HashMap of Maps. +transposeMap :: Map DateSpan (HashMap AccountName MixedAmount) + -> HashMap AccountName (Map DateSpan MixedAmount) +transposeMap xs = M.foldrWithKey addSpan mempty xs + where + addSpan span acctmap seen = HM.foldrWithKey (addAcctSpan span) seen acctmap + + addAcctSpan span acct a = HM.alter f acct + where f = Just . M.insert span a . fromMaybe emptySpanMap + + emptySpanMap = nullmixedamt <$ xs + + -- Local debug helper -- add a prefix to this function's debug output dbg s = let p = "multiBalanceReport" in Hledger.Utils.dbg3 (p++" "++s) @@ -411,7 +434,6 @@ dbg'' s = let p = "multiBalanceReport" in Hledger.Utils.dbg5 (p++" "++s) -- dbg = const id -- exclude this function from debug output -- common rendering helper, XXX here for now - tableAsText :: ReportOpts -> (a -> String) -> Table String String a -> String tableAsText (ReportOpts{pretty_tables_ = pretty}) showcell = unlines diff --git a/hledger-lib/hledger-lib.cabal b/hledger-lib/hledger-lib.cabal index b492d2739..12a16163e 100644 --- a/hledger-lib/hledger-lib.cabal +++ b/hledger-lib/hledger-lib.cabal @@ -1,10 +1,10 @@ cabal-version: 1.12 --- This file has been generated from package.yaml by hpack version 0.33.0. +-- This file has been generated from package.yaml by hpack version 0.33.1. -- -- see: https://github.com/sol/hpack -- --- hash: c30491f8c77b1d38a1992455cc9c340cbcb17e95ec5c07085f9987b289747ba1 +-- hash: dd7c200231996bc96dfb65f042843355e9f7db7002d68c953ada6e89cedd5cc5 name: hledger-lib version: 1.18.99 @@ -149,6 +149,7 @@ library , timeit , transformers >=0.2 , uglymemo + , unordered-containers >=0.2 , utf8-string >=0.3.5 default-language: Haskell2010 @@ -202,6 +203,7 @@ test-suite doctest , timeit , transformers >=0.2 , uglymemo + , unordered-containers >=0.2 , utf8-string >=0.3.5 if (impl(ghc < 8.2)) buildable: False @@ -257,6 +259,7 @@ test-suite unittest , timeit , transformers >=0.2 , uglymemo + , unordered-containers >=0.2 , utf8-string >=0.3.5 buildable: True default-language: Haskell2010 diff --git a/hledger-lib/package.yaml b/hledger-lib/package.yaml index 47a308172..9f29ceed6 100644 --- a/hledger-lib/package.yaml +++ b/hledger-lib/package.yaml @@ -82,6 +82,7 @@ dependencies: - time >=1.5 - timeit - transformers >=0.2 +- unordered-containers >=0.2 - uglymemo - utf8-string >=0.3.5 - extra >=1.6.3 From b2bed03b4c63fff35e027d386c56ebda650f047f Mon Sep 17 00:00:00 2001 From: Stephen Morgan Date: Fri, 12 Jun 2020 12:36:28 +1000 Subject: [PATCH 06/24] lib: multiBalanceReport: Don't import BalanceReport in MultiBalanceReport. --- .../Hledger/Reports/MultiBalanceReport.hs | 52 +++++++++++++++---- 1 file changed, 42 insertions(+), 10 deletions(-) diff --git a/hledger-lib/Hledger/Reports/MultiBalanceReport.hs b/hledger-lib/Hledger/Reports/MultiBalanceReport.hs index 33ac5b215..48000c945 100644 --- a/hledger-lib/Hledger/Reports/MultiBalanceReport.hs +++ b/hledger-lib/Hledger/Reports/MultiBalanceReport.hs @@ -42,7 +42,6 @@ import Hledger.Utils import Hledger.Read (mamountp') import Hledger.Reports.ReportOptions import Hledger.Reports.ReportTypes -import Hledger.Reports.BalanceReport -- | A multi balance report is a kind of periodic report, where the amounts @@ -294,10 +293,10 @@ multiBalanceReportWith ropts@ReportOpts{..} q j priceoracle = -- Balances at report start date, from all earlier postings which otherwise match the query. -- These balances are unvalued except maybe converted to cost. startingBalances :: ReportOpts -> Query -> Journal -> DateSpan -> HashMap AccountName MixedAmount -startingBalances ropts q j reportspan = HM.fromList $ map (\(a,_,_,b) -> (a,b)) startbalanceitems +startingBalances ropts q j reportspan = acctchanges where - (startbalanceitems,_) = dbg'' "starting balance report" $ - balanceReport ropts''{value_=Nothing, percent_=False} startbalq j + acctchanges = acctChangesFromPostings ropts'' startbalq . map fst $ + getPostings ropts'' startbalq j -- 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), @@ -325,7 +324,7 @@ getPostings ropts q = filterJournalAmounts symq . -- remove amount parts excluded by cur: filterJournalPostings reportq -- remove postings not matched by (adjusted) query where - symq = dbg "symq" . filterQuery queryIsSym $ dbg1 "requested q" q + symq = dbg "symq" . filterQuery queryIsSym $ dbg "requested q" q -- 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). @@ -342,9 +341,9 @@ makeReportQuery ropts reportspan q | reportspan == nulldatespan = depthlessq | otherwise = And [dateless depthlessq, reportspandatesq] where - depthlessq = dbg1 "depthless" $ filterQuery (not . queryIsDepth) q - reportspandatesq = dbg1 "reportspandatesq" $ dateqcons reportspan - dateless = dbg1 "dateless" . filterQuery (not . queryIsDateOrDate2) + depthlessq = dbg "depthless" $ filterQuery (not . queryIsDepth) q + reportspandatesq = dbg "reportspandatesq" $ dateqcons reportspan + dateless = dbg "dateless" . filterQuery (not . queryIsDateOrDate2) dateqcons = if date2_ ropts then Date2 else Date -- | Calculate the DateSpans to be used for the columns of the report. @@ -377,7 +376,7 @@ acctChangesFromPostings ropts q ps = depthLimit | tree_ ropts = filter ((depthq `matchesAccount`) . aname) -- exclude deeper balances | otherwise = clipAccountsAndAggregate $ queryDepth depthq -- aggregate deeper balances at the depth limit - depthq = dbg1 "depthq" $ filterQuery queryIsDepth q + depthq = dbg "depthq" $ filterQuery queryIsDepth q -- | Gather the account balance changes into a regular matrix including the accounts -- from all columns @@ -400,7 +399,8 @@ calculateAccountChanges ropts q startbals colps = acctchanges -- 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 + -> ([(AccountName, AccountName, Int, MixedAmount)], MixedAmount) balanceReportFromMultiBalanceReport opts q j = (rows', total) where PeriodicReport _ rows (PeriodicReportRow _ _ totals _ _) = @@ -425,6 +425,38 @@ transposeMap xs = M.foldrWithKey addSpan mempty xs emptySpanMap = nullmixedamt <$ xs +-- | A sorting helper: sort a list of things (eg report rows) keyed by account name +-- to match the provided ordering of those same account names. +sortAccountItemsLike :: [AccountName] -> [(AccountName, b)] -> [b] +sortAccountItemsLike sortedas items = + concatMap (\a -> maybe [] (:[]) $ lookup a items) sortedas + +-- | Helper to unify a MixedAmount to a single commodity value. +-- Like normaliseMixedAmount, this consolidates amounts of the same commodity +-- and discards zero amounts; but this one insists on simplifying to +-- a single commodity, and will throw a program-terminating error if +-- this is not possible. +unifyMixedAmount :: MixedAmount -> Amount +unifyMixedAmount mixedAmount = foldl combine (num 0) (amounts mixedAmount) + where + combine amount result = + if amountIsZero amount + then result + else if amountIsZero result + then amount + else if acommodity amount == acommodity result + then amount + result + else error' "Cannot calculate percentages for accounts with multiple commodities. (Hint: Try --cost, -V or similar flags.)" + +-- | Helper to calculate the percentage from two mixed. Keeps the sign of the first argument. +-- Uses unifyMixedAmount to unify each argument and then divides them. +perdivide :: MixedAmount -> MixedAmount -> MixedAmount +perdivide a b = + let a' = unifyMixedAmount a + b' = unifyMixedAmount b + in if amountIsZero a' || amountIsZero b' || acommodity a' == acommodity b' + then mixed [per $ if aquantity b' == 0 then 0 else (aquantity a' / abs (aquantity b') * 100)] + else error' "Cannot calculate percentages if accounts have different commodities. (Hint: Try --cost, -V or similar flags.)" -- Local debug helper -- add a prefix to this function's debug output From a81c6d0397060f000a512fdd4c84d876ae5ba7bb Mon Sep 17 00:00:00 2001 From: Stephen Morgan Date: Fri, 12 Jun 2020 12:55:33 +1000 Subject: [PATCH 07/24] lib: multiBalanceReport: Split accumValueAmounts into a separate function. --- .../Hledger/Reports/MultiBalanceReport.hs | 76 +++++++++++-------- 1 file changed, 43 insertions(+), 33 deletions(-) diff --git a/hledger-lib/Hledger/Reports/MultiBalanceReport.hs b/hledger-lib/Hledger/Reports/MultiBalanceReport.hs index 48000c945..104d25450 100644 --- a/hledger-lib/Hledger/Reports/MultiBalanceReport.hs +++ b/hledger-lib/Hledger/Reports/MultiBalanceReport.hs @@ -22,7 +22,6 @@ module Hledger.Reports.MultiBalanceReport ( ) where -import Data.Foldable (toList) import Data.List import Data.List.Extra (nubSort) import Data.HashMap.Strict (HashMap) @@ -128,8 +127,6 @@ multiBalanceReportWith ropts@ReportOpts{..} q j priceoracle = -- 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 = dbg'' "startaccts" $ HM.keys startbals - -- Helpers to look up an account's starting balance. - startingBalanceFor a = HM.lookupDefault nullmixedamt a startbals -- Postings matching the query within the report period. ps :: [(Posting, Day)] = dbg'' "ps" $ getPostings ropts reportq j' @@ -144,6 +141,9 @@ multiBalanceReportWith ropts@ReportOpts{..} q j priceoracle = -- Each account's balance changes across all columns. acctchanges = dbg'' "acctchanges" $ calculateAccountChanges ropts q startbals colps + -- Process changes into normal, cumulative, or historical amounts, plus value them + accumvalued = dbg'' "accumvalued" $ accumValueAmounts ropts j priceoracle startbals acctchanges + ---------------------------------------------------------------------- -- 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. @@ -166,40 +166,15 @@ multiBalanceReportWith ropts@ReportOpts{..} q j priceoracle = -- One row per account, with account name info, row amounts, row total and row average. rows :: [MultiBalanceReportRow] = dbg'' "rows" $ - [ PeriodicReportRow a (accountNameLevel a) valuedrowbals rowtot rowavg - | (a,changesMap) <- HM.toList acctchanges - , let changes = toList changesMap - -- The row amounts to be displayed: per-period changes, - -- zero-based cumulative totals, or - -- starting-balance-based historical balances. - , let rowbals = dbg'' "rowbals" $ case balancetype_ of - PeriodChange -> changes - CumulativeChange -> drop 1 $ scanl (+) 0 changes - HistoricalBalance -> drop 1 $ scanl (+) (startingBalanceFor a) changes - -- We may be converting amounts to value, per hledger_options.m4.md "Effect of --value on reports". - , let valuedrowbals = dbg'' "valuedrowbals" $ [avalue periodlastday amt | (amt,periodlastday) <- zip rowbals lastdays] + [ PeriodicReportRow a (accountNameLevel a) rowbals rowtot rowavg + | (a,rowbals) <- HM.toList accumvalued -- 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 = if balancetype_==PeriodChange then sum valuedrowbals else 0 - , let rowavg = averageMixedAmounts valuedrowbals - , empty_ || depth == 0 || any (not . mixedAmountLooksZero) valuedrowbals + , let rowtot = if balancetype_==PeriodChange then sum rowbals else 0 + , let rowavg = averageMixedAmounts rowbals + , empty_ || depth == 0 || any (not . mixedAmountLooksZero) rowbals ] - where - avalue periodlast = - maybe id (mixedAmountApplyValuation priceoracle styles periodlast mreportlast today multiperiod) value_ - where - -- Some things needed if doing valuation. - styles = journalCommodityStyles j - mreportlast = reportPeriodLastDay ropts - today = fromMaybe (error' "multiBalanceReport: could not pick a valuation date, ReportOpts today_ is unset") today_ -- XXX shouldn't happen - multiperiod = interval_ /= NoInterval - -- The last day of each column's subperiod. - lastdays = - map ((maybe - (error' "multiBalanceReport: expected all spans to have an end date") -- XXX should not happen - (addDays (-1))) - . spanEnd) colspans ---------------------------------------------------------------------- -- 7. Sort the report rows. @@ -394,6 +369,41 @@ calculateAccountChanges ropts q startbals colps = acctchanges zeros = nullmixedamt <$ colacctchanges +-- | Accumulate and value amounts, as specified by the report options. +accumValueAmounts :: ReportOpts -> Journal -> PriceOracle + -> HashMap ClippedAccountName MixedAmount + -> HashMap ClippedAccountName (Map DateSpan MixedAmount) + -> HashMap ClippedAccountName [MixedAmount] +accumValueAmounts ropts j priceoracle startbals = HM.mapWithKey processRow + where + processRow name col = zipWith valueAcct spans $ rowbals name amts + where (spans, amts) = unzip $ M.toList col + + -- The row amounts to be displayed: per-period changes, + -- zero-based cumulative totals, or + -- starting-balance-based historical balances. + rowbals name changes = dbg'' "rowbals" $ case balancetype_ ropts of + PeriodChange -> changes + CumulativeChange -> drop 1 $ scanl (+) 0 changes + HistoricalBalance -> drop 1 $ scanl (+) (startingBalanceFor name) changes + + -- We may be converting amounts to value, per hledger_options.m4.md "Effect of --value on reports". + valueAcct (DateSpan _ (Just end)) = avalue periodlast + where periodlast = addDays (-1) end + valueAcct _ = error' "multiBalanceReport: expected all spans to have an end date" -- XXX should not happen + + avalue periodlast = maybe id + (mixedAmountApplyValuation priceoracle styles periodlast mreportlast today multiperiod) $ + value_ ropts + where + -- Some things needed if doing valuation. + styles = journalCommodityStyles j + mreportlast = reportPeriodLastDay ropts + today = fromMaybe (error' "multiBalanceReport: could not pick a valuation date, ReportOpts today_ is unset") $ today_ ropts -- XXX shouldn't happen + multiperiod = interval_ ropts /= NoInterval + + startingBalanceFor a = HM.lookupDefault nullmixedamt a startbals + -- | Generates a simple non-columnar BalanceReport, but using multiBalanceReport, -- in order to support --historical. Does not support tree-mode boring parent eliding. From 7a9bf998e56e0fcb43e0a2df191bdbb13724bf11 Mon Sep 17 00:00:00 2001 From: Stephen Morgan Date: Fri, 12 Jun 2020 13:08:00 +1000 Subject: [PATCH 08/24] lib: multiBalanceReport: Split buildReportRows into a separate function, remove unnecessary query and valuation. --- .../Hledger/Reports/MultiBalanceReport.hs | 44 +++++++++---------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/hledger-lib/Hledger/Reports/MultiBalanceReport.hs b/hledger-lib/Hledger/Reports/MultiBalanceReport.hs index 104d25450..22ed41b8d 100644 --- a/hledger-lib/Hledger/Reports/MultiBalanceReport.hs +++ b/hledger-lib/Hledger/Reports/MultiBalanceReport.hs @@ -118,18 +118,15 @@ multiBalanceReportWith ropts@ReportOpts{..} q j priceoracle = -- handles the hledger-ui+future txns case above). reportq = dbg "reportq" $ makeReportQuery ropts reportspan q - -- If doing cost valuation, convert amounts to cost. - j' = journalSelectingAmountFromOpts ropts j - -- The matched accounts with a starting balance. All of these shold appear -- in the report, even if they have no postings during the report period. - startbals = dbg' "startbals" $ startingBalances ropts q j' reportspan + startbals = dbg' "startbals" $ startingBalances ropts reportq j reportspan -- 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 = dbg'' "startaccts" $ HM.keys startbals -- Postings matching the query within the report period. - ps :: [(Posting, Day)] = dbg'' "ps" $ getPostings ropts reportq j' + ps :: [(Posting, Day)] = dbg'' "ps" $ getPostings ropts reportq j days = map snd ps -- The date spans to be included as report columns. @@ -160,21 +157,8 @@ multiBalanceReportWith ropts@ReportOpts{..} q j priceoracle = allpostedaccts :: [AccountName] = dbg'' "allpostedaccts" . sort . accountNamesFromPostings $ map fst ps - ---------------------------------------------------------------------- - -- 6. Build the report rows. - - -- One row per account, with account name info, row amounts, row total and row average. - rows :: [MultiBalanceReportRow] = - dbg'' "rows" $ - [ PeriodicReportRow a (accountNameLevel a) rowbals rowtot rowavg - | (a,rowbals) <- HM.toList accumvalued - -- 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 = if balancetype_==PeriodChange then sum rowbals else 0 - , let rowavg = averageMixedAmounts rowbals - , empty_ || depth == 0 || any (not . mixedAmountLooksZero) rowbals - ] + -- All the rows of the report. + rows = dbg'' "rows" $ buildReportRows ropts reportq accumvalued ---------------------------------------------------------------------- -- 7. Sort the report rows. @@ -313,10 +297,9 @@ getPostings ropts q = -- | Remove any date queries and insert queries from the report span. makeReportQuery :: ReportOpts -> DateSpan -> Query -> Query makeReportQuery ropts reportspan q - | reportspan == nulldatespan = depthlessq - | otherwise = And [dateless depthlessq, reportspandatesq] + | reportspan == nulldatespan = q + | otherwise = And [dateless q, reportspandatesq] where - depthlessq = dbg "depthless" $ filterQuery (not . queryIsDepth) q reportspandatesq = dbg "reportspandatesq" $ dateqcons reportspan dateless = dbg "dateless" . filterQuery (not . queryIsDateOrDate2) dateqcons = if date2_ ropts then Date2 else Date @@ -404,6 +387,21 @@ accumValueAmounts ropts j priceoracle startbals = HM.mapWithKey processRow startingBalanceFor a = HM.lookupDefault nullmixedamt a startbals +-- | Build the report rows. +-- +-- One row per account, with account name info, row amounts, row total and row average. +buildReportRows :: ReportOpts -> Query -> HashMap AccountName [MixedAmount] -> [MultiBalanceReportRow] +buildReportRows ropts q acctvalues = + [ PeriodicReportRow a (accountNameLevel a) rowbals rowtot rowavg + | (a,rowbals) <- HM.toList acctvalues + -- 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 = if balancetype_ ropts == PeriodChange then sum rowbals else 0 + , let rowavg = averageMixedAmounts rowbals + , empty_ ropts || queryDepth q == 0 || any (not . mixedAmountLooksZero) rowbals -- TODO: Remove this eventually, to be handled elswhere + ] + -- | Generates a simple non-columnar BalanceReport, but using multiBalanceReport, -- in order to support --historical. Does not support tree-mode boring parent eliding. From 0b30b2f9fb483fd180a96d7f801cbb48a26fbe09 Mon Sep 17 00:00:00 2001 From: Stephen Morgan Date: Fri, 12 Jun 2020 13:14:45 +1000 Subject: [PATCH 09/24] lib: multiBalanceReport: Split sortRows into a separate function. --- .../Hledger/Reports/MultiBalanceReport.hs | 87 +++++++++---------- 1 file changed, 40 insertions(+), 47 deletions(-) diff --git a/hledger-lib/Hledger/Reports/MultiBalanceReport.hs b/hledger-lib/Hledger/Reports/MultiBalanceReport.hs index 22ed41b8d..eb387bce9 100644 --- a/hledger-lib/Hledger/Reports/MultiBalanceReport.hs +++ b/hledger-lib/Hledger/Reports/MultiBalanceReport.hs @@ -141,10 +141,6 @@ multiBalanceReportWith ropts@ReportOpts{..} q j priceoracle = -- Process changes into normal, cumulative, or historical amounts, plus value them accumvalued = dbg'' "accumvalued" $ accumValueAmounts ropts j priceoracle startbals acctchanges - ---------------------------------------------------------------------- - -- 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. displayaccts :: [ClippedAccountName] = dbg'' "displayaccts" $ @@ -160,49 +156,8 @@ multiBalanceReportWith ropts@ReportOpts{..} q j priceoracle = -- All the rows of the report. rows = dbg'' "rows" $ buildReportRows ropts reportq accumvalued - ---------------------------------------------------------------------- - -- 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 ? - sortedrows :: [MultiBalanceReportRow] = - dbg' "sortedrows" $ - sortrows rows - where - sortrows - | sort_amount_ && accountlistmode_ == ALTree = sortTreeMBRByAmount - | sort_amount_ = sortFlatMBRByAmount - | otherwise = sortMBRByAccountDeclaration - where - -- Sort the report rows, representing a tree of accounts, by row total at each level. - -- Similar to sortMBRByAccountDeclaration/sortAccountNamesByDeclaration. - sortTreeMBRByAmount :: [MultiBalanceReportRow] -> [MultiBalanceReportRow] - sortTreeMBRByAmount rows = sortedrows - where - anamesandrows = [(prrName r, r) | r <- rows] - anames = map fst anamesandrows - atotals = [(prrName r, prrTotal r) | r <- rows] - accounttree = accountTree "root" anames - accounttreewithbals = mapAccounts setibalance accounttree - where - -- should not happen, but it's dangerous; TODO - setibalance a = a{aibalance=fromMaybe (error "sortTreeMBRByAmount 1") $ lookup (aname a) atotals} - sortedaccounttree = sortAccountTreeByAmount (fromMaybe NormallyPositive normalbalance_) accounttreewithbals - sortedanames = map aname $ drop 1 $ flattenAccounts sortedaccounttree - sortedrows = sortAccountItemsLike sortedanames anamesandrows - - -- Sort the report rows, representing a flat account list, by row total. - sortFlatMBRByAmount = sortBy (maybeflip $ comparing (normaliseMixedAmountSquashPricesForDisplay . prrTotal)) - where - maybeflip = if normalbalance_ == Just NormallyNegative then id else flip - - -- Sort the report rows by account declaration order then account name. - sortMBRByAccountDeclaration rows = sortedrows - where - anamesandrows = [(prrName r, r) | r <- rows] - anames = map fst anamesandrows - sortedanames = sortAccountNamesByDeclaration j (tree_ ropts) anames - sortedrows = sortAccountItemsLike sortedanames anamesandrows + -- Sorted report rows. + sortedrows = dbg' "sortedrows" $ sortRows ropts j rows ---------------------------------------------------------------------- -- 8. Build the report totals row. @@ -402,6 +357,44 @@ buildReportRows ropts q acctvalues = , empty_ ropts || queryDepth q == 0 || any (not . mixedAmountLooksZero) rowbals -- TODO: Remove this eventually, to be handled elswhere ] +-- | 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 ? +sortRows :: ReportOpts -> Journal -> [MultiBalanceReportRow] -> [MultiBalanceReportRow] +sortRows ropts j + | sort_amount_ ropts && accountlistmode_ ropts == ALTree = sortTreeMBRByAmount + | sort_amount_ ropts = sortFlatMBRByAmount + | otherwise = sortMBRByAccountDeclaration + where + -- Sort the report rows, representing a tree of accounts, by row total at each level. + -- Similar to sortMBRByAccountDeclaration/sortAccountNamesByDeclaration. + sortTreeMBRByAmount :: [MultiBalanceReportRow] -> [MultiBalanceReportRow] + sortTreeMBRByAmount rows = sortedrows + where + anamesandrows = [(prrName r, r) | r <- rows] + anames = map fst anamesandrows + atotals = [(prrName r, prrTotal r) | r <- rows] + accounttree = accountTree "root" anames + accounttreewithbals = mapAccounts setibalance accounttree + where + -- should not happen, but it's dangerous; TODO + setibalance a = a{aibalance=fromMaybe (error "sortTreeMBRByAmount 1") $ lookup (aname a) atotals} + sortedaccounttree = sortAccountTreeByAmount (fromMaybe NormallyPositive $ normalbalance_ ropts) accounttreewithbals + sortedanames = map aname $ drop 1 $ flattenAccounts sortedaccounttree + sortedrows = sortAccountItemsLike sortedanames anamesandrows + + -- Sort the report rows, representing a flat account list, by row total. + sortFlatMBRByAmount = sortBy (maybeflip $ comparing (normaliseMixedAmountSquashPricesForDisplay . prrTotal)) + where + maybeflip = if normalbalance_ ropts == Just NormallyNegative then id else flip + + -- Sort the report rows by account declaration order then account name. + sortMBRByAccountDeclaration rows = sortedrows + where + anamesandrows = [(prrName r, r) | r <- rows] + anames = map fst anamesandrows + sortedanames = sortAccountNamesByDeclaration j (tree_ ropts) anames + sortedrows = sortAccountItemsLike sortedanames anamesandrows + -- | Generates a simple non-columnar BalanceReport, but using multiBalanceReport, -- in order to support --historical. Does not support tree-mode boring parent eliding. From baa5844d4e6acb0d6949dadc645874b6479ee597 Mon Sep 17 00:00:00 2001 From: Stephen Morgan Date: Fri, 12 Jun 2020 13:16:33 +1000 Subject: [PATCH 10/24] lib: multiBalanceReport: Change indentation. --- .../Hledger/Reports/MultiBalanceReport.hs | 192 +++++++++--------- 1 file changed, 96 insertions(+), 96 deletions(-) diff --git a/hledger-lib/Hledger/Reports/MultiBalanceReport.hs b/hledger-lib/Hledger/Reports/MultiBalanceReport.hs index eb387bce9..46fe7f042 100644 --- a/hledger-lib/Hledger/Reports/MultiBalanceReport.hs +++ b/hledger-lib/Hledger/Reports/MultiBalanceReport.hs @@ -91,115 +91,115 @@ multiBalanceReport today ropts j = -- function directly. multiBalanceReportWith :: ReportOpts -> Query -> Journal -> PriceOracle -> MultiBalanceReport multiBalanceReportWith ropts@ReportOpts{..} q j priceoracle = - (if invert_ then prNegate else id) $ - PeriodicReport colspans mappedsortedrows mappedtotalsrow - where - ---------------------------------------------------------------------- - -- 1. Queries, report/column dates. + (if invert_ then prNegate else id) $ + PeriodicReport colspans mappedsortedrows mappedtotalsrow + where + ---------------------------------------------------------------------- + -- 1. Queries, report/column dates. - depthq = dbg "depthq" $ filterQuery queryIsDepth q - depth = queryDepth depthq - -- The date span specified by -b/-e/-p options and query args if any. - requestedspan = dbg "requestedspan" $ queryDateSpan date2_ 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' = dbg "requestedspan'" $ requestedspan `spanDefaultsFrom` journalDateSpan date2_ 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 = dbg "intervalspans" $ splitSpan interval_ requestedspan' - -- The requested span enlarged to enclose a whole number of intervals. - -- This can be the null span if there were no intervals. - reportspan = dbg "reportspan" $ DateSpan (maybe Nothing spanStart $ headMay intervalspans) - (maybe Nothing spanEnd $ lastMay intervalspans) - -- 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 = dbg "reportq" $ makeReportQuery ropts reportspan q + depthq = dbg "depthq" $ filterQuery queryIsDepth q + depth = queryDepth depthq + -- The date span specified by -b/-e/-p options and query args if any. + requestedspan = dbg "requestedspan" $ queryDateSpan date2_ 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' = dbg "requestedspan'" $ requestedspan `spanDefaultsFrom` journalDateSpan date2_ 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 = dbg "intervalspans" $ splitSpan interval_ requestedspan' + -- The requested span enlarged to enclose a whole number of intervals. + -- This can be the null span if there were no intervals. + reportspan = dbg "reportspan" $ DateSpan (maybe Nothing spanStart $ headMay intervalspans) + (maybe Nothing spanEnd $ lastMay intervalspans) + -- 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 = dbg "reportq" $ makeReportQuery ropts reportspan q - -- The matched accounts with a starting balance. All of these shold appear - -- in the report, even if they have no postings during the report period. - startbals = dbg' "startbals" $ startingBalances ropts reportq j reportspan - -- 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 = dbg'' "startaccts" $ HM.keys startbals + -- The matched accounts with a starting balance. All of these shold appear + -- in the report, even if they have no postings during the report period. + startbals = dbg' "startbals" $ startingBalances ropts reportq j reportspan + -- 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 = dbg'' "startaccts" $ HM.keys startbals - -- Postings matching the query within the report period. - ps :: [(Posting, Day)] = dbg'' "ps" $ getPostings ropts reportq j - days = map snd ps + -- Postings matching the query within the report period. + ps :: [(Posting, Day)] = dbg'' "ps" $ getPostings ropts reportq j + days = map snd ps - -- The date spans to be included as report columns. - colspans = dbg "colspans" $ calculateColSpans ropts reportspan days + -- The date spans to be included as report columns. + colspans = dbg "colspans" $ calculateColSpans ropts reportspan days - -- Group postings into their columns. - colps = dbg'' "colps" $ calculateColumns colspans ps + -- Group postings into their columns. + colps = dbg'' "colps" $ calculateColumns colspans ps - -- Each account's balance changes across all columns. - acctchanges = dbg'' "acctchanges" $ calculateAccountChanges ropts q startbals colps + -- Each account's balance changes across all columns. + acctchanges = dbg'' "acctchanges" $ calculateAccountChanges ropts q startbals colps - -- Process changes into normal, cumulative, or historical amounts, plus value them - accumvalued = dbg'' "accumvalued" $ accumValueAmounts ropts j priceoracle startbals acctchanges + -- Process changes into normal, cumulative, or historical amounts, plus value them + accumvalued = dbg'' "accumvalued" $ accumValueAmounts ropts j priceoracle startbals acctchanges - -- All account names that will be displayed, possibly depth-clipped. - displayaccts :: [ClippedAccountName] = - dbg'' "displayaccts" $ - (if tree_ ropts then expandAccountNames else id) $ - nub $ map (clipOrEllipsifyAccountName depth) $ - if empty_ || balancetype_ == HistoricalBalance - then nubSort $ startaccts ++ allpostedaccts - else allpostedaccts - where - allpostedaccts :: [AccountName] = - dbg'' "allpostedaccts" . sort . accountNamesFromPostings $ map fst ps + -- All account names that will be displayed, possibly depth-clipped. + displayaccts :: [ClippedAccountName] = + dbg'' "displayaccts" $ + (if tree_ ropts then expandAccountNames else id) $ + nub $ map (clipOrEllipsifyAccountName depth) $ + if empty_ || balancetype_ == HistoricalBalance + then nubSort $ startaccts ++ allpostedaccts + else allpostedaccts + where + allpostedaccts :: [AccountName] = + dbg'' "allpostedaccts" . sort . accountNamesFromPostings $ map fst ps - -- All the rows of the report. - rows = dbg'' "rows" $ buildReportRows ropts reportq accumvalued + -- All the rows of the report. + rows = dbg'' "rows" $ buildReportRows ropts reportq accumvalued - -- Sorted report rows. - sortedrows = dbg' "sortedrows" $ sortRows ropts j rows + -- Sorted report rows. + sortedrows = dbg' "sortedrows" $ sortRows ropts j rows - ---------------------------------------------------------------------- - -- 8. Build the report totals row. + ---------------------------------------------------------------------- + -- 8. Build the report totals row. - -- Calculate the column totals. These are always the sum of column amounts. - highestlevelaccts = [a | a <- displayaccts, not $ any (`elem` displayaccts) $ init $ expandAccountName a] - colamts = transpose . map prrAmounts $ filter isHighest rows - where isHighest row = not (tree_ ropts) || prrName row `elem` highestlevelaccts - coltotals :: [MixedAmount] = - dbg'' "coltotals" $ map sum colamts - -- Calculate the grand total and average. These are always the sum/average - -- of the column totals. - [grandtotal,grandaverage] = - let amts = map ($ map sum colamts) - [if balancetype_==PeriodChange then sum else const 0 - ,averageMixedAmounts - ] - in amts - -- Totals row. - totalsrow :: PeriodicReportRow () MixedAmount = - dbg' "totalsrow" $ PeriodicReportRow () 0 coltotals grandtotal grandaverage + -- Calculate the column totals. These are always the sum of column amounts. + highestlevelaccts = [a | a <- displayaccts, not $ any (`elem` displayaccts) $ init $ expandAccountName a] + colamts = transpose . map prrAmounts $ filter isHighest rows + where isHighest row = not (tree_ ropts) || prrName row `elem` highestlevelaccts + coltotals :: [MixedAmount] = + dbg'' "coltotals" $ map sum colamts + -- Calculate the grand total and average. These are always the sum/average + -- of the column totals. + [grandtotal,grandaverage] = + let amts = map ($ map sum colamts) + [if balancetype_==PeriodChange then sum else const 0 + ,averageMixedAmounts + ] + in amts + -- Totals row. + totalsrow :: PeriodicReportRow () MixedAmount = + dbg' "totalsrow" $ PeriodicReportRow () 0 coltotals grandtotal 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 - mappedsortedrows :: [MultiBalanceReportRow] = - if not percent_ then sortedrows - else dbg'' "mappedsortedrows" - [ PeriodicReportRow aname alevel - (zipWith perdivide rowvals coltotals) - (rowtotal `perdivide` grandtotal) - (rowavg `perdivide` grandaverage) - | PeriodicReportRow aname alevel rowvals rowtotal rowavg <- sortedrows - ] - mappedtotalsrow :: PeriodicReportRow () MixedAmount - | percent_ = dbg'' "mappedtotalsrow" $ PeriodicReportRow () 0 - (map (\t -> perdivide t t) coltotals) - (perdivide grandtotal grandtotal) - (perdivide grandaverage grandaverage) - | otherwise = totalsrow + ---------------------------------------------------------------------- + -- 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 + mappedsortedrows :: [MultiBalanceReportRow] = + if not percent_ then sortedrows + else dbg'' "mappedsortedrows" + [ PeriodicReportRow aname alevel + (zipWith perdivide rowvals coltotals) + (rowtotal `perdivide` grandtotal) + (rowavg `perdivide` grandaverage) + | PeriodicReportRow aname alevel rowvals rowtotal rowavg <- sortedrows + ] + mappedtotalsrow :: PeriodicReportRow () MixedAmount + | percent_ = dbg'' "mappedtotalsrow" $ PeriodicReportRow () 0 + (map (\t -> perdivide t t) coltotals) + (perdivide grandtotal grandtotal) + (perdivide grandaverage grandaverage) + | otherwise = totalsrow -- | Calculate starting balances, if needed for -H From b1068503919ecec0e6b7d9d0eded83e40ba9558c Mon Sep 17 00:00:00 2001 From: Stephen Morgan Date: Fri, 12 Jun 2020 14:13:53 +1000 Subject: [PATCH 11/24] lib: multiBalanceReport: Split postprocessReport and calculateTotalsRow into separate functions. --- .../Hledger/Reports/MultiBalanceReport.hs | 84 +++++++++---------- 1 file changed, 40 insertions(+), 44 deletions(-) diff --git a/hledger-lib/Hledger/Reports/MultiBalanceReport.hs b/hledger-lib/Hledger/Reports/MultiBalanceReport.hs index 46fe7f042..996c81eda 100644 --- a/hledger-lib/Hledger/Reports/MultiBalanceReport.hs +++ b/hledger-lib/Hledger/Reports/MultiBalanceReport.hs @@ -1,4 +1,5 @@ {-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE ScopedTypeVariables #-} @@ -90,9 +91,7 @@ multiBalanceReport today ropts j = -- once for efficiency, passing it to each report by calling this -- function directly. multiBalanceReportWith :: ReportOpts -> Query -> Journal -> PriceOracle -> MultiBalanceReport -multiBalanceReportWith ropts@ReportOpts{..} q j priceoracle = - (if invert_ then prNegate else id) $ - PeriodicReport colspans mappedsortedrows mappedtotalsrow +multiBalanceReportWith ropts@ReportOpts{..} q j priceoracle = report where ---------------------------------------------------------------------- -- 1. Queries, report/column dates. @@ -159,47 +158,12 @@ multiBalanceReportWith ropts@ReportOpts{..} q j priceoracle = -- Sorted report rows. sortedrows = dbg' "sortedrows" $ sortRows ropts j rows - ---------------------------------------------------------------------- - -- 8. Build the report totals row. + -- Calculate column totals + totalsrow = dbg' "totalsrow" $ calculateTotalsRow ropts displayaccts sortedrows - -- Calculate the column totals. These are always the sum of column amounts. - highestlevelaccts = [a | a <- displayaccts, not $ any (`elem` displayaccts) $ init $ expandAccountName a] - colamts = transpose . map prrAmounts $ filter isHighest rows - where isHighest row = not (tree_ ropts) || prrName row `elem` highestlevelaccts - coltotals :: [MixedAmount] = - dbg'' "coltotals" $ map sum colamts - -- Calculate the grand total and average. These are always the sum/average - -- of the column totals. - [grandtotal,grandaverage] = - let amts = map ($ map sum colamts) - [if balancetype_==PeriodChange then sum else const 0 - ,averageMixedAmounts - ] - in amts - -- Totals row. - totalsrow :: PeriodicReportRow () MixedAmount = - dbg' "totalsrow" $ PeriodicReportRow () 0 coltotals grandtotal 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 - mappedsortedrows :: [MultiBalanceReportRow] = - if not percent_ then sortedrows - else dbg'' "mappedsortedrows" - [ PeriodicReportRow aname alevel - (zipWith perdivide rowvals coltotals) - (rowtotal `perdivide` grandtotal) - (rowavg `perdivide` grandaverage) - | PeriodicReportRow aname alevel rowvals rowtotal rowavg <- sortedrows - ] - mappedtotalsrow :: PeriodicReportRow () MixedAmount - | percent_ = dbg'' "mappedtotalsrow" $ PeriodicReportRow () 0 - (map (\t -> perdivide t t) coltotals) - (perdivide grandtotal grandtotal) - (perdivide grandaverage grandaverage) - | otherwise = totalsrow + -- Postprocess the report, negating balances and taking percentages if needed + report = dbg' "report" . postprocessReport ropts $ + PeriodicReport colspans sortedrows totalsrow -- | Calculate starting balances, if needed for -H @@ -229,7 +193,6 @@ startingBalances ropts q j reportspan = acctchanges DateSpan Nothing Nothing -> emptydatespan a -> a - -- | Gather postings matching the query within the report period. getPostings :: ReportOpts -> Query -> Journal -> [(Posting, Day)] getPostings ropts q = @@ -395,6 +358,39 @@ sortRows ropts j sortedanames = sortAccountNamesByDeclaration j (tree_ ropts) anames sortedrows = sortAccountItemsLike sortedanames anamesandrows +-- | Build the report totals row. +-- +-- Calculate the column totals. These are always the sum of column amounts. +calculateTotalsRow :: ReportOpts -> [ClippedAccountName] + -> [MultiBalanceReportRow] -> PeriodicReportRow () MixedAmount +calculateTotalsRow ropts displayaccts rows = + PeriodicReportRow () 0 coltotals grandtotal grandaverage + where + highestlevelaccts = [a | a <- displayaccts, not $ any (`elem` displayaccts) $ init $ expandAccountName a] + + colamts = transpose . map prrAmounts $ filter isHighest rows + where isHighest row = not (tree_ ropts) || prrName row `elem` highestlevelaccts + + coltotals :: [MixedAmount] = dbg'' "coltotals" $ map sum colamts + + -- Calculate the grand total and average. These are always the sum/average + -- of the column totals. + grandtotal = if balancetype_ ropts == PeriodChange then sum coltotals else 0 + grandaverage = averageMixedAmounts coltotals + +-- | Map the report rows to percentages and negate if needed +postprocessReport :: ReportOpts -> MultiBalanceReport -> MultiBalanceReport +postprocessReport ropts (PeriodicReport spans rows totalrow) = + maybeInvert $ PeriodicReport spans (map percentage rows) (percentage totalrow) + where + maybeInvert = if invert_ ropts then prNegate else id + percentage = if not (percent_ ropts) then id else \case + PeriodicReportRow name d rowvals rowtotal rowavg -> + PeriodicReportRow name d + (zipWith perdivide rowvals $ prrAmounts totalrow) + (perdivide rowtotal $ prrTotal totalrow) + (perdivide rowavg $ prrAverage totalrow) + -- | Generates a simple non-columnar BalanceReport, but using multiBalanceReport, -- in order to support --historical. Does not support tree-mode boring parent eliding. From 0e89a389d640a05dc91e1ffe0074472c87d99af3 Mon Sep 17 00:00:00 2001 From: Stephen Morgan Date: Fri, 12 Jun 2020 14:28:32 +1000 Subject: [PATCH 12/24] lib: multiBalanceReport: Split displayedAccounts, calculateReportQuery into separate functions. --- .../Hledger/Reports/MultiBalanceReport.hs | 101 ++++++++++-------- 1 file changed, 54 insertions(+), 47 deletions(-) diff --git a/hledger-lib/Hledger/Reports/MultiBalanceReport.hs b/hledger-lib/Hledger/Reports/MultiBalanceReport.hs index 996c81eda..f29555fe6 100644 --- a/hledger-lib/Hledger/Reports/MultiBalanceReport.hs +++ b/hledger-lib/Hledger/Reports/MultiBalanceReport.hs @@ -93,36 +93,13 @@ multiBalanceReport today ropts j = multiBalanceReportWith :: ReportOpts -> Query -> Journal -> PriceOracle -> MultiBalanceReport multiBalanceReportWith ropts@ReportOpts{..} q j priceoracle = report where - ---------------------------------------------------------------------- - -- 1. Queries, report/column dates. + -- Queries, report/column dates. + reportspan = dbg "reportspan" $ calculateReportSpan ropts q j + reportq = dbg "reportq" $ makeReportQuery ropts reportspan q - depthq = dbg "depthq" $ filterQuery queryIsDepth q - depth = queryDepth depthq - -- The date span specified by -b/-e/-p options and query args if any. - requestedspan = dbg "requestedspan" $ queryDateSpan date2_ 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' = dbg "requestedspan'" $ requestedspan `spanDefaultsFrom` journalDateSpan date2_ 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 = dbg "intervalspans" $ splitSpan interval_ requestedspan' - -- The requested span enlarged to enclose a whole number of intervals. - -- This can be the null span if there were no intervals. - reportspan = dbg "reportspan" $ DateSpan (maybe Nothing spanStart $ headMay intervalspans) - (maybe Nothing spanEnd $ lastMay intervalspans) - -- 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 = dbg "reportq" $ makeReportQuery ropts reportspan q - - -- The matched accounts with a starting balance. All of these shold appear + -- 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. startbals = dbg' "startbals" $ startingBalances ropts reportq j reportspan - -- 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 = dbg'' "startaccts" $ HM.keys startbals -- Postings matching the query within the report period. ps :: [(Posting, Day)] = dbg'' "ps" $ getPostings ropts reportq j @@ -141,16 +118,7 @@ multiBalanceReportWith ropts@ReportOpts{..} q j priceoracle = report accumvalued = dbg'' "accumvalued" $ accumValueAmounts ropts j priceoracle startbals acctchanges -- All account names that will be displayed, possibly depth-clipped. - displayaccts :: [ClippedAccountName] = - dbg'' "displayaccts" $ - (if tree_ ropts then expandAccountNames else id) $ - nub $ map (clipOrEllipsifyAccountName depth) $ - if empty_ || balancetype_ == HistoricalBalance - then nubSort $ startaccts ++ allpostedaccts - else allpostedaccts - where - allpostedaccts :: [AccountName] = - dbg'' "allpostedaccts" . sort . accountNamesFromPostings $ map fst ps + displayaccts = dbg'' "displayaccts" $ displayedAccounts ropts q startbals ps -- All the rows of the report. rows = dbg'' "rows" $ buildReportRows ropts reportq accumvalued @@ -166,6 +134,39 @@ multiBalanceReportWith ropts@ReportOpts{..} q j priceoracle = report PeriodicReport colspans sortedrows totalsrow +-- | Calculate the span of the report to be generated. +calculateReportSpan :: ReportOpts -> Query -> Journal -> DateSpan +calculateReportSpan ropts q j = reportspan + where + -- The date span specified by -b/-e/-p options and query args if any. + requestedspan = dbg "requestedspan" $ queryDateSpan (date2_ ropts) 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' = dbg "requestedspan'" $ + requestedspan `spanDefaultsFrom` journalDateSpan (date2_ ropts) 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 = dbg "intervalspans" $ splitSpan (interval_ ropts) 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 (spanStart =<< headMay intervalspans) + (spanEnd =<< lastMay intervalspans) + +-- | 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 :: ReportOpts -> DateSpan -> Query -> Query +makeReportQuery ropts reportspan q + | reportspan == nulldatespan = q + | otherwise = And [dateless q, reportspandatesq] + where + reportspandatesq = dbg "reportspandatesq" $ dateqcons reportspan + dateless = dbg "dateless" . filterQuery (not . queryIsDateOrDate2) + dateqcons = if date2_ ropts then Date2 else Date + -- | Calculate starting balances, if needed for -H -- -- Balances at report start date, from all earlier postings which otherwise match the query. @@ -212,16 +213,6 @@ getPostings ropts q = PrimaryDate -> postingDate SecondaryDate -> postingDate2 --- | Remove any date queries and insert queries from the report span. -makeReportQuery :: ReportOpts -> DateSpan -> Query -> Query -makeReportQuery ropts reportspan q - | reportspan == nulldatespan = q - | otherwise = And [dateless q, reportspandatesq] - where - reportspandatesq = dbg "reportspandatesq" $ dateqcons reportspan - dateless = dbg "dateless" . filterQuery (not . queryIsDateOrDate2) - dateqcons = if date2_ ropts then Date2 else Date - -- | Calculate the DateSpans to be used for the columns of the report. calculateColSpans :: ReportOpts -> DateSpan -> [Day] -> [DateSpan] calculateColSpans ropts reportspan days = @@ -320,6 +311,22 @@ buildReportRows ropts q acctvalues = , empty_ ropts || queryDepth q == 0 || any (not . mixedAmountLooksZero) rowbals -- TODO: Remove this eventually, to be handled elswhere ] +-- | Calculate accounts which are to be displayed in the report +displayedAccounts :: ReportOpts -> Query + -> HashMap AccountName MixedAmount + -> [(Posting, Day)] + -> [AccountName] +displayedAccounts ropts q startbals ps = + (if tree_ ropts then expandAccountNames else id) $ + nub $ map (clipOrEllipsifyAccountName depth) $ + if empty_ ropts || balancetype_ ropts == HistoricalBalance + then nubSort $ (HM.keys startbals) ++ allpostedaccts + else allpostedaccts + where + allpostedaccts :: [AccountName] = + dbg'' "allpostedaccts" . sort . accountNamesFromPostings $ map fst ps + depth = queryDepth q + -- | 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 ? sortRows :: ReportOpts -> Journal -> [MultiBalanceReportRow] -> [MultiBalanceReportRow] From 1e7e80504fcc91d83075e6fbf607027a67a0b4c7 Mon Sep 17 00:00:00 2001 From: Stephen Morgan Date: Fri, 12 Jun 2020 17:26:27 +1000 Subject: [PATCH 13/24] lib: multiBalanceReport: Keep Account around longer so we can use both aibalance and aebalance. --- .../Hledger/Reports/MultiBalanceReport.hs | 109 ++++++++++-------- 1 file changed, 62 insertions(+), 47 deletions(-) diff --git a/hledger-lib/Hledger/Reports/MultiBalanceReport.hs b/hledger-lib/Hledger/Reports/MultiBalanceReport.hs index f29555fe6..d2bb386e3 100644 --- a/hledger-lib/Hledger/Reports/MultiBalanceReport.hs +++ b/hledger-lib/Hledger/Reports/MultiBalanceReport.hs @@ -115,10 +115,10 @@ multiBalanceReportWith ropts@ReportOpts{..} q j priceoracle = report acctchanges = dbg'' "acctchanges" $ calculateAccountChanges ropts q startbals colps -- Process changes into normal, cumulative, or historical amounts, plus value them - accumvalued = dbg'' "accumvalued" $ accumValueAmounts ropts j priceoracle startbals acctchanges + accumvalued = dbg'' "accumvalued" $ accumValueAmounts ropts j priceoracle colspans startbals acctchanges -- All account names that will be displayed, possibly depth-clipped. - displayaccts = dbg'' "displayaccts" $ displayedAccounts ropts q startbals ps + displayaccts = dbg'' "displayaccts" $ displayedAccounts ropts q startbals accumvalued -- All the rows of the report. rows = dbg'' "rows" $ buildReportRows ropts reportq accumvalued @@ -171,7 +171,7 @@ makeReportQuery ropts reportspan q -- -- Balances at report start date, from all earlier postings which otherwise match the query. -- These balances are unvalued except maybe converted to cost. -startingBalances :: ReportOpts -> Query -> Journal -> DateSpan -> HashMap AccountName MixedAmount +startingBalances :: ReportOpts -> Query -> Journal -> DateSpan -> HashMap AccountName Account startingBalances ropts q j reportspan = acctchanges where acctchanges = acctChangesFromPostings ropts'' startbalq . map fst $ @@ -233,56 +233,62 @@ calculateColumns colspans = foldr addPosting emptyMap -- | Calculate account balance changes in each column. -- -- In each column, gather the accounts that have postings and their change amount. -acctChangesFromPostings :: ReportOpts -> Query -> [Posting] -> HashMap ClippedAccountName MixedAmount -acctChangesFromPostings ropts q ps = - HM.fromList [(aname a, (if tree_ ropts then aibalance else aebalance) a) | a <- as] +acctChangesFromPostings :: ReportOpts -> Query -> [Posting] -> HashMap ClippedAccountName Account +acctChangesFromPostings ropts q ps = HM.fromList [(aname a, a) | a <- as] where - as = depthLimit $ - (if tree_ ropts then id else filter ((>0).anumpostings)) $ - drop 1 $ accountsFromPostings ps - depthLimit - | tree_ ropts = filter ((depthq `matchesAccount`) . aname) -- exclude deeper balances - | otherwise = clipAccountsAndAggregate $ queryDepth depthq -- aggregate deeper balances at the depth limit + as = filterAccounts . drop 1 $ accountsFromPostings ps + filterAccounts + | tree_ ropts = filter ((depthq `matchesAccount`) . aname) -- exclude deeper balances + | otherwise = clipAccountsAndAggregate (queryDepth depthq) . -- aggregate deeper balances at the depth limit. + filter ((0<) . anumpostings) depthq = dbg "depthq" $ filterQuery queryIsDepth q -- | Gather the account balance changes into a regular matrix including the accounts -- from all columns calculateAccountChanges :: ReportOpts -> Query - -> HashMap ClippedAccountName MixedAmount + -> HashMap ClippedAccountName Account -> Map DateSpan [Posting] - -> HashMap ClippedAccountName (Map DateSpan MixedAmount) + -> HashMap ClippedAccountName (Map DateSpan Account) calculateAccountChanges ropts q startbals colps = acctchanges where -- Transpose to get each account's balance changes across all columns. - acctchanges = transposeMap colacctchanges <> (zeros <$ startbals) + acctchanges = transposeMap colacctchanges <> (mempty <$ startbals) - colacctchanges :: Map DateSpan (HashMap ClippedAccountName MixedAmount) = + colacctchanges :: Map DateSpan (HashMap ClippedAccountName Account) = dbg'' "colacctchanges" $ fmap (acctChangesFromPostings ropts q) colps - zeros = nullmixedamt <$ colacctchanges - -- | Accumulate and value amounts, as specified by the report options. -accumValueAmounts :: ReportOpts -> Journal -> PriceOracle - -> HashMap ClippedAccountName MixedAmount - -> HashMap ClippedAccountName (Map DateSpan MixedAmount) - -> HashMap ClippedAccountName [MixedAmount] -accumValueAmounts ropts j priceoracle startbals = HM.mapWithKey processRow +-- +-- Makes sure all report columns have an entry. +accumValueAmounts :: ReportOpts -> Journal -> PriceOracle -> [DateSpan] + -> HashMap ClippedAccountName Account + -> HashMap ClippedAccountName (Map DateSpan Account) + -> HashMap ClippedAccountName [Account] +accumValueAmounts ropts j priceoracle colspans startbals = HM.mapWithKey processRow where + -- Must accumulate before valuing, since valuation can change without any + -- postings processRow name col = zipWith valueAcct spans $ rowbals name amts - where (spans, amts) = unzip $ M.toList col + where (spans, amts) = unzip . M.toList $ col <> zeros -- The row amounts to be displayed: per-period changes, -- zero-based cumulative totals, or -- starting-balance-based historical balances. rowbals name changes = dbg'' "rowbals" $ case balancetype_ ropts of PeriodChange -> changes - CumulativeChange -> drop 1 $ scanl (+) 0 changes - HistoricalBalance -> drop 1 $ scanl (+) (startingBalanceFor name) changes + CumulativeChange -> drop 1 $ scanl sumAcct nullacct changes + HistoricalBalance -> drop 1 $ scanl sumAcct (startingBalanceFor name) changes + + -- Add the values of two accounts. Should be right-biased, since it's used + -- in scanl, so other properties (such as anumpostings) stay in the right place + sumAcct Account{aibalance=i1,aebalance=e1} a@Account{aibalance=i2,aebalance=e2} = + a{aibalance = i1 + i2, aebalance = e1 + e2} -- We may be converting amounts to value, per hledger_options.m4.md "Effect of --value on reports". - valueAcct (DateSpan _ (Just end)) = avalue periodlast - where periodlast = addDays (-1) end - valueAcct _ = error' "multiBalanceReport: expected all spans to have an end date" -- XXX should not happen + valueAcct (DateSpan _ (Just end)) acct = + acct{aibalance = value (aibalance acct), aebalance = value (aebalance acct)} + where value = avalue (addDays (-1) end) + valueAcct _ _ = error' "multiBalanceReport: expected all spans to have an end date" -- XXX should not happen avalue periodlast = maybe id (mixedAmountApplyValuation priceoracle styles periodlast mreportlast today multiperiod) $ @@ -294,15 +300,20 @@ accumValueAmounts ropts j priceoracle startbals = HM.mapWithKey processRow today = fromMaybe (error' "multiBalanceReport: could not pick a valuation date, ReportOpts today_ is unset") $ today_ ropts -- XXX shouldn't happen multiperiod = interval_ ropts /= NoInterval - startingBalanceFor a = HM.lookupDefault nullmixedamt a startbals + startingBalanceFor a = HM.lookupDefault nullacct a startbals + + zeros = M.fromList [(span, nullacct) | span <- colspans] -- | Build the report rows. -- -- One row per account, with account name info, row amounts, row total and row average. -buildReportRows :: ReportOpts -> Query -> HashMap AccountName [MixedAmount] -> [MultiBalanceReportRow] +buildReportRows :: ReportOpts -> Query + -> HashMap AccountName [Account] + -> [MultiBalanceReportRow] buildReportRows ropts q acctvalues = [ PeriodicReportRow a (accountNameLevel a) rowbals rowtot rowavg - | (a,rowbals) <- HM.toList acctvalues + | (a,accts) <- HM.toList acctvalues + , let rowbals = map balance accts -- 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. @@ -310,21 +321,24 @@ buildReportRows ropts q acctvalues = , let rowavg = averageMixedAmounts rowbals , empty_ ropts || queryDepth q == 0 || any (not . mixedAmountLooksZero) rowbals -- TODO: Remove this eventually, to be handled elswhere ] + where + balance = if tree_ ropts then aibalance else aebalance --- | Calculate accounts which are to be displayed in the report +-- | Calculate accounts which are to be displayed in the report, as well as +-- their name and depth displayedAccounts :: ReportOpts -> Query - -> HashMap AccountName MixedAmount - -> [(Posting, Day)] - -> [AccountName] -displayedAccounts ropts q startbals ps = + -> HashMap AccountName Account + -> HashMap AccountName [Account] + -> HashMap AccountName (AccountName, Int) +displayedAccounts ropts q startbals valuedaccts = + HM.fromList $ map (\a -> (a, (a, 0))) . (if tree_ ropts then expandAccountNames else id) $ nub $ map (clipOrEllipsifyAccountName depth) $ if empty_ ropts || balancetype_ ropts == HistoricalBalance then nubSort $ (HM.keys startbals) ++ allpostedaccts else allpostedaccts where - allpostedaccts :: [AccountName] = - dbg'' "allpostedaccts" . sort . accountNamesFromPostings $ map fst ps + allpostedaccts = dbg'' "allpostedaccts" $ HM.keys valuedaccts depth = queryDepth q -- | Sort the rows by amount or by account declaration order. This is a bit tricky. @@ -368,15 +382,16 @@ sortRows ropts j -- | Build the report totals row. -- -- Calculate the column totals. These are always the sum of column amounts. -calculateTotalsRow :: ReportOpts -> [ClippedAccountName] +calculateTotalsRow :: ReportOpts -> HashMap ClippedAccountName (ClippedAccountName, Int) -> [MultiBalanceReportRow] -> PeriodicReportRow () MixedAmount calculateTotalsRow ropts displayaccts rows = PeriodicReportRow () 0 coltotals grandtotal grandaverage where - highestlevelaccts = [a | a <- displayaccts, not $ any (`elem` displayaccts) $ init $ expandAccountName a] + highestlevelaccts = HM.filterWithKey (\a _ -> isHighest a) displayaccts + where isHighest = not . any (`HM.member` displayaccts) . init . expandAccountName colamts = transpose . map prrAmounts $ filter isHighest rows - where isHighest row = not (tree_ ropts) || prrName row `elem` highestlevelaccts + where isHighest row = not (tree_ ropts) || prrName row `HM.member` highestlevelaccts coltotals :: [MixedAmount] = dbg'' "coltotals" $ map sum colamts @@ -418,16 +433,16 @@ balanceReportFromMultiBalanceReport opts q j = (rows', total) -- | Transpose a Map of HashMaps to a HashMap of Maps. -transposeMap :: Map DateSpan (HashMap AccountName MixedAmount) - -> HashMap AccountName (Map DateSpan MixedAmount) +-- +-- Makes sure that all DateSpans are present in all rows. +transposeMap :: Map DateSpan (HashMap AccountName a) + -> HashMap AccountName (Map DateSpan a) transposeMap xs = M.foldrWithKey addSpan mempty xs where addSpan span acctmap seen = HM.foldrWithKey (addAcctSpan span) seen acctmap addAcctSpan span acct a = HM.alter f acct - where f = Just . M.insert span a . fromMaybe emptySpanMap - - emptySpanMap = nullmixedamt <$ xs + where f = Just . M.insert span a . fromMaybe mempty -- | A sorting helper: sort a list of things (eg report rows) keyed by account name -- to match the provided ordering of those same account names. From 0dedcfbe15ce6db634c7156020c31a240324fa80 Mon Sep 17 00:00:00 2001 From: Stephen Morgan Date: Fri, 12 Jun 2020 19:59:43 +1000 Subject: [PATCH 14/24] lib: multiBalanceReport: Miscellaneous simplifications. --- .../Hledger/Reports/MultiBalanceReport.hs | 39 ++++++++++++------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/hledger-lib/Hledger/Reports/MultiBalanceReport.hs b/hledger-lib/Hledger/Reports/MultiBalanceReport.hs index d2bb386e3..a0f4a4b5e 100644 --- a/hledger-lib/Hledger/Reports/MultiBalanceReport.hs +++ b/hledger-lib/Hledger/Reports/MultiBalanceReport.hs @@ -24,7 +24,6 @@ module Hledger.Reports.MultiBalanceReport ( where import Data.List -import Data.List.Extra (nubSort) import Data.HashMap.Strict (HashMap) import qualified Data.HashMap.Strict as HM import Data.Map (Map) @@ -118,7 +117,7 @@ multiBalanceReportWith ropts@ReportOpts{..} q j priceoracle = report accumvalued = dbg'' "accumvalued" $ accumValueAmounts ropts j priceoracle colspans startbals acctchanges -- All account names that will be displayed, possibly depth-clipped. - displayaccts = dbg'' "displayaccts" $ displayedAccounts ropts q startbals accumvalued + displayaccts = dbg'' "displayaccts" $ displayedAccounts ropts q accumvalued -- All the rows of the report. rows = dbg'' "rows" $ buildReportRows ropts reportq accumvalued @@ -170,12 +169,15 @@ makeReportQuery ropts reportspan q -- | 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. +-- These balances are unvalued. +-- TODO: Do we want to check whether to bother calculating these? isHistorical +-- and startDate is not nothing, otherwise mempty? This currently gives a +-- failure with some totals which are supposed to be 0 being blank. startingBalances :: ReportOpts -> Query -> Journal -> DateSpan -> HashMap AccountName Account startingBalances ropts q j reportspan = acctchanges where - acctchanges = acctChangesFromPostings ropts'' startbalq . map fst $ - getPostings ropts'' startbalq j + acctchanges = acctChangesFromPostings ropts' startbalq . map fst $ + getPostings ropts' startbalq j -- 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), @@ -183,9 +185,8 @@ startingBalances ropts q j reportspan = acctchanges startbalq = dbg'' "startbalq" $ And [datelessq, precedingspanq] datelessq = dbg "datelessq" $ filterQuery (not . queryIsDateOrDate2) q - ropts' | tree_ ropts = ropts{no_elide_=True} - | otherwise = ropts{accountlistmode_=ALFlat} - ropts'' = ropts'{period_ = precedingperiod} + ropts' | tree_ ropts = ropts{no_elide_=True, period_=precedingperiod} + | otherwise = ropts{accountlistmode_=ALFlat, period_=precedingperiod} precedingperiod = dateSpanAsPeriod . spanIntersect precedingspan . periodAsDateSpan $ period_ ropts @@ -327,18 +328,26 @@ buildReportRows ropts q acctvalues = -- | Calculate accounts which are to be displayed in the report, as well as -- their name and depth displayedAccounts :: ReportOpts -> Query - -> HashMap AccountName Account -> HashMap AccountName [Account] -> HashMap AccountName (AccountName, Int) -displayedAccounts ropts q startbals valuedaccts = - HM.fromList $ map (\a -> (a, (a, 0))) . +displayedAccounts ropts q valuedaccts = + HM.fromList $ map (\a -> (a, elidedName a)) . (if tree_ ropts then expandAccountNames else id) $ nub $ map (clipOrEllipsifyAccountName depth) $ - if empty_ ropts || balancetype_ ropts == HistoricalBalance - then nubSort $ (HM.keys startbals) ++ allpostedaccts - else allpostedaccts + allpostedaccts where allpostedaccts = dbg'' "allpostedaccts" $ HM.keys valuedaccts + + elidedName name + | depth == 0 = ("...", 0) + | otherwise = (elided, accountNameLevel name - boringParents) + where + elided = accountNameFromComponents . reverse . map accountLeafName $ + name : takeWhile (not . isDisplayed) parents + boringParents = length $ filter (not . isDisplayed) parents + parents = parentAccountNames name + + isDisplayed = const True depth = queryDepth q -- | Sort the rows by amount or by account declaration order. This is a bit tricky. @@ -393,6 +402,8 @@ calculateTotalsRow ropts displayaccts rows = colamts = transpose . map prrAmounts $ filter isHighest rows where isHighest row = not (tree_ ropts) || prrName row `HM.member` highestlevelaccts + -- TODO: If colamts is null, then this is empty. Do we want it to be a full + -- column of zeros? coltotals :: [MixedAmount] = dbg'' "coltotals" $ map sum colamts -- Calculate the grand total and average. These are always the sum/average From 5f0918217a1495d28b132533b01c052d38c2d56c Mon Sep 17 00:00:00 2001 From: Stephen Morgan Date: Sat, 13 Jun 2020 19:58:58 +1000 Subject: [PATCH 15/24] multibalanceReport: Move responsibility for determining displayed name in multiBalanceReportWith, not at point of consumption. --- hledger-lib/Hledger/Reports/BudgetReport.hs | 49 ++++++----- .../Hledger/Reports/MultiBalanceReport.hs | 83 ++++++++++--------- hledger-lib/Hledger/Reports/ReportTypes.hs | 69 ++++++++++++++- hledger/Hledger/Cli/Commands/Balance.hs | 15 ++-- hledger/Hledger/Cli/CompoundBalanceCommand.hs | 10 +-- 5 files changed, 150 insertions(+), 76 deletions(-) diff --git a/hledger-lib/Hledger/Reports/BudgetReport.hs b/hledger-lib/Hledger/Reports/BudgetReport.hs index 22698f28e..4d51dd584 100644 --- a/hledger-lib/Hledger/Reports/BudgetReport.hs +++ b/hledger-lib/Hledger/Reports/BudgetReport.hs @@ -47,8 +47,8 @@ type BudgetAverage = Average -- | A budget report tracks expected and actual changes per account and subperiod. type BudgetCell = (Maybe Change, Maybe BudgetGoal) -type BudgetReport = PeriodicReport AccountName BudgetCell -type BudgetReportRow = PeriodicReportRow AccountName BudgetCell +type BudgetReport = PeriodicReport DisplayName BudgetCell +type BudgetReportRow = PeriodicReportRow DisplayName BudgetCell -- | Calculate budget goals from all periodic transactions, -- actual balance changes from the regular transactions, @@ -99,9 +99,9 @@ sortBudgetReport ropts j (PeriodicReport ps rows trow) = PeriodicReport ps sorte sortTreeBURByActualAmount :: [BudgetReportRow] -> [BudgetReportRow] sortTreeBURByActualAmount rows = sortedrows where - anamesandrows = [(prrName r, r) | r <- rows] + anamesandrows = [(prrFullName r, r) | r <- rows] anames = map fst anamesandrows - atotals = [(a, tot) | PeriodicReportRow a _ _ (tot,_) _ <- rows] + atotals = [(displayFull a, tot) | PeriodicReportRow a _ (tot,_) _ <- rows] accounttree = accountTree "root" anames accounttreewithbals = mapAccounts setibalance accounttree where @@ -124,8 +124,8 @@ sortBudgetReport ropts j (PeriodicReport ps rows trow) = PeriodicReport ps sorte -- remains at the top. sortByAccountDeclaration rows = sortedrows where - (unbudgetedrow,rows') = partition ((=="") . prrName) rows - anamesandrows = [(prrName r, r) | r <- rows'] + (unbudgetedrow,rows') = partition ((==unbudgetedAccountName) . prrFullName) rows + anamesandrows = [(prrFullName r, r) | r <- rows'] anames = map fst anamesandrows sortedanames = sortAccountNamesByDeclaration j (tree_ ropts) anames sortedrows = unbudgetedrow ++ sortAccountItemsLike sortedanames anamesandrows @@ -189,17 +189,17 @@ budgetRollUp budgetedaccts showunbudgeted j = j { jtxns = remapTxn <$> jtxns j } -- combineBudgetAndActual :: MultiBalanceReport -> MultiBalanceReport -> BudgetReport combineBudgetAndActual - (PeriodicReport budgetperiods budgetrows (PeriodicReportRow _ _ budgettots budgetgrandtot budgetgrandavg)) - (PeriodicReport actualperiods actualrows (PeriodicReportRow _ _ actualtots actualgrandtot actualgrandavg)) = + (PeriodicReport budgetperiods budgetrows (PeriodicReportRow _ budgettots budgetgrandtot budgetgrandavg)) + (PeriodicReport actualperiods actualrows (PeriodicReportRow _ actualtots actualgrandtot actualgrandavg)) = PeriodicReport periods rows totalrow where periods = nubSort . filter (/= nulldatespan) $ budgetperiods ++ actualperiods -- first, combine any corresponding budget goals with actual changes rows1 = - [ PeriodicReportRow acct treeindent amtandgoals totamtandgoal avgamtandgoal - | PeriodicReportRow acct treeindent actualamts actualtot actualavg <- actualrows - , let mbudgetgoals = Map.lookup acct budgetGoalsByAcct :: Maybe ([BudgetGoal], BudgetTotal, BudgetAverage) + [ PeriodicReportRow acct amtandgoals totamtandgoal avgamtandgoal + | PeriodicReportRow acct actualamts actualtot actualavg <- actualrows + , let mbudgetgoals = Map.lookup (displayFull acct) budgetGoalsByAcct :: Maybe ([BudgetGoal], BudgetTotal, BudgetAverage) , let budgetmamts = maybe (replicate (length periods) Nothing) (map Just . first3) mbudgetgoals :: [Maybe BudgetGoal] , let mbudgettot = second3 <$> mbudgetgoals :: Maybe BudgetTotal , let mbudgetavg = third3 <$> mbudgetgoals :: Maybe BudgetAverage @@ -211,14 +211,14 @@ combineBudgetAndActual ] where budgetGoalsByAcct :: Map AccountName ([BudgetGoal], BudgetTotal, BudgetAverage) = - Map.fromList [ (acct, (amts, tot, avg)) - | PeriodicReportRow acct _ amts tot avg <- budgetrows ] + Map.fromList [ (displayFull acct, (amts, tot, avg)) + | PeriodicReportRow acct amts tot avg <- budgetrows ] -- next, make rows for budget goals with no actual changes rows2 = - [ PeriodicReportRow acct treeindent amtandgoals totamtandgoal avgamtandgoal - | PeriodicReportRow acct treeindent budgetgoals budgettot budgetavg <- budgetrows - , acct `notElem` map prrName rows1 + [ PeriodicReportRow acct amtandgoals totamtandgoal avgamtandgoal + | PeriodicReportRow acct budgetgoals budgettot budgetavg <- budgetrows + , displayFull acct `notElem` map prrFullName rows1 , let acctBudgetByPeriod = Map.fromList $ zip budgetperiods budgetgoals :: Map DateSpan BudgetGoal , let amtandgoals = [ (Nothing, Map.lookup p acctBudgetByPeriod) | p <- periods ] :: [BudgetCell] , let totamtandgoal = (Nothing, Just budgettot) @@ -230,10 +230,10 @@ combineBudgetAndActual -- TODO: respect --sort-amount -- TODO: add --sort-budget to sort by budget goal amount rows :: [BudgetReportRow] = - sortOn prrName $ rows1 ++ rows2 + sortOn prrFullName $ rows1 ++ rows2 -- TODO: grand total & average shows 0% when there are no actual amounts, inconsistent with other cells - totalrow = PeriodicReportRow () 0 + totalrow = PeriodicReportRow () [ (Map.lookup p totActualByPeriod, Map.lookup p totBudgetByPeriod) | p <- periods ] ( Just actualgrandtot, Just budgetgrandtot ) ( Just actualgrandavg, Just budgetgrandavg ) @@ -311,7 +311,7 @@ budgetReportAsText ropts@ReportOpts{..} budgetr = budgetReportAsTable :: ReportOpts -> BudgetReport -> Table String String (Maybe MixedAmount, Maybe MixedAmount) budgetReportAsTable ropts - (PeriodicReport periods rows (PeriodicReportRow _ _ coltots grandtot grandavg)) = + (PeriodicReport periods rows (PeriodicReportRow _ coltots grandtot grandavg)) = addtotalrow $ Table (T.Group NoLine $ map Header accts) @@ -322,10 +322,13 @@ budgetReportAsTable ++ [" Total" | row_total_ ropts] ++ ["Average" | average_ ropts] accts = map renderacct rows - renderacct (PeriodicReportRow a i _ _ _) - | tree_ ropts = replicate ((i-1)*2) ' ' ++ T.unpack (accountLeafName a) - | otherwise = T.unpack $ maybeAccountNameDrop ropts a - rowvals (PeriodicReportRow _ _ as rowtot rowavg) = + -- FIXME. Have to check explicitly for which to render here, since + -- budgetReport sets accountlistmode to ALTree. Find a principled way to do + -- this. + renderacct row + | tree_ ropts = replicate ((prrDepth row - 1)*2) ' ' ++ T.unpack (prrDisplayName row) + | otherwise = T.unpack . maybeAccountNameDrop ropts $ prrFullName row + rowvals (PeriodicReportRow _ as rowtot rowavg) = as ++ [rowtot | row_total_ ropts] ++ [rowavg | average_ ropts] addtotalrow | no_total_ ropts = id diff --git a/hledger-lib/Hledger/Reports/MultiBalanceReport.hs b/hledger-lib/Hledger/Reports/MultiBalanceReport.hs index a0f4a4b5e..54ebc7529 100644 --- a/hledger-lib/Hledger/Reports/MultiBalanceReport.hs +++ b/hledger-lib/Hledger/Reports/MultiBalanceReport.hs @@ -50,9 +50,7 @@ import Hledger.Reports.ReportTypes -- -- 2. a list of rows, each containing: -- --- * the full account name --- --- * the account's depth +-- * the full account name, display name, and display depth -- -- * A list of amounts, one for each column. -- @@ -63,8 +61,8 @@ import Hledger.Reports.ReportTypes -- 3. the column totals, and the overall grand total (or zero for -- cumulative/historical reports) and grand average. -type MultiBalanceReport = PeriodicReport AccountName MixedAmount -type MultiBalanceReportRow = PeriodicReportRow AccountName MixedAmount +type MultiBalanceReport = PeriodicReport DisplayName MixedAmount +type MultiBalanceReportRow = PeriodicReportRow DisplayName MixedAmount -- type alias just to remind us which AccountNames might be depth-clipped, below. type ClippedAccountName = AccountName @@ -78,7 +76,7 @@ type ClippedAccountName = AccountName -- command (in multiperiod mode) and (via multiBalanceReport') by the bs/cf/is commands. multiBalanceReport :: Day -> ReportOpts -> Journal -> MultiBalanceReport multiBalanceReport today ropts j = - multiBalanceReportWith ropts q j (journalPriceOracle infer j) + multiBalanceReportWith ropts q j (journalPriceOracle infer j) where q = queryFromOpts today ropts infer = infer_value_ ropts @@ -93,46 +91,55 @@ multiBalanceReportWith :: ReportOpts -> Query -> Journal -> PriceOracle -> Multi multiBalanceReportWith ropts@ReportOpts{..} q j priceoracle = report where -- Queries, report/column dates. - reportspan = dbg "reportspan" $ calculateReportSpan ropts q j - reportq = dbg "reportq" $ makeReportQuery ropts reportspan q + ropts' = dbg "ropts'" $ setDefaultAccountListMode ALFlat ropts + reportspan = dbg "reportspan" $ calculateReportSpan ropts' q j + reportq = dbg "reportq" $ makeReportQuery ropts' reportspan q -- 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. - startbals = dbg' "startbals" $ startingBalances ropts reportq j reportspan + startbals = dbg' "startbals" $ startingBalances ropts' reportq j reportspan -- Postings matching the query within the report period. - ps :: [(Posting, Day)] = dbg'' "ps" $ getPostings ropts reportq j + ps :: [(Posting, Day)] = dbg'' "ps" $ getPostings ropts' reportq j days = map snd ps -- The date spans to be included as report columns. - colspans = dbg "colspans" $ calculateColSpans ropts reportspan days + colspans = dbg "colspans" $ calculateColSpans ropts' reportspan days -- Group postings into their columns. colps = dbg'' "colps" $ calculateColumns colspans ps -- Each account's balance changes across all columns. - acctchanges = dbg'' "acctchanges" $ calculateAccountChanges ropts q startbals colps + acctchanges = dbg'' "acctchanges" $ calculateAccountChanges ropts' q startbals colps -- Process changes into normal, cumulative, or historical amounts, plus value them - accumvalued = dbg'' "accumvalued" $ accumValueAmounts ropts j priceoracle colspans startbals acctchanges + accumvalued = dbg'' "accumvalued" $ accumValueAmounts ropts' j priceoracle colspans startbals acctchanges -- All account names that will be displayed, possibly depth-clipped. - displayaccts = dbg'' "displayaccts" $ displayedAccounts ropts q accumvalued + displayaccts = dbg'' "displayaccts" $ displayedAccounts ropts' q accumvalued -- All the rows of the report. - rows = dbg'' "rows" $ buildReportRows ropts reportq accumvalued + rows = dbg'' "rows" $ buildReportRows ropts' reportq accumvalued -- Sorted report rows. - sortedrows = dbg' "sortedrows" $ sortRows ropts j rows + sortedrows = dbg' "sortedrows" $ sortRows ropts' j rows -- Calculate column totals - totalsrow = dbg' "totalsrow" $ calculateTotalsRow ropts displayaccts sortedrows + totalsrow = dbg' "totalsrow" $ calculateTotalsRow ropts' displayaccts sortedrows -- Postprocess the report, negating balances and taking percentages if needed - report = dbg' "report" . postprocessReport ropts $ + report = dbg' "report" . postprocessReport ropts' $ PeriodicReport colspans sortedrows totalsrow +-- | Calculate the span of the report to be generated. +setDefaultAccountListMode :: AccountListMode -> ReportOpts -> ReportOpts +setDefaultAccountListMode def ropts = ropts{accountlistmode_=mode} + where + mode = case accountlistmode_ ropts of + ALDefault -> def + a -> a + -- | Calculate the span of the report to be generated. calculateReportSpan :: ReportOpts -> Query -> Journal -> DateSpan calculateReportSpan ropts q j = reportspan @@ -312,7 +319,7 @@ buildReportRows :: ReportOpts -> Query -> HashMap AccountName [Account] -> [MultiBalanceReportRow] buildReportRows ropts q acctvalues = - [ PeriodicReportRow a (accountNameLevel a) rowbals rowtot rowavg + [ PeriodicReportRow (name a) rowbals rowtot rowavg | (a,accts) <- HM.toList acctvalues , let rowbals = map balance accts -- The total and average for the row. @@ -323,6 +330,7 @@ buildReportRows ropts q acctvalues = , empty_ ropts || queryDepth q == 0 || any (not . mixedAmountLooksZero) rowbals -- TODO: Remove this eventually, to be handled elswhere ] where + name = if tree_ ropts then treeDisplayName else flatDisplayName balance = if tree_ ropts then aibalance else aebalance -- | Calculate accounts which are to be displayed in the report, as well as @@ -363,9 +371,9 @@ sortRows ropts j sortTreeMBRByAmount :: [MultiBalanceReportRow] -> [MultiBalanceReportRow] sortTreeMBRByAmount rows = sortedrows where - anamesandrows = [(prrName r, r) | r <- rows] + anamesandrows = [(prrFullName r, r) | r <- rows] anames = map fst anamesandrows - atotals = [(prrName r, prrTotal r) | r <- rows] + atotals = [(prrFullName r, prrTotal r) | r <- rows] accounttree = accountTree "root" anames accounttreewithbals = mapAccounts setibalance accounttree where @@ -383,7 +391,7 @@ sortRows ropts j -- Sort the report rows by account declaration order then account name. sortMBRByAccountDeclaration rows = sortedrows where - anamesandrows = [(prrName r, r) | r <- rows] + anamesandrows = [(prrFullName r, r) | r <- rows] anames = map fst anamesandrows sortedanames = sortAccountNamesByDeclaration j (tree_ ropts) anames sortedrows = sortAccountItemsLike sortedanames anamesandrows @@ -394,13 +402,13 @@ sortRows ropts j calculateTotalsRow :: ReportOpts -> HashMap ClippedAccountName (ClippedAccountName, Int) -> [MultiBalanceReportRow] -> PeriodicReportRow () MixedAmount calculateTotalsRow ropts displayaccts rows = - PeriodicReportRow () 0 coltotals grandtotal grandaverage + PeriodicReportRow () coltotals grandtotal grandaverage where highestlevelaccts = HM.filterWithKey (\a _ -> isHighest a) displayaccts where isHighest = not . any (`HM.member` displayaccts) . init . expandAccountName colamts = transpose . map prrAmounts $ filter isHighest rows - where isHighest row = not (tree_ ropts) || prrName row `HM.member` highestlevelaccts + where isHighest row = not (tree_ ropts) || prrFullName row `HM.member` highestlevelaccts -- TODO: If colamts is null, then this is empty. Do we want it to be a full -- column of zeros? @@ -418,8 +426,8 @@ postprocessReport ropts (PeriodicReport spans rows totalrow) = where maybeInvert = if invert_ ropts then prNegate else id percentage = if not (percent_ ropts) then id else \case - PeriodicReportRow name d rowvals rowtotal rowavg -> - PeriodicReportRow name d + PeriodicReportRow name rowvals rowtotal rowavg -> + PeriodicReportRow name (zipWith perdivide rowvals $ prrAmounts totalrow) (perdivide rowtotal $ prrTotal totalrow) (perdivide rowavg $ prrAverage totalrow) @@ -431,16 +439,17 @@ postprocessReport ropts (PeriodicReport spans rows totalrow) = -- (see ReportOpts and CompoundBalanceCommand). balanceReportFromMultiBalanceReport :: ReportOpts -> Query -> Journal -> ([(AccountName, AccountName, Int, MixedAmount)], MixedAmount) -balanceReportFromMultiBalanceReport opts q j = (rows', total) +balanceReportFromMultiBalanceReport ropts q j = (rows', total) where - PeriodicReport _ rows (PeriodicReportRow _ _ totals _ _) = - multiBalanceReportWith opts q j (journalPriceOracle (infer_value_ opts) j) - rows' = [( a - , if flat_ opts then a else accountLeafName a -- BalanceReport expects full account name here with --flat - , if tree_ opts then d-1 else 0 -- BalanceReport uses 0-based account depths + PeriodicReport _ rows (PeriodicReportRow _ totals _ _) = + multiBalanceReportWith ropts q j (journalPriceOracle (infer_value_ ropts) j) + rows' = [( displayFull a + , leafName a + , if tree_ ropts then displayDepth a - 1 else 0 -- BalanceReport uses 0-based account depths , headDef nullmixedamt amts -- 0 columns is illegal, should not happen, return zeroes if it does - ) | PeriodicReportRow a d amts _ _ <- rows] + ) | PeriodicReportRow a amts _ _ <- rows] total = headDef nullmixedamt totals + leafName = if flat_ ropts then displayFull else displayName -- BalanceReport expects full account name here with --flat -- | Transpose a Map of HashMaps to a HashMap of Maps. @@ -519,8 +528,8 @@ tests_MultiBalanceReport = tests "MultiBalanceReport" [ (opts,journal) `gives` r = do let (eitems, etotal) = r (PeriodicReport _ aitems atotal) = multiBalanceReport nulldate opts journal - showw (PeriodicReportRow acct indent lAmt amt amt') - = (acct, accountLeafName acct, indent, map showMixedAmountDebug lAmt, showMixedAmountDebug amt, showMixedAmountDebug amt') + showw (PeriodicReportRow a lAmt amt amt') + = (displayFull a, displayName a, displayDepth a, map showMixedAmountDebug lAmt, showMixedAmountDebug amt, showMixedAmountDebug amt') (map showw aitems) @?= (map showw eitems) showMixedAmountDebug (prrTotal atotal) @?= showMixedAmountDebug etotal -- we only check the sum of the totals in @@ -531,8 +540,8 @@ tests_MultiBalanceReport = tests "MultiBalanceReport" [ ,test "with -H on a populated period" $ (defreportopts{period_= PeriodBetween (fromGregorian 2008 1 1) (fromGregorian 2008 1 2), balancetype_=HistoricalBalance}, samplejournal) `gives` ( - [ PeriodicReportRow "assets:bank:checking" 3 [mamountp' "$1.00"] (Mixed [nullamt]) (Mixed [amt0 {aquantity=1}]) - , PeriodicReportRow "income:salary" 2 [mamountp' "$-1.00"] (Mixed [nullamt]) (Mixed [amt0 {aquantity=(-1)}]) + [ PeriodicReportRow (flatDisplayName "assets:bank:checking") [mamountp' "$1.00"] (Mixed [nullamt]) (Mixed [amt0 {aquantity=1}]) + , PeriodicReportRow (flatDisplayName "income:salary") [mamountp' "$-1.00"] (Mixed [nullamt]) (Mixed [amt0 {aquantity=(-1)}]) ], Mixed [nullamt]) diff --git a/hledger-lib/Hledger/Reports/ReportTypes.hs b/hledger-lib/Hledger/Reports/ReportTypes.hs index 63f77efdc..1d4084d20 100644 --- a/hledger-lib/Hledger/Reports/ReportTypes.hs +++ b/hledger-lib/Hledger/Reports/ReportTypes.hs @@ -17,10 +17,22 @@ module Hledger.Reports.ReportTypes , periodicReportSpan , prNegate , prNormaliseSign + +, prMapName +, prMapMaybeName + +, DisplayName(..) +, flatDisplayName +, treeDisplayName + +, prrFullName +, prrDisplayName +, prrDepth ) where import Data.Aeson import Data.Decimal +import Data.Maybe (mapMaybe) import GHC.Generics (Generic) import Hledger.Data @@ -72,7 +84,6 @@ data PeriodicReport a b = data PeriodicReportRow a b = PeriodicReportRow { prrName :: a -- An account name. - , prrDepth :: Int -- Indent level for displaying this account name in tree mode. 0, 1, 2... , prrAmounts :: [b] -- The data value for each subperiod. , prrTotal :: b -- The total of this row's values. , prrAverage :: b -- The average of this row's values. @@ -94,5 +105,57 @@ prNegate :: Num b => PeriodicReport a b -> PeriodicReport a b prNegate (PeriodicReport colspans rows totalsrow) = PeriodicReport colspans (map rowNegate rows) (rowNegate totalsrow) where - rowNegate (PeriodicReportRow name indent amts tot avg) = - PeriodicReportRow name indent (map negate amts) (-tot) (-avg) + rowNegate (PeriodicReportRow name amts tot avg) = + PeriodicReportRow name (map negate amts) (-tot) (-avg) + +-- | Map a function over the row names. +prMapName :: (a -> b) -> PeriodicReport a c -> PeriodicReport b c +prMapName f report = report{prRows = map (prrMapName f) $ prRows report} + +-- | Map a function over the row names, possibly discarding some. +prMapMaybeName :: (a -> Maybe b) -> PeriodicReport a c -> PeriodicReport b c +prMapMaybeName f report = report{prRows = mapMaybe (prrMapMaybeName f) $ prRows report} + +-- | Map a function over the row names of the PeriodicReportRow. +prrMapName :: (a -> b) -> PeriodicReportRow a c -> PeriodicReportRow b c +prrMapName f row = row{prrName = f $ prrName row} + +-- | Map maybe a function over the row names of the PeriodicReportRow. +prrMapMaybeName :: (a -> Maybe b) -> PeriodicReportRow a c -> Maybe (PeriodicReportRow b c) +prrMapMaybeName f row = case f $ prrName row of + Nothing -> Nothing + Just a -> Just row{prrName = a} + + +-- | A full name, display name, and depth for an account. +data DisplayName = DisplayName + { displayFull :: AccountName + , displayName :: AccountName + , displayDepth :: Int + } deriving (Show, Eq, Ord) + +instance ToJSON DisplayName where + toJSON = toJSON . displayFull + toEncoding = toEncoding . displayFull + +-- | Construct a flat display name, where the full name is also displayed at +-- depth 0 +flatDisplayName :: AccountName -> DisplayName +flatDisplayName a = DisplayName a a 0 + +-- | Construct a tree display name, where only the leaf is displayed at its +-- given depth +treeDisplayName :: AccountName -> DisplayName +treeDisplayName a = DisplayName a (accountLeafName a) (accountNameLevel a) +-- | Get the full, canonical, name of a PeriodicReportRow tagged by a +-- DisplayName. +prrFullName :: PeriodicReportRow DisplayName a -> AccountName +prrFullName = displayFull . prrName + +-- | Get the display name of a PeriodicReportRow tagged by a DisplayName. +prrDisplayName :: PeriodicReportRow DisplayName a -> AccountName +prrDisplayName = displayName . prrName + +-- | Get the display depth of a PeriodicReportRow tagged by a DisplayName. +prrDepth :: PeriodicReportRow DisplayName a -> Int +prrDepth = displayDepth . prrName diff --git a/hledger/Hledger/Cli/Commands/Balance.hs b/hledger/Hledger/Cli/Commands/Balance.hs index e20b839fa..1b4ef5593 100644 --- a/hledger/Hledger/Cli/Commands/Balance.hs +++ b/hledger/Hledger/Cli/Commands/Balance.hs @@ -463,18 +463,18 @@ renderComponent1 opts (acctname, depth, total) (FormatField ljust min max field) -- and will include the final totals row unless --no-total is set. multiBalanceReportAsCsv :: ReportOpts -> MultiBalanceReport -> CSV multiBalanceReportAsCsv opts@ReportOpts{average_, row_total_} - (PeriodicReport colspans items (PeriodicReportRow _ _ coltotals tot avg)) = + (PeriodicReport colspans items (PeriodicReportRow _ coltotals tot avg)) = maybetranspose $ ("Account" : map showDateSpan colspans ++ ["Total" | row_total_] ++ ["Average" | average_] ) : - [T.unpack (maybeAccountNameDrop opts a) : + [T.unpack (displayFull a) : map showMixedAmountOneLineWithoutPrice (amts ++ [rowtot | row_total_] ++ [rowavg | average_]) - | PeriodicReportRow a _ amts rowtot rowavg <- items] + | PeriodicReportRow a amts rowtot rowavg <- items] ++ if no_total_ opts then [] @@ -603,7 +603,7 @@ multiBalanceReportAsText ropts@ReportOpts{..} r = -- | Build a 'Table' from a multi-column balance report. balanceReportAsTable :: ReportOpts -> MultiBalanceReport -> Table String String MixedAmount balanceReportAsTable opts@ReportOpts{average_, row_total_, balancetype_} - (PeriodicReport colspans items (PeriodicReportRow _ _ coltotals tot avg)) = + (PeriodicReport colspans items (PeriodicReportRow _ coltotals tot avg)) = maybetranspose $ addtotalrow $ Table @@ -619,10 +619,9 @@ balanceReportAsTable opts@ReportOpts{average_, row_total_, balancetype_} ++ [" Total" | totalscolumn] ++ ["Average" | average_] accts = map renderacct items - renderacct (PeriodicReportRow a i _ _ _) - | tree_ opts = replicate ((i-1)*2) ' ' ++ T.unpack (accountLeafName a) - | otherwise = T.unpack $ maybeAccountNameDrop opts a - rowvals (PeriodicReportRow _ _ as rowtot rowavg) = as + renderacct row = + replicate ((prrDepth row - 1) * 2) ' ' ++ T.unpack (prrDisplayName row) + rowvals (PeriodicReportRow _ as rowtot rowavg) = as ++ [rowtot | totalscolumn] ++ [rowavg | average_] addtotalrow | no_total_ opts = id diff --git a/hledger/Hledger/Cli/CompoundBalanceCommand.hs b/hledger/Hledger/Cli/CompoundBalanceCommand.hs index a92e9c3bc..872df830e 100644 --- a/hledger/Hledger/Cli/CompoundBalanceCommand.hs +++ b/hledger/Hledger/Cli/CompoundBalanceCommand.hs @@ -203,7 +203,7 @@ compoundBalanceCommand CompoundBalanceCommandSpec{..} opts@CliOpts{reportopts_=r -- "2008/01/01-2008/12/31", not "2008"). titledatestr | balancetype == HistoricalBalance = showEndDates enddates - | otherwise = showDateSpan requestedspan + | otherwise = showDateSpan requestedspan where enddates = map (addDays (-1)) $ catMaybes $ map spanEnd colspans -- these spans will always have a definite end date requestedspan = queryDateSpan date2_ userq `spanDefaultsFrom` journalDateSpan date2_ j @@ -271,12 +271,12 @@ compoundBalanceSubreport ropts@ReportOpts{..} userq j priceoracle subreportqfn s where nonzeroaccounts = dbg5 "nonzeroaccounts" $ - mapMaybe (\(PeriodicReportRow act _ amts _ _) -> - if not (all mixedAmountLooksZero amts) then Just act else Nothing) rows + mapMaybe (\(PeriodicReportRow act amts _ _) -> + if not (all mixedAmountLooksZero amts) then Just (displayFull act) else Nothing) rows rows' = filter (not . emptyRow) rows where - emptyRow (PeriodicReportRow act _ amts _ _) = - all mixedAmountLooksZero amts && not (any (act `isAccountNamePrefixOf`) nonzeroaccounts) + emptyRow (PeriodicReportRow act amts _ _) = + all mixedAmountLooksZero amts && not (any (displayFull act `isAccountNamePrefixOf`) nonzeroaccounts) -- | Render a compound balance report as plain text suitable for console output. {- Eg: From 6467c252c711cd609d935717728ace7fb604d1d2 Mon Sep 17 00:00:00 2001 From: Stephen Morgan Date: Sat, 13 Jun 2020 22:08:04 +1000 Subject: [PATCH 16/24] lib: multiBalanceReport: Move responsibility for display name into displayedAccounts --- .../Hledger/Reports/MultiBalanceReport.hs | 45 +++++++++++-------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/hledger-lib/Hledger/Reports/MultiBalanceReport.hs b/hledger-lib/Hledger/Reports/MultiBalanceReport.hs index 54ebc7529..bc2a89f88 100644 --- a/hledger-lib/Hledger/Reports/MultiBalanceReport.hs +++ b/hledger-lib/Hledger/Reports/MultiBalanceReport.hs @@ -128,7 +128,7 @@ multiBalanceReportWith ropts@ReportOpts{..} q j priceoracle = report totalsrow = dbg' "totalsrow" $ calculateTotalsRow ropts' displayaccts sortedrows -- Postprocess the report, negating balances and taking percentages if needed - report = dbg' "report" . postprocessReport ropts' $ + report = dbg' "report" . postprocessReport ropts' displayaccts $ PeriodicReport colspans sortedrows totalsrow @@ -319,7 +319,7 @@ buildReportRows :: ReportOpts -> Query -> HashMap AccountName [Account] -> [MultiBalanceReportRow] buildReportRows ropts q acctvalues = - [ PeriodicReportRow (name a) rowbals rowtot rowavg + [ PeriodicReportRow (flatDisplayName a) rowbals rowtot rowavg | (a,accts) <- HM.toList acctvalues , let rowbals = map balance accts -- The total and average for the row. @@ -330,25 +330,25 @@ buildReportRows ropts q acctvalues = , empty_ ropts || queryDepth q == 0 || any (not . mixedAmountLooksZero) rowbals -- TODO: Remove this eventually, to be handled elswhere ] where - name = if tree_ ropts then treeDisplayName else flatDisplayName balance = if tree_ ropts then aibalance else aebalance -- | Calculate accounts which are to be displayed in the report, as well as -- their name and depth displayedAccounts :: ReportOpts -> Query -> HashMap AccountName [Account] - -> HashMap AccountName (AccountName, Int) + -> HashMap AccountName DisplayName displayedAccounts ropts q valuedaccts = - HM.fromList $ map (\a -> (a, elidedName a)) . + HM.fromList $ map (\a -> (a, displayedName a)) $ (if tree_ ropts then expandAccountNames else id) $ nub $ map (clipOrEllipsifyAccountName depth) $ allpostedaccts where allpostedaccts = dbg'' "allpostedaccts" $ HM.keys valuedaccts - elidedName name - | depth == 0 = ("...", 0) - | otherwise = (elided, accountNameLevel name - boringParents) + displayedName name + | depth == 0 = DisplayName "..." "..." 0 + | tree_ ropts = treeDisplayName name + | otherwise = flatDisplayName name where elided = accountNameFromComponents . reverse . map accountLeafName $ name : takeWhile (not . isDisplayed) parents @@ -399,7 +399,7 @@ sortRows ropts j -- | Build the report totals row. -- -- Calculate the column totals. These are always the sum of column amounts. -calculateTotalsRow :: ReportOpts -> HashMap ClippedAccountName (ClippedAccountName, Int) +calculateTotalsRow :: ReportOpts -> HashMap ClippedAccountName DisplayName -> [MultiBalanceReportRow] -> PeriodicReportRow () MixedAmount calculateTotalsRow ropts displayaccts rows = PeriodicReportRow () coltotals grandtotal grandaverage @@ -420,17 +420,24 @@ calculateTotalsRow ropts displayaccts rows = grandaverage = averageMixedAmounts coltotals -- | Map the report rows to percentages and negate if needed -postprocessReport :: ReportOpts -> MultiBalanceReport -> MultiBalanceReport -postprocessReport ropts (PeriodicReport spans rows totalrow) = - maybeInvert $ PeriodicReport spans (map percentage rows) (percentage totalrow) +postprocessReport :: ReportOpts -> HashMap AccountName DisplayName + -> MultiBalanceReport -> MultiBalanceReport +postprocessReport ropts displaynames = + maybeInvert . maybePercent . setNames where - maybeInvert = if invert_ ropts then prNegate else id - percentage = if not (percent_ ropts) then id else \case - PeriodicReportRow name rowvals rowtotal rowavg -> - PeriodicReportRow name - (zipWith perdivide rowvals $ prrAmounts totalrow) - (perdivide rowtotal $ prrTotal totalrow) - (perdivide rowavg $ prrAverage totalrow) + setNames = prMapMaybeName $ (`HM.lookup` displaynames) . displayFull + + maybeInvert = if invert_ ropts then prNegate else id + maybePercent = if percent_ ropts then prPercent else id + + prPercent (PeriodicReport spans rows totalrow) = + PeriodicReport spans (map percentRow rows) (percentRow totalrow) + where + percentRow (PeriodicReportRow name rowvals rowtotal rowavg) = + PeriodicReportRow name + (zipWith perdivide rowvals $ prrAmounts totalrow) + (perdivide rowtotal $ prrTotal totalrow) + (perdivide rowavg $ prrAverage totalrow) -- | Generates a simple non-columnar BalanceReport, but using multiBalanceReport, From cd41404fd4588a25a559d550987f1bf626d3d0bf Mon Sep 17 00:00:00 2001 From: Stephen Morgan Date: Sat, 13 Jun 2020 22:21:14 +1000 Subject: [PATCH 17/24] lib: multiBalanceReport: Get --drop working with multiBalanceReports. --- hledger-lib/Hledger/Reports/MultiBalanceReport.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hledger-lib/Hledger/Reports/MultiBalanceReport.hs b/hledger-lib/Hledger/Reports/MultiBalanceReport.hs index bc2a89f88..293012623 100644 --- a/hledger-lib/Hledger/Reports/MultiBalanceReport.hs +++ b/hledger-lib/Hledger/Reports/MultiBalanceReport.hs @@ -348,7 +348,7 @@ displayedAccounts ropts q valuedaccts = displayedName name | depth == 0 = DisplayName "..." "..." 0 | tree_ ropts = treeDisplayName name - | otherwise = flatDisplayName name + | otherwise = DisplayName name (accountNameDrop (drop_ ropts) name) 0 where elided = accountNameFromComponents . reverse . map accountLeafName $ name : takeWhile (not . isDisplayed) parents From edb28d51c569825147859b62ebf260735d1fa008 Mon Sep 17 00:00:00 2001 From: Stephen Morgan Date: Sun, 14 Jun 2020 00:24:03 +1000 Subject: [PATCH 18/24] lib: multiBalanceReport: Get boring parent ellision working for multiBalanceReport. --- hledger-lib/Hledger/Reports/BudgetReport.hs | 4 +- .../Hledger/Reports/MultiBalanceReport.hs | 95 +++++++++++++------ 2 files changed, 68 insertions(+), 31 deletions(-) diff --git a/hledger-lib/Hledger/Reports/BudgetReport.hs b/hledger-lib/Hledger/Reports/BudgetReport.hs index 4d51dd584..e7c3d980d 100644 --- a/hledger-lib/Hledger/Reports/BudgetReport.hs +++ b/hledger-lib/Hledger/Reports/BudgetReport.hs @@ -72,9 +72,9 @@ budgetReport ropts' assrt reportspan d j = actualj = dbg1With (("actualj"++).show.jtxns) $ budgetRollUp budgetedaccts showunbudgeted j budgetj = dbg1With (("budgetj"++).show.jtxns) $ budgetJournal assrt ropts reportspan j actualreport@(PeriodicReport actualspans _ _) = - dbg1 "actualreport" $ multiBalanceReport d ropts actualj + dbg1 "actualreport" $ multiBalanceReport d ropts{empty_=True} actualj budgetgoalreport@(PeriodicReport _ budgetgoalitems budgetgoaltotals) = - dbg1 "budgetgoalreport" $ multiBalanceReport d (ropts{empty_=True}) budgetj + dbg1 "budgetgoalreport" $ multiBalanceReport d ropts{empty_=True} budgetj budgetgoalreport' -- If no interval is specified: -- budgetgoalreport's span might be shorter actualreport's due to periodic txns; diff --git a/hledger-lib/Hledger/Reports/MultiBalanceReport.hs b/hledger-lib/Hledger/Reports/MultiBalanceReport.hs index 293012623..b4c14a0f0 100644 --- a/hledger-lib/Hledger/Reports/MultiBalanceReport.hs +++ b/hledger-lib/Hledger/Reports/MultiBalanceReport.hs @@ -88,7 +88,7 @@ multiBalanceReport today ropts j = -- once for efficiency, passing it to each report by calling this -- function directly. multiBalanceReportWith :: ReportOpts -> Query -> Journal -> PriceOracle -> MultiBalanceReport -multiBalanceReportWith ropts@ReportOpts{..} q j priceoracle = report +multiBalanceReportWith ropts q j priceoracle = report where -- Queries, report/column dates. ropts' = dbg "ropts'" $ setDefaultAccountListMode ALFlat ropts @@ -110,7 +110,7 @@ multiBalanceReportWith ropts@ReportOpts{..} q j priceoracle = report colps = dbg'' "colps" $ calculateColumns colspans ps -- Each account's balance changes across all columns. - acctchanges = dbg'' "acctchanges" $ calculateAccountChanges ropts' q startbals colps + acctchanges = dbg'' "acctchanges" $ calculateAccountChanges ropts' q colspans startbals colps -- Process changes into normal, cumulative, or historical amounts, plus value them accumvalued = dbg'' "accumvalued" $ accumValueAmounts ropts' j priceoracle colspans startbals acctchanges @@ -119,7 +119,7 @@ multiBalanceReportWith ropts@ReportOpts{..} q j priceoracle = report displayaccts = dbg'' "displayaccts" $ displayedAccounts ropts' q accumvalued -- All the rows of the report. - rows = dbg'' "rows" $ buildReportRows ropts' reportq accumvalued + rows = dbg'' "rows" $ buildReportRows ropts' accumvalued -- Sorted report rows. sortedrows = dbg' "sortedrows" $ sortRows ropts' j rows @@ -253,11 +253,13 @@ acctChangesFromPostings ropts q ps = HM.fromList [(aname a, a) | a <- as] -- | Gather the account balance changes into a regular matrix including the accounts -- from all columns -calculateAccountChanges :: ReportOpts -> Query +calculateAccountChanges :: ReportOpts -> Query -> [DateSpan] -> HashMap ClippedAccountName Account -> Map DateSpan [Posting] -> HashMap ClippedAccountName (Map DateSpan Account) -calculateAccountChanges ropts q startbals colps = acctchanges +calculateAccountChanges ropts q colspans startbals colps + | queryDepth q == 0 = acctchanges <> elided + | otherwise = acctchanges where -- Transpose to get each account's balance changes across all columns. acctchanges = transposeMap colacctchanges <> (mempty <$ startbals) @@ -265,6 +267,8 @@ calculateAccountChanges ropts q startbals colps = acctchanges colacctchanges :: Map DateSpan (HashMap ClippedAccountName Account) = dbg'' "colacctchanges" $ fmap (acctChangesFromPostings ropts q) colps + elided = HM.singleton "..." $ M.fromList [(span, nullacct) | span <- colspans] + -- | Accumulate and value amounts, as specified by the report options. -- -- Makes sure all report columns have an entry. @@ -315,10 +319,8 @@ accumValueAmounts ropts j priceoracle colspans startbals = HM.mapWithKey process -- | Build the report rows. -- -- One row per account, with account name info, row amounts, row total and row average. -buildReportRows :: ReportOpts -> Query - -> HashMap AccountName [Account] - -> [MultiBalanceReportRow] -buildReportRows ropts q acctvalues = +buildReportRows :: ReportOpts -> HashMap AccountName [Account] -> [MultiBalanceReportRow] +buildReportRows ropts acctvalues = [ PeriodicReportRow (flatDisplayName a) rowbals rowtot rowavg | (a,accts) <- HM.toList acctvalues , let rowbals = map balance accts @@ -327,35 +329,62 @@ buildReportRows ropts q acctvalues = -- Total for a cumulative/historical report is always zero. , let rowtot = if balancetype_ ropts == PeriodChange then sum rowbals else 0 , let rowavg = averageMixedAmounts rowbals - , empty_ ropts || queryDepth q == 0 || any (not . mixedAmountLooksZero) rowbals -- TODO: Remove this eventually, to be handled elswhere ] - where - balance = if tree_ ropts then aibalance else aebalance + where balance = if tree_ ropts then aibalance else aebalance -- | Calculate accounts which are to be displayed in the report, as well as -- their name and depth displayedAccounts :: ReportOpts -> Query -> HashMap AccountName [Account] -> HashMap AccountName DisplayName -displayedAccounts ropts q valuedaccts = - HM.fromList $ map (\a -> (a, displayedName a)) $ - (if tree_ ropts then expandAccountNames else id) $ - nub $ map (clipOrEllipsifyAccountName depth) $ - allpostedaccts +displayedAccounts ropts q valuedaccts + | depth == 0 = HM.singleton "..." $ DisplayName "..." "..." 0 + | otherwise = HM.mapWithKey (\a _ -> displayedName a) displayedAccts where - allpostedaccts = dbg'' "allpostedaccts" $ HM.keys valuedaccts + -- Accounts which are to be displayed + displayedAccts = HM.filterWithKey keep (valuedaccts <> allParents) + where + keep name amts = isInteresting name amts || isInterestingParent name + + isDisplayed = (`HM.member` displayedAccts) displayedName name - | depth == 0 = DisplayName "..." "..." 0 - | tree_ ropts = treeDisplayName name - | otherwise = DisplayName name (accountNameDrop (drop_ ropts) name) 0 + | flat_ ropts = DisplayName name (accountNameDrop (drop_ ropts) name) 0 + | otherwise = DisplayName name leaf d where - elided = accountNameFromComponents . reverse . map accountLeafName $ + leaf = accountNameFromComponents . reverse . map accountLeafName $ name : takeWhile (not . isDisplayed) parents - boringParents = length $ filter (not . isDisplayed) parents + d | no_elide_ ropts = accountNameLevel name + | otherwise = accountNameLevel name - length boringParents + boringParents = filter (not . isDisplayed) parents parents = parentAccountNames name - isDisplayed = const True + -- Accounts interesting for their own sake + interestingAccounts = dbg'' "interestingAccounts" $ + HM.filterWithKey isInteresting valuedaccts + + isInteresting name amts = + d <= depth -- Throw out anything too deep + && (keepEmpty || not (isZeroRow balance amts)) -- Boring because has only zero entries + where + d = accountNameLevel name + balance = if tree_ ropts && d == depth then aibalance else aebalance + + -- Accounts interesting because they are a fork for interesting subaccounts + interestingParents = dbg'' "interestingParents" $ + forkingAccounts $ HM.keys interestingAccounts + + isInterestingParent + | flat_ ropts = const False + | empty_ ropts || no_elide_ ropts = const True + | otherwise = (`HM.member` interestingParents) + + allParents + | tree_ ropts = HM.fromList [(a,[]) | a <- expandAccountNames $ HM.keys interestingAccounts] + | otherwise = mempty + + isZeroRow balance = all (mixedAmountLooksZero . balance) + keepEmpty = empty_ ropts || depth == 0 depth = queryDepth q -- | Sort the rows by amount or by account declaration order. This is a bit tricky. @@ -449,14 +478,15 @@ balanceReportFromMultiBalanceReport :: ReportOpts -> Query -> Journal balanceReportFromMultiBalanceReport ropts q j = (rows', total) where PeriodicReport _ rows (PeriodicReportRow _ totals _ _) = - multiBalanceReportWith ropts q j (journalPriceOracle (infer_value_ ropts) j) + multiBalanceReportWith ropts' q j (journalPriceOracle (infer_value_ ropts) j) rows' = [( displayFull a , leafName a - , if tree_ ropts then displayDepth a - 1 else 0 -- BalanceReport uses 0-based account depths + , if tree_ ropts' then displayDepth a - 1 else 0 -- BalanceReport uses 0-based account depths , headDef nullmixedamt amts -- 0 columns is illegal, should not happen, return zeroes if it does ) | PeriodicReportRow a amts _ _ <- rows] total = headDef nullmixedamt totals - leafName = if flat_ ropts then displayFull else displayName -- BalanceReport expects full account name here with --flat + leafName = if flat_ ropts' then displayFull else displayName -- BalanceReport expects full account name here with --flat + ropts' = setDefaultAccountListMode ALTree ropts -- | Transpose a Map of HashMaps to a HashMap of Maps. @@ -474,8 +504,15 @@ transposeMap xs = M.foldrWithKey addSpan mempty xs -- | A sorting helper: sort a list of things (eg report rows) keyed by account name -- to match the provided ordering of those same account names. sortAccountItemsLike :: [AccountName] -> [(AccountName, b)] -> [b] -sortAccountItemsLike sortedas items = - concatMap (\a -> maybe [] (:[]) $ lookup a items) sortedas +sortAccountItemsLike sortedas items = mapMaybe (`lookup` items) sortedas + +-- | Given a list of account names, find all forking parent accounts, i.e. +-- those which fork between different branches +forkingAccounts :: [AccountName] -> HashMap AccountName Int +forkingAccounts as = HM.filter (>1) $ foldr incrementParent mempty allaccts + where + allaccts = expandAccountNames as + incrementParent a = HM.insertWith (+) (parentAccountName a) 1 -- | Helper to unify a MixedAmount to a single commodity value. -- Like normaliseMixedAmount, this consolidates amounts of the same commodity From e079c8b8081bd9baf753dbcd6dc7b9c84557521a Mon Sep 17 00:00:00 2001 From: Stephen Morgan Date: Sun, 14 Jun 2020 00:44:22 +1000 Subject: [PATCH 19/24] lib: multiBalanceReport: Remove old balanceReport code, update some tests. --- hledger-lib/Hledger/Reports/BalanceReport.hs | 175 +----------------- hledger-lib/Hledger/Reports/BudgetReport.hs | 1 - .../Hledger/Reports/MultiBalanceReport.hs | 5 +- hledger/Hledger/Cli/Commands/Balance.hs | 4 +- tests/balance/373-layout.test | 14 +- tests/balance/sorting.test | 6 +- 6 files changed, 22 insertions(+), 183 deletions(-) diff --git a/hledger-lib/Hledger/Reports/BalanceReport.hs b/hledger-lib/Hledger/Reports/BalanceReport.hs index a1027969d..37b4e64df 100644 --- a/hledger-lib/Hledger/Reports/BalanceReport.hs +++ b/hledger-lib/Hledger/Reports/BalanceReport.hs @@ -11,18 +11,12 @@ module Hledger.Reports.BalanceReport ( BalanceReportItem, balanceReport, flatShowsExclusiveBalance, - sortAccountItemsLike, - unifyMixedAmount, - perdivide, -- * Tests tests_BalanceReport ) where -import Data.List -import Data.Ord -import Data.Maybe import Data.Time.Calendar import Hledger.Data @@ -30,6 +24,7 @@ import Hledger.Read (mamountp') import Hledger.Query import Hledger.Utils import Hledger.Reports.ReportOptions +import Hledger.Reports.MultiBalanceReport (balanceReportFromMultiBalanceReport) -- | A simple balance report. It has: @@ -66,166 +61,8 @@ flatShowsExclusiveBalance = True -- This is like PeriodChangeReport with a single column (but more mature, -- eg this can do hierarchical display). balanceReport :: ReportOpts -> Query -> Journal -> BalanceReport -balanceReport ropts@ReportOpts{..} q j = - (if invert_ then brNegate else id) $ - (mappedsorteditems, mappedtotal) - where - -- dbg = const id -- exclude from debug output - dbg s = let p = "balanceReport" in Hledger.Utils.dbg4 (p++" "++s) -- add prefix in debug output - dbg' s = let p = "balanceReport" in Hledger.Utils.dbg5 (p++" "++s) -- add prefix in debug output +balanceReport = balanceReportFromMultiBalanceReport - -- Get all the summed accounts & balances, according to the query, as an account tree. - -- If doing cost valuation, amounts will be converted to cost first. - accttree = ledgerRootAccount $ ledgerFromJournal q $ journalSelectingAmountFromOpts ropts j - - -- For other kinds of valuation, convert the summed amounts to value, - -- per hledger_options.m4.md "Effect of --value on reports". - valuedaccttree = mapAccounts avalue accttree - where - avalue a@Account{..} = a{aebalance=maybevalue aebalance, aibalance=maybevalue aibalance} - where - maybevalue = maybe id applyvaluation value_ - where - applyvaluation = mixedAmountApplyValuation priceoracle styles periodlast mreportlast today multiperiod - where - priceoracle = journalPriceOracle infer_value_ j - styles = journalCommodityStyles j - periodlast = fromMaybe - (error' "balanceReport: expected a non-empty journal") $ -- XXX shouldn't happen - reportPeriodOrJournalLastDay ropts j - mreportlast = reportPeriodLastDay ropts - today = fromMaybe (error' "balanceReport: could not pick a valuation date, ReportOpts today_ is unset") today_ - multiperiod = interval_ /= NoInterval - - -- Modify this tree for display - depth limit, boring parents, zeroes - and convert to a list. - displayaccts :: [Account] - | queryDepth q == 0 = - dbg' "displayaccts" $ - take 1 $ clipAccountsAndAggregate (queryDepth q) $ flattenAccounts valuedaccttree - | flat_ ropts = dbg' "displayaccts" $ - filterzeros $ - filterempty $ - drop 1 $ clipAccountsAndAggregate (queryDepth q) $ flattenAccounts valuedaccttree - | otherwise = dbg' "displayaccts" $ - filter (not.aboring) $ - drop 1 $ flattenAccounts $ - markboring $ - prunezeros $ - sortAccountTreeByAmount (fromMaybe NormallyPositive normalbalance_) $ - clipAccounts (queryDepth q) valuedaccttree - where - balance = if flat_ ropts then aebalance else aibalance - filterzeros = if empty_ then id else filter (not . mixedAmountLooksZero . balance) - filterempty = filter (\a -> anumpostings a > 0 || not (mixedAmountLooksZero (balance a))) - prunezeros = if empty_ then id else fromMaybe nullacct . pruneAccounts (mixedAmountLooksZero . balance) - markboring = if no_elide_ then id else markBoringParentAccounts - - -- Make a report row for each account. - items = dbg "items" $ map (balanceReportItem ropts q) displayaccts - - -- Sort report rows (except sorting by amount in tree mode, which was done above). - sorteditems - | sort_amount_ && tree_ ropts = items - | sort_amount_ = sortFlatBRByAmount items - | otherwise = sortBRByAccountDeclaration items - where - -- Sort the report rows, representing a flat account list, by row total. - sortFlatBRByAmount :: [BalanceReportItem] -> [BalanceReportItem] - sortFlatBRByAmount = sortBy (maybeflip $ comparing (normaliseMixedAmountSquashPricesForDisplay . fourth4)) - where - maybeflip = if normalbalance_ == Just NormallyNegative then id else flip - -- Sort the report rows by account declaration order then account name. - sortBRByAccountDeclaration :: [BalanceReportItem] -> [BalanceReportItem] - sortBRByAccountDeclaration rows = sortedrows - where - anamesandrows = [(first4 r, r) | r <- rows] - anames = map fst anamesandrows - sortedanames = sortAccountNamesByDeclaration j (tree_ ropts) anames - sortedrows = sortAccountItemsLike sortedanames anamesandrows - - -- Calculate the grand total. - total | not (flat_ ropts) = dbg "total" $ sum [amt | (_,_,indent,amt) <- items, indent == 0] - | otherwise = dbg "total" $ - if flatShowsExclusiveBalance - then sum $ map fourth4 items - else sum $ map aebalance $ clipAccountsAndAggregate 1 displayaccts - - -- Calculate percentages if needed. - mappedtotal | percent_ = dbg "mappedtotal" $ total `perdivide` total - | otherwise = total - mappedsorteditems | percent_ = - dbg "mappedsorteditems" $ - map (\(fname, sname, indent, amount) -> (fname, sname, indent, amount `perdivide` total)) sorteditems - | otherwise = sorteditems - --- | A sorting helper: sort a list of things (eg report rows) keyed by account name --- to match the provided ordering of those same account names. -sortAccountItemsLike :: [AccountName] -> [(AccountName, b)] -> [b] -sortAccountItemsLike sortedas items = - concatMap (\a -> maybe [] (:[]) $ lookup a items) sortedas - --- | In an account tree with zero-balance leaves removed, mark the --- elidable parent accounts (those with one subaccount and no balance --- of their own). -markBoringParentAccounts :: Account -> Account -markBoringParentAccounts = tieAccountParents . mapAccounts mark - where - mark a | length (asubs a) == 1 && mixedAmountLooksZero (aebalance a) = a{aboring=True} - | otherwise = a - -balanceReportItem :: ReportOpts -> Query -> Account -> BalanceReportItem -balanceReportItem opts q a - | flat_ opts = (name, name, 0, (if flatShowsExclusiveBalance then aebalance else aibalance) a) - | otherwise = (name, elidedname, indent, aibalance a) - where - name | queryDepth q > 0 = aname a - | otherwise = "..." - elidedname = accountNameFromComponents (adjacentboringparentnames ++ [accountLeafName name]) - adjacentboringparentnames = reverse $ map (accountLeafName.aname) $ takeWhile aboring parents - indent = length $ filter (not.aboring) parents - -- parents exclude the tree's root node - parents = case parentAccounts a of [] -> [] - as -> init as - --- -- the above using the newer multi balance report code: --- balanceReport' opts q j = (items, total) --- where --- MultiBalanceReport (_,mbrrows,mbrtotals) = PeriodChangeReport opts q j --- items = [(a,a',n, headDef 0 bs) | ((a,a',n), bs) <- mbrrows] --- total = headDef 0 mbrtotals - --- | Flip the sign of all amounts in a BalanceReport. -brNegate :: BalanceReport -> BalanceReport -brNegate (is, tot) = (map brItemNegate is, -tot) - where - brItemNegate (a, a', d, amt) = (a, a', d, -amt) - --- | Helper to unify a MixedAmount to a single commodity value. --- Like normaliseMixedAmount, this consolidates amounts of the same commodity --- and discards zero amounts; but this one insists on simplifying to --- a single commodity, and will throw a program-terminating error if --- this is not possible. -unifyMixedAmount :: MixedAmount -> Amount -unifyMixedAmount mixedAmount = foldl combine (num 0) (amounts mixedAmount) - where - combine amount result = - if amountIsZero amount - then result - else if amountIsZero result - then amount - else if acommodity amount == acommodity result - then amount + result - else error' "Cannot calculate percentages for accounts with multiple commodities. (Hint: Try --cost, -V or similar flags.)" - --- | Helper to calculate the percentage from two mixed. Keeps the sign of the first argument. --- Uses unifyMixedAmount to unify each argument and then divides them. -perdivide :: MixedAmount -> MixedAmount -> MixedAmount -perdivide a b = - let a' = unifyMixedAmount a - b' = unifyMixedAmount b - in if amountIsZero a' || amountIsZero b' || acommodity a' == acommodity b' - then mixed [per $ if aquantity b' == 0 then 0 else (aquantity a' / abs (aquantity b') * 100)] - else error' "Cannot calculate percentages if accounts have different commodities. (Hint: Try --cost, -V or similar flags.)" -- tests @@ -259,13 +96,13 @@ tests_BalanceReport = tests "BalanceReport" [ let (eitems, etotal) = r (aitems, atotal) = balanceReport opts (queryFromOpts nulldate opts) journal showw (acct,acct',indent,amt) = (acct, acct', indent, showMixedAmountDebug amt) - (map showw eitems) @?= (map showw aitems) + (map showw aitems) @?= (map showw eitems) (showMixedAmountDebug etotal) @?= (showMixedAmountDebug atotal) in tests "balanceReport" [ test "no args, null journal" $ - (defreportopts, nulljournal) `gives` ([], Mixed [nullamt]) + (defreportopts, nulljournal) `gives` ([], Mixed []) ,test "no args, sample journal" $ (defreportopts, samplejournal) `gives` @@ -303,7 +140,7 @@ tests_BalanceReport = tests "BalanceReport" [ ,test "with date:" $ (defreportopts{query_="date:'in 2009'"}, samplejournal2) `gives` ([], - Mixed [nullamt]) + Mixed []) ,test "with date2:" $ (defreportopts{query_="date2:'in 2009'"}, samplejournal2) `gives` @@ -345,7 +182,7 @@ tests_BalanceReport = tests "BalanceReport" [ ,test "with period on an unpopulated period" $ (defreportopts{period_= PeriodBetween (fromGregorian 2008 1 2) (fromGregorian 2008 1 3)}, samplejournal) `gives` - ([],Mixed [nullamt]) + ([],Mixed []) diff --git a/hledger-lib/Hledger/Reports/BudgetReport.hs b/hledger-lib/Hledger/Reports/BudgetReport.hs index e7c3d980d..446d8ae50 100644 --- a/hledger-lib/Hledger/Reports/BudgetReport.hs +++ b/hledger-lib/Hledger/Reports/BudgetReport.hs @@ -37,7 +37,6 @@ import Hledger.Utils --import Hledger.Read (mamountp') import Hledger.Reports.ReportOptions import Hledger.Reports.ReportTypes -import Hledger.Reports.BalanceReport (sortAccountItemsLike) import Hledger.Reports.MultiBalanceReport diff --git a/hledger-lib/Hledger/Reports/MultiBalanceReport.hs b/hledger-lib/Hledger/Reports/MultiBalanceReport.hs index b4c14a0f0..2f37cf718 100644 --- a/hledger-lib/Hledger/Reports/MultiBalanceReport.hs +++ b/hledger-lib/Hledger/Reports/MultiBalanceReport.hs @@ -18,6 +18,8 @@ module Hledger.Reports.MultiBalanceReport ( balanceReportFromMultiBalanceReport, tableAsText, + sortAccountItemsLike, + -- -- * Tests tests_MultiBalanceReport ) @@ -480,12 +482,11 @@ balanceReportFromMultiBalanceReport ropts q j = (rows', total) PeriodicReport _ rows (PeriodicReportRow _ totals _ _) = multiBalanceReportWith ropts' q j (journalPriceOracle (infer_value_ ropts) j) rows' = [( displayFull a - , leafName a + , displayName a , if tree_ ropts' then displayDepth a - 1 else 0 -- BalanceReport uses 0-based account depths , headDef nullmixedamt amts -- 0 columns is illegal, should not happen, return zeroes if it does ) | PeriodicReportRow a amts _ _ <- rows] total = headDef nullmixedamt totals - leafName = if flat_ ropts' then displayFull else displayName -- BalanceReport expects full account name here with --flat ropts' = setDefaultAccountListMode ALTree ropts diff --git a/hledger/Hledger/Cli/Commands/Balance.hs b/hledger/Hledger/Cli/Commands/Balance.hs index 1b4ef5593..b6146a866 100644 --- a/hledger/Hledger/Cli/Commands/Balance.hs +++ b/hledger/Hledger/Cli/Commands/Balance.hs @@ -355,7 +355,7 @@ balance opts@CliOpts{rawopts_=rawopts,reportopts_=ropts@ReportOpts{..}} j = do balanceReportAsCsv :: ReportOpts -> BalanceReport -> CSV balanceReportAsCsv opts (items, total) = ["account","balance"] : - [[T.unpack (maybeAccountNameDrop opts a), showMixedAmountOneLineWithoutPrice b] | (a, _, _, b) <- items] + [[T.unpack a, showMixedAmountOneLineWithoutPrice b] | (a, _, _, b) <- items] ++ if no_total_ opts then [] @@ -404,7 +404,7 @@ This implementation turned out to be a bit convoluted but implements the followi balanceReportItemAsText :: ReportOpts -> StringFormat -> BalanceReportItem -> [String] balanceReportItemAsText opts fmt (_, accountName, depth, amt) = renderBalanceReportItem opts fmt ( - maybeAccountNameDrop opts accountName, + accountName, depth, normaliseMixedAmountSquashPricesForDisplay amt ) diff --git a/tests/balance/373-layout.test b/tests/balance/373-layout.test index 2ee042839..214957d4d 100644 --- a/tests/balance/373-layout.test +++ b/tests/balance/373-layout.test @@ -87,12 +87,14 @@ Balance changes in 2015: $ hledger -f - bal -Y --tree Balance changes in 2015: - || 2015 -===========++====== - 3 || 1 - 5 || 1 ------------++------ - || + || 2015 +=========++====== + 1:2 || 0 + 3 || 1 + 4 || 0 + 5 || 1 +---------++------ + || 0 # 6. TODO: after 5, test account code sorting # account 1:2:3 100 diff --git a/tests/balance/sorting.test b/tests/balance/sorting.test index dead581e1..8d328337a 100644 --- a/tests/balance/sorting.test +++ b/tests/balance/sorting.test @@ -32,7 +32,7 @@ Balance changes in 2018: >= # 2. Tree mode. Missing parent accounts are added (b). -$ hledger -f- bal -NY --tree +$ hledger -f- bal -NY --tree --no-elide Balance changes in 2018: || 2018 @@ -90,7 +90,7 @@ Balance changes in 2018: # 4. With account directives, tree mode. # Missing parent accounts are added (b). -$ hledger -f- bal -NY --tree +$ hledger -f- bal -NY --tree --no-elide Balance changes in 2018: || 2018 @@ -141,7 +141,7 @@ Balance changes in 2018: 2018/1/1 (a:k) 1 -$ hledger -f- bal -NY --sort-amount --tree +$ hledger -f- bal -NY --sort-amount --tree --no-elide Balance changes in 2018: || 2018 From fcaec1540a5bef610906175f11566f1332bf33fa Mon Sep 17 00:00:00 2001 From: Stephen Morgan Date: Sun, 14 Jun 2020 01:23:55 +1000 Subject: [PATCH 20/24] lib: multiBalanceReport: Remove unnecessary addition of parent accounts. --- hledger-lib/Hledger/Reports/MultiBalanceReport.hs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/hledger-lib/Hledger/Reports/MultiBalanceReport.hs b/hledger-lib/Hledger/Reports/MultiBalanceReport.hs index 2f37cf718..7dfacf987 100644 --- a/hledger-lib/Hledger/Reports/MultiBalanceReport.hs +++ b/hledger-lib/Hledger/Reports/MultiBalanceReport.hs @@ -344,7 +344,7 @@ displayedAccounts ropts q valuedaccts | otherwise = HM.mapWithKey (\a _ -> displayedName a) displayedAccts where -- Accounts which are to be displayed - displayedAccts = HM.filterWithKey keep (valuedaccts <> allParents) + displayedAccts = HM.filterWithKey keep valuedaccts where keep name amts = isInteresting name amts || isInterestingParent name @@ -381,10 +381,6 @@ displayedAccounts ropts q valuedaccts | empty_ ropts || no_elide_ ropts = const True | otherwise = (`HM.member` interestingParents) - allParents - | tree_ ropts = HM.fromList [(a,[]) | a <- expandAccountNames $ HM.keys interestingAccounts] - | otherwise = mempty - isZeroRow balance = all (mixedAmountLooksZero . balance) keepEmpty = empty_ ropts || depth == 0 depth = queryDepth q From 5168d136ed9cf4f4519aade98bad3f1d58059cac Mon Sep 17 00:00:00 2001 From: Stephen Morgan Date: Mon, 15 Jun 2020 14:44:12 +1000 Subject: [PATCH 21/24] lib: multiBalanceReport: Enable --drop for tree mode. --- .../Hledger/Reports/MultiBalanceReport.hs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/hledger-lib/Hledger/Reports/MultiBalanceReport.hs b/hledger-lib/Hledger/Reports/MultiBalanceReport.hs index 7dfacf987..e7ff30ad3 100644 --- a/hledger-lib/Hledger/Reports/MultiBalanceReport.hs +++ b/hledger-lib/Hledger/Reports/MultiBalanceReport.hs @@ -351,15 +351,16 @@ displayedAccounts ropts q valuedaccts isDisplayed = (`HM.member` displayedAccts) displayedName name - | flat_ ropts = DisplayName name (accountNameDrop (drop_ ropts) name) 0 + | flat_ ropts = DisplayName name droppedName 0 | otherwise = DisplayName name leaf d where leaf = accountNameFromComponents . reverse . map accountLeafName $ - name : takeWhile (not . isDisplayed) parents - d | no_elide_ ropts = accountNameLevel name - | otherwise = accountNameLevel name - length boringParents + droppedName : takeWhile (not . isDisplayed) parents + d | no_elide_ ropts = accountNameLevel droppedName + | otherwise = accountNameLevel droppedName - length boringParents boringParents = filter (not . isDisplayed) parents - parents = parentAccountNames name + parents = parentAccountNames droppedName + droppedName = accountNameDrop (drop_ ropts) name -- Accounts interesting for their own sake interestingAccounts = dbg'' "interestingAccounts" $ @@ -374,7 +375,8 @@ displayedAccounts ropts q valuedaccts -- Accounts interesting because they are a fork for interesting subaccounts interestingParents = dbg'' "interestingParents" $ - forkingAccounts $ HM.keys interestingAccounts + HM.filterWithKey (\name i -> i > 1 && accountNameLevel name > drop_ ropts) . + subaccountTallies $ HM.keys interestingAccounts isInterestingParent | flat_ ropts = const False @@ -505,8 +507,8 @@ sortAccountItemsLike sortedas items = mapMaybe (`lookup` items) sortedas -- | Given a list of account names, find all forking parent accounts, i.e. -- those which fork between different branches -forkingAccounts :: [AccountName] -> HashMap AccountName Int -forkingAccounts as = HM.filter (>1) $ foldr incrementParent mempty allaccts +subaccountTallies :: [AccountName] -> HashMap AccountName Int +subaccountTallies as = foldr incrementParent mempty allaccts where allaccts = expandAccountNames as incrementParent a = HM.insertWith (+) (parentAccountName a) 1 From 7a5416928ee212b089d01b054a43082f27423c91 Mon Sep 17 00:00:00 2001 From: Stephen Morgan Date: Tue, 16 Jun 2020 19:57:09 +1000 Subject: [PATCH 22/24] cli: multiBalanceReport: Remove redundant code branch. --- hledger/Hledger/Cli/Commands/Balance.hs | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/hledger/Hledger/Cli/Commands/Balance.hs b/hledger/Hledger/Cli/Commands/Balance.hs index b6146a866..36246cf4c 100644 --- a/hledger/Hledger/Cli/Commands/Balance.hs +++ b/hledger/Hledger/Cli/Commands/Balance.hs @@ -314,9 +314,9 @@ balance opts@CliOpts{rawopts_=rawopts,reportopts_=ropts@ReportOpts{..}} j = do if budget then do -- single or multi period budget report reportspan <- reportSpan j ropts - let budgetreport = dbg1 "budgetreport" $ budgetReport ropts assrt reportspan d j + let budgetreport = dbg4 "budgetreport" $ budgetReport ropts assrt reportspan d j where - assrt = not $ ignore_assertions_ $ inputopts_ opts + assrt = not $ ignore_assertions_ $ inputopts_ opts render = case fmt of "txt" -> budgetReportAsText ropts "json" -> (++"\n") . TL.unpack . toJsonText @@ -335,13 +335,7 @@ balance opts@CliOpts{rawopts_=rawopts,reportopts_=ropts@ReportOpts{..}} j = do writeOutput opts $ render report else do -- single period simple balance report - let report - | balancetype_ `elem` [HistoricalBalance, CumulativeChange] - = let ropts' | flat_ ropts = ropts - | otherwise = ropts{accountlistmode_=ALTree} - in balanceReportFromMultiBalanceReport ropts' (queryFromOpts d ropts) j - -- for historical balances we must use balanceReportFromMultiBalanceReport (also forces --no-elide) - | otherwise = balanceReport ropts (queryFromOpts d ropts) j -- simple Ledger-style balance report + let report = balanceReport ropts (queryFromOpts d ropts) j -- simple Ledger-style balance report render = case fmt of "txt" -> balanceReportAsText "csv" -> \ropts r -> (++ "\n") $ printCSV $ balanceReportAsCsv ropts r From 826faa18c25caa7b39a1fb87e6a69ba3837a0268 Mon Sep 17 00:00:00 2001 From: Stephen Morgan Date: Mon, 22 Jun 2020 23:08:22 +1000 Subject: [PATCH 23/24] lib: Tweaks to work with stack-8.2. --- hledger-lib/Hledger/Reports/MultiBalanceReport.hs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/hledger-lib/Hledger/Reports/MultiBalanceReport.hs b/hledger-lib/Hledger/Reports/MultiBalanceReport.hs index e7ff30ad3..ad585d7a6 100644 --- a/hledger-lib/Hledger/Reports/MultiBalanceReport.hs +++ b/hledger-lib/Hledger/Reports/MultiBalanceReport.hs @@ -1,5 +1,5 @@ +{-# LANGUAGE CPP #-} {-# LANGUAGE FlexibleInstances #-} -{-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE ScopedTypeVariables #-} @@ -32,6 +32,9 @@ import Data.Map (Map) import qualified Data.Map as M import Data.Maybe import Data.Ord +#if !(MIN_VERSION_base(4,11,0)) +import Data.Semigroup ((<>)) +#endif import Data.Time.Calendar import Safe import Text.Tabular as T From ded4f42218c19c665fb18f15e01eec2a6a717c66 Mon Sep 17 00:00:00 2001 From: Stephen Morgan Date: Tue, 23 Jun 2020 12:31:34 +1000 Subject: [PATCH 24/24] lib: multiBalanceReport: Accounts with no subaccounts are never interesting parents, even if no_elide_ is set. --- .../Hledger/Reports/MultiBalanceReport.hs | 25 ++++++++----------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/hledger-lib/Hledger/Reports/MultiBalanceReport.hs b/hledger-lib/Hledger/Reports/MultiBalanceReport.hs index ad585d7a6..72bf2752d 100644 --- a/hledger-lib/Hledger/Reports/MultiBalanceReport.hs +++ b/hledger-lib/Hledger/Reports/MultiBalanceReport.hs @@ -349,7 +349,7 @@ displayedAccounts ropts q valuedaccts -- Accounts which are to be displayed displayedAccts = HM.filterWithKey keep valuedaccts where - keep name amts = isInteresting name amts || isInterestingParent name + keep name amts = isInteresting name amts || name `HM.member` interestingParents isDisplayed = (`HM.member` displayedAccts) @@ -366,28 +366,23 @@ displayedAccounts ropts q valuedaccts droppedName = accountNameDrop (drop_ ropts) name -- Accounts interesting for their own sake - interestingAccounts = dbg'' "interestingAccounts" $ - HM.filterWithKey isInteresting valuedaccts - isInteresting name amts = - d <= depth -- Throw out anything too deep - && (keepEmpty || not (isZeroRow balance amts)) -- Boring because has only zero entries + d <= depth -- Throw out anything too deep + && (empty_ ropts || depth == 0 || not (isZeroRow balance amts)) -- Boring because has only zero entries where d = accountNameLevel name balance = if tree_ ropts && d == depth then aibalance else aebalance -- Accounts interesting because they are a fork for interesting subaccounts - interestingParents = dbg'' "interestingParents" $ - HM.filterWithKey (\name i -> i > 1 && accountNameLevel name > drop_ ropts) . - subaccountTallies $ HM.keys interestingAccounts - - isInterestingParent - | flat_ ropts = const False - | empty_ ropts || no_elide_ ropts = const True - | otherwise = (`HM.member` interestingParents) + interestingParents = dbg'' "interestingParents" $ HM.filterWithKey keepParent tallies + where + keepParent name subaccts + | flat_ ropts = False + | no_elide_ ropts = subaccts > 0 && accountNameLevel name > drop_ ropts + | otherwise = subaccts > 1 && accountNameLevel name > drop_ ropts + tallies = subaccountTallies . HM.keys $ HM.filterWithKey isInteresting valuedaccts isZeroRow balance = all (mixedAmountLooksZero . balance) - keepEmpty = empty_ ropts || depth == 0 depth = queryDepth q -- | Sort the rows by amount or by account declaration order. This is a bit tricky.