hledger/BalanceCommand.hs

251 lines
10 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 (--subtotal), 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 subtotal 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.
showBalanceReport :: [Opt] -> [String] -> Ledger -> String
showBalanceReport opts args l = acctsstr ++ (if collapse then "" else totalstr)
where
acctsstr = concatMap showatree $ subs t
showatree t = showAccountTreeWithBalances matchedacctnames t
matchedacctnames = balancereportacctnames l sub apats t
t = (if empty then id else pruneZeroBalanceLeaves) $ ledgerAccountTree maxdepth l
apats = fst $ parseAccountDescriptionArgs opts args
maxdepth = fromMaybe 9999 $ depthFromOpts opts
sub = SubTotal `elem` opts || (isJust $ depthFromOpts opts)
empty = Empty `elem` opts
collapse = Collapse `elem` opts
totalstr = if isZeroMixedAmount total
then ""
else printf "--------------------\n%s\n" $ padleft 20 $ showMixedAmount total
total = sum $ map (abalance . ledgerAccount l) $ nonredundantaccts
nonredundantaccts = filter (not . hasparentshowing) matchedacctnames
hasparentshowing aname = (parentAccountName $ aname) `elem` matchedacctnames
-- | Identify the accounts we are interested in seeing balances for in the
-- balance report, based on the -s flag and account patterns. See Tests.hs.
balancereportacctnames :: Ledger -> Bool -> [String] -> Tree Account -> [AccountName]
balancereportacctnames l False [] t = filter (/= "top") $ map aname $ flatten $ treeprune 1 t
balancereportacctnames l False pats t = filter (/= "top") $ ns
where
ns = filter (matchpats_balance pats) $ map aname $ flatten t'
t' | null $ positivepats pats = treeprune 1 t
| otherwise = t
balancereportacctnames l True pats t = nub $ map aname $ addsubaccts l $ as
where
as = map (ledgerAccount l) ns
ns = balancereportacctnames l False pats t
-- add (in tree order) any missing subaccounts to a list of accounts
addsubaccts :: Ledger -> [Account] -> [Account]
addsubaccts l as = concatMap addsubs as where addsubs = maybe [] flatten . ledgerAccountTreeAt l
-- | Remove all sub-trees whose accounts have a zero balance.
pruneZeroBalanceLeaves :: Tree Account -> Tree Account
pruneZeroBalanceLeaves = treefilter (not . isZeroMixedAmount . abalance)
-- | Show this tree of accounts with balances, eliding boring parent
-- accounts and omitting uninteresting subaccounts based on the provided
-- list of account names we want to see balances for.
showAccountTreeWithBalances :: [AccountName] -> Tree Account -> String
showAccountTreeWithBalances matchednames t = showAccountTreeWithBalances' matchednames 0 "" t
where
showAccountTreeWithBalances' :: [AccountName] -> Int -> String -> Tree Account -> String
showAccountTreeWithBalances' matchednames indent prefix (Node (Account fullname _ bal) subs)
| isboringparent && hasmatchedsubs = subsprefixed
| ismatched = this ++ subsindented
| otherwise = subsnoindent
where
subsprefixed = showsubs indent (prefix++leafname++":")
subsnoindent = showsubs indent ""
subsindented = showsubs (indent+1) ""
showsubs i p = concatMap (showAccountTreeWithBalances' matchednames i p) subs
hasmatchedsubs = any ((`elem` matchednames) . aname) $ concatMap flatten subs
amt = padleft 20 $ showMixedAmount bal
this = concatTopPadded [amt, spaces ++ prefix ++ leafname] ++ "\n"
spaces = " " ++ replicate (indent * 2) ' '
leafname = accountLeafName fullname
ismatched = fullname `elem` matchednames
-- XXX
isboringparent = numsubs >= 1 && (bal == subbal || not ismatched)
subbal = abalance $ root $ head subs
numsubs = length subs
{- gives:
### Failure in: 52:balance report elides zero-balance root account(s)
expected: ""
but got: " 0 test\n"
Cases: 58 Tried: 58 Errors: 0 Failures: 1
Eg:
~/src/hledger$ hledger -f sample2.ledger -s bal
0 test
$2 a:aa
$-2 b
~/src/hledger$ ledger -f sample2.ledger -s bal
$2 test:a:aa
$-2 test:b
-}
-- isboringparent = hassubs && (not ismatched || (bal `mixedAmountEquals` subsbal))
-- hassubs = not $ null subs
-- subsbal = sum $ map (abalance . root) subs
{- gives:
### Failure in: 37:balance report with -s
expected: " $-1 assets\n $1 bank:saving\n $-2 cash\n $2 expenses\n $1 food\n $1 supplies\n $-2 income\n $-1 gifts\n $-1 salary\n $1 liabilities:debts\n"
but got: " $1 assets:bank:saving\n $-2 assets:cash\n $1 expenses:food\n $1 expenses:supplies\n $-1 income:gifts\n $-1 income:salary\n $1 liabilities:debts\n"
### Failure in: 39:balance report --depth activates -s
expected: " $-1 assets\n $1 bank\n $-2 cash\n $2 expenses\n $1 food\n $1 supplies\n $-2 income\n $-1 gifts\n $-1 salary\n $1 liabilities:debts\n"
but got: " $1 assets:bank\n $-2 assets:cash\n $1 expenses:food\n $1 expenses:supplies\n $-1 income:gifts\n $-1 income:salary\n $1 liabilities:debts\n"
### Failure in: 41:balance report with account pattern o and -s
expected: " $1 expenses:food\n $-2 income\n $-1 gifts\n $-1 salary\n--------------------\n $-1\n"
but got: " $1 expenses:food\n $-1 income:gifts\n $-1 income:salary\n--------------------\n $-1\n"
### Failure in: 42:balance report with account pattern a
expected: " $-1 assets\n $1 bank:saving\n $-2 cash\n $-1 income:salary\n $1 liabilities\n--------------------\n $-1\n"
but got: " $1 assets:bank:saving\n $-2 assets:cash\n $-1 income:salary\n $1 liabilities\n--------------------\n $-1\n"
### Failure in: 43:balance report with account pattern e
expected: " $-1 assets\n $2 expenses\n $1 supplies\n $-2 income\n $1 liabilities:debts\n"
but got: " $-1 assets\n $1 expenses:supplies\n $-2 income\n $1 liabilities:debts\n"
### Failure in: 49:balance report with -E shows zero-balance accounts
expected: " $-1 assets\n $1 bank\n $0 checking\n $1 saving\n $-2 cash\n--------------------\n $-1\n"
but got: " $0 assets:bank:checking\n $1 assets:bank:saving\n $-2 assets:cash\n--------------------\n $-1\n"
### Failure in: 52:balance report elides zero-balance root account(s)
expected: ""
but got: " 0 test\n"
Cases: 58 Tried: 58 Errors: 0 Failures: 7
Eg:
~/src/hledger$ hledger -f sample.ledger -s bal
$1 assets:bank:saving
$-2 assets:cash
$1 expenses:food
$1 expenses:supplies
$-1 income:gifts
$-1 income:salary
$1 liabilities:debts
~/src/hledger$ ledger -f sample.ledger -s bal
$-1 assets
$1 bank:saving
$-2 cash
$2 expenses
$1 food
$1 supplies
$-2 income
$-1 gifts
$-1 salary
$1 liabilities:debts
-}