2014-02-06 01:02:24 +04:00
#!/ usr / bin / env runhaskell
{- |
2014-02-06 06:59:27 +04:00
hledger - rewrite [ PATTERNS ] --add-posting "ACCT AMTEXPR" ...
2014-02-06 01:02:24 +04:00
2014-02-06 06:59:27 +04:00
A start at a generic rewriter of journal entries .
2014-02-06 01:02:24 +04:00
Reads the default journal and prints the entries , like print ,
but adds the specified postings to any entries matching PATTERNS .
2014-02-06 06:59:27 +04:00
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 rewrite " .
Tested - with : hledger HEAD ~ 2014 / 2 / 4
2014-02-06 01:56:49 +04:00
2014-02-06 01:02:24 +04:00
|- }
2014-02-06 06:59:27 +04:00
-- hledger lib, cli and cmdargs utils
2014-02-06 01:02:24 +04:00
import Hledger.Cli
2014-02-06 06:59:27 +04:00
-- more utils for parsing
import Control.Applicative hiding ( many )
import Text.ParserCombinators.Parsec
2014-02-06 01:02:24 +04:00
2014-02-06 01:56:49 +04:00
2014-02-06 06:59:27 +04:00
cmdmode :: Mode RawOpts
cmdmode = ( defCommandMode [ " hledger-rewrite " ] ) {
modeArgs = ( [] , Just $ argsFlag " [PATTERNS] --add-posting \ " ACCT AMTEXPR \ " ... " )
2014-05-01 18:22:44 +04:00
, modeHelp = " print all journal entries, with custom postings added to the matched ones "
2014-02-06 06:59:27 +04:00
, modeGroupFlags = Group {
groupNamed = [ ( " Input " , inputflags )
, ( " Reporting " , reportflags )
, ( " Misc " , helpflags )
]
2014-05-01 18:22:44 +04:00
, 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. " ]
2014-02-06 06:59:27 +04:00
, groupHidden = []
}
}
type PostingExpr = ( AccountName , AmountExpr )
data AmountExpr = AmountLiteral String | AmountMultiplier Quantity deriving ( Show )
addPostingExprsFromOpts :: RawOpts -> [ PostingExpr ]
addPostingExprsFromOpts = map ( either parseerror id . parseWithCtx nullctx postingexprp ) . map stripquotes . listofstringopt " add-posting "
postingexprp = do
a <- accountnamep
spacenonewline >> many1 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 -> either parseerror ( const . mixed ) $ parseWithCtx nullctx amountp s
AmountMultiplier n -> ( ` divideMixedAmount ` ( 1 / n ) ) . ( ` firstAmountMatching ` q )
where
firstAmountMatching :: Transaction -> Query -> MixedAmount
firstAmountMatching t q = pamount $ head $ filter ( q ` matchesPosting ` ) $ tpostings t
2014-02-06 01:56:49 +04:00
rewriteTransaction :: Transaction -> [ ( AccountName , Transaction -> MixedAmount ) ] -> Transaction
rewriteTransaction t addps = t { tpostings = tpostings t ++ map ( uncurry ( generatePosting t ) ) addps }
2014-02-06 06:59:27 +04:00
where
generatePosting :: Transaction -> AccountName -> ( Transaction -> MixedAmount ) -> Posting
generatePosting t acct amtfn = nullposting { paccount = accountNameWithoutPostingType acct
, ptype = accountNamePostingType acct
, pamount = amtfn t
, ptransaction = Just t
}
2014-02-06 01:56:49 +04:00
2014-02-06 06:59:27 +04:00
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'