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 +