dev: balanceTransaction -> balanceSingleTransaction

This commit is contained in:
Simon Michael 2025-11-18 09:39:16 -10:00
parent 69b63695fc
commit b2a0de75e2
3 changed files with 22 additions and 33 deletions

View File

@ -16,7 +16,7 @@ module Hledger.Data.Balancing
, defbalancingopts , defbalancingopts
-- * transaction balancing -- * transaction balancing
, isTransactionBalanced , isTransactionBalanced
, balanceTransaction , balanceSingleTransaction
, balanceTransactionHelper , balanceTransactionHelper
-- * assertion validation -- * assertion validation
, transactionCheckAssertions , transactionCheckAssertions
@ -173,32 +173,21 @@ transactionCheckAssertions bopts j t =
Right _ -> Right t Right _ -> Right t
Left e -> Left e Left e -> Left e
-- | Balance this transaction, ensuring that its postings -- | Balance this isolated transaction, ensuring that its postings
-- (and its balanced virtual postings) sum to 0, -- (and its balanced virtual postings) sum to 0,
-- by inferring a missing amount or conversion price(s) if needed. -- by inferring a missing amount or conversion price(s) if needed.
-- Or if balancing is not possible, because the amounts don't sum to 0 or -- Or if balancing is not possible, because the amounts don't sum to 0 or
-- because there's more than one missing amount, return an error message. -- because there's more than one missing amount, return an error message.
-- --
-- Transactions with balance assignments can have more than one -- Note this is not as accurate as @balanceTransactionInJournal@,
-- missing amount; to balance those you should use the more powerful -- which considers the whole journal when calculating balance assignments and balance assertions.
-- journalBalanceTransactions. balanceSingleTransaction :: BalancingOpts -> Transaction -> Either String Transaction
-- balanceSingleTransaction bopts = fmap fst . balanceTransactionHelper bopts
-- The "sum to 0" test is done using commodity display precisions,
-- if provided, so that the result agrees with the numbers users can see.
--
balanceTransaction ::
BalancingOpts
-> Transaction
-> Either String Transaction
balanceTransaction bopts = fmap fst . balanceTransactionHelper bopts
-- | Helper used by balanceTransaction and balanceTransactionWithBalanceAssignmentAndCheckAssertionsB; -- | Helper used by balanceSingleTransaction and balanceTransactionWithBalanceAssignmentAndCheckAssertionsB;
-- use one of those instead. -- use one of those instead.
-- It also returns a list of accounts and amounts that were inferred. -- It also returns a list of accounts and amounts that were inferred.
balanceTransactionHelper :: balanceTransactionHelper :: BalancingOpts -> Transaction -> Either String (Transaction, [(AccountName, MixedAmount)])
BalancingOpts
-> Transaction
-> Either String (Transaction, [(AccountName, MixedAmount)])
balanceTransactionHelper bopts t = do balanceTransactionHelper bopts t = do
let lbl = lbl_ "balanceTransactionHelper" let lbl = lbl_ "balanceTransactionHelper"
(t', inferredamtsandaccts) <- t (t', inferredamtsandaccts) <- t
@ -411,7 +400,7 @@ costInferrerFor t pt = maybe id infercost inferFromAndTo
-- journalBalanceTransactions -- journalBalanceTransactions
-- runST -- runST
-- runExceptT -- runExceptT
-- balanceTransaction (Transaction.hs) -- balanceSingleTransaction (Transaction.hs)
-- balanceTransactionHelper -- balanceTransactionHelper
-- runReaderT -- runReaderT
-- balanceTransactionAndCheckAssertionsB -- balanceTransactionAndCheckAssertionsB
@ -422,7 +411,7 @@ costInferrerFor t pt = maybe id infercost inferFromAndTo
-- journalCheckBalanceAssertions -- journalCheckBalanceAssertions
-- journalBalanceTransactions -- journalBalanceTransactions
-- transactionWizard, postingsBalanced (Add.hs), tests (Transaction.hs) -- transactionWizard, postingsBalanced (Add.hs), tests (Transaction.hs)
-- balanceTransaction (Transaction.hs) XXX hledger add won't allow balance assignments + missing amount ? -- balanceSingleTransaction (Transaction.hs) XXX hledger add won't allow balance assignments + missing amount ?
-- | Monad used for statefully balancing/amount-inferring/assertion-checking -- | Monad used for statefully balancing/amount-inferring/assertion-checking
-- a sequence of transactions. -- a sequence of transactions.
@ -532,7 +521,7 @@ journalBalanceTransactions bopts' j' =
-- postponing those which do until later. The balanced ones are split into their postings, -- postponing those which do until later. The balanced ones are split into their postings,
-- keeping these and the not-yet-balanced transactions in the same relative order. -- keeping these and the not-yet-balanced transactions in the same relative order.
psandts :: [Either Posting Transaction] <- fmap concat $ forM ts $ \case psandts :: [Either Posting Transaction] <- fmap concat $ forM ts $ \case
t | null $ assignmentPostings t -> case balanceTransaction bopts t of t | null $ assignmentPostings t -> case balanceSingleTransaction bopts t of
Left e -> throwError e Left e -> throwError e
Right t' -> do Right t' -> do
lift $ writeArray balancedtxns (tindex t') t' lift $ writeArray balancedtxns (tindex t') t'
@ -810,10 +799,10 @@ tests_Balancing =
(fst <$> transactionInferBalancingAmount M.empty nulltransaction{tpostings = ["a" `post` usd (-5), "b" `post` (eur 3 @@ usd 4), "c" `post` missingamt]}) @?= (fst <$> transactionInferBalancingAmount M.empty nulltransaction{tpostings = ["a" `post` usd (-5), "b" `post` (eur 3 @@ usd 4), "c" `post` missingamt]}) @?=
Right nulltransaction{tpostings = ["a" `post` usd (-5), "b" `post` (eur 3 @@ usd 4), "c" `post` usd 1]} Right nulltransaction{tpostings = ["a" `post` usd (-5), "b" `post` (eur 3 @@ usd 4), "c" `post` usd 1]}
, testGroup "balanceTransaction" [ , testGroup "balanceSingleTransaction" [
testCase "detect unbalanced entry, sign error" $ testCase "detect unbalanced entry, sign error" $
assertLeft assertLeft
(balanceTransaction defbalancingopts (balanceSingleTransaction defbalancingopts
(Transaction (Transaction
0 0
"" ""
@ -828,7 +817,7 @@ tests_Balancing =
[posting {paccount = "a", pamount = mixedAmount (usd 1)}, posting {paccount = "b", pamount = mixedAmount (usd 1)}])) [posting {paccount = "a", pamount = mixedAmount (usd 1)}, posting {paccount = "b", pamount = mixedAmount (usd 1)}]))
,testCase "detect unbalanced entry, multiple missing amounts" $ ,testCase "detect unbalanced entry, multiple missing amounts" $
assertLeft $ assertLeft $
balanceTransaction defbalancingopts balanceSingleTransaction defbalancingopts
(Transaction (Transaction
0 0
"" ""
@ -845,7 +834,7 @@ tests_Balancing =
]) ])
,testCase "one missing amount is inferred" $ ,testCase "one missing amount is inferred" $
(pamount . last . tpostings <$> (pamount . last . tpostings <$>
balanceTransaction defbalancingopts balanceSingleTransaction defbalancingopts
(Transaction (Transaction
0 0
"" ""
@ -861,7 +850,7 @@ tests_Balancing =
Right (mixedAmount $ usd (-1)) Right (mixedAmount $ usd (-1))
,testCase "conversion price is inferred" $ ,testCase "conversion price is inferred" $
(pamount . headErr . tpostings <$> -- PARTIAL headErr succeeds because non-null postings list (pamount . headErr . tpostings <$> -- PARTIAL headErr succeeds because non-null postings list
balanceTransaction defbalancingopts balanceSingleTransaction defbalancingopts
(Transaction (Transaction
0 0
"" ""
@ -877,9 +866,9 @@ tests_Balancing =
, posting {paccount = "b", pamount = mixedAmount (eur (-1))} , posting {paccount = "b", pamount = mixedAmount (eur (-1))}
])) @?= ])) @?=
Right (mixedAmount $ usd 1.35 @@ eur 1) Right (mixedAmount $ usd 1.35 @@ eur 1)
,testCase "balanceTransaction balances based on cost if there are unit prices" $ ,testCase "balanceSingleTransaction balances based on cost if there are unit prices" $
assertRight $ assertRight $
balanceTransaction defbalancingopts balanceSingleTransaction defbalancingopts
(Transaction (Transaction
0 0
"" ""
@ -894,9 +883,9 @@ tests_Balancing =
[ posting {paccount = "a", pamount = mixedAmount $ usd 1 `at` eur 2} [ posting {paccount = "a", pamount = mixedAmount $ usd 1 `at` eur 2}
, posting {paccount = "a", pamount = mixedAmount $ usd (-2) `at` eur 1} , posting {paccount = "a", pamount = mixedAmount $ usd (-2) `at` eur 1}
]) ])
,testCase "balanceTransaction balances based on cost if there are total prices" $ ,testCase "balanceSingleTransaction balances based on cost if there are total prices" $
assertRight $ assertRight $
balanceTransaction defbalancingopts balanceSingleTransaction defbalancingopts
(Transaction (Transaction
0 0
"" ""

View File

@ -96,7 +96,7 @@ validateTransaction ::
-> FormResult (Transaction, FilePath) -> FormResult (Transaction, FilePath)
validateTransaction deffile dateRes descRes postingsRes fileRes = validateTransaction deffile dateRes descRes postingsRes fileRes =
case makeTransaction <$> dateRes <*> descRes <*> postingsRes <*> fileRes of case makeTransaction <$> dateRes <*> descRes <*> postingsRes <*> fileRes of
FormSuccess (txn,f) -> case balanceTransaction defbalancingopts txn of FormSuccess (txn,f) -> case balanceSingleTransaction defbalancingopts txn of
Left e -> FormFailure [T.pack e] Left e -> FormFailure [T.pack e]
Right txn' -> FormSuccess (txn',f) Right txn' -> FormSuccess (txn',f)
x -> x x -> x

View File

@ -346,7 +346,7 @@ descriptionAndCommentWizard PrevInput{..} EntryState{..} = do
return $ Just (desc, comment) return $ Just (desc, comment)
postingsBalanced :: [Posting] -> Bool postingsBalanced :: [Posting] -> Bool
postingsBalanced ps = isRight $ balanceTransaction defbalancingopts nulltransaction{tpostings=ps} postingsBalanced ps = isRight $ balanceSingleTransaction defbalancingopts nulltransaction{tpostings=ps}
accountWizard PrevInput{..} EntryState{..} = do accountWizard PrevInput{..} EntryState{..} = do
let pnum = length esPostings + 1 let pnum = length esPostings + 1