From aaf8a0caf681299e9331fd62a5dc21d9eb930d81 Mon Sep 17 00:00:00 2001 From: Simon Michael Date: Sun, 15 Mar 2009 05:12:47 +0000 Subject: [PATCH] gather all balance report docs, rewrite, check with doctest --- BalanceCommand.hs | 332 +++++++++++++++++++++++++++++++++++++--------- 1 file changed, 270 insertions(+), 62 deletions(-) diff --git a/BalanceCommand.hs b/BalanceCommand.hs index f2dadbf4a..5adda3fc4 100644 --- a/BalanceCommand.hs +++ b/BalanceCommand.hs @@ -1,88 +1,84 @@ {-| -A ledger-compatible @balance@ command. Here's how it should work: +A ledger-compatible @balance@ command. -A sample account tree (as in the sample.ledger file): +ledger's balance command is easy to use but hard to describe precisely. +Here are some attempts. + + +I. high level description with examples +--------------------------------------- + +We'll use sample.ledger, which has the following account tree: @ assets - cash - checking - saving + bank + checking + saving + cash expenses - food - supplies + food + supplies income - gifts - salary + gifts + salary liabilities - debts + debts @ -The balance command shows top-level accounts by default: +The balance command shows accounts with their aggregate balances. +Subaccounts are displayed with more indentation. Each balance is the sum +of any transactions in that account plus any balances from subaccounts: @ - \> ledger balance - $-1 assets - $2 expenses - $-2 income - $1 liabilities + $ hledger -f sample.ledger balance + $-1 assets + $1 bank:saving + $-2 cash + $2 expenses + $1 food + $1 supplies + $-2 income + $-1 gifts + $-1 salary + $1 liabilities:debts @ -With -s (--subtotal), also show the subaccounts: +Usually, the non-interesting accounts are elided or omitted. To be +precise, an interesting account is one with: a non-zero balance, or a +balance different from its single subaccount, or two or more interesting +subaccounts. (More subtleties to be filled in here.) + +So, above, @checking@ is omitted because it has no interesting subaccounts +and a zero balance. @bank@ is elided because it has only a single +interesting subaccount (saving) and it would be showing the same balance +($1). Ditto for @liabilities@. + +With one or more account pattern arguments, the balance command shows +accounts whose name matches one of the patterns, plus their parents +(elided) and subaccounts. So with the pattern o we get: @ - $-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 + $ hledger -f sample.ledger balance o + $1 expenses:food + $-2 income + $-1 gifts + $-1 salary -------------------- - $-1 + $-1 @ -- The o matched @food@ and @income@, so they are shown. +The o pattern matched @food@ and @income@, so they are shown. Unmatched +parents of matched accounts are also shown (elided) for context (@expenses@). -- Parents of matched accounts are also shown for context (@expenses@). +Also, the balance report shows the total of all displayed accounts, when +that is non-zero. Here, it is displayed because the accounts shown add up +to $-1. -- 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: +II. Some notes for the implementation +------------------------------------- - a simple balance report shows top-level accounts @@ -100,6 +96,218 @@ Some notes for the implementation: - the sum of the balances shown is displayed at the end, if it is non-zero + +III. John's description 2009/02 +------------------------------- + +johnw: \"Since you've been re-implementing the balance report in Haskell, I thought +I'd share with you in pseudocode how I rewrote it for Ledger 3.0, since +the old method never stopped with the bugs. The new scheme uses a 5 stage +algorithm, with each stage gathering information for the next: + +STEP 1 + +Based on the user's query, walk through all the transactions in their +journal, finding which ones to include in the account subtotals. For each +transaction that matches, mark the account as VISITED. + +STEP 2 + +Recursively walk the accounts tree, depth-first, computing aggregate +totals and aggregate \"counts\" (number of transactions contributing to the +aggregate total). + +STEP 3 + +Walk the account tree again, breadth-first, and for every VISITED account, +check whether it matches the user's \"display predicate\". If so, mark the +account as MATCHING. + +STEP 4 + +Do an in-order traversal of the account tree. Except for the top-most +account (which serves strictly as a container for the other accounts): + +a. If the account was MATCHING, or two or more of its children are + MATCHING or had descendents who were MATCHING, display the account. + +b. Otherwise, if the account had *any* children or descendants who + were VISITED and *no* children or descendants who were MATCHING, + then apply the display predicate from STEP 3 to the account. If + it matches, also print this account. (This step allows -E to + report empty accounts which otherwise did match the original + query). + +STEP 5 + +When printing an account, display a \"depth spacer\" followed by the \"partial name\". +tal +The partial name is found by taking the base account's name, then +prepending to it every non-MATCHING parent until a MATCHING parent is +found. + +The depth spacer is found by outputting two spaces for every MATCHING parent. + +This way, \"Assets:Bank:Checking\" might be reported as: + + Assets + Bank + Checking + +or + + Assets + Bank:Checking + +or + + Assets:Bank:Checking + +Depending on whether the parents were or were not reported for their own reasons. +\" + +\"I just had to add one more set of tree traversals, to correctly determine +whether a final balance should be displayed + +without --flat, sort at each level in the hierarchy +with --flat, sort across all accounts\" + +IV. A functional description +----------------------------- + +1. filter the transactions, keeping only those included in the calculation. + Remember the subset of accounts involved. (VISITED) + +2. generate a full account & balance tree from all transactions + +3. Remember the subset of VISITED accounts which are matched for display. + (MATCHING) + +4. walk through the account tree: + + a. If the account is in MATCHING, or two or more of its children are or + have descendants who are, display it. + + b. Otherwise, if the account has any children or descendants in VISITED + but none in MATCHING, and it is matched for display, display it. + (allows -E to report empty accounts which otherwise did match the + original query). + +5. when printing an account, display a \"depth spacer\" followed by the + \"partial name\". The partial name is found by taking the base account's + name, then prepending to it every non-MATCHING parent until a MATCHING + parent is found. The depth spacer is two spaces per MATCHING parent. + +6. I just had to add one more set of tree traversals, to correctly + determine whether a final balance should be displayed + +7. without --flat, sort at each level in the hierarchy + with --flat, sort across all accounts + +V. Another functional description with new terminology +------------------------------------------------------ + +- included transactions are those included in the calculation, specified + by -b, -e, -p, -C, -R, account patterns and description patterns. + +- included accounts are the accounts referenced by included transactions. + +- matched transactions are the included transactions which match the + display predicate, specified by -d. + +- matched accounts are the included accounts which match the display + predicate, specified by -d, --depth, -E, -s + +- an account name tree is the full hierarchy of account names implied by a + set of transactions + +- an account tree is an account name tree augmented with the aggregate + balances and transaction counts for each named account + +- the included account tree is the account tree for the included transactions + +- a matched account tree contains one or more matched accounts + +- to generate the balance report, walk through the included account tree + and display each account if + + - it is matching + + - or it has two more more matching subtrees + + - or it has included offspring but no matching offspring + +- to display an account, display an indent then the \"partial name\". The + partial name is the account's name, prefixed by each unmatched parent + until a matched parent is found. The indent is two spaces per matched + parent. + + +VI. John's description 2009/03/11 +--------------------------------- + +johnw: \"Well, I had to rewrite the balance reporting code yet again, +because it wouldn't work with --depth correctly. Here's the new algorithm. + +STEP 1: Walk all postings, looking for those that match the user's query. + As a match is found, mark its account VISITED. + +STEP 2: Do a traversal of all accounts, sorting as need be, and collect + them all into an ordered list. + +STEP 3: Keeping that list on the side, do a *depth-first* traversal of + the account tree. + + visited = 0 + to_display = 0 + + (visited, to_display) += + + if account is VISITED or (no --flat and visited > 0): + if account matches display predicate and + (--flat or to_display != 1): + mark account as TO_DISPLAY + to_display = 1 + visited = 1 + + return (visited, to_display) + +STEP 4: top_displayed = 0 + + for every account in the ordered list: + if account has been marked TO_DISPLAY: + mark account as DISPLAYED + format the account and print + + if --flat and account is DISPLAYED: + top_displayed += 1 + + if no --flat: + for every top-most account: + if account is DISPLAYED or any children or DISPLAYED: + top_displayed += 1 + + if no --no-total and top_displayed > 1 and + top-most accounts sum to a non-zero balance: + output separator + output balance sum account as DISPLAYED + format the account and print + + if --flat and account is DISPLAYED: + top_displayed += 1 + + if no --flat: + for every top-most account: + if account is DISPLAYED or any children or DISPLAYED: + top_displayed += 1 + + if no --no-total and top_displayed > 1 and + top-most accounts sum to a non-zero balance: + output separator + output balance sum +\" + + -} module BalanceCommand