lib: ModifierTransaction -> TransactionModifier; try to clarify a bit

This commit is contained in:
Simon Michael 2018-07-30 19:29:45 +01:00
parent 007b9f8caa
commit efc54c4c25
8 changed files with 107 additions and 101 deletions

View File

@ -4,23 +4,23 @@
{-# LANGUAGE CPP #-}
{-|
This module provides utilities for applying automated transactions like
'ModifierTransaction' and 'PeriodicTransaction'.
This module provides helpers for applying 'TransactionModifier's
(eg generating automated postings) and generating 'PeriodicTransaction's.
-}
module Hledger.Data.AutoTransaction
(
-- * Transaction processors
runModifierTransaction
-- * Transaction modifiers
transactionModifierToFunction
-- * Periodic transactions
, runPeriodicTransaction
-- * Accessors
, mtvaluequery
, jdatespan
, periodTransactionInterval
-- * Misc
, checkPeriodicTransactionStartDate
-- -- * Accessors, seemingly unused
-- , tmParseQuery
-- , jdatespan
-- , periodTransactionInterval
)
where
@ -44,76 +44,79 @@ import Hledger.Query
-- >>> import Hledger.Data.Posting
-- >>> import Hledger.Data.Journal
-- | Builds a 'Transaction' transformer based on 'ModifierTransaction'.
-- | Converts a 'TransactionModifier' and a 'Query' to a
-- 'Transaction'-transforming function. The query allows injection of
-- additional restrictions on which postings to modify.
-- The transformer function will not call 'txnTieKnot', you will
-- probably want to call that after using it.
--
-- 'Query' parameter allows injection of additional restriction on posting
-- match. Don't forget to call 'txnTieKnot'.
--
-- >>> runModifierTransaction Any (ModifierTransaction "" ["pong" `post` usd 2]) nulltransaction{tpostings=["ping" `post` usd 1]}
-- >>> transactionModifierToFunction Any (TransactionModifier "" ["pong" `post` usd 2]) nulltransaction{tpostings=["ping" `post` usd 1]}
-- 0000/01/01
-- ping $1.00
-- pong $2.00
-- <BLANKLINE>
-- <BLANKLINE>
-- >>> runModifierTransaction Any (ModifierTransaction "miss" ["pong" `post` usd 2]) nulltransaction{tpostings=["ping" `post` usd 1]}
-- >>> transactionModifierToFunction Any (TransactionModifier "miss" ["pong" `post` usd 2]) nulltransaction{tpostings=["ping" `post` usd 1]}
-- 0000/01/01
-- ping $1.00
-- <BLANKLINE>
-- <BLANKLINE>
-- >>> runModifierTransaction None (ModifierTransaction "" ["pong" `post` usd 2]) nulltransaction{tpostings=["ping" `post` usd 1]}
-- >>> transactionModifierToFunction None (TransactionModifier "" ["pong" `post` usd 2]) nulltransaction{tpostings=["ping" `post` usd 1]}
-- 0000/01/01
-- ping $1.00
-- <BLANKLINE>
-- <BLANKLINE>
-- >>> runModifierTransaction Any (ModifierTransaction "ping" ["pong" `post` amount{amultiplier=True, aquantity=3}]) nulltransaction{tpostings=["ping" `post` usd 2]}
-- >>> transactionModifierToFunction Any (TransactionModifier "ping" ["pong" `post` amount{amultiplier=True, aquantity=3}]) nulltransaction{tpostings=["ping" `post` usd 2]}
-- 0000/01/01
-- ping $2.00
-- pong $6.00
-- <BLANKLINE>
-- <BLANKLINE>
runModifierTransaction :: Query -> ModifierTransaction -> (Transaction -> Transaction)
runModifierTransaction q mt = modifier where
q' = simplifyQuery $ And [q, mtvaluequery mt (error "query cannot depend on current time")]
mods = map runModifierPosting $ mtpostings mt
transactionModifierToFunction :: Query -> TransactionModifier -> (Transaction -> Transaction)
transactionModifierToFunction q mt =
\t@(tpostings -> ps) -> t { tpostings = generatePostings ps } -- TODO add modifier txn comment/tags ?
where
q' = simplifyQuery $ And [q, tmParseQuery mt (error' "a transaction modifier's query cannot depend on current date")]
mods = map tmPostingToFunction $ tmpostings mt
generatePostings ps = [p' | p <- ps
, p' <- if q' `matchesPosting` p then p:[ m p | m <- mods] else [p]]
modifier t@(tpostings -> ps) = t { tpostings = generatePostings ps }
-- | Extract 'Query' equivalent of 'mtvalueexpr' from 'ModifierTransaction'
-- | Parse the 'Query' from a 'TransactionModifier's 'tmquerytxt',
-- and return it as a function requiring the current date.
--
-- >>> mtvaluequery (ModifierTransaction "" []) undefined
-- >>> mtvaluequery (TransactionModifier "" []) undefined
-- Any
-- >>> mtvaluequery (ModifierTransaction "ping" []) undefined
-- >>> mtvaluequery (TransactionModifier "ping" []) undefined
-- Acct "ping"
-- >>> mtvaluequery (ModifierTransaction "date:2016" []) undefined
-- >>> mtvaluequery (TransactionModifier "date:2016" []) undefined
-- Date (DateSpan 2016)
-- >>> mtvaluequery (ModifierTransaction "date:today" []) (read "2017-01-01")
-- >>> mtvaluequery (TransactionModifier "date:today" []) (read "2017-01-01")
-- Date (DateSpan 2017/01/01)
mtvaluequery :: ModifierTransaction -> (Day -> Query)
mtvaluequery mt = fst . flip parseQuery (mtvalueexpr mt)
tmParseQuery :: TransactionModifier -> (Day -> Query)
tmParseQuery mt = fst . flip parseQuery (tmquerytxt mt)
-- | 'DateSpan' of all dates mentioned in 'Journal'
--
-- >>> jdatespan nulljournal
-- DateSpan -
-- >>> jdatespan nulljournal{jtxns=[nulltransaction{tdate=read "2016-01-01"}] }
-- DateSpan 2016/01/01
-- >>> jdatespan nulljournal{jtxns=[nulltransaction{tdate=read "2016-01-01", tpostings=[nullposting{pdate=Just $ read "2016-02-01"}]}] }
-- DateSpan 2016/01/01-2016/02/01
jdatespan :: Journal -> DateSpan
jdatespan j
| null dates = nulldatespan
| otherwise = DateSpan (Just $ minimum dates) (Just $ 1 `addDays` maximum dates)
where
dates = concatMap tdates $ jtxns j
---- | 'DateSpan' of all dates mentioned in 'Journal'
----
---- >>> jdatespan nulljournal
---- DateSpan -
---- >>> jdatespan nulljournal{jtxns=[nulltransaction{tdate=read "2016-01-01"}] }
---- DateSpan 2016/01/01
---- >>> jdatespan nulljournal{jtxns=[nulltransaction{tdate=read "2016-01-01", tpostings=[nullposting{pdate=Just $ read "2016-02-01"}]}] }
---- DateSpan 2016/01/01-2016/02/01
--jdatespan :: Journal -> DateSpan
--jdatespan j
-- | null dates = nulldatespan
-- | otherwise = DateSpan (Just $ minimum dates) (Just $ 1 `addDays` maximum dates)
-- where
-- dates = concatMap tdates $ jtxns j
-- | 'DateSpan' of all dates mentioned in 'Transaction'
--
-- >>> tdates nulltransaction
-- [0000-01-01]
tdates :: Transaction -> [Day]
tdates t = tdate t : concatMap pdates (tpostings t) ++ maybeToList (tdate2 t) where
pdates p = catMaybes [pdate p, pdate2 p]
---- | 'DateSpan' of all dates mentioned in 'Transaction'
----
---- >>> tdates nulltransaction
---- [0000-01-01]
--tdates :: Transaction -> [Day]
--tdates t = tdate t : concatMap pdates (tpostings t) ++ maybeToList (tdate2 t) where
-- pdates p = catMaybes [pdate p, pdate2 p]
postingScale :: Posting -> Maybe Quantity
postingScale p =
@ -121,13 +124,16 @@ postingScale p =
[a] | amultiplier a -> Just $ aquantity a
_ -> Nothing
runModifierPosting :: Posting -> (Posting -> Posting)
runModifierPosting p' = modifier where
modifier p = renderPostingCommentDates $ p'
-- | Converts a 'TransactionModifier''s posting to a 'Posting'-generating function,
-- which will be used to make a new posting based on the old one (an "automated posting").
tmPostingToFunction :: Posting -> (Posting -> Posting)
tmPostingToFunction p' =
\p -> renderPostingCommentDates $ p'
{ pdate = pdate p
, pdate2 = pdate2 p
, pamount = amount' p
}
where
amount' = case postingScale p' of
Nothing -> const $ pamount p'
Just n -> \p -> withAmountType (head $ amounts $ pamount p') $ pamount p `divideMixedAmount` (1/n)
@ -321,13 +327,13 @@ checkPeriodicTransactionStartDate i s periodexpr =
"Unable to generate transactions according to "++show (T.unpack periodexpr)
++" because "++show d++" is not a first day of the "++x
-- | What is the interval of this 'PeriodicTransaction's period expression, if it can be parsed ?
periodTransactionInterval :: PeriodicTransaction -> Maybe Interval
periodTransactionInterval pt =
let
expr = ptperiodexpr pt
err = error' $ "Current date cannot be referenced in " ++ show (T.unpack expr)
in
case parsePeriodExpr err expr of
Left _ -> Nothing
Right (i,_) -> Just i
---- | What is the interval of this 'PeriodicTransaction's period expression, if it can be parsed ?
--periodTransactionInterval :: PeriodicTransaction -> Maybe Interval
--periodTransactionInterval pt =
-- let
-- expr = ptperiodexpr pt
-- err = error' $ "Current date cannot be referenced in " ++ show (T.unpack expr)
-- in
-- case parsePeriodExpr err expr of
-- Left _ -> Nothing
-- Right (i,_) -> Just i

View File

@ -13,7 +13,7 @@ other data format (see "Hledger.Read").
module Hledger.Data.Journal (
-- * Parsing helpers
addMarketPrice,
addModifierTransaction,
addTransactionModifier,
addPeriodicTransaction,
addTransaction,
journalBalanceTransactions,
@ -138,7 +138,7 @@ instance Show Journal where
-- showJournalDebug j = unlines [
-- show j
-- ,show (jtxns j)
-- ,show (jmodifiertxns j)
-- ,show (jtxnmodifiers j)
-- ,show (jperiodictxns j)
-- ,show $ jparsetimeclockentries j
-- ,show $ jmarketprices j
@ -170,7 +170,7 @@ instance Sem.Semigroup Journal where
,jcommodities = jcommodities j1 <> jcommodities j2
,jinferredcommodities = jinferredcommodities j1 <> jinferredcommodities j2
,jmarketprices = jmarketprices j1 <> jmarketprices j2
,jmodifiertxns = jmodifiertxns j1 <> jmodifiertxns j2
,jtxnmodifiers = jtxnmodifiers j1 <> jtxnmodifiers j2
,jperiodictxns = jperiodictxns j1 <> jperiodictxns j2
,jtxns = jtxns j1 <> jtxns j2
,jfinalcommentlines = jfinalcommentlines j2
@ -197,7 +197,7 @@ nulljournal = Journal {
,jcommodities = M.fromList []
,jinferredcommodities = M.fromList []
,jmarketprices = []
,jmodifiertxns = []
,jtxnmodifiers = []
,jperiodictxns = []
,jtxns = []
,jfinalcommentlines = ""
@ -217,8 +217,8 @@ mainfile = headDef ("", "") . jfiles
addTransaction :: Transaction -> Journal -> Journal
addTransaction t j = j { jtxns = t : jtxns j }
addModifierTransaction :: ModifierTransaction -> Journal -> Journal
addModifierTransaction mt j = j { jmodifiertxns = mt : jmodifiertxns j }
addTransactionModifier :: TransactionModifier -> Journal -> Journal
addTransactionModifier mt j = j { jtxnmodifiers = mt : jtxnmodifiers j }
addPeriodicTransaction :: PeriodicTransaction -> Journal -> Journal
addPeriodicTransaction pt j = j { jperiodictxns = pt : jperiodictxns j }
@ -497,7 +497,7 @@ journalFinalise t path txt assrt j@Journal{jfiles=fs} =
j {jfiles = (path,txt) : reverse fs
,jlastreadtime = t
,jtxns = reverse $ jtxns j -- NOTE: see addTransaction
,jmodifiertxns = reverse $ jmodifiertxns j -- NOTE: see addModifierTransaction
,jtxnmodifiers = reverse $ jtxnmodifiers j -- NOTE: see addTransactionModifier
,jperiodictxns = reverse $ jperiodictxns j -- NOTE: see addPeriodicTransaction
,jmarketprices = reverse $ jmarketprices j -- NOTE: see addMarketPrice
})

View File

@ -26,7 +26,7 @@ import Hledger.Query
instance Show Ledger where
show l = printf "Ledger with %d transactions, %d accounts\n" --"%s"
(length (jtxns $ ljournal l) +
length (jmodifiertxns $ ljournal l) +
length (jtxnmodifiers $ ljournal l) +
length (jperiodictxns $ ljournal l))
(length $ ledgerAccountNames l)
-- (showtree $ ledgerAccountNameTree l)

View File

@ -65,8 +65,8 @@ import Hledger.Data.Amount
instance Show Transaction where show = showTransactionUnelided
instance Show ModifierTransaction where
show t = "= " ++ T.unpack (mtvalueexpr t) ++ "\n" ++ unlines (map show (mtpostings t))
instance Show TransactionModifier where
show t = "= " ++ T.unpack (tmquerytxt t) ++ "\n" ++ unlines (map show (tmpostings t))
instance Show PeriodicTransaction where
show t = "~ " ++ T.unpack (ptperiodexpr t) ++ "\n" ++ unlines (map show (ptpostings t))

View File

@ -166,7 +166,7 @@ data Amount = Amount {
aquantity :: Quantity,
aprice :: Price, -- ^ the (fixed) price for this amount, if any
astyle :: AmountStyle,
amultiplier :: Bool -- ^ amount is a multipier for AutoTransactions
amultiplier :: Bool -- ^ amount is a multipier used in TransactionModifier postings
} deriving (Eq,Ord,Typeable,Data,Generic)
instance NFData Amount
@ -249,12 +249,12 @@ data Transaction = Transaction {
instance NFData Transaction
data ModifierTransaction = ModifierTransaction {
mtvalueexpr :: Text,
mtpostings :: [Posting]
data TransactionModifier = TransactionModifier {
tmquerytxt :: Text,
tmpostings :: [Posting]
} deriving (Eq,Typeable,Data,Generic)
instance NFData ModifierTransaction
instance NFData TransactionModifier
-- ^ A periodic transaction rule, describing a transaction that recurs.
data PeriodicTransaction = PeriodicTransaction {
@ -329,7 +329,7 @@ data Journal = Journal {
,jcommodities :: M.Map CommoditySymbol Commodity -- ^ commodities and formats declared by commodity directives
,jinferredcommodities :: M.Map CommoditySymbol AmountStyle -- ^ commodities and formats inferred from journal amounts XXX misnamed
,jmarketprices :: [MarketPrice]
,jmodifiertxns :: [ModifierTransaction]
,jtxnmodifiers :: [TransactionModifier]
,jperiodictxns :: [PeriodicTransaction]
,jtxns :: [Transaction]
,jfinalcommentlines :: Text -- ^ any final trailing comments in the (main) journal file

View File

@ -209,9 +209,9 @@ journalSourcePos p p' = JournalSourcePos (sourceName p) (fromIntegral . unPos $
generateAutomaticPostings :: Journal -> Journal
generateAutomaticPostings j = j { jtxns = map modifier $ jtxns j }
where
modifier = foldr (flip (.) . runModifierTransaction') id mtxns
runModifierTransaction' = fmap txnTieKnot . runModifierTransaction Q.Any
mtxns = jmodifiertxns j
modifier = foldr (flip (.) . transactionModifierToFunction') id mtxns
transactionModifierToFunction' = fmap txnTieKnot . transactionModifierToFunction Q.Any
mtxns = jtxnmodifiers j
-- | Given a megaparsec ParsedJournal parser, input options, file
-- path and file content: parse and post-process a Journal, or give an error.

View File

@ -154,7 +154,7 @@ addJournalItemP =
choice [
directivep
, transactionp >>= modify' . addTransaction
, modifiertransactionp >>= modify' . addModifierTransaction
, transactionmodifierp >>= modify' . addTransactionModifier
, periodictransactionp >>= modify' . addPeriodicTransaction
, marketpricedirectivep >>= modify' . addMarketPrice
, void (lift emptyorcommentlinep)
@ -470,14 +470,14 @@ commodityconversiondirectivep = do
--- ** transactions
-- TODO transactionmodifierp ? transactionrewritep ?
modifiertransactionp :: JournalParser m ModifierTransaction
modifiertransactionp = do
transactionmodifierp :: JournalParser m TransactionModifier
transactionmodifierp = do
char '=' <?> "modifier transaction"
lift (skipMany spacenonewline)
querytxt <- lift $ T.strip <$> descriptionp
(_comment, _tags) <- lift transactioncommentp -- TODO apply these to modified txns ?
postings <- postingsp Nothing
return $ ModifierTransaction querytxt postings
return $ TransactionModifier querytxt postings
-- | Parse a periodic transaction
periodictransactionp :: MonadIO m => JournalParser m PeriodicTransaction
@ -748,8 +748,8 @@ tests_Hledger_Read_JournalReader = TestList $ concat [
test_postingp,
test_transactionp,
[
"modifiertransactionp" ~: do
assertParse (parseWithState mempty modifiertransactionp "= (some value expr)\n some:postings 1\n")
"transactionmodifierp" ~: do
assertParse (parseWithState mempty transactionmodifierp "= (some value expr)\n some:postings 1\n")
,"periodictransactionp" ~: do
assertParse (parseWithState mempty periodictransactionp "~ (some period expr)\n some:postings 1\n")

View File

@ -178,12 +178,12 @@ but with these differences:
rewrite opts@CliOpts{rawopts_=rawopts,reportopts_=ropts} j@Journal{jtxns=ts} = do
d <- getCurrentDay
let q = queryFromOpts d ropts
modifier <- modifierTransactionFromOpts rawopts
modifier <- transactionModifierFromOpts rawopts
-- create re-writer
let modifiers = modifier : jmodifiertxns j
let modifiers = modifier : jtxnmodifiers j
-- Note that some query matches require transaction. Thus modifiers
-- pipeline should include txnTieKnot on every step.
modifier' = foldr (flip (.) . fmap txnTieKnot . runModifierTransaction q) id modifiers
modifier' = foldr (flip (.) . fmap txnTieKnot . transactionModifierToFunction q) id modifiers
-- rewrite matched transactions
let j' = j{jtxns=map modifier' ts}
-- run the print command, showing all transactions
@ -195,11 +195,11 @@ postingp' t = runJournalParser (postingp Nothing <* eof) t' >>= \case
Right p -> return p
where t' = " " <> t <> "\n" -- inject space and newline for proper parsing
modifierTransactionFromOpts :: RawOpts -> IO ModifierTransaction
modifierTransactionFromOpts opts = do
transactionModifierFromOpts :: RawOpts -> IO TransactionModifier
transactionModifierFromOpts opts = do
postings <- mapM (postingp' . stripquotes . T.pack) $ listofstringopt "add-posting" opts
return
ModifierTransaction { mtvalueexpr = T.empty, mtpostings = postings }
TransactionModifier { tmquerytxt = T.empty, tmpostings = postings }
outputFromOpts :: RawOpts -> (CliOpts -> Journal -> Journal -> IO ())
outputFromOpts opts