mirror of
https://github.com/simonmichael/hledger.git
synced 2024-09-20 02:37:12 +03:00
imp!: forecast: Implements more intuitive logic for the forecast interval. (#1648)
The forecast period begins on: - the start date supplied to the `--forecast` argument, if present - otherwise, the later of - the report start date if specified with -b/-p/date: - the day after the latest normal (non-periodic) transaction in the journal, if any - otherwise today. It ends on: - the end date supplied to the `--forecast` argument, if present - otherwise the report end date if specified with -e/-p/date: - otherwise 180 days (6 months) from today. Note that the previous behaviour did not quite match the documentation, so this also acts as a bug fix for #1665.
This commit is contained in:
parent
65e10aebd2
commit
c07ad29a87
@ -36,6 +36,7 @@ module Hledger.Read (
|
||||
findReader,
|
||||
splitReaderPrefix,
|
||||
module Hledger.Read.Common,
|
||||
module Hledger.Read.InputOptions,
|
||||
|
||||
-- * Tests
|
||||
tests_Read,
|
||||
@ -69,6 +70,7 @@ import System.IO (hPutStr, stderr)
|
||||
import Hledger.Data.Dates (getCurrentDay, parsedateM, showDate)
|
||||
import Hledger.Data.Types
|
||||
import Hledger.Read.Common
|
||||
import Hledger.Read.InputOptions
|
||||
import Hledger.Read.JournalReader as JournalReader
|
||||
import Hledger.Read.CsvReader (tests_CsvReader)
|
||||
-- import Hledger.Read.TimedotReader (tests_TimedotReader)
|
||||
|
@ -32,7 +32,6 @@ module Hledger.Read.Common (
|
||||
InputOpts(..),
|
||||
definputopts,
|
||||
rawOptsToInputOpts,
|
||||
forecastPeriodFromRawOpts,
|
||||
rawOptsToCommodityStylesOpts,
|
||||
|
||||
-- * parsing utilities
|
||||
@ -148,7 +147,7 @@ import qualified Data.Map as M
|
||||
import qualified Data.Semigroup as Sem
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
import Data.Time.Calendar (Day, addDays, fromGregorianValid, toGregorian)
|
||||
import Data.Time.Calendar (Day, fromGregorianValid, toGregorian)
|
||||
import Data.Time.Clock.POSIX (getPOSIXTime)
|
||||
import Data.Time.LocalTime (LocalTime(..), TimeOfDay(..))
|
||||
import Data.Word (Word8)
|
||||
@ -160,7 +159,7 @@ import Text.Megaparsec.Custom
|
||||
finalErrorBundlePretty, parseErrorAt, parseErrorAtRegion)
|
||||
|
||||
import Hledger.Data
|
||||
import Hledger.Query (Query(..), filterQuery, parseQueryTerm, queryEndDate, queryIsDate, simplifyQuery)
|
||||
import Hledger.Query (Query(..), filterQuery, parseQueryTerm, queryEndDate, queryStartDate, queryIsDate, simplifyQuery)
|
||||
import Hledger.Reports.ReportOptions (ReportOpts(..), queryFromFlags, rawOptsToReportOpts)
|
||||
import Hledger.Utils
|
||||
import Text.Printf (printf)
|
||||
@ -234,6 +233,14 @@ rawOptsToInputOpts :: RawOpts -> IO InputOpts
|
||||
rawOptsToInputOpts rawopts = do
|
||||
d <- getCurrentDay
|
||||
|
||||
let noinferprice = boolopt "strict" rawopts || stringopt "args" rawopts == "balancednoautoconversion"
|
||||
|
||||
-- Do we really need to do all this work just to get the requested end date? This is duplicating
|
||||
-- much of reportOptsToSpec.
|
||||
ropts = rawOptsToReportOpts d rawopts
|
||||
argsquery = lefts . rights . map (parseQueryTerm d) $ querystring_ ropts
|
||||
datequery = simplifyQuery . filterQuery queryIsDate . And $ queryFromFlags ropts : argsquery
|
||||
|
||||
return InputOpts{
|
||||
-- files_ = listofstringopt "file" rawopts
|
||||
mformat_ = Nothing
|
||||
@ -244,6 +251,7 @@ rawOptsToInputOpts rawopts = do
|
||||
,new_save_ = True
|
||||
,pivot_ = stringopt "pivot" rawopts
|
||||
,forecast_ = forecastPeriodFromRawOpts d rawopts
|
||||
,reportspan_ = DateSpan (queryStartDate False datequery) (queryEndDate False datequery)
|
||||
,auto_ = boolopt "auto" rawopts
|
||||
,balancingopts_ = balancingOpts{
|
||||
ignore_assertions_ = boolopt "ignore-assertions" rawopts
|
||||
@ -252,36 +260,21 @@ rawOptsToInputOpts rawopts = do
|
||||
}
|
||||
,strict_ = boolopt "strict" rawopts
|
||||
}
|
||||
where noinferprice = boolopt "strict" rawopts || stringopt "args" rawopts == "balancednoautoconversion"
|
||||
|
||||
-- | Get the date span from --forecast's PERIODEXPR argument, if any.
|
||||
-- This will fail with a usage error if the period expression cannot be parsed,
|
||||
-- or if it contains a report interval.
|
||||
forecastPeriodFromRawOpts :: Day -> RawOpts -> Maybe DateSpan
|
||||
forecastPeriodFromRawOpts d rawopts = case maybestringopt "forecast" rawopts of
|
||||
Nothing -> Nothing
|
||||
Just "" -> Just forecastspanDefault
|
||||
Just arg ->
|
||||
either
|
||||
(\e -> usageError $ "could not parse forecast period : "++customErrorBundlePretty e)
|
||||
(\(interval, requestedspan) ->
|
||||
case interval of
|
||||
NoInterval -> Just $ requestedspan `spanDefaultsFrom` forecastspanDefault
|
||||
_ -> usageError $ unlines
|
||||
[ "--forecast's argument should not contain a report interval"
|
||||
, "(" ++ show interval ++ " in \"" ++ arg ++ "\")"
|
||||
])
|
||||
(parsePeriodExpr d $ stripquotes $ T.pack arg)
|
||||
forecastPeriodFromRawOpts d rawopts = do
|
||||
arg <- maybestringopt "forecast" rawopts
|
||||
let period = parsePeriodExpr d . stripquotes $ T.pack arg
|
||||
return $ if null arg then nulldatespan else either badParse (getSpan arg) period
|
||||
where
|
||||
-- "They end on or before the specified report end date, or 180 days from today if unspecified."
|
||||
mspecifiedend = dbg2 "specifieddates" $ queryEndDate False datequery
|
||||
forecastendDefault = dbg2 "forecastendDefault" $ addDays 180 d
|
||||
forecastspanDefault = DateSpan Nothing $ mspecifiedend <|> Just forecastendDefault
|
||||
-- Do we really need to do all this work just to get the requested end date? This is duplicating
|
||||
-- much of reportOptsToSpec.
|
||||
ropts = rawOptsToReportOpts d rawopts
|
||||
argsquery = lefts . rights . map (parseQueryTerm d) $ querystring_ ropts
|
||||
datequery = simplifyQuery . filterQuery queryIsDate . And $ queryFromFlags ropts : argsquery
|
||||
badParse e = usageError $ "could not parse forecast period : "++customErrorBundlePretty e
|
||||
getSpan arg (interval, requestedspan) = case interval of
|
||||
NoInterval -> requestedspan
|
||||
_ -> usageError $ "--forecast's argument should not contain a report interval ("
|
||||
++ show interval ++ " in \"" ++ arg ++ "\")"
|
||||
|
||||
--- ** parsing utilities
|
||||
|
||||
@ -371,7 +364,7 @@ parseAndFinaliseJournal' parser iopts f txt = do
|
||||
-- - infer transaction-implied market prices from transaction prices
|
||||
--
|
||||
journalFinalise :: InputOpts -> FilePath -> Text -> ParsedJournal -> ExceptT String IO Journal
|
||||
journalFinalise InputOpts{forecast_,auto_,balancingopts_,strict_} f txt pj = do
|
||||
journalFinalise iopts@InputOpts{auto_,balancingopts_,strict_} f txt pj = do
|
||||
t <- liftIO getPOSIXTime
|
||||
d <- liftIO getCurrentDay
|
||||
-- Infer and apply canonical styles for each commodity (or throw an error).
|
||||
@ -390,7 +383,7 @@ journalFinalise InputOpts{forecast_,auto_,balancingopts_,strict_} f txt pj = do
|
||||
journalCheckCommoditiesDeclared j
|
||||
|
||||
-- Add forecast transactions if enabled
|
||||
journalAddForecast d forecast_ j
|
||||
journalAddForecast (forecastPeriod d iopts j) j
|
||||
-- Add auto postings if enabled
|
||||
& (if auto_ && not (null $ jtxnmodifiers j) then journalAddAutoPostings d balancingopts_ else pure)
|
||||
-- Balance all transactions and maybe check balance assertions.
|
||||
@ -412,9 +405,9 @@ journalAddAutoPostings d bopts =
|
||||
--
|
||||
-- The start & end date for generated periodic transactions are determined in
|
||||
-- a somewhat complicated way; see the hledger manual -> Periodic transactions.
|
||||
journalAddForecast :: Day -> Maybe DateSpan -> Journal -> Journal
|
||||
journalAddForecast _ Nothing j = j
|
||||
journalAddForecast d (Just requestedspan) j = j{jtxns = jtxns j ++ forecasttxns}
|
||||
journalAddForecast :: Maybe DateSpan -> Journal -> Journal
|
||||
journalAddForecast Nothing j = j
|
||||
journalAddForecast (Just forecastspan) j = j{jtxns = jtxns j ++ forecasttxns}
|
||||
where
|
||||
forecasttxns =
|
||||
map (txnTieKnot . transactionTransformPostings (postingApplyCommodityStyles $ journalCommodityStyles j))
|
||||
@ -422,14 +415,6 @@ journalAddForecast d (Just requestedspan) j = j{jtxns = jtxns j ++ forecasttxns}
|
||||
. concatMap (`runPeriodicTransaction` forecastspan)
|
||||
$ jperiodictxns j
|
||||
|
||||
-- "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
|
||||
forecastbeginDefault = dbg2 "forecastbeginDefault" $ mjournalend <|> Just d
|
||||
|
||||
-- "They end on or before the specified report end date, or 180 days from today if unspecified."
|
||||
forecastspan = dbg2 "forecastspan" $ dbg2 "forecastspan flag" requestedspan
|
||||
`spanDefaultsFrom` DateSpan forecastbeginDefault (Just $ addDays 180 d)
|
||||
|
||||
-- | Check that all the journal's transactions have payees declared with
|
||||
-- payee directives, returning an error message otherwise.
|
||||
journalCheckPayeesDeclared :: Journal -> Either String ()
|
||||
|
@ -6,30 +6,36 @@ Similar to CliOptions.inputflags, simplifies the journal-reading functions.
|
||||
-}
|
||||
|
||||
module Hledger.Read.InputOptions (
|
||||
-- * Types and helpers for input options
|
||||
InputOpts(..)
|
||||
, definputopts
|
||||
)
|
||||
where
|
||||
-- * Types and helpers for input options
|
||||
InputOpts(..)
|
||||
, definputopts
|
||||
, forecastPeriod
|
||||
) where
|
||||
|
||||
import Control.Applicative ((<|>))
|
||||
import Data.Time (Day, addDays)
|
||||
|
||||
import Hledger.Data.Types
|
||||
import Hledger.Data.Transaction
|
||||
import Hledger.Data.Dates()
|
||||
import Hledger.Data.Transaction (BalancingOpts(..), balancingOpts)
|
||||
import Hledger.Data.Journal (journalEndDate)
|
||||
import Hledger.Data.Dates (nulldatespan)
|
||||
import Hledger.Utils
|
||||
|
||||
data InputOpts = InputOpts {
|
||||
-- files_ :: [FilePath]
|
||||
mformat_ :: Maybe StorageFormat -- ^ a file/storage format to try, unless overridden
|
||||
-- by a filename prefix. Nothing means try all.
|
||||
,mrules_file_ :: Maybe FilePath -- ^ a conversion rules file to use (when reading CSV)
|
||||
,aliases_ :: [String] -- ^ account name aliases to apply
|
||||
,anon_ :: Bool -- ^ do light anonymisation/obfuscation of the data
|
||||
,new_ :: Bool -- ^ read only new transactions since this file was last read
|
||||
,new_save_ :: Bool -- ^ save latest new transactions state for next time
|
||||
,pivot_ :: String -- ^ use the given field's value as the account name
|
||||
,forecast_ :: Maybe DateSpan -- ^ span in which to generate forecast transactions
|
||||
,auto_ :: Bool -- ^ generate automatic postings when journal is parsed
|
||||
,balancingopts_ :: BalancingOpts -- ^ options for balancing transactions
|
||||
,strict_ :: Bool -- ^ do extra error checking (eg, all posted accounts are declared, no prices are inferred)
|
||||
mformat_ :: Maybe StorageFormat -- ^ a file/storage format to try, unless overridden
|
||||
-- by a filename prefix. Nothing means try all.
|
||||
,mrules_file_ :: Maybe FilePath -- ^ a conversion rules file to use (when reading CSV)
|
||||
,aliases_ :: [String] -- ^ account name aliases to apply
|
||||
,anon_ :: Bool -- ^ do light anonymisation/obfuscation of the data
|
||||
,new_ :: Bool -- ^ read only new transactions since this file was last read
|
||||
,new_save_ :: Bool -- ^ save latest new transactions state for next time
|
||||
,pivot_ :: String -- ^ use the given field's value as the account name
|
||||
,forecast_ :: Maybe DateSpan -- ^ span in which to generate forecast transactions
|
||||
,reportspan_ :: DateSpan -- ^ a dirty hack keeping the query dates in InputOpts. This rightfully lives in ReportSpec, but is duplicated here.
|
||||
,auto_ :: Bool -- ^ generate automatic postings when journal is parsed
|
||||
,balancingopts_ :: BalancingOpts -- ^ options for balancing transactions
|
||||
,strict_ :: Bool -- ^ do extra error checking (eg, all posted accounts are declared, no prices are inferred)
|
||||
} deriving (Show)
|
||||
|
||||
definputopts :: InputOpts
|
||||
@ -42,7 +48,28 @@ definputopts = InputOpts
|
||||
, new_save_ = True
|
||||
, pivot_ = ""
|
||||
, forecast_ = Nothing
|
||||
, reportspan_ = nulldatespan
|
||||
, auto_ = False
|
||||
, balancingopts_ = balancingOpts
|
||||
, strict_ = False
|
||||
}
|
||||
|
||||
-- | Get the Maybe the DateSpan to generate forecast options from.
|
||||
-- This begins on:
|
||||
-- - the start date supplied to the `--forecast` argument, if present
|
||||
-- - otherwise, the later of
|
||||
-- - the report start date if specified with -b/-p/date:
|
||||
-- - the day after the latest normal (non-periodic) transaction in the journal, if any
|
||||
-- - otherwise today.
|
||||
-- It ends on:
|
||||
-- - the end date supplied to the `--forecast` argument, if present
|
||||
-- - otherwise the report end date if specified with -e/-p/date:
|
||||
-- - otherwise 180 days (6 months) from today.
|
||||
forecastPeriod :: Day -> InputOpts -> Journal -> Maybe DateSpan
|
||||
forecastPeriod d iopts j = do
|
||||
DateSpan requestedStart requestedEnd <- forecast_ iopts
|
||||
let forecastStart = requestedStart <|> max mjournalend reportStart <|> Just d
|
||||
forecastEnd = requestedEnd <|> reportEnd <|> Just (addDays 180 d)
|
||||
mjournalend = dbg2 "journalEndDate" $ journalEndDate False j -- ignore secondary dates
|
||||
DateSpan reportStart reportEnd = reportspan_ iopts
|
||||
return . dbg2 "forecastspan" $ DateSpan forecastStart forecastEnd
|
||||
|
@ -7,7 +7,6 @@ module Hledger.UI.UIState
|
||||
where
|
||||
|
||||
import Brick.Widgets.Edit
|
||||
import Control.Applicative ((<|>))
|
||||
import Data.List ((\\), foldl', sort)
|
||||
import Data.Semigroup (Max(..))
|
||||
import qualified Data.Text as T
|
||||
@ -157,11 +156,11 @@ toggleHistorical ui@UIState{aopts=uopts@UIOpts{cliopts_=copts@CliOpts{reportspec
|
||||
-- (which are usually but not necessarily future-dated).
|
||||
-- In normal mode, both of these are hidden.
|
||||
toggleForecast :: Day -> UIState -> UIState
|
||||
toggleForecast d ui@UIState{aopts=UIOpts{cliopts_=copts@CliOpts{inputopts_=iopts}}} =
|
||||
toggleForecast d ui@UIState{aopts=UIOpts{cliopts_=CliOpts{inputopts_=iopts}}} =
|
||||
uiSetForecast ui $
|
||||
case forecast_ iopts of
|
||||
Just _ -> Nothing
|
||||
Nothing -> forecastPeriodFromRawOpts d (rawopts_ copts) <|> Just nulldatespan
|
||||
Nothing -> forecastPeriod d iopts{forecast_=Just nulldatespan} (ajournal ui)
|
||||
|
||||
-- | Helper: set forecast mode (with the given forecast period) on or off in the UI state.
|
||||
uiSetForecast :: UIState -> Maybe DateSpan -> UIState
|
||||
|
@ -3165,15 +3165,16 @@ transactions generated "just now":
|
||||
`_generated-transaction:~ PERIODICEXPR`.
|
||||
|
||||
Periodic transactions are generated within some forecast period.
|
||||
By default, this
|
||||
|
||||
- begins on the later of
|
||||
This begins on:
|
||||
- the start date supplied to the `--forecast` argument, if present
|
||||
- otherwise, the later of
|
||||
- the report start date if specified with -b/-p/date:
|
||||
- the day after the latest normal (non-periodic) transaction in the journal,
|
||||
or today if there are no normal transactions.
|
||||
|
||||
- ends on the report end date if specified with -e/-p/date:,
|
||||
or 6 months (180 days) from today.
|
||||
- the day after the latest normal (non-periodic) transaction in the journal, if any
|
||||
- otherwise today.
|
||||
It ends on:
|
||||
- the end date supplied to the `--forecast` argument, if present
|
||||
- otherwise the report end date if specified with -e/-p/date:
|
||||
- otherwise 180 days (6 months) from today.
|
||||
|
||||
This means that periodic transactions will begin only after the latest
|
||||
recorded transaction. And a recorded transaction dated in the future can
|
||||
|
@ -196,21 +196,6 @@ $ hledger -f - reg --forecast date:202001
|
||||
2020-01-28 (a) 1,000.00 USD 2,000.00 USD
|
||||
>=0
|
||||
|
||||
<
|
||||
2021-01-01
|
||||
(a) 1000
|
||||
|
||||
~ daily
|
||||
(a) 1
|
||||
|
||||
# 11. Forecast transactions are generated up to the day before the requested end date
|
||||
$ hledger -f - reg -b 2021-01-01 -e 2021-01-05 --forecast
|
||||
2021-01-01 (a) 1000 1000
|
||||
2021-01-02 (a) 1 1001
|
||||
2021-01-03 (a) 1 1002
|
||||
2021-01-04 (a) 1 1003
|
||||
>=0
|
||||
|
||||
<
|
||||
2021-09-01 Normal Balance Assertion Works
|
||||
Checking = -60
|
||||
@ -224,7 +209,7 @@ $ hledger -f - reg -b 2021-01-01 -e 2021-01-05 --forecast
|
||||
Checking = -120
|
||||
Costs
|
||||
|
||||
# 12. Forecast transactions work with balance assignments
|
||||
# 11. Forecast transactions work with balance assignments
|
||||
$ hledger -f - print -x --forecast -e 2021-11
|
||||
2021-09-01 Normal Balance Assertion Works
|
||||
Checking -60 = -60
|
||||
@ -250,7 +235,7 @@ $ hledger -f - print -x --forecast -e 2021-11
|
||||
income:client1 -10 USD
|
||||
assets:receivables:contractor1
|
||||
|
||||
# 13. Generated forecast for weekday transactions
|
||||
# 12. Generated forecast for weekday transactions
|
||||
$ hledger -f - reg --forecast -b "2021-09-01" -e "2021-09-15" --forecast -w 100
|
||||
2021-09-01 income:client1 -10 USD -10 USD
|
||||
assets:receivables:contractor1 10 USD 0
|
||||
@ -282,7 +267,7 @@ $ hledger -f - reg --forecast -b "2021-09-01" -e "2021-09-15" --forecast -w 100
|
||||
income:client1 -10 USD
|
||||
assets:receivables:contractor1
|
||||
|
||||
# 14. Generated forecast for weekend transactions
|
||||
# 13. Generated forecast for weekend transactions
|
||||
$ hledger -f - reg --forecast -b "2021-09-01" -e "2021-09-15" --forecast -w 100
|
||||
2021-09-04 income:client1 -10 USD -10 USD
|
||||
assets:receivables:contractor1 10 USD 0
|
||||
@ -293,3 +278,68 @@ $ hledger -f - reg --forecast -b "2021-09-01" -e "2021-09-15" --forecast -w 100
|
||||
2021-09-12 income:client1 -10 USD -10 USD
|
||||
assets:receivables:contractor1 10 USD 0
|
||||
>=0
|
||||
|
||||
<
|
||||
2021-01-01
|
||||
(a) 1000
|
||||
|
||||
~ daily
|
||||
(a) 1
|
||||
|
||||
# 14. Arguments to --forecast take precedence over anything. Only generate up to the day before the end date.
|
||||
$ hledger -f - reg --forecast="2020-01-01..2020-01-05" -b 2019-12-01 -e 2020-02-01 -H
|
||||
2020-01-01 (a) 1 1
|
||||
2020-01-02 (a) 1 2
|
||||
2020-01-03 (a) 1 3
|
||||
2020-01-04 (a) 1 4
|
||||
>=0
|
||||
|
||||
# 15. With no arguments to --forecast, we use the report start date if it's after the journal end date.
|
||||
$ hledger -f - reg --forecast -b 2021-02-01 -e 2021-02-05 -H
|
||||
2021-02-01 (a) 1 1001
|
||||
2021-02-02 (a) 1 1002
|
||||
2021-02-03 (a) 1 1003
|
||||
2021-02-04 (a) 1 1004
|
||||
>=0
|
||||
|
||||
# 16. With no arguments to --forecast, we use journal end date if it's after the report start date.
|
||||
$ hledger -f - reg --forecast -b 2020-12-01 -e 2021-01-05 -H
|
||||
2021-01-01 (a) 1000 1000
|
||||
2021-01-02 (a) 1 1001
|
||||
2021-01-03 (a) 1 1002
|
||||
2021-01-04 (a) 1 1003
|
||||
>=0
|
||||
|
||||
# 17. With no arguments to --forecast, and no report start, generate from journal end to 180 days from today.
|
||||
# We use here the fact that we are at least 180 days from 2021-01-01. This test will fail if you travel back in time!
|
||||
$ hledger -f - reg --forecast -H
|
||||
> /1 1360/
|
||||
>=0
|
||||
|
||||
<
|
||||
~ daily
|
||||
(a) 1
|
||||
|
||||
# 18. No real transactions.
|
||||
# Arguments to --forecast take precedence over anything. Only generate up to the day before the end date.
|
||||
$ hledger -f - reg --forecast="2020-01-01..2020-01-05" -b 2019-12-01 -e 2020-01-05 -H
|
||||
2020-01-01 (a) 1 1
|
||||
2020-01-02 (a) 1 2
|
||||
2020-01-03 (a) 1 3
|
||||
2020-01-04 (a) 1 4
|
||||
>=0
|
||||
|
||||
# 19. No real transactions.
|
||||
# With no arguments to --forecast, we use the report start date.
|
||||
$ hledger -f - reg --forecast -b 2021-02-01 -e 2021-02-05 -H
|
||||
2021-02-01 (a) 1 1
|
||||
2021-02-02 (a) 1 2
|
||||
2021-02-03 (a) 1 3
|
||||
2021-02-04 (a) 1 4
|
||||
>=0
|
||||
|
||||
# 20. No real transactions.
|
||||
# With no arguments to --forecast, and no report start, generate from today to 180 days from today.
|
||||
$ hledger -f - reg --forecast -H
|
||||
> /1 180/
|
||||
>=0
|
||||
|
Loading…
Reference in New Issue
Block a user