haskell, hledger
May 27, 2013

Balance assertions, session 3

Continuing to work on balance-assertions. I’ve been thinking about how exactly to implement the summing and checking. I woke last night and spent about half an hour jotting down code by candlelight (better than turning on the computer in sleep time..)

Today I found myself with some hacking time, sans notebook or internet access. I tried to recreate my pseudo code and use fold and monads more than usual, from memory, and this went better than expected. I can now check one balance assertion, the first. Subsequent assertions are failing because the balance isn’t accumulating properly.

Here’s the code so far - remember it’s the first thing I got working, so we won’t expect it to be pretty or sensible. If you see the bug, or some nice cleanups, !

-- | Check any balance assertions in the journal and return an
-- error message if any of them fail.
journalCheckBalanceAssertions :: Journal -> Either String Journal
journalCheckBalanceAssertions j = do
  let ps = journalPostings j
      postingsByAccount = groupBy (\p1 p2 -> paccount p1 == paccount p2) ps
                          -- groupByAccountPreservingOrder ps :: [[Posting]]
  forM_ postingsByAccount checkBalanceAssertionsForAccount
  Right j

-- Check any balance assertions in this sequence of postings to a single account.
checkBalanceAssertionsForAccount :: [Posting] -> Either String ()
checkBalanceAssertionsForAccount ps
  | null errs = Right ()
  | otherwise = Left $ head errs
  where errs = fst $ foldl' checkBalanceAssertion ([],nullmixedamt) $ lstrace (paccount $ head ps) $ splitAssertions ps

-- Given a starting balance, accumulated errors, and a non-null sequence of
-- postings to a single account with a balance assertion in the last:
-- check that the final balance matches the balance assertion.
-- If it does, return the new balance, otherwise add an error to the
-- error list. Intended to be called from a fold.
checkBalanceAssertion :: ([String],MixedAmount) -> [Posting] -> ([String],MixedAmount)
checkBalanceAssertion (errs,bal) ps
  | null ps = (errs,bal)
  | isNothing assertion = (errs,bal)
  | bal' /= assertedbal = (errs++[err], bal')
  | otherwise = (errs,bal')
  where
    p = last ps
    assertion = pbalanceassertion p
    Just assertedbal = assertion
    bal' = sum $ [bal] ++ map pamount ps
    err = printf "Balance assertion failed:\nexpected balance in \"%s\" is %s, actual balance was %s\nafter %s in\n%s\n"
                 (paccount p)
                 (showMixedAmount assertedbal)
                 (showMixedAmount bal')
                 (show p)
                 (show $ ptransaction p)

-- Given a sequence of postings to a single account, split it into
-- sub-sequences consisting of ordinary postings followed by a single
-- balance-asserting posting. Trailing postings not followed by a
-- balance assertion are discarded.
splitAssertions :: [Posting] -> [[Posting]]
splitAssertions ps
  | null rest = [[]]
  | otherwise = (ps'++[head rest]):splitAssertions (tail rest)
  where
    (ps',rest) = break (isJust . pbalanceassertion) ps

Plan for next session: fix bugs, more testing, commit some stuff.