From 2a87ea56ffce5ec9ec415631644cdee6d4ba44a5 Mon Sep 17 00:00:00 2001 From: Simon Michael Date: Fri, 30 Mar 2018 03:26:32 +0100 Subject: [PATCH] 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. --- hledger/Hledger/Cli/Commands/Balance.hs | 90 ++++++++++++++----------- tests/budget/budget.test | 50 +++++++++++--- 2 files changed, 92 insertions(+), 48 deletions(-) diff --git a/hledger/Hledger/Cli/Commands/Balance.hs b/hledger/Hledger/Cli/Commands/Balance.hs index 6950346d8..43ea65aa9 100644 --- a/hledger/Hledger/Cli/Commands/Balance.hs +++ b/hledger/Hledger/Cli/Commands/Balance.hs @@ -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 diff --git a/tests/budget/budget.test b/tests/budget/budget.test index 4a6caec1a..7a8a9ba62 100644 --- a/tests/budget/budget.test +++ b/tests/budget/budget.test @@ -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: :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]