lib: inclusive balance assertions (=* and ==*)

This commit is contained in:
Simon Michael 2019-02-17 19:50:22 -08:00
parent 3b47b58aec
commit 8789a442a8
7 changed files with 130 additions and 85 deletions

View File

@ -733,13 +733,13 @@ inferFromAssignmentB p@Posting{paccount=acc} =
-- checks the posting's balance assertion if any. Or if the posting has no
-- amount, runs the supplied fallback action.
addAmountAndCheckBalanceAssertionB ::
(Posting -> Balancing s Posting) -- ^ fallback action
(Posting -> Balancing s Posting) -- ^ fallback action XXX why ?
-> Posting
-> Balancing s Posting
addAmountAndCheckBalanceAssertionB _ p | hasAmount p = do
newAmt <- addToBalanceB (paccount p) (pamount p)
assrt <- R.reader bsAssrt
lift $ when assrt $ ExceptT $ return $ checkBalanceAssertion p newAmt
when assrt $ checkBalanceAssertionB p newAmt
return p
addAmountAndCheckBalanceAssertionB fallback p = fallback p
@ -747,42 +747,54 @@ addAmountAndCheckBalanceAssertionB fallback p = fallback p
-- return an error if the assertion is not satisfied.
-- If the assertion is partial, unasserted commodities in the actual balance
-- are ignored; if it is total, they will cause the assertion to fail.
checkBalanceAssertion :: Posting -> MixedAmount -> Either String ()
checkBalanceAssertion p@Posting{pbalanceassertion=Just (BalanceAssertion{baamount,batotal})} actualbal =
foldl' f (Right ()) assertedamts
checkBalanceAssertionB :: Posting -> MixedAmount -> Balancing s ()
checkBalanceAssertionB p@Posting{pbalanceassertion=Just (BalanceAssertion{baamount,batotal})} actualbal =
forM_ assertedamts $ \amt -> checkBalanceAssertionOneCommodityB p amt actualbal
where
f (Right _) assertedamt = checkBalanceAssertionOneCommodity p assertedamt actualbal
f err _ = err
assertedamts = baamount : otheramts
where
assertedcomm = acommodity baamount
otheramts | batotal = map (\a -> a{ aquantity = 0 }) $ amounts $ filterMixedAmount (\a -> acommodity a /= assertedcomm) actualbal
otheramts | batotal = map (\a -> a{aquantity=0}) $ amounts $ filterMixedAmount ((/=assertedcomm).acommodity) actualbal
| otherwise = []
checkBalanceAssertion _ _ = Right ()
checkBalanceAssertionB _ _ = return ()
-- | Does this (single commodity) expected balance match the amount of that
-- commodity in the given (multicommodity) actual balance ? If not, returns a
-- balance assertion failure message based on the provided posting. To match,
-- the amounts must be exactly equal (display precision is ignored here).
checkBalanceAssertionOneCommodity :: Posting -> Amount -> MixedAmount -> Either String ()
checkBalanceAssertionOneCommodity p assertedamt actualbal
| pass = Right ()
| otherwise = Left errmsg
where
-- If the assertion is inclusive, the expected amount is compared with the account's
-- subaccount-inclusive balance; otherwise, with the subaccount-exclusive balance.
checkBalanceAssertionOneCommodityB :: Posting -> Amount -> MixedAmount -> Balancing s ()
checkBalanceAssertionOneCommodityB p@Posting{paccount=assertedacct} assertedamt actualbal = do
-- sum the running balances of this account and any subaccounts seen so far
bals <- R.asks bsBalances
actualibal <- liftB $ const $ H.foldM
(\bal (acc, amt) -> return $
if assertedacct==acc || assertedacct `isAccountNamePrefixOf` acc
then bal + amt
else bal)
0
bals
let
isinclusive = maybe False bainclusive $ pbalanceassertion p
actualbal'
| isinclusive = actualibal
| otherwise = actualbal
assertedcomm = acommodity assertedamt
actualbalincommodity = fromMaybe nullamt $ find ((== assertedcomm) . acommodity) (amounts actualbal)
actualbalincomm = headDef 0 $ amounts $ filterMixedAmountByCommodity assertedcomm actualbal'
pass =
aquantity
-- traceWith (("asserted:"++).showAmountDebug)
assertedamt ==
aquantity
-- traceWith (("actual:"++).showAmountDebug)
actualbalincommodity
actualbalincomm
errmsg = printf (unlines
[ "balance assertion: %s",
"\nassertion details:",
"date: %s",
"account: %s",
"account: %s%s",
"commodity: %s",
-- "display precision: %d",
"calculated: %s", -- (at display precision: %s)",
@ -800,13 +812,16 @@ checkBalanceAssertionOneCommodity p assertedamt actualbal
)
(showDate $ postingDate p)
(T.unpack $ paccount p) -- XXX pack
(if isinclusive then " (and subs)" else "" :: String)
assertedcomm
-- (asprecision $ astyle actualbalincommodity) -- should be the standard display precision I think
(show $ aquantity actualbalincommodity)
(show $ aquantity actualbalincomm)
-- (showAmount actualbalincommodity)
(show $ aquantity assertedamt)
-- (showAmount assertedamt)
(show $ aquantity assertedamt - aquantity actualbalincommodity)
(show $ aquantity assertedamt - aquantity actualbalincomm)
when (not pass) $ throwError errmsg
-- | Choose and apply a consistent display format to the posting
-- amounts in each commodity. Each commodity's format is specified by

View File

@ -105,6 +105,7 @@ nullassertion, assertion :: BalanceAssertion
nullassertion = BalanceAssertion
{baamount=nullamt
,batotal=False
,bainclusive=False
,baposition=nullsourcepos
}
assertion = nullassertion

View File

@ -235,7 +235,7 @@ postingAsLines elideamount onelineamounts pstoalignwith p = concat [
| postingblock <- postingblocks]
where
postingblocks = [map rstrip $ lines $ concatTopPadded [statusandaccount, " ", amount, assertion, samelinecomment] | amount <- shownAmounts]
assertion = maybe "" ((" = " ++) . showAmountWithZeroCommodity . baamount) $ pbalanceassertion p
assertion = maybe "" ((' ':).showBalanceAssertion) $ pbalanceassertion p
statusandaccount = indent $ fitString (Just $ minwidth) Nothing False True $ pstatusandacct p
where
-- pad to the maximum account name width, plus 2 to leave room for status flags, to keep amounts aligned
@ -259,6 +259,10 @@ postingAsLines elideamount onelineamounts pstoalignwith p = concat [
case renderCommentLines (pcomment p) of [] -> ("",[])
c:cs -> (c,cs)
-- | Render a balance assertion, as the =[=][*] symbol and expected amount.
showBalanceAssertion BalanceAssertion{..} =
"=" ++ ['=' | batotal] ++ ['*' | bainclusive] ++ " " ++ showAmountWithZeroCommodity baamount
-- | Render a posting, simply. Used in balance assertion errors.
-- showPostingLine p =
-- indent $
@ -374,6 +378,9 @@ storeTransactionB t = liftB $ \bs ->
-- by inferring a missing amount or conversion price(s) if needed.
-- Or if balancing is not possible, because of unbalanced amounts or
-- more than one missing amount, returns an error message.
-- Note this function may be unable to balance some transactions
-- that journalBalanceTransactions/balanceTransactionB can balance
-- (eg ones with balance assignments).
-- Whether postings "sum to 0" depends on commodity display precisions,
-- so those can optionally be provided.
balanceTransaction ::

View File

@ -278,6 +278,7 @@ instance Show Status where -- custom show.. bad idea.. don't do it..
data BalanceAssertion = BalanceAssertion {
baamount :: Amount, -- ^ the expected balance in a particular commodity
batotal :: Bool, -- ^ disallow additional non-asserted commodities ?
bainclusive :: Bool, -- ^ include subaccounts when calculating the actual balance ?
baposition :: GenericSourcePos -- ^ the assertion's file position, for error reporting
} deriving (Eq,Typeable,Data,Generic,Show)

View File

@ -729,6 +729,7 @@ balanceassertionp = do
sourcepos <- genericSourcePos <$> lift getSourcePos
char '='
istotal <- fmap isJust $ optional $ try $ char '='
isinclusive <- fmap isJust $ optional $ try $ char '*'
lift (skipMany spacenonewline)
-- this amount can have a price; balance assertions ignore it,
-- but balance assignments will use it
@ -736,6 +737,7 @@ balanceassertionp = do
return BalanceAssertion
{ baamount = a
, batotal = istotal
, bainclusive = isinclusive
, baposition = sourcepos
}

View File

@ -337,8 +337,8 @@ Virtual postings have some legitimate uses, but those are few. You can usually f
hledger supports
[Ledger-style balance assertions](http://ledger-cli.org/3.0/doc/ledger3.html#Balance-assertions)
in journal files.
These look like `=EXPECTEDBALANCE` following a posting's amount. Eg in
this example we assert the expected dollar balance in accounts a and b after
These look like, for example, `= EXPECTEDBALANCE` following a posting's amount.
Eg here we assert the expected dollar balance in accounts a and b after
each posting:
```journal
@ -355,7 +355,7 @@ After reading a journal file, hledger will check all balance
assertions and report an error if any of them fail. Balance assertions
can protect you from, eg, inadvertently disrupting reconciled balances
while cleaning up old entries. You can disable them temporarily with
the `--ignore-assertions` flag, which can be useful for
the `-I/--ignore-assertions` flag, which can be useful for
troubleshooting or for reading Ledger files.
### Assertions and ordering
@ -399,10 +399,10 @@ We could call this a "partial" balance assertion.
To assert the balance of more than one commodity in an account,
you can write multiple postings, each asserting one commodity's balance.
You can make a stronger kind of balance assertion, by writing a
You can make a stronger "total" balance assertion by writing a
double equals sign (`== EXPECTEDBALANCE`).
This "complete" balance assertion asserts the absence of other commodities
(or, that their balance is 0, which to hledger is equivalent.)
This asserts that there are no other unasserted commodities in the account
(or, that their balance is 0).
``` {.journal}
2013/1/1
@ -453,21 +453,16 @@ and because [balance *assignments*](#balance-assignments) do use them (see below
### Assertions and subaccounts
Balance assertions do not count the balance from subaccounts; they check
the posted account's exclusive balance. For example:
The balance assertions above (`=` and `==`) do not count the balance
from subaccounts; they check the account's exclusive balance only.
You can assert the balance including subaccounts by writing `=*` or `==*`, eg:
```journal
1/1
checking:fund 1 = 1 ; post to this subaccount, its balance is now 1
checking 1 = 1 ; post to the parent account, its exclusive balance is now 1
equity
```
The balance report's flat mode shows these exclusive balances more clearly:
```shell
$ hledger bal checking --flat
1 checking
1 checking:fund
--------------------
2
2019/1/1
equity:opening balances
checking:a 5
checking:b 5
checking 1 ==* 11
```
### Assertions and virtual postings

View File

@ -311,7 +311,7 @@ hledger -f - stats
>>>2
>>>=0
# 17. Exact assertions parse correctly
# 17. Total assertions (==) parse correctly
hledger -f - stats
<<<
2016/1/1
@ -324,7 +324,7 @@ hledger -f - stats
>>>2
>>>=0
# 18. Exact assertions consider entire account
# 18. Total assertions consider entire multicommodity amount
hledger -f - stats
<<<
2016/1/1
@ -340,7 +340,7 @@ hledger -f - stats
>>>2 /balance assertion.*line 10, column 15/
>>>=1
# 19. Mix different commodities and exact assignments
# 19. Mix different commodities and total assignments
hledger -f - stats
<<<
2016/1/1
@ -440,3 +440,27 @@ commodity $1000.00
>>>2 /difference: 0\.0001/
>>>=1
# 26. Inclusive assertions include balances from subaccounts.
hledger -f- print
<<<
2019/1/1
(a) X1
(a) Y3
(a:b) Y7
(a) 0 =* X1
(a) 0 =* Y10
(a:b) 0 =* Y7
(a:b) 0 ==* Y7
>>>
2019/01/01
(a) X1
(a) Y3
(a:b) Y7
(a) 0 =* X1
(a) 0 =* Y10
(a:b) 0 =* Y7
(a:b) 0 ==* Y7
>>>2
>>>=0