mirror of
https://github.com/simonmichael/hledger.git
synced 2024-11-07 21:15:19 +03:00
smooth: smoothes out irregular transactions (experimental addon)
This commit is contained in:
parent
d5430e7ddf
commit
aebd6c50d6
138
bin/hledger-smooth.hs
Executable file
138
bin/hledger-smooth.hs
Executable file
@ -0,0 +1,138 @@
|
||||
#!/usr/bin/env stack
|
||||
{- stack runghc --verbosity info
|
||||
--package hledger-lib
|
||||
--package hledger
|
||||
--package here
|
||||
--package text
|
||||
--package time
|
||||
--package safe
|
||||
-}
|
||||
-- Requires latest hledger/hledger-lib from master.
|
||||
-- Run it inside an up to date hledger source tree, eg: bin/hledger-smooth.hs ACCT
|
||||
-- Or add bin/ to $PATH and [stack ghc bin/hledger-smooth;] hledger smooth ACCT
|
||||
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# OPTIONS_GHC -Wno-missing-signatures -Wno-name-shadowing #-}
|
||||
|
||||
import Data.List
|
||||
import Data.Maybe
|
||||
import Data.String.Here
|
||||
import qualified Data.Text as T
|
||||
import Data.Time.Calendar
|
||||
import Safe
|
||||
import Hledger
|
||||
import Hledger.Cli
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
cmdmode = hledgerCommandMode
|
||||
[here| smooth
|
||||
Like the print command, but splits any posting to ACCT (a full account name)
|
||||
into multiple daily postings having a similar overall effect.
|
||||
|
||||
Each posting is smoothed across the period until the next ACCT posting, and
|
||||
the last one is smoothed until the report end date, or today.
|
||||
Eg: $30 on 1/1 and $50 on 1/4, if smoothed on 1/6 with no end date specified,
|
||||
becomes $10 on 1/1, $10 on 1/2, $10 on 1/3, $25 on 1/4, $25 on 1/5.
|
||||
|
||||
The last new posting's amount is left blank to ensure a balanced transaction.
|
||||
It can differ from the others.
|
||||
|
||||
Useful for preprocessing a journal to smooth out irregular revenues or
|
||||
expenses in daily/weekly/monthly reports, eg:
|
||||
hledger smooth revenues:consulting | hledger -f- incomestatement -W
|
||||
|
||||
FLAGS
|
||||
|]
|
||||
[]
|
||||
[generalflagsgroup1]
|
||||
[]
|
||||
([], Just $ argsFlag "ACCT")
|
||||
------------------------------------------------------------------------------
|
||||
-- we could smooth postings across the journal period, or within standard intervals: --smooth-interval=posting|journal|weekly|monthly|...
|
||||
-- we could perhaps split transactions instead: --smooth-split=postings|transactions
|
||||
|
||||
main :: IO ()
|
||||
main = do
|
||||
copts' <- getHledgerCliOpts cmdmode
|
||||
withJournalDo copts' $ \copts@CliOpts{reportopts_=ropts, rawopts_} j -> do
|
||||
today <- getCurrentDay
|
||||
menddate <- specifiedEndDate ropts
|
||||
let
|
||||
args = words' $ query_ ropts
|
||||
q = queryFromOpts today ropts
|
||||
acct = T.pack $ headDef (error' "Please provide an account name argument") args
|
||||
pr = postingsReport ropts (And [Acct $ accountNameToAccountRegex acct, q]) j
|
||||
|
||||
-- dates of postings to acct (in report)
|
||||
pdates = map (postingDate . fourth5) (snd pr)
|
||||
-- the specified report end date or today's date
|
||||
enddate = fromMaybe today menddate
|
||||
dates = pdates ++ [enddate]
|
||||
(_,ts') = mapAccumL (splitTransactionPostings q acct) dates $ jtxns j
|
||||
j' = j{jtxns=ts'}
|
||||
copts' = copts{
|
||||
-- One of our postings will probably have a missing amount; this ensures it's
|
||||
-- explicit on all the others.
|
||||
rawopts_=("explicit",""):rawopts_
|
||||
-- Don't let our ACCT argument be interpreted as a query by print
|
||||
,reportopts_=ropts{query_=""}
|
||||
}
|
||||
print' copts' j'
|
||||
|
||||
-- | Split a transaction's postings to acct, if the transaction is matched by q,
|
||||
-- into equivalent daily postings up to the next given end date,
|
||||
-- keeping track of remaining end dates.
|
||||
splitTransactionPostings :: Query -> AccountName -> [Day] -> Transaction -> ([Day], Transaction)
|
||||
splitTransactionPostings q acct dates t
|
||||
-- | q `matchesTransaction` t = (dates', t')
|
||||
-- | otherwise = (dates, t)
|
||||
| otherwise = (dates', t')
|
||||
where
|
||||
(dates', pss') = mapAccumL (splitPosting acct) dates $ tpostings t
|
||||
t' = txnTieKnot t{tpostings=concat pss'}
|
||||
|
||||
-- | Split a posting to acct into equivalent daily postings
|
||||
-- up to the next given end date, keeping track of remaining end dates.
|
||||
-- We assume we will see postings in number and order corresponding the given end dates.
|
||||
splitPosting :: AccountName -> [Day] -> Posting -> ([Day], [Posting])
|
||||
splitPosting acct dates p@Posting{paccount,pamount}
|
||||
| paccount == acct = (dates', ps')
|
||||
| otherwise = (dates, [p])
|
||||
where
|
||||
start = dbg4 "start" $ postingDate p
|
||||
(end, dates') =
|
||||
case dbg4 "dates" dates of
|
||||
(d1:d2:ds) -> if d1==start then (d2, d2:ds) else error' "splitPosting got wrong date, should not happen"
|
||||
[d] -> (d, [])
|
||||
[] -> error' "splitPosting ran out of dates, should not happen"
|
||||
days = initSafe [start..end]
|
||||
amt = pamount `divideMixedAmount` (fromIntegral $ length days)
|
||||
-- give one of the postings an exact balancing amount to ensure the transaction is balanced
|
||||
-- lastamt = pamount - ptrace (amt `multiplyMixedAmount` (fromIntegral $ length days))
|
||||
lastamt = missingmixedamt
|
||||
daysamts = zip days (take (length days - 1) (repeat amt) ++ [lastamt])
|
||||
ps' = [postingSetDate (Just d) p{pamount=a} | (d,a) <- daysamts ]
|
||||
|
||||
-- | Set a posting's (primary) date, as if it had been parsed from the journal entry:
|
||||
-- Updates the date field,
|
||||
-- adds a "date" tag to the parsed tag list (replacing any existing "date" tags there),
|
||||
-- and adds the "date" tag to the unparsed comment field as well, for display purposes.
|
||||
-- If the date is Nothing, unsets the date and removes it from the tags list.
|
||||
-- Does not remove existing date tags from the comment field.
|
||||
postingSetDate :: Maybe Day -> Posting -> Posting
|
||||
postingSetDate md p@Posting{ptags,pcomment} = p{pdate=md, ptags=ptags'', pcomment=pcomment'}
|
||||
where
|
||||
ptags'' = case md of
|
||||
Nothing -> ptags'
|
||||
Just d -> ptags'++[("date", T.pack $ show d)]
|
||||
where
|
||||
ptags' = filter (not.(=="date").fst) ptags
|
||||
|
||||
pcomment' = case md of
|
||||
Nothing -> pcomment
|
||||
Just d -> pcomment `add` T.pack ("date:"++show d)
|
||||
where
|
||||
c1 `add` c2 | T.head c1 == '\n' = c2 <> c1
|
||||
c1 `add` c2 = c1 <> c2
|
@ -58,6 +58,7 @@ module Hledger.Data.Amount (
|
||||
-- ** arithmetic
|
||||
costOfAmount,
|
||||
divideAmount,
|
||||
multiplyAmount,
|
||||
amountValue,
|
||||
-- ** rendering
|
||||
amountstyle,
|
||||
@ -84,6 +85,7 @@ module Hledger.Data.Amount (
|
||||
-- ** arithmetic
|
||||
costOfMixedAmount,
|
||||
divideMixedAmount,
|
||||
multiplyMixedAmount,
|
||||
averageMixedAmounts,
|
||||
isNegativeAmount,
|
||||
isNegativeMixedAmount,
|
||||
@ -217,6 +219,10 @@ costOfAmount a@Amount{aquantity=q, aprice=price} =
|
||||
divideAmount :: Amount -> Quantity -> Amount
|
||||
divideAmount a@Amount{aquantity=q} d = a{aquantity=q/d}
|
||||
|
||||
-- | Multiply an amount's quantity by a constant.
|
||||
multiplyAmount :: Amount -> Quantity -> Amount
|
||||
multiplyAmount a@Amount{aquantity=q} d = a{aquantity=q*d}
|
||||
|
||||
-- | Is this amount negative ? The price is ignored.
|
||||
isNegativeAmount :: Amount -> Bool
|
||||
isNegativeAmount Amount{aquantity=q} = q < 0
|
||||
@ -539,6 +545,10 @@ costOfMixedAmount (Mixed as) = Mixed $ map costOfAmount as
|
||||
divideMixedAmount :: MixedAmount -> Quantity -> MixedAmount
|
||||
divideMixedAmount (Mixed as) d = Mixed $ map (`divideAmount` d) as
|
||||
|
||||
-- | Multiply a mixed amount's quantities by a constant.
|
||||
multiplyMixedAmount :: MixedAmount -> Quantity -> MixedAmount
|
||||
multiplyMixedAmount (Mixed as) d = Mixed $ map (`multiplyAmount` d) as
|
||||
|
||||
-- | Calculate the average of some mixed amounts.
|
||||
averageMixedAmounts :: [MixedAmount] -> MixedAmount
|
||||
averageMixedAmounts [] = 0
|
||||
|
Loading…
Reference in New Issue
Block a user