lib!: modifyTransactions now takes a Map of commodity styles, and will

style amounts according to that argument. journalAddForecast and
journalTransform now return an Either String Journal.

This improves efficiency, as we no longer have to restyle all amounts in
the journal after generating auto postings or periodic transactions.
Changing the return type of journalAddForecast and journalTransform
reduces partiality.

To get the previous behaviour for modifyTransaction, use modifyTransaction mempty.
This commit is contained in:
Stephen Morgan 2021-07-27 00:37:08 +10:00 committed by Simon Michael
parent 2da0e67141
commit fc8aa602cf
7 changed files with 50 additions and 46 deletions

View File

@ -723,7 +723,7 @@ journalUntieTransactions t@Transaction{tpostings=ps} = t{tpostings=map (\p -> p{
-- relative dates in transaction modifier queries. -- relative dates in transaction modifier queries.
journalModifyTransactions :: Day -> Journal -> Either String Journal journalModifyTransactions :: Day -> Journal -> Either String Journal
journalModifyTransactions d j = journalModifyTransactions d j =
case modifyTransactions d (jtxnmodifiers j) (jtxns j) of case modifyTransactions (journalCommodityStyles j) d (jtxnmodifiers j) (jtxns j) of
Right ts -> Right j{jtxns=ts} Right ts -> Right j{jtxns=ts}
Left err -> Left err Left err -> Left err
@ -1066,20 +1066,12 @@ checkBalanceAssignmentUnassignableAccountB p = do
-- amounts in each commodity (see journalCommodityStyles). -- amounts in each commodity (see journalCommodityStyles).
-- Can return an error message eg if inconsistent number formats are found. -- Can return an error message eg if inconsistent number formats are found.
journalApplyCommodityStyles :: Journal -> Either String Journal journalApplyCommodityStyles :: Journal -> Either String Journal
journalApplyCommodityStyles j@Journal{jtxns=ts, jpricedirectives=pds} = journalApplyCommodityStyles = fmap fixjournal . journalInferCommodityStyles
case journalInferCommodityStyles j of
Left e -> Left e
Right j' -> Right j''
where where
styles = journalCommodityStyles j' fixjournal j@Journal{jpricedirectives=pds} =
j'' = j'{jtxns=map fixtransaction ts journalMapPostings (postingApplyCommodityStyles styles) j{jpricedirectives=map fixpricedirective pds}
,jpricedirectives=map fixpricedirective pds where
} styles = journalCommodityStyles j
fixtransaction t@Transaction{tpostings=ps} = t{tpostings=map fixposting ps}
fixposting p = p{pamount=styleMixedAmount styles $ pamount p
,pbalanceassertion=fixbalanceassertion <$> pbalanceassertion p}
-- balance assertion amounts are always displayed (by print) at full precision, per docs
fixbalanceassertion ba = ba{baamount=styleAmountExceptPrecision styles $ baamount ba}
fixpricedirective pd@PriceDirective{pdamount=a} = pd{pdamount=styleAmountExceptPrecision styles a} fixpricedirective pd@PriceDirective{pdamount=a} = pd{pdamount=styleAmountExceptPrecision styles a}
-- | Get the canonical amount styles for this journal, whether (in order of precedence): -- | Get the canonical amount styles for this journal, whether (in order of precedence):

View File

@ -38,6 +38,7 @@ module Hledger.Data.Posting (
relatedPostings, relatedPostings,
postingStripPrices, postingStripPrices,
postingApplyAliases, postingApplyAliases,
postingApplyCommodityStyles,
-- * date operations -- * date operations
postingDate, postingDate,
postingDate2, postingDate2,
@ -298,6 +299,14 @@ postingApplyAliases aliases p@Posting{paccount} =
err = "problem while applying account aliases:\n" ++ pshow aliases err = "problem while applying account aliases:\n" ++ pshow aliases
++ "\n to account name: "++T.unpack paccount++"\n "++e ++ "\n to account name: "++T.unpack paccount++"\n "++e
-- | Choose and apply a consistent display style to the posting
-- amounts in each commodity (see journalCommodityStyles).
postingApplyCommodityStyles :: M.Map CommoditySymbol AmountStyle -> Posting -> Posting
postingApplyCommodityStyles styles p = p{pamount=styleMixedAmount styles $ pamount p
,pbalanceassertion=fixbalanceassertion <$> pbalanceassertion p}
where
fixbalanceassertion ba = ba{baamount=styleAmountExceptPrecision styles $ baamount ba}
-- | Rewrite an account name using all matching aliases from the given list, in sequence. -- | Rewrite an account name using all matching aliases from the given list, in sequence.
-- Each alias sees the result of applying the previous aliases. -- Each alias sees the result of applying the previous aliases.
-- Or, return any error arising from a bad regular expression in the aliases. -- Or, return any error arising from a bad regular expression in the aliases.

View File

@ -13,6 +13,7 @@ module Hledger.Data.TransactionModifier (
where where
import Control.Applicative ((<|>), liftA2) import Control.Applicative ((<|>), liftA2)
import qualified Data.Map as M
import Data.Maybe (catMaybes) import Data.Maybe (catMaybes)
import qualified Data.Text as T import qualified Data.Text as T
import Data.Time.Calendar (Day) import Data.Time.Calendar (Day)
@ -22,7 +23,7 @@ import Hledger.Data.Amount
import Hledger.Data.Transaction (txnTieKnot) import Hledger.Data.Transaction (txnTieKnot)
import Hledger.Query (Query, filterQuery, matchesAmount, matchesPosting, import Hledger.Query (Query, filterQuery, matchesAmount, matchesPosting,
parseQuery, queryIsAmt, queryIsSym, simplifyQuery) parseQuery, queryIsAmt, queryIsSym, simplifyQuery)
import Hledger.Data.Posting (commentJoin, commentAddTag) import Hledger.Data.Posting (commentJoin, commentAddTag, postingApplyCommodityStyles)
import Hledger.Utils (dbg6, wrap) import Hledger.Utils (dbg6, wrap)
-- $setup -- $setup
@ -35,9 +36,9 @@ import Hledger.Utils (dbg6, wrap)
-- Or if any of them fails to be parsed, return the first error. A reference -- Or if any of them fails to be parsed, return the first error. A reference
-- date is provided to help interpret relative dates in transaction modifier -- date is provided to help interpret relative dates in transaction modifier
-- queries. -- queries.
modifyTransactions :: Day -> [TransactionModifier] -> [Transaction] -> Either String [Transaction] modifyTransactions :: M.Map CommoditySymbol AmountStyle -> Day -> [TransactionModifier] -> [Transaction] -> Either String [Transaction]
modifyTransactions d tmods ts = do modifyTransactions styles d tmods ts = do
fs <- mapM (transactionModifierToFunction d) tmods -- convert modifiers to functions, or return a parse error fs <- mapM (transactionModifierToFunction styles d) tmods -- convert modifiers to functions, or return a parse error
let let
modifytxn t = t'' modifytxn t = t''
where where
@ -61,7 +62,7 @@ modifyTransactions d tmods ts = do
-- >>> import qualified Data.Text.IO as T -- >>> import qualified Data.Text.IO as T
-- >>> t = nulltransaction{tpostings=["ping" `post` usd 1]} -- >>> t = nulltransaction{tpostings=["ping" `post` usd 1]}
-- >>> tmpost acc amt = TMPostingRule (acc `post` amt) False -- >>> tmpost acc amt = TMPostingRule (acc `post` amt) False
-- >>> test = either putStr (T.putStr.showTransaction) . fmap ($ t) . transactionModifierToFunction nulldate -- >>> test = either putStr (T.putStr.showTransaction) . fmap ($ t) . transactionModifierToFunction mempty nulldate
-- >>> test $ TransactionModifier "" ["pong" `tmpost` usd 2] -- >>> test $ TransactionModifier "" ["pong" `tmpost` usd 2]
-- 0000-01-01 -- 0000-01-01
-- ping $1.00 -- ping $1.00
@ -77,11 +78,11 @@ modifyTransactions d tmods ts = do
-- pong $3.00 ; generated-posting: = ping -- pong $3.00 ; generated-posting: = ping
-- <BLANKLINE> -- <BLANKLINE>
-- --
transactionModifierToFunction :: Day -> TransactionModifier -> Either String (Transaction -> Transaction) transactionModifierToFunction :: M.Map CommoditySymbol AmountStyle -> Day -> TransactionModifier -> Either String (Transaction -> Transaction)
transactionModifierToFunction refdate TransactionModifier{tmquerytxt, tmpostingrules} = do transactionModifierToFunction styles refdate TransactionModifier{tmquerytxt, tmpostingrules} = do
q <- simplifyQuery . fst <$> parseQuery refdate tmquerytxt q <- simplifyQuery . fst <$> parseQuery refdate tmquerytxt
let let
fs = map (tmPostingRuleToFunction q tmquerytxt) tmpostingrules fs = map (tmPostingRuleToFunction styles q tmquerytxt) tmpostingrules
generatePostings ps = concatMap (\p -> p : map ($p) (if q `matchesPosting` p then fs else [])) ps generatePostings ps = concatMap (\p -> p : map ($p) (if q `matchesPosting` p then fs else [])) ps
Right $ \t@(tpostings -> ps) -> txnTieKnot t{tpostings=generatePostings ps} Right $ \t@(tpostings -> ps) -> txnTieKnot t{tpostings=generatePostings ps}
@ -93,9 +94,9 @@ transactionModifierToFunction refdate TransactionModifier{tmquerytxt, tmpostingr
-- and a hidden _generated-posting: tag which does not. -- and a hidden _generated-posting: tag which does not.
-- The TransactionModifier's query text is also provided, and saved -- The TransactionModifier's query text is also provided, and saved
-- as the tags' value. -- as the tags' value.
tmPostingRuleToFunction :: Query -> T.Text -> TMPostingRule -> (Posting -> Posting) tmPostingRuleToFunction :: M.Map CommoditySymbol AmountStyle -> Query -> T.Text -> TMPostingRule -> (Posting -> Posting)
tmPostingRuleToFunction query querytxt tmpr = tmPostingRuleToFunction styles query querytxt tmpr =
\p -> renderPostingCommentDates $ pr \p -> postingApplyCommodityStyles styles . renderPostingCommentDates $ pr
{ pdate = pdate pr <|> pdate p { pdate = pdate pr <|> pdate p
, pdate2 = pdate2 pr <|> pdate2 p , pdate2 = pdate2 pr <|> pdate2 p
, pamount = amount' p , pamount = amount' p

View File

@ -362,8 +362,6 @@ journalFinalise InputOpts{auto_,balancingopts_,strict_} f txt pj' = do
-- (Note adding auto postings after balancing means #893b fails; -- (Note adding auto postings after balancing means #893b fails;
-- adding them before balancing probably means #893a, #928, #938 fail.) -- adding them before balancing probably means #893a, #928, #938 fail.)
>>= journalModifyTransactions d >>= journalModifyTransactions d
-- then apply commodity styles once more, to style the auto posting amounts. (XXX inefficient ?)
>>= journalApplyCommodityStyles
-- then check balance assertions. -- then check balance assertions.
>>= journalBalanceTransactions balancingopts_ >>= journalBalanceTransactions balancingopts_

View File

@ -88,7 +88,7 @@ hledgerWebTest = do
Right rs -> rs Right rs -> rs
copts = defcliopts{reportspec_=rspec, file_=[""]} -- non-empty, see file_ note above copts = defcliopts{reportspec_=rspec, file_=[""]} -- non-empty, see file_ note above
wopts = defwebopts{cliopts_=copts} wopts = defwebopts{cliopts_=copts}
j <- fmap (journalTransform copts) $ readJournal' (T.pack $ unlines -- PARTIAL: readJournal' should not fail j <- fmap (either error id . journalTransform copts) $ readJournal' (T.pack $ unlines -- PARTIAL: readJournal' should not fail
["~ monthly" ["~ monthly"
," assets 10" ," assets 10"
," income" ," income"

View File

@ -41,7 +41,7 @@ rewrite opts@CliOpts{rawopts_=rawopts,reportspec_=rspec} j@Journal{jtxns=ts} = d
-- rewrite matched transactions -- rewrite matched transactions
d <- getCurrentDay d <- getCurrentDay
let modifiers = transactionModifierFromOpts opts : jtxnmodifiers j let modifiers = transactionModifierFromOpts opts : jtxnmodifiers j
let j' = j{jtxns=either error' id $ modifyTransactions d modifiers ts} -- PARTIAL: let j' = j{jtxns=either error' id $ modifyTransactions mempty d modifiers ts} -- PARTIAL:
-- run the print command, showing all transactions, or show diffs -- run the print command, showing all transactions, or show diffs
printOrDiff rawopts opts{reportspec_=rspec{_rsQuery=Any}} j j' printOrDiff rawopts opts{reportspec_=rspec{_rsQuery=Any}} j j'

View File

@ -75,7 +75,7 @@ withJournalDo opts cmd = do
-- to let the add command work. -- to let the add command work.
journalpaths <- journalFilePathFromOpts opts journalpaths <- journalFilePathFromOpts opts
files <- readJournalFiles (inputopts_ opts) journalpaths files <- readJournalFiles (inputopts_ opts) journalpaths
let transformed = journalTransform opts <$> files let transformed = journalTransform opts =<< files
either error' cmd transformed -- PARTIAL: either error' cmd transformed -- PARTIAL:
-- | Apply some extra post-parse transformations to the journal, if -- | Apply some extra post-parse transformations to the journal, if
@ -86,12 +86,14 @@ withJournalDo opts cmd = do
-- - pivoting account names (--pivot) -- - pivoting account names (--pivot)
-- - anonymising (--anonymise). -- - anonymising (--anonymise).
-- --
journalTransform :: CliOpts -> Journal -> Journal -- This will return an error message if the query in any auto posting rule fails
-- to parse, or the generated transactions are not balanced.
journalTransform :: CliOpts -> Journal -> Either String Journal
journalTransform opts = journalTransform opts =
anonymiseByOpts opts fmap (anonymiseByOpts opts)
-- - converting amounts to market value (--value) -- - converting amounts to market value (--value)
-- . journalApplyValue ropts -- . journalApplyValue ropts
. pivotByOpts opts . fmap (pivotByOpts opts)
. journalAddForecast opts . journalAddForecast opts
-- | Apply the pivot transformation on a journal, if option is present. -- | Apply the pivot transformation on a journal, if option is present.
@ -113,26 +115,28 @@ anonymiseByOpts opts =
-- --
-- When --auto is active, auto posting rules will be applied to the -- When --auto is active, auto posting rules will be applied to the
-- generated transactions. If the query in any auto posting rule fails -- generated transactions. If the query in any auto posting rule fails
-- to parse, this function will raise an error. -- to parse, or the generated transactions are not balanced, this function will
-- return an error message.
-- --
-- The start & end date for generated periodic transactions are determined in -- The start & end date for generated periodic transactions are determined in
-- a somewhat complicated way; see the hledger manual -> Periodic transactions. -- a somewhat complicated way; see the hledger manual -> Periodic transactions.
-- --
journalAddForecast :: CliOpts -> Journal -> Journal journalAddForecast :: CliOpts -> Journal -> Either String Journal
journalAddForecast CliOpts{inputopts_=iopts, reportspec_=rspec} j = journalAddForecast CliOpts{inputopts_=iopts, reportspec_=rspec} j =
case forecast_ ropts of case forecast_ ropts of
Nothing -> j Nothing -> return j
Just _ -> either error id $ do -- PARTIAL: Just _ -> do
forecasttxns <- addAutoTxns =<< mapM (balanceTransaction (balancingopts_ iopts)) forecasttxns <- addAutoTxns =<< mapM (balanceTransaction (balancingopts_ iopts))
[ txnTieKnot t | pt <- jperiodictxns j [ txnTieKnot $ transactionTransformPostings (postingApplyCommodityStyles styles) t
| pt <- jperiodictxns j
, t <- runPeriodicTransaction pt forecastspan , t <- runPeriodicTransaction pt forecastspan
, spanContainsDate forecastspan (tdate t) , spanContainsDate forecastspan (tdate t)
] ]
journalBalanceTransactions (balancingopts_ iopts) j{ jtxns = concat [jtxns j, forecasttxns] } journalBalanceTransactions (balancingopts_ iopts) j{ jtxns = concat [jtxns j, forecasttxns] }
>>= journalApplyCommodityStyles
where where
today = _rsDay rspec today = _rsDay rspec
ropts = _rsReportOpts rspec ropts = _rsReportOpts rspec
styles = journalCommodityStyles j
-- "They can start no earlier than: the day following the latest normal transaction in the journal (or today if there are none)." -- "They can start no earlier than: the day following the latest normal transaction in the journal (or today if there are none)."
mjournalend = dbg2 "journalEndDate" $ journalEndDate False j -- ignore secondary dates mjournalend = dbg2 "journalEndDate" $ journalEndDate False j -- ignore secondary dates
@ -147,7 +151,7 @@ journalAddForecast CliOpts{inputopts_=iopts, reportspec_=rspec} j =
(fromMaybe nulldatespan $ dbg2 "forecastspan flag" $ forecast_ ropts) (fromMaybe nulldatespan $ dbg2 "forecastspan flag" $ forecast_ ropts)
(DateSpan (Just forecastbeginDefault) (Just forecastendDefault)) (DateSpan (Just forecastbeginDefault) (Just forecastendDefault))
addAutoTxns = if auto_ iopts then modifyTransactions today (jtxnmodifiers j) else return addAutoTxns = if auto_ iopts then modifyTransactions styles today (jtxnmodifiers j) else return
-- | Write some output to stdout or to a file selected by --output-file. -- | Write some output to stdout or to a file selected by --output-file.
-- If the file exists it will be overwritten. -- If the file exists it will be overwritten.
@ -196,7 +200,7 @@ journalReload :: CliOpts -> IO (Either String Journal)
journalReload opts = do journalReload opts = do
journalpaths <- dbg6 "reloading files" <$> journalFilePathFromOpts opts journalpaths <- dbg6 "reloading files" <$> journalFilePathFromOpts opts
files <- readJournalFiles (inputopts_ opts) journalpaths files <- readJournalFiles (inputopts_ opts) journalpaths
return $ journalTransform opts <$> files return $ journalTransform opts =<< files
-- | Has the specified file changed since the journal was last read ? -- | Has the specified file changed since the journal was last read ?
-- Typically this is one of the journal's journalFilePaths. These are -- Typically this is one of the journal's journalFilePaths. These are