lib: Refactor multiBalanceReportWith into getPostingsbyColumn and generateMultiBalanceReport.
This commit is contained in:
parent
1f707ab0d8
commit
d09a90b38b
@ -26,6 +26,7 @@ module Hledger.Reports.MultiBalanceReport (
|
||||
where
|
||||
|
||||
import Control.Monad (guard)
|
||||
import Data.Foldable (toList)
|
||||
import Data.List (sortBy, transpose)
|
||||
import Data.HashMap.Strict (HashMap)
|
||||
import qualified Data.HashMap.Strict as HM
|
||||
@ -101,41 +102,12 @@ multiBalanceReportWith ropts q j priceoracle = report
|
||||
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
|
||||
|
||||
-- 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.
|
||||
colps = dbg'' "colps" $ calculateColumns colspans ps
|
||||
|
||||
-- Each account's balance changes across all columns.
|
||||
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
|
||||
|
||||
-- All account names that will be displayed, possibly depth-clipped.
|
||||
displayaccts = dbg'' "displayaccts" $ displayedAccounts ropts' q accumvalued
|
||||
|
||||
-- All the rows of the report.
|
||||
rows = dbg'' "rows" $ buildReportRows ropts' accumvalued
|
||||
|
||||
-- Sorted report rows.
|
||||
sortedrows = dbg' "sortedrows" $ sortRows ropts' j rows
|
||||
|
||||
-- Calculate column totals
|
||||
totalsrow = dbg' "totalsrow" $ calculateTotalsRow ropts' displayaccts sortedrows
|
||||
colps = dbg'' "colps" $ getPostingsByColumn ropts' reportq j reportspan
|
||||
colspans = dbg "colspans" $ M.keys colps
|
||||
|
||||
-- Postprocess the report, negating balances and taking percentages if needed
|
||||
report = dbg' "report" . postprocessReport ropts' displayaccts $
|
||||
PeriodicReport colspans sortedrows totalsrow
|
||||
report = dbg' "report" $ generateMultiBalanceReport ropts' reportq j priceoracle reportspan colspans colps
|
||||
|
||||
|
||||
-- | Calculate the span of the report to be generated.
|
||||
@ -146,6 +118,35 @@ setDefaultAccountListMode def ropts = ropts{accountlistmode_=mode}
|
||||
ALDefault -> def
|
||||
a -> a
|
||||
|
||||
-- | 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.
|
||||
-- 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
|
||||
|
||||
-- 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, period_=precedingperiod}
|
||||
| otherwise = ropts{accountlistmode_=ALFlat, 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
|
||||
|
||||
-- | Calculate the span of the report to be generated.
|
||||
calculateReportSpan :: ReportOpts -> Query -> Journal -> DateSpan
|
||||
calculateReportSpan ropts q j = reportspan
|
||||
@ -179,34 +180,21 @@ makeReportQuery ropts reportspan q
|
||||
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.
|
||||
-- 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
|
||||
-- | Group postings, grouped by their column
|
||||
getPostingsByColumn :: ReportOpts -> Query -> Journal -> DateSpan -> Map DateSpan [Posting]
|
||||
getPostingsByColumn ropts q j reportspan = columns
|
||||
where
|
||||
acctchanges = acctChangesFromPostings ropts' startbalq . map fst $
|
||||
getPostings ropts' startbalq j
|
||||
-- Postings matching the query within the report period.
|
||||
ps :: [(Posting, Day)] = dbg'' "ps" $ getPostings ropts q j
|
||||
days = map snd ps
|
||||
|
||||
-- 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
|
||||
-- The date spans to be included as report columns.
|
||||
colspans = calculateColSpans ropts reportspan days
|
||||
addPosting (p, d) = maybe id (M.adjust (p:)) $ latestSpanContaining colspans d
|
||||
emptyMap = M.fromList . zip colspans $ repeat []
|
||||
|
||||
ropts' | tree_ ropts = ropts{no_elide_=True, period_=precedingperiod}
|
||||
| otherwise = ropts{accountlistmode_=ALFlat, 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
|
||||
-- Group postings into their columns
|
||||
columns = foldr addPosting emptyMap ps
|
||||
|
||||
-- | Gather postings matching the query within the report period.
|
||||
getPostings :: ReportOpts -> Query -> Journal -> [(Posting, Day)]
|
||||
@ -237,12 +225,6 @@ 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 []
|
||||
|
||||
-- | Calculate account balance changes in each column.
|
||||
--
|
||||
@ -260,15 +242,14 @@ 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 -> [DateSpan]
|
||||
-> HashMap ClippedAccountName Account
|
||||
-> Map DateSpan [Posting]
|
||||
-> HashMap ClippedAccountName (Map DateSpan Account)
|
||||
calculateAccountChanges ropts q colspans startbals colps
|
||||
calculateAccountChanges ropts q colspans 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)
|
||||
acctchanges = transposeMap colacctchanges
|
||||
|
||||
colacctchanges :: Map DateSpan (HashMap ClippedAccountName Account) =
|
||||
dbg'' "colacctchanges" $ fmap (acctChangesFromPostings ropts q) colps
|
||||
@ -281,21 +262,22 @@ calculateAccountChanges ropts q colspans startbals colps
|
||||
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
|
||||
-> HashMap ClippedAccountName (Map DateSpan Account)
|
||||
accumValueAmounts ropts j priceoracle colspans startbals acctchanges =
|
||||
HM.mapWithKey processRow $ acctchanges <> (mempty <$ startbals)
|
||||
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 <> zeros
|
||||
-- postings. Make sure every column has an entry.
|
||||
processRow name changes = M.mapWithKey valueAcct . rowbals name $ changes <> 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 sumAcct nullacct changes
|
||||
HistoricalBalance -> drop 1 $ scanl sumAcct (startingBalanceFor name) changes
|
||||
CumulativeChange -> snd $ M.mapAccum f nullacct changes
|
||||
HistoricalBalance -> snd $ M.mapAccum f (startingBalanceFor name) changes
|
||||
where f a b = let s = sumAcct a b in (s, s)
|
||||
|
||||
-- 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
|
||||
@ -319,17 +301,52 @@ accumValueAmounts ropts j priceoracle colspans startbals = HM.mapWithKey process
|
||||
multiperiod = interval_ ropts /= NoInterval
|
||||
|
||||
startingBalanceFor a = HM.lookupDefault nullacct a startbals
|
||||
|
||||
zeros = M.fromList [(span, nullacct) | span <- colspans]
|
||||
|
||||
|
||||
-- | Group a date-separated list of postings into a regular matrix with rows
|
||||
-- given by AccountName and columns by DateSpan, then generate a MultiBalanceReport
|
||||
-- from the columns.
|
||||
generateMultiBalanceReport :: ReportOpts -> Query -> Journal -> PriceOracle
|
||||
-> DateSpan -> [DateSpan]
|
||||
-> Map DateSpan [Posting]
|
||||
-> MultiBalanceReport
|
||||
generateMultiBalanceReport ropts q j priceoracle reportspan colspans colps = report
|
||||
where
|
||||
-- Each account's balance changes across all columns.
|
||||
acctchanges = dbg'' "acctchanges" $ calculateAccountChanges ropts q colspans colps
|
||||
|
||||
-- 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 q j reportspan
|
||||
|
||||
-- Process changes into normal, cumulative, or historical amounts, plus value them
|
||||
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
|
||||
|
||||
-- All the rows of the report.
|
||||
rows = dbg'' "rows" $ buildReportRows ropts accumvalued
|
||||
|
||||
-- Calculate column totals
|
||||
totalsrow = dbg' "totalsrow" $ calculateTotalsRow ropts displayaccts rows
|
||||
|
||||
-- Sorted report rows.
|
||||
sortedrows = dbg' "sortedrows" $ sortRows ropts j rows
|
||||
|
||||
-- Postprocess the report, negating balances and taking percentages if needed
|
||||
report = postprocessReport ropts displayaccts $
|
||||
PeriodicReport colspans sortedrows totalsrow
|
||||
|
||||
-- | Build the report rows.
|
||||
--
|
||||
-- One row per account, with account name info, row amounts, row total and row average.
|
||||
buildReportRows :: ReportOpts -> HashMap AccountName [Account] -> [MultiBalanceReportRow]
|
||||
buildReportRows :: ReportOpts -> HashMap AccountName (Map DateSpan Account) -> [MultiBalanceReportRow]
|
||||
buildReportRows ropts acctvalues =
|
||||
[ PeriodicReportRow (flatDisplayName a) rowbals rowtot rowavg
|
||||
| (a,accts) <- HM.toList acctvalues
|
||||
, let rowbals = map balance accts
|
||||
, let rowbals = map balance $ toList 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.
|
||||
@ -341,7 +358,7 @@ buildReportRows ropts 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 (Map DateSpan Account)
|
||||
-> HashMap AccountName DisplayName
|
||||
displayedAccounts ropts q valuedaccts
|
||||
| depth == 0 = HM.singleton "..." $ DisplayName "..." "..." 1
|
||||
|
||||
Loading…
Reference in New Issue
Block a user