haskell, hledger
May 26, 2013

Balance assertions, session 2

Second session on balance assertions. I made a plan:

And here’s what actually happened in today’s 45m session. I disabled the HTF tests again, and remembered the plan for balance assertions: check them after parsing the whole journal, near where we check for balanced entries. It will be something like:

    collect the assertions
    group them by account
    for each account with assertions
     get its running total up to the last assertion's date (with a register report)
     check each asserted balance against the running total at that point, fail if it doesn't match

So where is that place ? Using M-. (find-tag in emacs), I jumped to readJournalFile, then followed the trail to readJournal, readers, JournalReader, where I saw that the journal file parser is parseJournalWith.

-- | Given a JournalUpdate-generating parsec parser, file path and data string,
-- parse and post-process a Journal so that it's ready to use, or give an error.
parseJournalWith :: (GenParser Char JournalContext (JournalUpdate,JournalContext)) -> FilePath -> String -> ErrorT String IO Journal
parseJournalWith p f s = do
  tc <- liftIO getClockTime
  tl <- liftIO getCurrentLocalTime
  y <- liftIO getCurrentYear
  case runParser p nullctx{ctxYear=Just y} f s of
    Right (updates,ctx) -> do
                           j <- updates `ap` return nulljournal
                           case journalFinalise tc tl f s ctx j of
                             Right j'  -> return j'
                             Left estr -> throwError estr
    Left e -> throwError $ show e

journalFinalise! That’s the place I’m thinking of.

-- | Do post-parse processing on a journal to make it ready for use: check
-- all transactions balance, canonicalise amount formats, close any open
-- timelog entries and so on.
journalFinalise :: ClockTime -> LocalTime -> FilePath -> String -> JournalContext -> Journal -> Either String Journal
journalFinalise tclock tlocal path txt ctx j@Journal{files=fs} =
    journalBalanceTransactions $
    journalCanonicaliseAmounts $
    journalCloseTimeLogEntries tlocal
    j{files=(path,txt):fs, filereadtime=tclock, jContext=ctx}

At what stage should it check balance assertions ? I think after all of the above. I got it compiling with a stub like so:

-- | Do post-parse processing on a journal to make it ready for use: check
-- all transactions balance, canonicalise amount formats, close any open
-- timelog entries and so on.
journalFinalise :: ClockTime -> LocalTime -> FilePath -> String -> JournalContext -> Journal -> Either String Journal
journalFinalise tclock tlocal path txt ctx j@Journal{files=fs} = do
  (journalBalanceTransactions $
    journalCanonicaliseAmounts $
    journalCloseTimeLogEntries tlocal
    j{files=(path,txt):fs, filereadtime=tclock, jContext=ctx})
  >>= journalCheckBalanceAssertions

-- | Check any balance assertions that have been found and return an
-- error message if any of them fail.
journalCheckBalanceAssertions :: Journal -> Either String Journal
journalCheckBalanceAssertions j@Journal{jtxns=ts} =
  Right j

Then started fleshing it out a little, using strace to see if the output is reasonable. (I forget if you need to sortBy before groupBy.) Playing around with this took up the rest of the session. I started with:

journalCheckBalanceAssertions j =
  -- - collect the assertions
  -- - group them by account
  -- - for each account with assertions
  --   - get its running total up to the last assertion's date (with a register report)
  --   - check each asserted balance against the running total at that point, fail if it doesn't match
  let ps = journalPostings j
      aps = [p | p <- ps, isJust $ pbalanceassertion p]
      apsByAcct = strace $ groupBy (\p1 p2 -> paccount p1 == paccount p2) aps
  in
  Right j

Compiles, but no debug output. Ah, I needed to force evaluation, eg with:

apsByAcct `seq` Right j

The output was an empty list… ah, I had no assertions in my journal. Time to make a test journal:

$ cat t.j
2013/1/1
  a  1 =1
  b

2013/1/2
  a  1 =2
  b

This didn’t parse, reminding me that balance assertions without a commodity symbol don’t work yet. I added $ to all amounts. Then:

$ hledger stat -f t.j >/dev/null
[[a                                $1
,a                                $1
]]

Hmm, not what I expected.. my logic was wrong. After tweaking code and data, I started to see balance assertions grouped by account:

let ps = journalPostings j
    aps = [p | p <- ps, isJust $ pbalanceassertion p]
    apsByAcct = groupBy (\p1 p2 -> paccount p1 == paccount p2)
                -- sortBy (comparing paccount)
                aps
in
strace (map (map pbalanceassertion) apsByAcct) `seq` Right j

journal:

2013/1/1
  a   $1  =$1
  b

2013/1/2
  a   $1
  b  $-1  =$-2

output:

$ bin/hledgerdev stat -f t.j >/dev/null
[[Just Mixed [Amount {acommodity="$", aquantity=1.0, aprice=, astyle=AmountStyle {ascommodityside = L, ascommodityspaced = False, asprecision = 0, asdecimalpoint = '.', asseparator = ',', asseparatorpositions = []}}]],[Just Mixed [Amount {acommodity="$", aquantity=-2.0, aprice=, astyle=AmountStyle {ascommodityside = L, ascommodityspaced = False, asprecision = 0, asdecimalpoint = '.', asseparator = ',', asseparatorpositions = []}}]]]

I haven’t fully tested the grouping, but time is up. I need to add some more test entries, and get some prettier and briefer output for debugging.

Session review: I started drafting this blog post at the beginning of the session and continued throughout, going into far too much detail. Also cleaning up and publishing took me about half an hour over time. In due course I’ll get the blog looking better.

Till next time..