hledger/BalanceCommand.hs

185 lines
6.0 KiB
Haskell

{-|
A ledger-compatible @balance@ command. Here's how it should work:
A sample account tree (as in the sample.ledger file):
@
assets
cash
checking
saving
expenses
food
supplies
income
gifts
salary
liabilities
debts
@
The balance command shows top-level accounts by default:
@
\> ledger balance
$-1 assets
$2 expenses
$-2 income
$1 liabilities
@
With -s (--showsubs), also show the subaccounts:
@
$-1 assets
$-2 cash
$1 saving
$2 expenses
$1 food
$1 supplies
$-2 income
$-1 gifts
$-1 salary
$1 liabilities:debts
@
- @checking@ is not shown because it has a zero balance and no interesting
subaccounts.
- @liabilities@ is displayed only as a prefix because it has the same balance
as its single subaccount.
With an account pattern, show only the accounts with matching names:
@
\> ledger balance o
$1 expenses:food
$-2 income
--------------------
$-1
@
- The o matched @food@ and @income@, so they are shown.
- Parents of matched accounts are also shown for context (@expenses@).
- This time the grand total is also shown, because it is not zero.
Again, -s adds the subaccounts:
@
\> ledger -s balance o
$1 expenses:food
$-2 income
$-1 gifts
$-1 salary
--------------------
$-1
@
- @food@ has no subaccounts. @income@ has two, so they are shown.
- We do not add the subaccounts of parents included for context (@expenses@).
Some notes for the implementation:
- a simple balance report shows top-level accounts
- with an account pattern, it shows accounts whose leafname matches, plus their parents
- with the showsubs option, it also shows all subaccounts of the above
- zero-balance leaf accounts are removed
- the resulting account tree is displayed with each account's aggregated
balance, with boring parents prefixed to the next line
- a boring parent has the same balance as its child and is not explicitly
matched by the display options.
- the sum of the balances shown is displayed at the end, if it is non-zero
-}
module BalanceCommand
where
import Ledger.Utils
import Ledger.Types
import Ledger.Amount
import Ledger.AccountName
import Ledger.Ledger
import Options
import Utils
-- | Print a balance report.
balance :: [Opt] -> [String] -> Ledger -> IO ()
balance opts args l = putStr $ showBalanceReport opts args l
-- | Generate balance report output for a ledger, based on options.
showBalanceReport :: [Opt] -> [String] -> Ledger -> String
showBalanceReport opts args l = acctsstr ++ totalstr
where
acctsstr = concatMap (showAccountTreeWithBalances acctnamestoshow) $ subs treetoshow
totalstr = if isZeroAmount total
then ""
else printf "--------------------\n%20s\n" $ showAmount total
showingsubs = ShowSubs `elem` opts
pats@(apats,dpats) = parseAccountDescriptionArgs args
maxdepth = if null args && not showingsubs then 1 else 9999
acctstoshow = balancereportaccts showingsubs apats l
acctnamestoshow = map aname acctstoshow
treetoshow = pruneZeroBalanceLeaves $ pruneUnmatchedAccounts $ treeprune maxdepth $ ledgerAccountTree 9999 l
total = sumAmounts $ map abalance $ nonredundantaccts
nonredundantaccts = filter (not . hasparentshowing) acctstoshow
hasparentshowing a = (parentAccountName $ aname a) `elem` acctnamestoshow
-- select accounts for which we should show balances, based on the options
balancereportaccts :: Bool -> [String] -> Ledger -> [Account]
balancereportaccts False [] l = topAccounts l
balancereportaccts False pats l = accountsMatching pats l
balancereportaccts True pats l = addsubaccts l $ balancereportaccts False pats l
-- add (in tree order) any missing subacccounts to a list of accounts
addsubaccts :: Ledger -> [Account] -> [Account]
addsubaccts l as = concatMap addsubs as where addsubs = maybe [] flatten . ledgerAccountTreeAt l
-- remove any accounts from the tree which are not one of the acctstoshow,
-- or one of their parents, or one of their subaccounts when doing --showsubs
pruneUnmatchedAccounts :: Tree Account -> Tree Account
pruneUnmatchedAccounts = treefilter matched
where
matched (Account name _ _)
| name `elem` acctnamestoshow = True
| any (name `isAccountNamePrefixOf`) acctnamestoshow = True
| showingsubs && any (`isAccountNamePrefixOf` name) acctnamestoshow = True
| otherwise = False
-- remove zero-balance leaf accounts (recursively)
pruneZeroBalanceLeaves :: Tree Account -> Tree Account
pruneZeroBalanceLeaves = treefilter (not . isZeroAmount . abalance)
-- | Show a tree of accounts with balances, for the balance report,
-- eliding boring parent accounts. Requires a list of the account names we
-- are interested in to help with that.
showAccountTreeWithBalances :: [AccountName] -> Tree Account -> String
showAccountTreeWithBalances matchedacctnames =
showAccountTreeWithBalances' matchedacctnames 0 ""
where
showAccountTreeWithBalances' :: [AccountName] -> Int -> String -> Tree Account -> String
showAccountTreeWithBalances' matchedacctnames indentlevel prefix (Node (Account fullname _ bal) subs) =
if isboringparent then showsubswithprefix else showacct ++ showsubswithindent
where
showsubswithprefix = showsubs indentlevel (fullname++":")
showsubswithindent = showsubs (indentlevel+1) ""
showsubs i p = concatMap (showAccountTreeWithBalances' matchedacctnames i p) subs
showacct = showbal ++ " " ++ indent ++ prefix ++ leafname ++ "\n"
showbal = printf "%20s" $ show bal
indent = replicate (indentlevel * 2) ' '
leafname = accountLeafName fullname
numsubs = length subs
subbal = abalance $ root $ head subs
matched = fullname `elem` matchedacctnames
isboringparent = numsubs >= 1 && (bal == subbal || not matched)