From 79ca4a767e174addf5900ede71e2792d394de9e6 Mon Sep 17 00:00:00 2001 From: Michael Kainer Date: Mon, 11 Nov 2019 21:06:58 +0100 Subject: [PATCH] cli: Add -% to compound balance commands This commit introduces the commandline argument -%/--percent to show percentages of the column's total instead of the absolute amounts for each account in reports. The signs of the values are preserved. This option is especially useful for the balance and incomestatement commands. If there are multiple commodities involved in a report hledger bails with an error message. This can be avoided by using --cost. Also note that if one uses -% with the balance command the chances are high that all numbers are 0. This is due to the fact that by default balance sums up to zero. If one wants to use -% in a meaningful way with balance one has to add a query. In order to keep the implementation as simple as possible --tree has no influence over how the percentages are calculated, i.e., the percentages always represent the fraction of the columns total. If one wants to know the percentages relative to a parent account, one has to use a query to narrow down the accounts. --- hledger-lib/Hledger/Data/Amount.hs | 2 + hledger-lib/Hledger/Reports/BalanceReport.hs | 34 ++++++++++++++- .../Hledger/Reports/MultiBalanceReport.hs | 23 +++++++++- hledger-lib/Hledger/Reports/ReportOptions.hs | 3 ++ hledger/Hledger/Cli/Commands/Balance.hs | 1 + hledger/Hledger/Cli/Commands/Balance.md | 30 +++++++++++++ hledger/Hledger/Cli/Commands/Balancesheet.md | 2 + hledger/Hledger/Cli/Commands/Cashflow.md | 2 + .../Hledger/Cli/Commands/Incomestatement.md | 2 + hledger/Hledger/Cli/CompoundBalanceCommand.hs | 13 +++--- tests/balance/percent.test | 42 +++++++++++++++++++ tests/incomestatement.test | 23 ++++++++++ 12 files changed, 167 insertions(+), 10 deletions(-) create mode 100644 tests/balance/percent.test diff --git a/hledger-lib/Hledger/Data/Amount.hs b/hledger-lib/Hledger/Data/Amount.hs index d06c2faa8..b25bf120c 100644 --- a/hledger-lib/Hledger/Data/Amount.hs +++ b/hledger-lib/Hledger/Data/Amount.hs @@ -51,6 +51,7 @@ module Hledger.Data.Amount ( usd, eur, gbp, + per, hrs, at, (@@), @@ -181,6 +182,7 @@ hrs n = amount{acommodity="h", aquantity=n, astyle=amountstyle{aspreci usd n = amount{acommodity="$", aquantity=roundTo 2 n, astyle=amountstyle{asprecision=2}} eur n = amount{acommodity="€", aquantity=roundTo 2 n, astyle=amountstyle{asprecision=2}} gbp n = amount{acommodity="£", aquantity=roundTo 2 n, astyle=amountstyle{asprecision=2}} +per n = amount{acommodity="%", aquantity=n, astyle=amountstyle{asprecision=1, ascommodityside=R, ascommodityspaced=True}} amt `at` priceamt = amt{aprice=Just $ UnitPrice priceamt} amt @@ priceamt = amt{aprice=Just $ TotalPrice priceamt} diff --git a/hledger-lib/Hledger/Reports/BalanceReport.hs b/hledger-lib/Hledger/Reports/BalanceReport.hs index 0d22d76c2..9730950f4 100644 --- a/hledger-lib/Hledger/Reports/BalanceReport.hs +++ b/hledger-lib/Hledger/Reports/BalanceReport.hs @@ -12,6 +12,8 @@ module Hledger.Reports.BalanceReport ( balanceReport, flatShowsExclusiveBalance, sortAccountItemsLike, + unifyMixedAmount, + perdivide, -- * Tests tests_BalanceReport @@ -66,7 +68,7 @@ flatShowsExclusiveBalance = True balanceReport :: ReportOpts -> Query -> Journal -> BalanceReport balanceReport ropts@ReportOpts{..} q j@Journal{..} = (if invert_ then brNegate else id) $ - (sorteditems, total) + (mappedsorteditems, mappedtotal) where -- dbg1 = const id -- exclude from debug output dbg1 s = let p = "balanceReport" in Hledger.Utils.dbg1 (p++" "++s) -- add prefix in debug output @@ -142,6 +144,14 @@ balanceReport ropts@ReportOpts{..} q j@Journal{..} = if flatShowsExclusiveBalance then sum $ map fourth4 items else sum $ map aebalance $ clipAccountsAndAggregate 1 displayaccts + + -- Calculate percentages if needed. + mappedtotal | percent_ = dbg1 "mappedtotal" $ total `perdivide` total + | otherwise = total + mappedsorteditems | percent_ = + dbg1 "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. @@ -185,6 +195,28 @@ 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. +unifyMixedAmount :: MixedAmount -> Amount +unifyMixedAmount mixedAmount = foldl combine (num 0) (amounts mixedAmount) + where + combine amount result = + if isReallyZeroAmount amount + then result + else if isReallyZeroAmount 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 isReallyZeroAmount a' || isReallyZeroAmount 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 diff --git a/hledger-lib/Hledger/Reports/MultiBalanceReport.hs b/hledger-lib/Hledger/Reports/MultiBalanceReport.hs index 315e75a54..f6dfb9438 100644 --- a/hledger-lib/Hledger/Reports/MultiBalanceReport.hs +++ b/hledger-lib/Hledger/Reports/MultiBalanceReport.hs @@ -106,7 +106,7 @@ multiBalanceReport ropts q j = multiBalanceReportWith ropts q j (journalPriceOra multiBalanceReportWith :: ReportOpts -> Query -> Journal -> PriceOracle -> MultiBalanceReport multiBalanceReportWith ropts@ReportOpts{..} q j@Journal{..} priceoracle = (if invert_ then mbrNegate else id) $ - MultiBalanceReport (colspans, sortedrows, totalsrow) + MultiBalanceReport (colspans, mappedsortedrows, mappedtotalsrow) where dbg1 s = let p = "multiBalanceReport" in Hledger.Utils.dbg1 (p++" "++s) -- add prefix in this function's debug output -- dbg1 = const id -- exclude this function from debug output @@ -162,7 +162,7 @@ multiBalanceReportWith ropts@ReportOpts{..} q j@Journal{..} priceoracle = -- These balances are unvalued except maybe converted to cost. startbals :: [(AccountName, MixedAmount)] = dbg1 "startbals" $ map (\(a,_,_,b) -> (a,b)) startbalanceitems where - (startbalanceitems,_) = dbg1 "starting balance report" $ balanceReport ropts''{value_=Nothing} startbalq j' + (startbalanceitems,_) = dbg1 "starting balance report" $ balanceReport ropts''{value_=Nothing, percent_=False} startbalq j' where ropts' | tree_ ropts = ropts{no_elide_=True} | otherwise = ropts{accountlistmode_=ALFlat} @@ -344,6 +344,25 @@ multiBalanceReportWith ropts@ReportOpts{..} q j@Journal{..} priceoracle = totalsrow :: MultiBalanceReportTotals = dbg1 "totalsrow" (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 dbg1 "mappedsortedrows" + [(aname, alname, alevel, zipWith perdivide rowvals coltotals, rowtotal `perdivide` grandtotal, rowavg `perdivide` grandaverage) + | (aname, alname, alevel, rowvals, rowtotal, rowavg) <- sortedrows + ] + mappedtotalsrow :: MultiBalanceReportTotals = + if not percent_ then totalsrow + else dbg1 "mappedtotalsrow" ( + map (\t -> perdivide t t) coltotals, + perdivide grandtotal grandtotal, + perdivide grandaverage grandaverage) + -- | Given a MultiBalanceReport and its normal balance sign, -- if it is known to be normally negative, convert it to normally positive. mbrNormaliseSign :: NormalSign -> MultiBalanceReport -> MultiBalanceReport diff --git a/hledger-lib/Hledger/Reports/ReportOptions.hs b/hledger-lib/Hledger/Reports/ReportOptions.hs index 87a24d724..c10bbea6e 100644 --- a/hledger-lib/Hledger/Reports/ReportOptions.hs +++ b/hledger-lib/Hledger/Reports/ReportOptions.hs @@ -113,6 +113,7 @@ data ReportOpts = ReportOpts { ,no_total_ :: Bool ,pretty_tables_ :: Bool ,sort_amount_ :: Bool + ,percent_ :: Bool ,invert_ :: Bool -- ^ if true, flip all amount signs in reports ,normalbalance_ :: Maybe NormalSign -- ^ This can be set when running balance reports on a set of accounts @@ -158,6 +159,7 @@ defreportopts = ReportOpts def def def + def rawOptsToReportOpts :: RawOpts -> IO ReportOpts rawOptsToReportOpts rawopts = checkReportOpts <$> do @@ -186,6 +188,7 @@ rawOptsToReportOpts rawopts = checkReportOpts <$> do ,row_total_ = boolopt "row-total" rawopts' ,no_total_ = boolopt "no-total" rawopts' ,sort_amount_ = boolopt "sort-amount" rawopts' + ,percent_ = boolopt "percent" rawopts' ,invert_ = boolopt "invert" rawopts' ,pretty_tables_ = boolopt "pretty-tables" rawopts' ,color_ = color diff --git a/hledger/Hledger/Cli/Commands/Balance.hs b/hledger/Hledger/Cli/Commands/Balance.hs index 2b96529d1..51209d3fe 100644 --- a/hledger/Hledger/Cli/Commands/Balance.hs +++ b/hledger/Hledger/Cli/Commands/Balance.hs @@ -292,6 +292,7 @@ balancemode = hledgerCommandMode ,flagNone ["budget"] (setboolopt "budget") "show performance compared to budget goals defined by periodic transactions" ,flagNone ["invert"] (setboolopt "invert") "display all amounts with reversed sign" ,flagNone ["transpose"] (setboolopt "transpose") "transpose rows and columns" + ,flagNone ["percent", "%"] (setboolopt "percent") "express values in percentage of each column's total" ] ++ outputflags ) diff --git a/hledger/Hledger/Cli/Commands/Balance.md b/hledger/Hledger/Cli/Commands/Balance.md index 53a9e1cb2..ca642cb43 100644 --- a/hledger/Hledger/Cli/Commands/Balance.md +++ b/hledger/Hledger/Cli/Commands/Balance.md @@ -156,6 +156,36 @@ Flat-mode balance reports, which normally show exclusive balances, show inclusiv +### Percentages + +With `-%` or `--percent`, balance reports show each account's value expressed +as a percentage of the column's total. This is useful to get an overview of +the relative sizes of account balances. For example to obtain an overview of +expenses: + +```shell +$ hledger balance expenses -% + 100.0 % expenses + 50.0 % food + 50.0 % supplies +-------------------- + 100.0 % +``` + +Note that `--tree` does not have an effect on `-%`. The percentages are always +relative to the total sum of each column, they are never relative to the parent +account. + +Since the percentages are relative to the columns sum, it is usually not useful +to calculate percentages if the signs of the amounts are mixed. Although the +results are technically correct, they are most likely useless. Especially in +a balance report that sums up to zero (eg `hledger balance -B`) all percentage +values will be zero. + +This flag does not work if the report contains any mixed commodity accounts. If +there are mixed commodity accounts in the report be sure to use `-V` +or `-B` to coerce the report into using a single commodity. + ### Multicolumn balance report Multicolumn or tabular balance reports are a very useful hledger feature, diff --git a/hledger/Hledger/Cli/Commands/Balancesheet.md b/hledger/Hledger/Cli/Commands/Balancesheet.md index 1732d0b3c..d9b0fc5ad 100644 --- a/hledger/Hledger/Cli/Commands/Balancesheet.md +++ b/hledger/Hledger/Cli/Commands/Balancesheet.md @@ -40,6 +40,8 @@ you can alter the report mode with `--change`/`--cumulative`/`--historical`. Normally balancesheet shows historical ending balances, which is what you need for a balance sheet; note this means it ignores report begin dates (and `-T/--row-total`, since summing end balances generally does not make sense). +Instead of absolute values [percentages](#percentages) can be displayed +with `-%`. This command also supports [output destination](hledger.html#output-destination) and diff --git a/hledger/Hledger/Cli/Commands/Cashflow.md b/hledger/Hledger/Cli/Commands/Cashflow.md index b928442df..610580314 100644 --- a/hledger/Hledger/Cli/Commands/Cashflow.md +++ b/hledger/Hledger/Cli/Commands/Cashflow.md @@ -31,6 +31,8 @@ will be shown, one for each report period. Normally cashflow shows changes in assets per period, though as with [multicolumn balance reports](#multicolumn-balance-reports) you can alter the report mode with `--change`/`--cumulative`/`--historical`. +Instead of absolute values [percentages](#percentages) can be displayed +with `-%`. This command also supports [output destination](hledger.html#output-destination) and diff --git a/hledger/Hledger/Cli/Commands/Incomestatement.md b/hledger/Hledger/Cli/Commands/Incomestatement.md index 747f71a34..057717702 100644 --- a/hledger/Hledger/Cli/Commands/Incomestatement.md +++ b/hledger/Hledger/Cli/Commands/Incomestatement.md @@ -42,6 +42,8 @@ will be shown, one for each report period. Normally incomestatement shows revenues/expenses per period, though as with [multicolumn balance reports](#multicolumn-balance-reports) you can alter the report mode with `--change`/`--cumulative`/`--historical`. +Instead of absolute values [percentages](#percentages) can be displayed +with `-%`. This command also supports [output destination](hledger.html#output-destination) and diff --git a/hledger/Hledger/Cli/CompoundBalanceCommand.hs b/hledger/Hledger/Cli/CompoundBalanceCommand.hs index 8009c6196..efce02b19 100644 --- a/hledger/Hledger/Cli/CompoundBalanceCommand.hs +++ b/hledger/Hledger/Cli/CompoundBalanceCommand.hs @@ -106,6 +106,7 @@ compoundBalanceCommandMode CompoundBalanceCommandSpec{..} = ,flagReq ["format"] (\s opts -> Right $ setopt "format" s opts) "FORMATSTR" "use this custom line format (in simple reports)" ,flagNone ["pretty-tables"] (setboolopt "pretty-tables") "use unicode when displaying tables" ,flagNone ["sort-amount","S"] (setboolopt "sort-amount") "sort by amount instead of account code/name" + ,flagNone ["percent", "%"] (setboolopt "percent") "express values in percentage of each column's total" ,outputFormatFlag ,outputFileFlag ] @@ -135,13 +136,11 @@ compoundBalanceCommand CompoundBalanceCommandSpec{..} opts@CliOpts{reportopts_=r -- are used in single column mode, since in that situation we will be using -- balanceReportFromMultiBalanceReport which does not support eliding boring parents, -- and tree mode hides this.. or something.. XXX - ropts' - | not (flat_ ropts) && - interval_==NoInterval && - balancetype `elem` [CumulativeChange, HistoricalBalance] - = ropts{balancetype_=balancetype, accountlistmode_=ALTree} - | otherwise - = ropts{balancetype_=balancetype} + ropts' = ropts{ + balancetype_=balancetype, + accountlistmode_=if not (flat_ ropts) && interval_==NoInterval && balancetype `elem` [CumulativeChange, HistoricalBalance] then ALTree else accountlistmode_, + no_total_=if percent_ && length cbcqueries > 1 then True else no_total_ + } userq = queryFromOpts d ropts' format = outputFormatFromOpts opts diff --git a/tests/balance/percent.test b/tests/balance/percent.test new file mode 100644 index 000000000..5c54d509a --- /dev/null +++ b/tests/balance/percent.test @@ -0,0 +1,42 @@ +#!/usr/bin/env shelltest +# 1. Single column percent +hledger -f sample.journal balance expenses -% +>>> + 100.0 % expenses + 50.0 % food + 50.0 % supplies +-------------------- + 100.0 % +>>>= 0 + +# 2. Multi column percent +hledger -f sample.journal balance expenses -% -p daily +>>> +Balance changes in 2008/06/03: + + || 2008/06/03 +===================++============ + expenses:food || 50.0 % + expenses:supplies || 50.0 % +-------------------++------------ + || 100.0 % +>>>= 0 + +# 3. In a balanced ledger everything should sum up to zero, therefore all percentages should be zero. +hledger -f sample.journal balance -% -p quarterly +>>> +Balance changes in 2008: + + || 2008q1 2008q2 2008q3 2008q4 +======================++================================ + assets:bank:checking || 0 0 0 0 + assets:bank:saving || 0 0 0 0 + assets:cash || 0 0 0 0 + expenses:food || 0 0 0 0 + expenses:supplies || 0 0 0 0 + income:gifts || 0 0 0 0 + income:salary || 0 0 0 0 + liabilities:debts || 0 0 0 0 +----------------------++-------------------------------- + || 0 0 0 0 +>>>= 0 diff --git a/tests/incomestatement.test b/tests/incomestatement.test index eb93b26e8..36b8c64cb 100644 --- a/tests/incomestatement.test +++ b/tests/incomestatement.test @@ -283,3 +283,26 @@ Income Statement 2008/01/31,,2008/12/31 (Historical Ending Balances) ===================++================================================================================================================================================ Net: || $1 $1 $1 $1 $1 0 0 0 0 0 0 0 >>>= 0 + +# 8. Percentage test +hledger -f sample.journal incomestatement -p 'quarterly 2008' -T --average -% +>>> +Income Statement 2008 + + || 2008q1 2008q2 2008q3 2008q4 Total Average +===================++==================================================== + Revenues || +-------------------++---------------------------------------------------- + income:gifts || 0 100.0 % 0 0 50.0 % 50.0 % + income:salary || 100.0 % 0 0 0 50.0 % 50.0 % +-------------------++---------------------------------------------------- + || 100.0 % 100.0 % 0 0 100.0 % 100.0 % +===================++==================================================== + Expenses || +-------------------++---------------------------------------------------- + expenses:food || 0 50.0 % 0 0 50.0 % 50.0 % + expenses:supplies || 0 50.0 % 0 0 50.0 % 50.0 % +-------------------++---------------------------------------------------- + || 0 100.0 % 0 0 100.0 % 100.0 % +>>>= 0 +