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.
This commit is contained in:
Michael Kainer 2019-11-11 21:06:58 +01:00 committed by Simon Michael
parent 87b82b6839
commit 79ca4a767e
12 changed files with 167 additions and 10 deletions

View File

@ -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}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
)

View File

@ -156,6 +156,36 @@ Flat-mode balance reports, which normally show exclusive balances, show inclusiv
<!-- $ for y in 2006 2007 2008 2009 2010; do echo; echo $y; hledger -f $y.journal balance ^expenses --depth 2; done -->
### 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,

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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