journal: clearer transaction balancing errors; refactor

As mentioned by netvor on IRC, the unbalanced transaction error was
not too clear when postings all have the same sign.

Some other wording has been clarified, and the main error message is
now shown on multiple lines for readability (at the cost of
predictability/grepability..)

There's also a probably unnoticeable change: selecting which parts of
the error to show is now based on display precisions (reusing the
balanced check logic), rather than original precisions.
This commit is contained in:
Simon Michael 2020-05-29 13:09:17 -07:00
parent 61264cfdc0
commit 924ec1c98d
4 changed files with 119 additions and 45 deletions

View File

@ -59,6 +59,7 @@ module Hledger.Data.Transaction (
) )
where where
import Data.List import Data.List
import Data.List.Extra (nubSort)
import Data.Maybe import Data.Maybe
import Data.Text (Text) import Data.Text (Text)
import qualified Data.Text as T import qualified Data.Text as T
@ -339,31 +340,55 @@ transactionPostingBalances t = (sumPostings $ realPostings t
,sumPostings $ virtualPostings t ,sumPostings $ virtualPostings t
,sumPostings $ balancedVirtualPostings t) ,sumPostings $ balancedVirtualPostings t)
-- | Check that this transaction appears balanced when rendered, -- | Check that this transaction would appear balanced to a human when displayed,
-- returning an appropriate error message if it is not. -- and return an appropriate error message otherwise.
-- In more detail: after converting amounts to cost using explicit --
-- transaction prices if any; and summing the real postings; and -- In more detail:
-- summing the balanced virtual postings; and applying the given -- For the real postings, and separately for the balanced virtual postings:
-- display styles, if provided (maybe affecting decimal places): --
-- do both posting totals appear to be zero when rendered ? -- 1. Convert amounts to cost where possible
--
-- 2. When there are multiple amounts which appear non-zero when displayed
-- (using the given display styles if provided),
-- are they a mix of positives and negatives ?
-- This is checked separately to give a clearer error message.
-- (Best effort, could be confused by postings with multicommodity amounts.)
--
-- 3. Does the amounts' sum appear non-zero when displayed ?
-- (using the given display styles if provided)
--
transactionCheckBalanced :: Maybe (M.Map CommoditySymbol AmountStyle) -> Transaction -> Maybe String transactionCheckBalanced :: Maybe (M.Map CommoditySymbol AmountStyle) -> Transaction -> Maybe String
transactionCheckBalanced mstyles t transactionCheckBalanced mstyles t
| rbalanced && bvbalanced = Nothing | rsignsok && bvsignsok && rzerosum && bvzerosum = Nothing
| otherwise = Just $ printf "could not balance this transaction (%s%s%s)" rmsg sep bvmsg | otherwise = Just msg
where where
(rsum, _, bvsum) = transactionPostingBalances t (rps, bvps) = (realPostings t, balancedVirtualPostings t)
canonicalise = maybe id canonicaliseMixedAmount mstyles
-- check mixed signs, detecting nonzeros at display precision
nonZeros ps = filter (not.isZeroMixedAmount) $ map (canonicalise.costOfMixedAmount.pamount) ps
signsOk ps =
case nonZeros ps of
nzs | length nzs > 1 -> length (nubSort $ mapMaybe isNegativeMixedAmount nzs) > 1
_ -> True
(rsignsok, bvsignsok) = (signsOk rps, signsOk bvps)
-- check zero sum, at display precision
(rsum, bvsum) = (sumPostings rps, sumPostings bvps)
(rsumcost, bvsumcost) = (costOfMixedAmount rsum, costOfMixedAmount bvsum) (rsumcost, bvsumcost) = (costOfMixedAmount rsum, costOfMixedAmount bvsum)
(rsumdisplay, bvsumdisplay) = (canonicalise rsumcost, canonicalise bvsumcost) (rsumdisplay, bvsumdisplay) = (canonicalise rsumcost, canonicalise bvsumcost)
where canonicalise = maybe id canonicaliseMixedAmount mstyles (rzerosum, bvzerosum) = (isZeroMixedAmount rsumdisplay, isZeroMixedAmount bvsumdisplay)
-- for checking balanced, use the display precision
(rbalanced, bvbalanced) = (isZeroMixedAmount rsumdisplay, isZeroMixedAmount bvsumdisplay) -- select & generate error messages, showing amounts with their original precision
-- for selecting and generating error messages, use the uncanonicalised full precision rmsg
-- XXX always correct ? | not rsignsok = "real postings all have the same sign"
rmsg | isReallyZeroMixedAmount rsumcost = "" | not rzerosum = "real postings' sum should be 0 but is: " ++ showMixedAmount rsumcost
| otherwise = "real postings are off by " ++ showMixedAmount rsumcost | otherwise = ""
bvmsg | isReallyZeroMixedAmount bvsumcost = "" bvmsg
| otherwise = "balanced virtual postings are off by " ++ showMixedAmount bvsumcost | not bvsignsok = "balanced virtual postings all have the same sign"
sep = if not (null rmsg) && not (null bvmsg) then "; " else "" :: String | not bvzerosum = "balanced virtual postings' sum should be 0 but is: " ++ showMixedAmount bvsumcost
| otherwise = ""
msg = intercalate "\n" $ ["could not balance this transaction:"] ++ filter (not.null) [rmsg, bvmsg]
-- | Legacy form of transactionCheckBalanced. -- | Legacy form of transactionCheckBalanced.
isTransactionBalanced :: Maybe (M.Map CommoditySymbol AmountStyle) -> Transaction -> Bool isTransactionBalanced :: Maybe (M.Map CommoditySymbol AmountStyle) -> Transaction -> Bool
@ -403,7 +428,7 @@ balanceTransactionHelper mstyles t = do
Just err -> Left $ annotateErrorWithTransaction t' err Just err -> Left $ annotateErrorWithTransaction t' err
annotateErrorWithTransaction :: Transaction -> String -> String annotateErrorWithTransaction :: Transaction -> String -> String
annotateErrorWithTransaction t s = intercalate "\n" [showGenericSourcePos $ tsourcepos t, s, showTransaction t] annotateErrorWithTransaction t s = unlines [showGenericSourcePos $ tsourcepos t, s, showTransaction t]
-- | Infer up to one missing amount for this transactions's real postings, and -- | Infer up to one missing amount for this transactions's real postings, and
-- likewise for its balanced virtual postings, if needed; or return an error -- likewise for its balanced virtual postings, if needed; or return an error

View File

@ -19,23 +19,18 @@ expecting date separator or digit
# A journal with an unbalanced transaction. # A journal with an unbalanced transaction.
< <
2018/1/1 2009/1/1
a 1 a 1
# 2. When read from stdin with no reader prefix, the journal reader is used, # 2. When read from stdin with no reader prefix, the journal reader is used
# and fails here. (Before 1.17, all readers were tried and the timedot reader # (before 1.17, all readers were tried and the timedot reader would succeed),
# would succeed.) # giving an unbalanced error in this case.
$ hledger -f - print $ hledger -f - print
>2 /could not balance/ >2 /real postings' sum should be 0 but is: 1/
>=1 >=1
# 3. So in these tests we must sometimes force the desired format, like so. # 3. We expect to have reference to line number with last posting
# Now we see the error from the journal reader. # NB: \< and \> match start and end of a word.
$ hledger -f journal:- print
>2 /could not balance this transaction \(real postings are off by 1\)/
>=1
# 4. We expect to have reference to line number with last posting
$ hledger -f journal:- print $ hledger -f journal:- print
>2 /\<2\>/ >2 /\<2\>/
>=1 >=1
@ -45,7 +40,7 @@ $ hledger -f journal:- print
2018/1/1 2018/1/1
(a) 1 (a) 1
# 5. hledger doesn't detect this as an error directly, it parses account name "(a) 1" and # 4. hledger doesn't detect this as an error directly, it parses account name "(a) 1" and
# amount 0 here. # amount 0 here.
$ hledger -f - print -x $ hledger -f - print -x
2018-01-01 2018-01-01
@ -53,7 +48,7 @@ $ hledger -f - print -x
>= >=
# 6. Two (or more) postings with implicit amount cannot be balanced. # 5. Two (or more) postings with implicit amount cannot be balanced.
< <
2018/1/1 2018/1/1
a 1 a 1
@ -64,7 +59,7 @@ $ hledger -f journal:- print
>2 /could not balance this transaction - can't have more than one real posting with no amount/ >2 /could not balance this transaction - can't have more than one real posting with no amount/
>=1 >=1
# 7. Two (or more) virtual postings with implicit amount cannot be balanced. # 6. Two (or more) virtual postings with implicit amount cannot be balanced.
# (And the error message contains line numbers). # (And the error message contains line numbers).
< <
2018/1/1 2018/1/1
@ -75,3 +70,64 @@ $ hledger -f journal:- print
$ hledger -f journal:- print $ hledger -f journal:- print
>2 /lines 1-4/ >2 /lines 1-4/
>=1 >=1
# 7. Balancing error messages show the recorded precision, not the display precision.
<
2010/1/1
a 1C @ $1.0049
a $-1.000
$ hledger -f- print
>2 /real postings' sum should be 0 but is: \$0.0049/
>=1
# 8. Multiple postings all with the same sign gets a distinct error message.
<
2020-01-01
a 1A
b 1A
$ hledger -f- print
>2 /real postings all have the same sign/
>=1
# 9. Zeros don't trigger the same sign error.
<
2020-01-01
a A 0
b B 0
$ hledger -f- print
2020-01-01
a 0
b 0
>=0
# 10. An inferred multicommodity amount doesn't trigger the same sign error.
<
2020-01-01
a A 1
b B 1
c
$ hledger -f- print
2020-01-01
a A 1
b B 1
c
>=0
# 11. For multiple postings all with the same sign but in different commodities,
# the error should show the original amounts, without inferring transaction prices.
<
2020-01-01
a 1A
b 1B
$ hledger -f- print
>2
hledger: "-" (lines 1-3)
could not balance this transaction:
real postings all have the same sign
2020-01-01
a 1A @@ 1B
b 1B
>=1

View File

@ -38,7 +38,7 @@ hledger -f - balance --no-total --cost --empty
2010/1/1 2010/1/1
a 1C @ $1.0049 a 1C @ $1.0049
a $-1.000 a $-1.000
>>>2 /off by \$0.0049/ >>>2 /should be 0 but is: \$0.0049/
>>>= 1 >>>= 1
# 4. with $'s display precision at 2 or less, this txn should balance # 4. with $'s display precision at 2 or less, this txn should balance

View File

@ -1,7 +0,0 @@
hledger -f - register
<<<
2009/1/1 a
b 1.1
c -1
>>>2 /could not balance this transaction \(real postings are off by 0.1\)/
>>>= 1