hledger/BalanceCommand.hs
2008-10-10 10:05:12 +00:00

182 lines
5.2 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 no transactions
of its own and only one 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@).
Here are some rules for account balance display, as seen above:
- grand total is omitted if it is 0
- leaf accounts and branches with 0 balance or 0 transactions are omitted
- inner accounts with 0 transactions and 1 subaccount are displayed inline
- in a filtered report, matched accounts are displayed with their parents
inline (a consequence of the above)
- in a showsubs report, all subaccounts of matched accounts are displayed
-}
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.
printbalance :: [Opt] -> [String] -> Ledger -> IO ()
printbalance opts args l = putStr $ balancereport opts args l
balancereport :: [Opt] -> [String] -> Ledger -> String
balancereport opts args l = showLedgerAccountBalances l depth
where
showsubs = (ShowSubs `elem` opts)
pats = parseAccountDescriptionArgs args
-- when there is no -s or pattern args, show with depth 1
depth = case (pats, showsubs) of
(([],[]), False) -> 1
otherwise -> 9999
-- | Generate balance report output for a ledger, to the specified depth.
showLedgerAccountBalances :: Ledger -> Int -> String
showLedgerAccountBalances l maxdepth =
concatMap (showAccountTree l maxdepth) acctbranches
++
if isZeroAmount total
then ""
else printf "--------------------\n%20s\n" $ showAmountRounded total
where
acctbranches = branches $ ledgerAccountTree maxdepth l
filteredacctbranches = branches $ ledgerFilteredAccountTree maxdepth (acctpat l) l
total = sum $ map (abalance . root) filteredacctbranches
-- | Get the string representation of a tree of accounts.
-- The ledger from which the accounts come is required so that
-- we can check for boring accounts.
showAccountTree :: Ledger -> Int -> Tree Account -> String
showAccountTree l maxdepth = showAccountTree' l maxdepth 0 ""
showAccountTree' :: Ledger -> Int -> Int -> String -> Tree Account -> String
showAccountTree' l maxdepth indentlevel prefix t
-- merge boring inner account names with the next line
| isBoringInnerAccount l maxdepth acct = subsindented 0 (fullname++":")
-- ditto with unmatched parent accounts when filtering by account
| filtering && doesnotmatch = subsindented 0 (fullname++":")
-- otherwise show this account's name & balance
| otherwise = bal ++ " " ++ indent ++ prefix ++ leafname ++ "\n" ++ (subsindented 1 "")
where
acct = root t
subs = branches t
subsindented i p = concatMap (showAccountTree' l maxdepth (indentlevel+i) p) subs
bal = printf "%20s" $ show $ abalance $ acct
indent = replicate (indentlevel * 2) ' '
fullname = aname acct
leafname = accountLeafName fullname
filtering = filteredaccountnames l /= (accountnames l)
doesnotmatch = not (containsRegex (acctpat l) leafname)
-- | Is this account a boring inner account in this ledger ?
-- Boring inner accounts have no transactions, one subaccount,
-- and depth less than the maximum display depth.
-- Also, they are unmatched parent accounts when account matching is in effect.
isBoringInnerAccount :: Ledger -> Int -> Account -> Bool
isBoringInnerAccount l maxdepth a
| name == "top" = False
| depth < maxdepth && numtxns == 0 && numsubs == 1 = True
| otherwise = False
where
name = aname a
depth = accountNameLevel name
numtxns = length $ atransactions a
-- how many (filter-matching) subaccounts has this account ?
numsubs = length $ subAccountNamesFrom (filteredaccountnames l) name
-- | Is the named account a boring inner account in this ledger ?
isBoringInnerAccountName :: Ledger -> Int -> AccountName -> Bool
isBoringInnerAccountName l maxdepth = isBoringInnerAccount l maxdepth . ledgerAccount l