hledger/extra/hledger-rewrite.hs
Simon Michael 6439744fa2 extra: give hledger-rewrite.hs a stack script header
and some guidance for compiling or interpreting it.
This may be helpful for getting started, although the suggested
commands still require that you're in the hledger source tree.
2016-09-07 07:39:10 -07:00

110 lines
4.4 KiB
Haskell
Executable File

#!/usr/bin/env stack
{- stack runghc --verbosity info
--package hledger-lib
--package hledger
--package megaparsec
--package text
-}
-- To compile this script: cd hledger; stack ghc extra/hledger-rewrite.hs
-- To run it "interpreted": cd hledger; extra/hledger-rewrite.hs ARGS
{-|
hledger-rewrite [PATTERNS] --add-posting "ACCT AMTEXPR" ...
A start at a generic rewriter of journal entries.
Reads the default journal and prints the entries, like print,
but adds the specified postings to any entries matching PATTERNS.
Examples:
hledger-rewrite.hs ^income --add-posting '(liabilities:tax) *.33' --add-posting '(reserve:gifts) $100'
hledger-rewrite.hs expenses:gifts --add-posting '(reserve:gifts) *-1"'
Note the single quotes to protect the dollar sign from bash, and the two spaces between account and amount.
See the command-line help for more details.
Currently does not work when invoked via hledger, run hledger-rewrite[.hs] directly.
Needs to work on unbalanced entries, eg while editing one.
/
Tested-with: hledger HEAD ~ 2016/3/2
|-}
import qualified Data.Text as T
-- hledger lib, cli and cmdargs utils
import Hledger.Cli
-- more utils for parsing
-- #if !MIN_VERSION_base(4,8,0)
-- import Control.Applicative.Compat ((<*))
-- #endif
import Text.Megaparsec
import Text.Megaparsec.Text
cmdmode :: Mode RawOpts
cmdmode = (defCommandMode ["hledger-rewrite"]) {
modeArgs = ([], Just $ argsFlag "[PATTERNS] --add-posting \"ACCT AMTEXPR\" ...")
,modeHelp = "print all journal entries, with custom postings added to the matched ones"
,modeGroupFlags = Group {
groupNamed = [("Input", inputflags)
,("Reporting", reportflags)
,("Misc", helpflags)
]
,groupUnnamed = [flagReq ["add-posting"] (\s opts -> Right $ setopt "add-posting" s opts) "'ACCT AMTEXPR'"
"add a posting to ACCT, which may be parenthesised. AMTEXPR is either a literal amount, or *N which means the transaction's first matched amount multiplied by N (a decimal number). Two spaces separate ACCT and AMTEXPR."]
,groupHidden = []
}
}
type PostingExpr = (AccountName, AmountExpr)
data AmountExpr = AmountLiteral String | AmountMultiplier Quantity deriving (Show)
addPostingExprsFromOpts :: RawOpts -> [PostingExpr]
addPostingExprsFromOpts = map (either parseerror id . runParser (postingexprp <* eof) "") . map (stripquotes . T.pack) . listofstringopt "add-posting"
postingexprp = do
a <- accountnamep
spacenonewline >> some spacenonewline
aex <- amountexprp
many spacenonewline
return (a,aex)
amountexprp =
choice [
AmountMultiplier <$> (do char '*'
many spacenonewline
(q,_,_,_) <- numberp
return q)
,AmountLiteral <$> many anyChar
]
amountExprRenderer :: Query -> AmountExpr -> (Transaction -> MixedAmount)
amountExprRenderer q aex =
case aex of
AmountLiteral s -> const (mamountp' s)
AmountMultiplier n -> (`divideMixedAmount` (1 / n)) . (`firstAmountMatching` q)
where
firstAmountMatching :: Transaction -> Query -> MixedAmount
firstAmountMatching t q = pamount $ head $ filter (q `matchesPosting`) $ tpostings t
rewriteTransaction :: Transaction -> [(AccountName, Transaction -> MixedAmount)] -> Transaction
rewriteTransaction t addps = t{tpostings=tpostings t ++ map (uncurry (generatePosting t)) addps}
where
generatePosting :: Transaction -> AccountName -> (Transaction -> MixedAmount) -> Posting
generatePosting t acct amtfn = nullposting{paccount = accountNameWithoutPostingType acct
,ptype = accountNamePostingType acct
,pamount = amtfn t
,ptransaction = Just t
}
main = do
opts@CliOpts{rawopts_=rawopts,reportopts_=ropts} <- getCliOpts cmdmode
d <- getCurrentDay
let q = queryFromOpts d ropts
addps = [(a, amountExprRenderer q aex) | (a, aex) <- addPostingExprsFromOpts rawopts]
withJournalDo opts $ \opts j@Journal{jtxns=ts} -> do
-- rewrite matched transactions
let j' = j{jtxns=map (\t -> if q `matchesTransaction` t then rewriteTransaction t addps else t) ts}
-- run the print command, showing all transactions
print' opts{reportopts_=ropts{query_=""}} j'