budget: refactor; show budget goals even with no or zero actual

This makes budget reports more intuitive. It is a temporary hack which
can misorder columns in some cases (if actual and budget activity
occur in a different range of columns). We should redo this in a more
principled way.
This commit is contained in:
Simon Michael 2018-03-30 03:26:32 +01:00
parent 2b34ff1184
commit 2a87ea56ff
2 changed files with 92 additions and 48 deletions

View File

@ -251,7 +251,7 @@ module Hledger.Cli.Commands.Balance (
) where
import Data.Decimal
import Data.List (intercalate, nub)
import Data.List
import Data.Maybe
import qualified Data.Map as Map
import qualified Data.Text as T
@ -329,7 +329,7 @@ balance opts@CliOpts{rawopts_=rawopts,reportopts_=ropts} j = do
_ | boolopt "budget" rawopts -> do
-- multi column budget report
reportspan <- dbg1 "reportspan" <$> reportSpan j ropts
reportspan <- reportSpan j ropts
let budget = budgetJournal opts reportspan j
j' = budgetRollUp opts budget j
report = dbg1 "report" $ multiBalanceReport ropts (queryFromOpts d ropts) j'
@ -655,7 +655,7 @@ type ActualAmountsReport = MultiBalanceReport
type BudgetAmountsReport = MultiBalanceReport
type ActualAmountsTable = Table String String MixedAmount
type BudgetAmountsTable = Table String String MixedAmount
type ActualAndBudgetAmountsTable = Table String String (MixedAmount, Maybe MixedAmount)
type ActualAndBudgetAmountsTable = Table String String (Maybe MixedAmount, Maybe MixedAmount)
type Percentage = Decimal
-- | Given two multi-column balance reports, the first representing a budget
@ -674,28 +674,29 @@ multiBalanceReportWithBudgetAsText opts budgetr actualr =
HistoricalBalance -> "Ending balances (historical)"
actualandbudgetamts :: ActualAndBudgetAmountsTable
actualandbudgetamts = combine (balanceReportAsTable opts actualr) (balanceReportAsTable opts budgetr)
actualandbudgetamts = combineTables (balanceReportAsTable opts actualr) (balanceReportAsTable opts budgetr)
showcell :: (ActualAmount, Maybe BudgetAmount) -> String
showcell (actual, mbudget) =
case (actual, mbudget) of
(actual, Nothing) ->
printf ("%"++show actualwidth++"s " ++ replicate (percentwidth + 7 + budgetwidth) ' ') (showamt actual)
(actual, Just budget) ->
case percentage actual budget of
Just pct ->
printf ("%"++show actualwidth++"s [%"++show percentwidth++"s%% of %"++show budgetwidth++"s]")
(showamt actual) (show $ roundTo 0 pct) (showamt budget)
Nothing ->
printf ("%"++show actualwidth++"s ["++replicate (percentwidth+5) ' '++"%"++show budgetwidth++"s]")
(showamt actual) (showamt budget)
showcell :: (Maybe ActualAmount, Maybe BudgetAmount) -> String
showcell (mactual, mbudget) = actualstr ++ " " ++ budgetstr
where
actualwidth = 7
percentwidth = 4
budgetwidth = 5
actualstr = printf ("%"++show actualwidth++"s") (maybe "" showamt mactual)
budgetstr = case (mactual, mbudget) of
(_, Nothing) -> replicate (percentwidth + 7 + budgetwidth) ' '
(mactual, Just budget) ->
case percentage mactual budget of
Just pct ->
printf ("[%"++show percentwidth++"s%% of %"++show budgetwidth++"s]")
(show $ roundTo 0 pct) (showamt budget)
Nothing ->
printf ("["++replicate (percentwidth+5) ' '++"%"++show budgetwidth++"s]")
(showamt budget)
percentage :: ActualAmount -> BudgetAmount -> Maybe Percentage
percentage actual budget =
percentage :: Maybe ActualAmount -> BudgetAmount -> Maybe Percentage
percentage Nothing _ = Nothing
percentage (Just actual) budget =
-- percentage of budget consumed is always computed in the cost basis
case (toCost actual, toCost budget) of
(Mixed [a1], Mixed [a2])
@ -711,28 +712,37 @@ multiBalanceReportWithBudgetAsText opts budgetr actualr =
| otherwise = showMixedAmountOneLineWithoutPrice
-- Combine a table of actual amounts and a table of budgeted amounts into
-- a single table of (actualamount, Maybe budgetamount) tuples.
-- The budget table's row/column titles should be a subset of the actual table's.
-- (This is satisfied by the construction of the budget report and the
-- process of rolling up account names.)
combine :: ActualAmountsTable -> BudgetAmountsTable -> ActualAndBudgetAmountsTable
combine (Table l t d) (Table l' t' d') = Table l t combinedRows
-- a single table of (Maybe actualamount, Maybe budgetamount) tuples.
-- The actual and budget table need not have the same account rows or date columns.
-- Every row and column from either table will appear in the combined table.
-- TODO better to combine the reports, not these tables which are just rendering helpers
combineTables :: ActualAmountsTable -> BudgetAmountsTable -> ActualAndBudgetAmountsTable
combineTables (Table aaccthdrs adatehdrs arows) (Table baccthdrs bdatehdrs brows) =
addtotalrow $ Table caccthdrs cdatehdrs crows
where
-- For all accounts that are present in the budget, zip actual amounts with budget amounts
combinedRows = [ combineRow row budgetRow
| (acct, row) <- zip (headerContents l) d
, let budgetRow =
if acct == "" then [] -- "" is totals row
else fromMaybe [] $ Map.lookup acct budgetAccts
]
-- Budget could cover smaller interval of time than the whole journal.
-- Headers for budget row will always be a sublist of headers of row
combineRow r br =
let reportRow = zip (headerContents t) r
budgetRow = Map.fromList $ zip (headerContents t') br
findBudgetVal hdr = Map.lookup hdr budgetRow
in map (\(hdr, val) -> (val, findBudgetVal hdr)) reportRow
budgetAccts = Map.fromList $ zip (headerContents l') d'
[aaccts, adates, baccts, bdates] = map headerContents [aaccthdrs, adatehdrs, baccthdrs, bdatehdrs]
-- combined account names
-- TODO Can't sort these or things will fall apart.
caccts = dbg2 "caccts" $ init $ (dbg2 "aaccts" $ filter (not . null) aaccts) `union` (dbg2 "baccts" baccts)
caccthdrs = T.Group NoLine $ map Header $ caccts
-- Actual column dates and budget column dates could be different.
-- TODO Can't easily combine these preserving correct order, will go wrong on monthly reports probably.
cdates = dbg2 "cdates" $ sort $ (dbg2 "adates" adates) `union` (dbg2 "bdates" bdates)
cdatehdrs = T.Group NoLine $ map Header cdates
-- corresponding rows of combined actual and/or budget amounts
crows = [ combineRow (actualRow a) (budgetRow a) | a <- caccts ]
-- totals row
addtotalrow | no_total_ opts = id
| otherwise = (+----+ (row "" $ combineRow (actualRow "") (budgetRow "")))
-- helpers
combineRow arow brow =
dbg1 "row" $ [(actualAmt d, budgetAmt d) | d <- cdates]
where
actualAmt date = Map.lookup date $ Map.fromList $ zip adates arow
budgetAmt date = Map.lookup date $ Map.fromList $ zip bdates brow
actualRow acct = fromMaybe [] $ Map.lookup acct $ Map.fromList $ zip aaccts arows
budgetRow acct = fromMaybe [] $ Map.lookup acct $ Map.fromList $ zip baccts brows
-- | Given a table representing a multi-column balance report (for example,
-- made using 'balanceReportAsTable'), render it in a format suitable for

View File

@ -41,7 +41,7 @@ Balance changes in 2016/12/01-2016/12/03:
expenses:food || $10 [ 100% of $10] $9 [ 90% of $10] $11 [ 110% of $10]
expenses:leisure || 0 [ 0% of $15] $5 [ 33% of $15] 0 [ 0% of $15]
-----------------------++------------------------------------------------------------------------------
|| 0 0 0
|| 0 [ 0% of 0] 0 [ 0% of 0] 0 [ 0% of 0]
# 2. --show-unbudgeted
$ hledger bal -D -b 2016-12-01 -e 2016-12-04 -f - --budget --show-unbudgeted
@ -55,7 +55,7 @@ Balance changes in 2016/12/01-2016/12/03:
expenses:leisure || 0 [ 0% of $15] $5 [ 33% of $15] 0 [ 0% of $15]
expenses:movies || 0 0 $25
------------------++------------------------------------------------------------------------------
|| 0 0 0
|| 0 [ 0% of 0] 0 [ 0% of 0] 0 [ 0% of 0]
# 3. Test that budget works with mix of commodities
<
@ -102,7 +102,8 @@ Balance changes in 2016/12/01-2016/12/03:
expenses:food || £10 [ 150% of $10] 20 CAD [ 210% of $10] $11 [ 110% of $10]
expenses:leisure || 0 [ 0% of $15] $5 [ 33% of $15] 0 [ 0% of $15]
-----------------------++---------------------------------------------------------------------------------------
|| $-15, £10 $-21.0, 20 CAD 0
|| $-15, £10 [ 0% of 0] $-21.0, 20 CAD [ 0% of 0] 0 [ 0% of 0]
# TODO zero totals ^
<
~ daily
@ -144,7 +145,8 @@ Balance changes in 2018/01/01-2018/01/03:
b || 1 0 1
c || 1 0 1
---++------------------------------------------------------------------------------
|| 3 0 3
|| 3 [ 30% of 10] 0 [ 0% of 10] 3 [ 30% of 10]
# TODO misleading totals ? ^
# 6. And with -W it selects the weekly budget, defined by all weekly periodic transactions.
$ hledger -f- bal --budget -W
@ -156,7 +158,7 @@ Balance changes in 2018/01/01w01:
b || 2 [ 2% of 100]
c || 2 [ 0% of 1000]
---++--------------------------
|| 6
|| 6 [ 1% of 1100]
# 7. A bounded two day budget. The end date is exclusive as usual.
<
@ -187,7 +189,7 @@ Balance changes in 2018/01/01-2018/01/04:
<unbudgeted>:b || 1 1 1 1
a || 1 1 [ 100% of 1] 1 [ 100% of 1] 1
----------------++--------------------------------------------------------------------------------------------------------
|| 2 2 2 2
|| 2 2 [ 200% of 1] 2 [ 200% of 1] 2
# 8. Multiple bounded budgets.
<
@ -216,7 +218,7 @@ Balance changes in 2018/01/01-2018/01/04:
===++========================================================================================================
a || 1 [ 100% of 1] 1 [ 100% of 1] 1 [ 10% of 10] 1 [ 10% of 10]
---++--------------------------------------------------------------------------------------------------------
|| 1 1 1 1
|| 1 [ 100% of 1] 1 [ 100% of 1] 1 [ 10% of 10] 1 [ 10% of 10]
# 9. A "from A to B" budget should not be included in a report beginning on B.
$ hledger -f- bal --budget -D -b 2018/1/3
@ -226,5 +228,37 @@ Balance changes in 2018/01/03-2018/01/04:
===++====================================================
a || 1 [ 10% of 10] 1 [ 10% of 10]
---++----------------------------------------------------
|| 1 1
|| 1 [ 10% of 10] 1 [ 10% of 10]
<
~ daily
(a) 1
2018/1/2
(a) 2
2018/1/2
(a) -2
# 10. accounts with non-zero budget should be shown by default
# even if there are no actual transactions in the period,
# or if the actual amount is zero.
# $ hledger -f- bal --budget -D date:2018/1/1-2018/1/3
# Balance changes in 2018/01/01-2018/01/02:
# || 2018/01/01 2018/01/02
# ===++====================================================
# a || [ 1] [ 1]
# ---++----------------------------------------------------
# || [ 1] [ 1]
# 11. With -E, zeroes are shown
$ hledger -f- bal --budget -D date:2018/1/1-2018/1/3 -E
Balance changes in 2018/01/01-2018/01/02:
|| 2018/01/01 2018/01/02
===++====================================================
a || 0 [ 0% of 1] 0 [ 0% of 1]
---++----------------------------------------------------
|| 0 [ 0% of 1] 0 [ 0% of 1]