dev:Transaction: refactor/clarify transactionInferCostsFromEquity
This commit is contained in:
parent
516a5cb448
commit
d19690e5bd
@ -53,7 +53,7 @@ module Hledger.Data.Transaction
|
|||||||
) where
|
) where
|
||||||
|
|
||||||
import Control.Monad.Trans.State (StateT(..), evalStateT)
|
import Control.Monad.Trans.State (StateT(..), evalStateT)
|
||||||
import Data.Bifunctor (first)
|
import Data.Bifunctor (first, second)
|
||||||
import Data.Foldable (foldlM)
|
import Data.Foldable (foldlM)
|
||||||
import Data.Maybe (fromMaybe, isJust, mapMaybe)
|
import Data.Maybe (fromMaybe, isJust, mapMaybe)
|
||||||
import Data.Semigroup (Endo(..))
|
import Data.Semigroup (Endo(..))
|
||||||
@ -70,6 +70,7 @@ import Hledger.Data.Dates
|
|||||||
import Hledger.Data.Posting
|
import Hledger.Data.Posting
|
||||||
import Hledger.Data.Amount
|
import Hledger.Data.Amount
|
||||||
import Hledger.Data.Valuation
|
import Hledger.Data.Valuation
|
||||||
|
import Data.Decimal (normalizeDecimal, decimalPlaces)
|
||||||
import Data.Functor ((<&>))
|
import Data.Functor ((<&>))
|
||||||
|
|
||||||
|
|
||||||
@ -229,7 +230,11 @@ transactionAddInferredEquityPostings verbosetags equityAcct t =
|
|||||||
|
|
||||||
type IdxPosting = (Int, Posting)
|
type IdxPosting = (Int, Posting)
|
||||||
|
|
||||||
-- XXX WARNING, the following code is twisty and hard to follow
|
-- XXX Warning: The following code - for analysing equity conversion postings,
|
||||||
|
-- inferring missing costs and ignoring redundant costs -
|
||||||
|
-- is twisty and hard to follow.
|
||||||
|
|
||||||
|
label s = ((s <> ": ")++)
|
||||||
|
|
||||||
-- | Add costs inferred from equity postings in this transaction.
|
-- | Add costs inferred from equity postings in this transaction.
|
||||||
-- For every adjacent pair of conversion postings, it will first search the postings
|
-- For every adjacent pair of conversion postings, it will first search the postings
|
||||||
@ -241,7 +246,6 @@ type IdxPosting = (Int, Posting)
|
|||||||
-- the costful and conversion postings, but don't add costs.
|
-- the costful and conversion postings, but don't add costs.
|
||||||
transactionInferCostsFromEquity :: Bool -> M.Map AccountName AccountType -> Transaction -> Either String Transaction
|
transactionInferCostsFromEquity :: Bool -> M.Map AccountName AccountType -> Transaction -> Either String Transaction
|
||||||
transactionInferCostsFromEquity dryrun acctTypes t = first (annotateErrorWithTransaction t . T.unpack) $ do
|
transactionInferCostsFromEquity dryrun acctTypes t = first (annotateErrorWithTransaction t . T.unpack) $ do
|
||||||
|
|
||||||
-- number the postings
|
-- number the postings
|
||||||
let npostings = zip [0..] $ tpostings t
|
let npostings = zip [0..] $ tpostings t
|
||||||
|
|
||||||
@ -253,51 +257,65 @@ transactionInferCostsFromEquity dryrun acctTypes t = first (annotateErrorWithTra
|
|||||||
-- 1. each pair of conversion postings, and the corresponding postings which balance them, are tagged for easy identification
|
-- 1. each pair of conversion postings, and the corresponding postings which balance them, are tagged for easy identification
|
||||||
-- 2. each pair of balancing postings which did't have an explicit cost, have had a cost calculated and added to one of them
|
-- 2. each pair of balancing postings which did't have an explicit cost, have had a cost calculated and added to one of them
|
||||||
-- 3. if any ambiguous situation was detected, an informative error is raised
|
-- 3. if any ambiguous situation was detected, an informative error is raised
|
||||||
updateposting <- transformIndexedPostingsF (addCostsToPostings dryrun) conversionPairs otherps
|
processposting <- transformIndexedPostingsF (addCostsToPostings dryrun) conversionPairs otherps
|
||||||
|
|
||||||
-- And if there was no error, use it to modify the transaction's postings
|
-- And if there was no error, use it to modify the transaction's postings.
|
||||||
return t{tpostings = map (snd . updateposting) npostings}
|
return t{tpostings = map (snd . processposting) npostings}
|
||||||
|
|
||||||
where
|
where
|
||||||
|
|
||||||
-- A helper to apply addCostsToPostings to each pair of conversion postings,
|
-- Generate the tricksy processposting function,
|
||||||
-- while statefully consuming the other postings.
|
-- which when applied to each posting in turn, rather magically has the effect of
|
||||||
transformIndexedPostingsF f = evalStateT . fmap (appEndo . foldMap Endo) . traverse f
|
-- applying addCostsToPostings to each pair of conversion postings in the transaction,
|
||||||
|
-- matching them with the other postings, tagging them and perhaps adding cost information to the other postings.
|
||||||
|
-- General type:
|
||||||
|
-- transformIndexedPostingsF :: (Monad m, Foldable t, Traversable t) =>
|
||||||
|
-- (a -> StateT s m (a1 -> a1)) ->
|
||||||
|
-- t a ->
|
||||||
|
-- s ->
|
||||||
|
-- m (a1 -> a1)
|
||||||
|
-- Concrete type:
|
||||||
|
transformIndexedPostingsF ::
|
||||||
|
((IdxPosting, IdxPosting) -> StateT ([IdxPosting],[IdxPosting]) (Either Text) (IdxPosting -> IdxPosting)) -> -- state update function (addCostsToPostings with the bool applied)
|
||||||
|
[(IdxPosting, IdxPosting)] -> -- initial state: the pairs of adjacent conversion postings in the transaction
|
||||||
|
([IdxPosting],[IdxPosting]) -> -- initial state: the other postings in the transaction, separated into costful and costless
|
||||||
|
(Either Text (IdxPosting -> IdxPosting)) -- returns an error message or a posting transform function
|
||||||
|
transformIndexedPostingsF updatefn = evalStateT . fmap (appEndo . foldMap Endo) . traverse (updatefn)
|
||||||
|
|
||||||
-- Generate a state-updating function to be used by the above. Approximately:
|
-- A tricksy state update helper for processposting/transformIndexedPostingsF.
|
||||||
-- given a pair of conversion postings, and two stateful lists representing the remaining
|
-- Approximately: given a pair of conversion postings to match,
|
||||||
-- costful and costless other postings,
|
-- and lists of the remaining unmatched costful and costless other postings,
|
||||||
-- 1. consume two other postings which match the conversion postings
|
-- 1. find (and consume) two other postings which match the two conversion postings
|
||||||
-- 2. add tags to the two conversion postings and the matched other postings
|
-- 2. add identifying tags to the four postings
|
||||||
-- 3. add an explicit cost, if there is none, to one of matched other postings
|
-- 3. add an explicit cost, if missing, to one of the matched other postings
|
||||||
-- or raise an informative error if ambiguity is found, or do nothing, as appropriate.
|
-- 4. or if there is a problem, raise an informative error or do nothing as appropriate.
|
||||||
--
|
-- Or, if the first argument is true:
|
||||||
-- Or, if the first argument is true, do a dry run instead:
|
-- do a dry run instead: find and consume, add tags, but don't add costs
|
||||||
-- identify and tag
|
-- (and if there are no costful postings at all, do nothing).
|
||||||
-- the costful and conversion postings, but don't add costs (and if there are no
|
addCostsToPostings :: Bool -> (IdxPosting, IdxPosting) -> StateT ([IdxPosting], [IdxPosting]) (Either Text) (IdxPosting -> IdxPosting)
|
||||||
-- costful postings at all, do nothing).
|
|
||||||
--
|
|
||||||
addCostsToPostings :: Bool -> (IdxPosting, IdxPosting)
|
|
||||||
-> StateT ([IdxPosting], [IdxPosting]) (Either Text) (IdxPosting -> IdxPosting)
|
|
||||||
addCostsToPostings dryrun' ((n1, cp1), (n2, cp2)) = StateT $ \(costps, otherps) -> do
|
addCostsToPostings dryrun' ((n1, cp1), (n2, cp2)) = StateT $ \(costps, otherps) -> do
|
||||||
-- Get the two conversion posting amounts, if possible
|
-- Get the two conversion posting amounts, if possible
|
||||||
ca1 <- conversionPostingAmountNoCost cp1
|
ca1 <- conversionPostingAmountNoCost cp1
|
||||||
ca2 <- conversionPostingAmountNoCost cp2
|
ca2 <- conversionPostingAmountNoCost cp2
|
||||||
let
|
let
|
||||||
-- Approximately: add a cost and/or tag to this posting if appropriate.
|
|
||||||
transformPostingF np costp (n,p) =
|
|
||||||
(n, if | n == np -> costp `postingAddTags` [("_price-matched","")]
|
|
||||||
| n == n1 || n == n2 -> p `postingAddTags` [("_conversion-matched","")]
|
|
||||||
| otherwise -> p)
|
|
||||||
|
|
||||||
-- All costful postings which match the conversion posting pair
|
-- All costful postings which match the conversion posting pair
|
||||||
matchingCostPs = mapMaybe (mapM $ costfulPostingIfMatchesBothAmounts ca1 ca2) costps
|
matchingCostPs =
|
||||||
|
dbg7With (label "matched costful postings".show.length) $
|
||||||
|
mapMaybe (mapM $ costfulPostingIfMatchesBothAmounts ca1 ca2) costps
|
||||||
|
|
||||||
-- All other single-commodity postings whose amount matches at least one of the conversion postings,
|
-- All other single-commodity postings whose amount matches at least one of the conversion postings,
|
||||||
-- with an explicit cost added. Or in dry run mode, all other single-commodity postings.
|
-- with an explicit cost added. Or in dry run mode, all other single-commodity postings.
|
||||||
matchingOtherPs
|
matchingOtherPs =
|
||||||
| dryrun' = [(n,(p, a)) | (n,p) <- otherps, let Just a = postingSingleAmount p]
|
dbg7With (label "matched costless postings".show.length) $
|
||||||
| otherwise = mapMaybe (mapM $ addCostIfMatchesOneAmount ca1 ca2) otherps
|
if dryrun'
|
||||||
|
then [(n,(p, a)) | (n,p) <- otherps, let Just a = postingSingleAmount p]
|
||||||
|
else mapMaybe (mapM $ addCostIfMatchesOneAmount ca1 ca2) otherps
|
||||||
|
|
||||||
|
-- A function that adds a cost and/or tag to a numbered posting if appropriate.
|
||||||
|
postingAddCostAndOrTag np costp (n,p) =
|
||||||
|
(n, if | n == np -> costp `postingAddTags` [("_price-matched","")]
|
||||||
|
| n == n1 || n == n2 -> p `postingAddTags` [("_conversion-matched","")]
|
||||||
|
| otherwise -> p)
|
||||||
|
|
||||||
-- Annotate any errors with the conversion posting pair
|
-- Annotate any errors with the conversion posting pair
|
||||||
first (annotateWithPostings [cp1, cp2]) $
|
first (annotateWithPostings [cp1, cp2]) $
|
||||||
@ -306,9 +324,9 @@ transactionInferCostsFromEquity dryrun acctTypes t = first (annotateErrorWithTra
|
|||||||
-- delete it from the list of costful postings in the state, delete the
|
-- delete it from the list of costful postings in the state, delete the
|
||||||
-- first matching costless posting from the list of costless postings
|
-- first matching costless posting from the list of costless postings
|
||||||
-- in the state, and return the transformation function with the new state.
|
-- in the state, and return the transformation function with the new state.
|
||||||
| [(np, (costp, _))] <- matchingCostPs
|
| [(np, costp)] <- matchingCostPs
|
||||||
, Just newcostps <- deleteIdx np costps
|
, Just newcostps <- deleteIdx np costps
|
||||||
-> Right (transformPostingF np costp, (if dryrun' then costps else newcostps, otherps))
|
-> Right (postingAddCostAndOrTag np costp, (if dryrun' then costps else newcostps, otherps))
|
||||||
|
|
||||||
-- If no costful postings match the conversion postings, but some
|
-- If no costful postings match the conversion postings, but some
|
||||||
-- of the costless postings match, check that the first such posting has a
|
-- of the costless postings match, check that the first such posting has a
|
||||||
@ -317,9 +335,9 @@ transactionInferCostsFromEquity dryrun acctTypes t = first (annotateErrorWithTra
|
|||||||
-- and return the transformation function with the new state.
|
-- and return the transformation function with the new state.
|
||||||
| [] <- matchingCostPs
|
| [] <- matchingCostPs
|
||||||
, (np, (costp, amt)):nps <- matchingOtherPs
|
, (np, (costp, amt)):nps <- matchingOtherPs
|
||||||
, not $ any (amountMatches amt . snd . snd) nps
|
, not $ any (amountsMatch amt . snd . snd) nps
|
||||||
, Just newotherps <- deleteIdx np otherps
|
, Just newotherps <- deleteIdx np otherps
|
||||||
-> Right (transformPostingF np costp, (costps, if dryrun' then otherps else newotherps))
|
-> Right (postingAddCostAndOrTag np costp, (costps, if dryrun' then otherps else newotherps))
|
||||||
|
|
||||||
-- Otherwise, do nothing, leaving the transaction unchanged.
|
-- Otherwise, do nothing, leaving the transaction unchanged.
|
||||||
-- We don't want to be over-zealous reporting problems here
|
-- We don't want to be over-zealous reporting problems here
|
||||||
@ -329,12 +347,16 @@ transactionInferCostsFromEquity dryrun acctTypes t = first (annotateErrorWithTra
|
|||||||
|
|
||||||
-- If a posting with cost matches both the conversion amounts, return it along
|
-- If a posting with cost matches both the conversion amounts, return it along
|
||||||
-- with the matching amount which must be present in another non-conversion posting.
|
-- with the matching amount which must be present in another non-conversion posting.
|
||||||
costfulPostingIfMatchesBothAmounts :: Amount -> Amount -> Posting -> Maybe (Posting, Amount)
|
costfulPostingIfMatchesBothAmounts :: Amount -> Amount -> Posting -> Maybe Posting
|
||||||
costfulPostingIfMatchesBothAmounts a1 a2 p = do
|
costfulPostingIfMatchesBothAmounts a1 a2 costfulp = do
|
||||||
a@Amount{aprice=Just _} <- postingSingleAmount p
|
a@Amount{aprice=Just _} <- postingSingleAmount costfulp
|
||||||
if | amountMatches (-a1) a && amountMatches a2 (amountCost a) -> Just (p, -a2)
|
if
|
||||||
| amountMatches (-a2) a && amountMatches a1 (amountCost a) -> Just (p, -a1)
|
| dbgamtmatch 1 a1 a (amountsMatch (-a1) a) && dbgcostmatch 2 a2 a (amountsMatch a2 (amountCost a)) -> Just costfulp
|
||||||
|
| dbgamtmatch 2 a2 a (amountsMatch (-a2) a) && dbgcostmatch 1 a1 a (amountsMatch a1 (amountCost a)) -> Just costfulp
|
||||||
| otherwise -> Nothing
|
| otherwise -> Nothing
|
||||||
|
where
|
||||||
|
dbgamtmatch n a b = dbg7 ("conversion posting " <>show n<>" "<>showAmount a<>" balances amount "<>showAmountWithoutPrice b <>" of costful posting "<>showAmount b<>" at precision "<>amountShowPrecision a<>" ?")
|
||||||
|
dbgcostmatch n a b = dbg7 ("and\nconversion posting "<>show n<>" "<>showAmount a<>" matches cost " <>showAmount (amountCost b)<>" of costful posting "<>showAmount b<>" at precision "<>amountShowPrecision a<>" ?")
|
||||||
|
|
||||||
-- Add a cost to a posting if it matches (negative) one of the
|
-- Add a cost to a posting if it matches (negative) one of the
|
||||||
-- supplied conversion amounts, adding the other amount as the cost.
|
-- supplied conversion amounts, adding the other amount as the cost.
|
||||||
@ -342,9 +364,10 @@ transactionInferCostsFromEquity dryrun acctTypes t = first (annotateErrorWithTra
|
|||||||
addCostIfMatchesOneAmount a1 a2 p = do
|
addCostIfMatchesOneAmount a1 a2 p = do
|
||||||
a <- postingSingleAmount p
|
a <- postingSingleAmount p
|
||||||
let newp cost = p{pamount = mixedAmount a{aprice = Just $ TotalPrice cost}}
|
let newp cost = p{pamount = mixedAmount a{aprice = Just $ TotalPrice cost}}
|
||||||
if | amountMatches (-a1) a -> Just (newp a2, a2)
|
if
|
||||||
| amountMatches (-a2) a -> Just (newp a1, a1)
|
| amountsMatch (-a1) a -> Just (newp a2, a2)
|
||||||
| otherwise -> Nothing
|
| amountsMatch (-a2) a -> Just (newp a1, a1)
|
||||||
|
| otherwise -> Nothing
|
||||||
|
|
||||||
-- Get the single-commodity costless amount from a conversion posting, or raise an error.
|
-- Get the single-commodity costless amount from a conversion posting, or raise an error.
|
||||||
conversionPostingAmountNoCost p = case postingSingleAmount p of
|
conversionPostingAmountNoCost p = case postingSingleAmount p of
|
||||||
@ -368,6 +391,11 @@ transactionInferCostsFromEquity dryrun acctTypes t = first (annotateErrorWithTra
|
|||||||
deleteUniqueMatch _ [] = Nothing
|
deleteUniqueMatch _ [] = Nothing
|
||||||
annotateWithPostings xs str = T.unlines $ str : postingsAsLines False xs
|
annotateWithPostings xs str = T.unlines $ str : postingsAsLines False xs
|
||||||
|
|
||||||
|
amountShowPrecision a =
|
||||||
|
case asprecision $ astyle a of
|
||||||
|
Precision n -> show n
|
||||||
|
NaturalPrecision -> show $ decimalPlaces $ normalizeDecimal $ aquantity a
|
||||||
|
|
||||||
-- Using the provided account types map, sort the given indexed postings
|
-- Using the provided account types map, sort the given indexed postings
|
||||||
-- into three lists of posting numbers (stored in two pairs), like so:
|
-- into three lists of posting numbers (stored in two pairs), like so:
|
||||||
-- (conversion postings, (costful other postings, costless other postings)).
|
-- (conversion postings, (costful other postings, costless other postings)).
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user