{-| A 'Ledger' stores, for efficiency, a 'RawLedger' plus its tree of account names, a map from account names to 'Account's, and the display precision. Typically it has also has had the uninteresting 'Entry's filtered out. In addition, it stores the account filter pattern and a second set of fields providing the filtered entries & transactions. -} module Ledger.Ledger ( cacheLedger, filterLedgerEntries, accountnames, ledgerAccount, ledgerTransactions, ledgerAccountTree, addDataToAccountNameTree, printentries, printregister, showLedgerAccountBalances, showAccountTree, isBoringInnerAccount, isBoringInnerAccountName, -- pruneBoringBranches, ) where import qualified Data.Map as Map import Data.Map ((!)) import Ledger.Utils import Ledger.Types import Ledger.Amount import Ledger.Account import Ledger.AccountName import Ledger.Transaction import Ledger.RawLedger import Ledger.Entry instance Show Ledger where show l = printf "Ledger with %d entries, %d accounts: %s" ((length $ entries $ rawledger l) + (length $ modifier_entries $ rawledger l) + (length $ periodic_entries $ rawledger l)) (length $ accountnames l) (show $ accountnames l) ++ "\n" ++ (showtree $ accountnametree l) ++ "\n" ++ (showtree $ filteredaccountnametree l) -- | Convert a raw ledger to a more efficient cached type, described above. cacheLedger :: Regex -> RawLedger -> Ledger cacheLedger acctpat l = let ant = rawLedgerAccountNameTree l anames = flatten ant ts = rawLedgerTransactions l sortedts = sortBy (comparing account) ts groupedts = groupBy (\t1 t2 -> account t1 == account t2) sortedts txnmap = Map.union (Map.fromList [(account $ head g, g) | g <- groupedts]) (Map.fromList [(a,[]) | a <- anames]) txnsof = (txnmap !) subacctsof a = filter (isAccountNamePrefixOf a) anames subtxnsof a = concat [txnsof a | a <- [a] ++ subacctsof a] balmap = Map.union (Map.fromList [(a, (sumTransactions $ subtxnsof a){precision=maxprecision}) | a <- anames]) (Map.fromList [(a,nullamt) | a <- anames]) amap = Map.fromList [(a, Account a (txnmap ! a) (balmap ! a)) | a <- anames] -- the same again, considering only accounts and transactions matching the account pattern matchacct :: AccountName -> Bool matchacct = containsRegex acctpat . accountLeafName filteredant = treefilter matchacct ant -- rebuild the tree after filtering to include all parents filteredanames = flatten $ accountNameTreeFrom $ filter matchacct anames filteredts = filter (matchacct . account) ts filteredsortedts = sortBy (comparing account) filteredts filteredgroupedts = groupBy (\t1 t2 -> account t1 == account t2) filteredsortedts filteredtxnmap = Map.union (Map.fromList [(account $ head g, g) | g <- filteredgroupedts]) (Map.fromList [(a,[]) | a <- filteredanames]) filteredtxnsof = (filteredtxnmap !) filteredsubacctsof a = filter (isAccountNamePrefixOf a) filteredanames filteredsubtxnsof a = concat [filteredtxnsof a | a <- [a] ++ filteredsubacctsof a] filteredbalmap = Map.union (Map.fromList [(a, (sumTransactions $ filteredsubtxnsof a){precision=maxprecision}) | a <- filteredanames]) (Map.fromList [(a,nullamt) | a <- filteredanames]) filteredamap = Map.fromList [(a, Account a (filteredtxnmap ! a) (filteredbalmap ! a)) | a <- filteredanames] maxprecision = maximum $ map (precision . amount) ts in Ledger l ant amap maxprecision acctpat filteredant filteredamap -- | Remove ledger entries we are not interested in. -- Keep only those which fall between the begin and end dates, match the -- description patterns, or transact with an account matching the account -- patterns. filterLedgerEntries :: String -> String -> Regex -> Regex -> RawLedger -> RawLedger filterLedgerEntries begin end acctpat descpat = filterLedgerEntriesByDate begin end . filterLedgerEntriesByDescription descpat -- | Keep only entries whose description matches the description pattern. filterLedgerEntriesByDescription :: Regex -> RawLedger -> RawLedger filterLedgerEntriesByDescription descpat (RawLedger ms ps es f) = RawLedger ms ps (filter matchdesc es) f where matchdesc :: Entry -> Bool matchdesc e = case matchRegex descpat (edescription e) of Nothing -> False otherwise -> True -- | Keep only entries which fall between begin and end dates. -- We include entries on the begin date and exclude entries on the end -- date, like ledger. An empty date string means no restriction. filterLedgerEntriesByDate :: String -> String -> RawLedger -> RawLedger filterLedgerEntriesByDate begin end (RawLedger ms ps es f) = RawLedger ms ps (filter matchdate es) f where matchdate :: Entry -> Bool matchdate e = (begin == "" || entrydate >= begindate) && (end == "" || entrydate < enddate) where begindate = parsedate begin :: UTCTime enddate = parsedate end entrydate = parsedate $ edate e -- | List a 'Ledger' 's account names. accountnames :: Ledger -> [AccountName] accountnames l = drop 1 $ flatten $ accountnametree l -- | List a 'Ledger' 's account names filtered by the account match pattern. filteredaccountnames :: Ledger -> [AccountName] filteredaccountnames l = filter (containsRegex (acctpat l) . accountLeafName) $ accountnames l -- | Get the named account from a ledger. ledgerAccount :: Ledger -> AccountName -> Account ledgerAccount l a = (accounts l) ! a -- | Get the named filtered account from a ledger. ledgerFilteredAccount :: Ledger -> AccountName -> Account ledgerFilteredAccount l a = (filteredaccounts l) ! a -- | List a ledger's transactions. -- -- NB this sets the amount precisions to that of the highest-precision -- amount, to help with report output. It should perhaps be done in the -- display functions, but those are far removed from the ledger. Keep in -- mind if doing more arithmetic with these. ledgerTransactions :: Ledger -> [Transaction] ledgerTransactions l = setprecisions $ rawLedgerTransactions $ rawledger l where setprecisions = map (transactionSetPrecision (lprecision l)) -- | Get a ledger's tree of accounts to the specified depth. ledgerAccountTree :: Int -> Ledger -> Tree Account ledgerAccountTree depth l = addDataToAccountNameTree l depthpruned where nametree = filteredaccountnametree l -- depthpruned = treeprune depth nametree -- | Get a ledger's tree of accounts to the specified depth, filtered by -- the account pattern. ledgerFilteredAccountTree :: Int -> Regex -> Ledger -> Tree Account ledgerFilteredAccountTree depth acctpat l = addFilteredDataToAccountNameTree l $ treeprune depth $ filteredaccountnametree l -- | Convert a tree of account names into a tree of accounts, using their -- parent ledger. addDataToAccountNameTree :: Ledger -> Tree AccountName -> Tree Account addDataToAccountNameTree = treemap . ledgerAccount -- | Convert a tree of account names into a tree of accounts, using their -- parent ledger's filtered account data. addFilteredDataToAccountNameTree :: Ledger -> Tree AccountName -> Tree Account addFilteredDataToAccountNameTree l = treemap (ledgerFilteredAccount l) -- | Print a print report. printentries :: Ledger -> IO () printentries l = putStr $ showEntries $ setprecisions $ entries $ rawledger l where setprecisions = map (entrySetPrecision (lprecision l)) -- | Print a register report. printregister :: Ledger -> IO () printregister l = putStr $ showTransactionsWithBalances (sortBy (comparing date) $ ledgerTransactions l) nullamt{precision=lprecision l} {-| This and the helper functions below generate ledger-compatible balance report output. 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 -} 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 also 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