packages feed

hledger-0.1: BalanceCommand.hs

{-| 

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)