hledger/hledger-lib/Hledger/Reports/ReportOptions.hs
Stephen Morgan 918c243fa9 fix: forecast: Generate forecast transactions at journal finalisation,
rather than as a postprocessing step. (#1638)

This allows us to have a uniform procedure for balancing transactions,
whether they are normal transactions or forecast transactions, including
dealing with balance assignments, balance assertions, and auto postings.
2021-08-05 21:10:47 -10:00

932 lines
38 KiB
Haskell

{-|
Options common to most hledger reports.
-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TypeFamilies #-}
module Hledger.Reports.ReportOptions (
ReportOpts(..),
HasReportOpts(..),
ReportSpec(..),
HasReportSpec(..),
overWithReport,
setWithReport,
BalanceCalculation(..),
BalanceAccumulation(..),
AccountListMode(..),
ValuationType(..),
defreportopts,
rawOptsToReportOpts,
defreportspec,
reportOptsToSpec,
updateReportSpec,
updateReportSpecWith,
rawOptsToReportSpec,
balanceAccumulationOverride,
flat_,
tree_,
reportOptsToggleStatus,
simplifyStatuses,
whichDateFromOpts,
journalApplyValuationFromOpts,
journalApplyValuationFromOptsWith,
mixedAmountApplyValuationAfterSumFromOptsWith,
valuationAfterSum,
intervalFromRawOpts,
queryFromFlags,
transactionDateFn,
postingDateFn,
reportSpan,
reportSpanBothDates,
reportStartDate,
reportEndDate,
reportPeriodStart,
reportPeriodOrJournalStart,
reportPeriodLastDay,
reportPeriodOrJournalLastDay,
reportPeriodName
)
where
import Control.Applicative (Const(..), (<|>))
import Control.Monad ((<=<), join)
import Data.Either.Extra (eitherToMaybe)
import Data.Functor.Identity (Identity(..))
import Data.List.Extra (nubSort)
import Data.Maybe (fromMaybe, mapMaybe)
import qualified Data.Text as T
import Data.Time.Calendar (Day, addDays)
import Data.Default (Default(..))
import Lens.Micro (Lens')
import Safe (headMay, lastDef, lastMay, maximumMay)
import Text.Megaparsec.Custom
import Hledger.Data
import Hledger.Query
import Hledger.Utils
-- | What to calculate for each cell in a balance report.
-- "Balance report types -> Calculation type" in the hledger manual.
data BalanceCalculation =
CalcChange -- ^ Sum of posting amounts in the period.
| CalcBudget -- ^ Sum of posting amounts and the goal for the period.
| CalcValueChange -- ^ Change from previous period's historical end value to this period's historical end value.
deriving (Eq, Show)
instance Default BalanceCalculation where def = CalcChange
-- | How to accumulate calculated values across periods (columns) in a balance report.
-- "Balance report types -> Accumulation type" in the hledger manual.
data BalanceAccumulation =
PerPeriod -- ^ No accumulation. Eg, shows the change of balance in each period.
| Cumulative -- ^ Accumulate changes across periods, starting from zero at report start.
| Historical -- ^ Accumulate changes across periods, including any from before report start.
-- Eg, shows the historical end balance of each period.
deriving (Eq,Show)
instance Default BalanceAccumulation where def = PerPeriod
-- | Should accounts be displayed: in the command's default style, hierarchically, or as a flat list ?
data AccountListMode = ALFlat | ALTree deriving (Eq, Show)
instance Default AccountListMode where def = ALFlat
-- | Standard options for customising report filtering and output.
-- Most of these correspond to standard hledger command-line options
-- or query arguments, but not all. Some are used only by certain
-- commands, as noted below.
data ReportOpts = ReportOpts {
-- for most reports:
period_ :: Period
,interval_ :: Interval
,statuses_ :: [Status] -- ^ Zero, one, or two statuses to be matched
,cost_ :: Costing -- ^ Should we convert amounts to cost, when present?
,value_ :: Maybe ValuationType -- ^ What value should amounts be converted to ?
,infer_value_ :: Bool -- ^ Infer market prices from transactions ?
,depth_ :: Maybe Int
,date2_ :: Bool
,empty_ :: Bool
,no_elide_ :: Bool
,real_ :: Bool
,format_ :: StringFormat
,querystring_ :: [T.Text]
--
,average_ :: Bool
-- for posting reports (register)
,related_ :: Bool
-- for account transactions reports (aregister)
,txn_dates_ :: Bool
-- for balance reports (bal, bs, cf, is)
,balancecalc_ :: BalanceCalculation -- ^ What to calculate in balance report cells
,balanceaccum_ :: BalanceAccumulation -- ^ How to accumulate balance report values over time
,budgetpat_ :: Maybe T.Text -- ^ A case-insensitive description substring
-- to select periodic transactions for budget reports.
-- (Not a regexp, nor a full hledger query, for now.)
,accountlistmode_ :: AccountListMode
,drop_ :: Int
,row_total_ :: Bool
,no_total_ :: Bool
,show_costs_ :: Bool -- ^ Whether to show costs for reports which normally don't show them
,pretty_tables_ :: Bool
,sort_amount_ :: Bool
,percent_ :: Bool
,invert_ :: Bool -- ^ if true, flip all amount signs in reports
,normalbalance_ :: Maybe NormalSign
-- ^ This can be set when running balance reports on a set of accounts
-- with the same normal balance type (eg all assets, or all incomes).
-- - It helps --sort-amount know how to sort negative numbers
-- (eg in the income section of an income statement)
-- - It helps compound balance report commands (is, bs etc.) do
-- sign normalisation, converting normally negative subreports to
-- normally positive for a more conventional display.
,color_ :: Bool
-- ^ Whether to use ANSI color codes in text output.
-- Influenced by the --color/colour flag (cf CliOptions),
-- whether stdout is an interactive terminal, and the value of
-- TERM and existence of NO_COLOR environment variables.
,transpose_ :: Bool
} deriving (Show)
instance Default ReportOpts where def = defreportopts
defreportopts :: ReportOpts
defreportopts = ReportOpts
{ period_ = PeriodAll
, interval_ = NoInterval
, statuses_ = []
, cost_ = NoCost
, value_ = Nothing
, infer_value_ = False
, depth_ = Nothing
, date2_ = False
, empty_ = False
, no_elide_ = False
, real_ = False
, format_ = def
, querystring_ = []
, average_ = False
, related_ = False
, txn_dates_ = False
, balancecalc_ = def
, balanceaccum_ = def
, budgetpat_ = Nothing
, accountlistmode_ = ALFlat
, drop_ = 0
, row_total_ = False
, no_total_ = False
, show_costs_ = False
, pretty_tables_ = False
, sort_amount_ = False
, percent_ = False
, invert_ = False
, normalbalance_ = Nothing
, color_ = False
, transpose_ = False
}
-- | Generate a ReportOpts from raw command-line input, given a day.
-- This will fail with a usage error if it is passed
-- - an invalid --format argument,
-- - an invalid --value argument,
-- - if --valuechange is called with a valuation type other than -V/--value=end.
rawOptsToReportOpts :: Day -> RawOpts -> ReportOpts
rawOptsToReportOpts d rawopts =
let formatstring = T.pack <$> maybestringopt "format" rawopts
querystring = map T.pack $ listofstringopt "args" rawopts -- doesn't handle an arg like "" right
(costing, valuation) = valuationTypeFromRawOpts rawopts
format = case parseStringFormat <$> formatstring of
Nothing -> defaultBalanceLineFormat
Just (Right x) -> x
Just (Left err) -> usageError $ "could not parse format option: " ++ err
in defreportopts
{period_ = periodFromRawOpts d rawopts
,interval_ = intervalFromRawOpts rawopts
,statuses_ = statusesFromRawOpts rawopts
,cost_ = costing
,value_ = valuation
,infer_value_ = boolopt "infer-market-price" rawopts
,depth_ = maybeposintopt "depth" rawopts
,date2_ = boolopt "date2" rawopts
,empty_ = boolopt "empty" rawopts
,no_elide_ = boolopt "no-elide" rawopts
,real_ = boolopt "real" rawopts
,format_ = format
,querystring_ = querystring
,average_ = boolopt "average" rawopts
,related_ = boolopt "related" rawopts
,txn_dates_ = boolopt "txn-dates" rawopts
,balancecalc_ = balancecalcopt rawopts
,balanceaccum_ = balanceaccumopt rawopts
,budgetpat_ = maybebudgetpatternopt rawopts
,accountlistmode_ = accountlistmodeopt rawopts
,drop_ = posintopt "drop" rawopts
,row_total_ = boolopt "row-total" rawopts
,no_total_ = boolopt "no-total" rawopts
,show_costs_ = boolopt "show-costs" rawopts
,sort_amount_ = boolopt "sort-amount" rawopts
,percent_ = boolopt "percent" rawopts
,invert_ = boolopt "invert" rawopts
,pretty_tables_ = boolopt "pretty-tables" rawopts
,color_ = useColorOnStdout -- a lower-level helper
,transpose_ = boolopt "transpose" rawopts
}
-- | The result of successfully parsing a ReportOpts on a particular
-- Day. Any ambiguous dates are completed and Queries are parsed,
-- ensuring that there are no regular expression errors. Values here
-- should be used in preference to re-deriving them from ReportOpts.
-- If you change the query_ in ReportOpts, you should call
-- `reportOptsToSpec` to regenerate the ReportSpec with the new
-- Query.
data ReportSpec = ReportSpec
{ _rsReportOpts :: ReportOpts -- ^ The underlying ReportOpts used to generate this ReportSpec
, _rsDay :: Day -- ^ The Day this ReportSpec is generated for
, _rsQuery :: Query -- ^ The generated Query for the given day
, _rsQueryOpts :: [QueryOpt] -- ^ A list of QueryOpts for the given day
} deriving (Show)
instance Default ReportSpec where def = defreportspec
defreportspec :: ReportSpec
defreportspec = ReportSpec
{ _rsReportOpts = def
, _rsDay = nulldate
, _rsQuery = Any
, _rsQueryOpts = []
}
-- | Generate a ReportSpec from a set of ReportOpts on a given day.
reportOptsToSpec :: Day -> ReportOpts -> Either String ReportSpec
reportOptsToSpec day ropts = do
(argsquery, queryopts) <- parseQueryList day $ querystring_ ropts
return ReportSpec
{ _rsReportOpts = ropts
, _rsDay = day
, _rsQuery = simplifyQuery $ And [queryFromFlags ropts, argsquery]
, _rsQueryOpts = queryopts
}
-- | Update the ReportOpts and the fields derived from it in a ReportSpec,
-- or return an error message if there is a problem such as missing or
-- unparseable options data. This is the safe way to change a ReportSpec,
-- ensuring that all fields (_rsQuery, _rsReportOpts, querystring_, etc.) are in sync.
updateReportSpec :: ReportOpts -> ReportSpec -> Either String ReportSpec
updateReportSpec = setWithReport reportOpts
-- | Like updateReportSpec, but takes a ReportOpts-modifying function.
updateReportSpecWith :: (ReportOpts -> ReportOpts) -> ReportSpec -> Either String ReportSpec
updateReportSpecWith = overWithReport reportOpts
-- | Generate a ReportSpec from RawOpts and the current date.
rawOptsToReportSpec :: RawOpts -> IO ReportSpec
rawOptsToReportSpec rawopts = do
d <- getCurrentDay
let ropts = rawOptsToReportOpts d rawopts
either fail return $ reportOptsToSpec d ropts
accountlistmodeopt :: RawOpts -> AccountListMode
accountlistmodeopt =
fromMaybe ALFlat . choiceopt parse where
parse = \case
"tree" -> Just ALTree
"flat" -> Just ALFlat
_ -> Nothing
-- Get the argument of the --budget option if any, or the empty string.
maybebudgetpatternopt :: RawOpts -> Maybe T.Text
maybebudgetpatternopt = fmap T.pack . maybestringopt "budget"
balancecalcopt :: RawOpts -> BalanceCalculation
balancecalcopt =
fromMaybe CalcChange . choiceopt parse where
parse = \case
"sum" -> Just CalcChange
"valuechange" -> Just CalcValueChange
"budget" -> Just CalcBudget
_ -> Nothing
balanceaccumopt :: RawOpts -> BalanceAccumulation
balanceaccumopt = fromMaybe PerPeriod . balanceAccumulationOverride
balanceAccumulationOverride :: RawOpts -> Maybe BalanceAccumulation
balanceAccumulationOverride rawopts = choiceopt parse rawopts <|> reportbal
where
parse = \case
"historical" -> Just Historical
"cumulative" -> Just Cumulative
"change" -> Just PerPeriod
_ -> Nothing
reportbal = case balancecalcopt rawopts of
CalcValueChange -> Just PerPeriod
_ -> Nothing
-- Get the period specified by any -b/--begin, -e/--end and/or -p/--period
-- options appearing in the command line.
-- Its bounds are the rightmost begin date specified by a -b or -p, and
-- the rightmost end date specified by a -e or -p. Cf #1011.
-- Today's date is provided to help interpret any relative dates.
periodFromRawOpts :: Day -> RawOpts -> Period
periodFromRawOpts d rawopts =
case (mlastb, mlaste) of
(Nothing, Nothing) -> PeriodAll
(Just b, Nothing) -> PeriodFrom b
(Nothing, Just e) -> PeriodTo e
(Just b, Just e) -> simplifyPeriod $ PeriodBetween b e
where
mlastb = case beginDatesFromRawOpts d rawopts of
[] -> Nothing
bs -> Just $ last bs
mlaste = case endDatesFromRawOpts d rawopts of
[] -> Nothing
es -> Just $ last es
-- Get all begin dates specified by -b/--begin or -p/--period options, in order,
-- using the given date to interpret relative date expressions.
beginDatesFromRawOpts :: Day -> RawOpts -> [Day]
beginDatesFromRawOpts d = collectopts (begindatefromrawopt d)
where
begindatefromrawopt d (n,v)
| n == "begin" =
either (\e -> usageError $ "could not parse "++n++" date: "++customErrorBundlePretty e) Just $
fixSmartDateStrEither' d (T.pack v)
| n == "period" =
case
either (\e -> usageError $ "could not parse period option: "++customErrorBundlePretty e) id $
parsePeriodExpr d (stripquotes $ T.pack v)
of
(_, DateSpan (Just b) _) -> Just b
_ -> Nothing
| otherwise = Nothing
-- Get all end dates specified by -e/--end or -p/--period options, in order,
-- using the given date to interpret relative date expressions.
endDatesFromRawOpts :: Day -> RawOpts -> [Day]
endDatesFromRawOpts d = collectopts (enddatefromrawopt d)
where
enddatefromrawopt d (n,v)
| n == "end" =
either (\e -> usageError $ "could not parse "++n++" date: "++customErrorBundlePretty e) Just $
fixSmartDateStrEither' d (T.pack v)
| n == "period" =
case
either (\e -> usageError $ "could not parse period option: "++customErrorBundlePretty e) id $
parsePeriodExpr d (stripquotes $ T.pack v)
of
(_, DateSpan _ (Just e)) -> Just e
_ -> Nothing
| otherwise = Nothing
-- | Get the report interval, if any, specified by the last of -p/--period,
-- -D/--daily, -W/--weekly, -M/--monthly etc. options.
-- An interval from --period counts only if it is explicitly defined.
intervalFromRawOpts :: RawOpts -> Interval
intervalFromRawOpts = lastDef NoInterval . collectopts intervalfromrawopt
where
intervalfromrawopt (n,v)
| n == "period" =
either
(\e -> usageError $ "could not parse period option: "++customErrorBundlePretty e)
extractIntervalOrNothing $
parsePeriodExpr
(error' "intervalFromRawOpts: did not expect to need today's date here") -- PARTIAL: should not happen; we are just getting the interval, which does not use the reference date
(stripquotes $ T.pack v)
| n == "daily" = Just $ Days 1
| n == "weekly" = Just $ Weeks 1
| n == "monthly" = Just $ Months 1
| n == "quarterly" = Just $ Quarters 1
| n == "yearly" = Just $ Years 1
| otherwise = Nothing
-- | Extract the interval from the parsed -p/--period expression.
-- Return Nothing if an interval is not explicitly defined.
extractIntervalOrNothing :: (Interval, DateSpan) -> Maybe Interval
extractIntervalOrNothing (NoInterval, _) = Nothing
extractIntervalOrNothing (interval, _) = Just interval
-- | Get any statuses to be matched, as specified by -U/--unmarked,
-- -P/--pending, -C/--cleared flags. -UPC is equivalent to no flags,
-- so this returns a list of 0-2 unique statuses.
statusesFromRawOpts :: RawOpts -> [Status]
statusesFromRawOpts = simplifyStatuses . collectopts statusfromrawopt
where
statusfromrawopt (n,_)
| n == "unmarked" = Just Unmarked
| n == "pending" = Just Pending
| n == "cleared" = Just Cleared
| otherwise = Nothing
-- | Reduce a list of statuses to just one of each status,
-- and if all statuses are present return the empty list.
simplifyStatuses l
| length l' >= numstatuses = []
| otherwise = l'
where
l' = nubSort l
numstatuses = length [minBound .. maxBound :: Status]
-- | Add/remove this status from the status list. Used by hledger-ui.
reportOptsToggleStatus s ropts@ReportOpts{statuses_=ss}
| s `elem` ss = ropts{statuses_=filter (/= s) ss}
| otherwise = ropts{statuses_=simplifyStatuses (s:ss)}
-- | Parse the type of valuation and costing to be performed, if any,
-- specified by -B/--cost, -V, -X/--exchange, or --value flags. It is
-- allowed to combine -B/--cost with any other valuation type. If
-- there's more than one valuation type, the rightmost flag wins.
-- This will fail with a usage error if an invalid argument is passed
-- to --value, or if --valuechange is called with a valuation type
-- other than -V/--value=end.
valuationTypeFromRawOpts :: RawOpts -> (Costing, Maybe ValuationType)
valuationTypeFromRawOpts rawopts = (costing, valuation)
where
costing = if (any ((Cost==) . fst) valuationopts) then Cost else NoCost
valuation = case balancecalcopt rawopts of
CalcValueChange -> case directval of
Nothing -> Just $ AtEnd Nothing -- If no valuation requested for valuechange, use AtEnd
Just (AtEnd _) -> directval -- If AtEnd valuation requested, use it
Just _ -> usageError "--valuechange only produces sensible results with --value=end"
_ -> directval -- Otherwise, use requested valuation
where directval = lastMay $ mapMaybe snd valuationopts
valuationopts = collectopts valuationfromrawopt rawopts
valuationfromrawopt (n,v) -- option name, value
| n == "B" = Just (Cost, Nothing) -- keep supporting --value=cost for now
| n == "V" = Just (NoCost, Just $ AtEnd Nothing)
| n == "X" = Just (NoCost, Just $ AtEnd (Just $ T.pack v))
| n == "value" = Just $ valueopt v
| otherwise = Nothing
valueopt v
| t `elem` ["cost","c"] = (Cost, AtEnd . Just <$> mc) -- keep supporting --value=cost,COMM for now
| t `elem` ["then" ,"t"] = (NoCost, Just $ AtThen mc)
| t `elem` ["end" ,"e"] = (NoCost, Just $ AtEnd mc)
| t `elem` ["now" ,"n"] = (NoCost, Just $ AtNow mc)
| otherwise = case parsedateM t of
Just d -> (NoCost, Just $ AtDate d mc)
Nothing -> usageError $ "could not parse \""++t++"\" as valuation type, should be: then|end|now|t|e|n|YYYY-MM-DD"
where
-- parse --value's value: TYPE[,COMM]
(t,c') = break (==',') v
mc = case drop 1 c' of
"" -> Nothing
c -> Just $ T.pack c
-- | Select the Transaction date accessor based on --date2.
transactionDateFn :: ReportOpts -> (Transaction -> Day)
transactionDateFn ReportOpts{..} = if date2_ then transactionDate2 else tdate
-- | Select the Posting date accessor based on --date2.
postingDateFn :: ReportOpts -> (Posting -> Day)
postingDateFn ReportOpts{..} = if date2_ then postingDate2 else postingDate
-- | Report which date we will report on based on --date2.
whichDateFromOpts :: ReportOpts -> WhichDate
whichDateFromOpts ReportOpts{..} = if date2_ then SecondaryDate else PrimaryDate
-- | Legacy-compatible convenience aliases for accountlistmode_.
tree_ :: ReportOpts -> Bool
tree_ ReportOpts{accountlistmode_ = ALTree} = True
tree_ ReportOpts{accountlistmode_ = ALFlat} = False
flat_ :: ReportOpts -> Bool
flat_ = not . tree_
-- depthFromOpts :: ReportOpts -> Int
-- depthFromOpts opts = min (fromMaybe 99999 $ depth_ opts) (queryDepth $ queryFromOpts nulldate opts)
-- | Convert this journal's postings' amounts to cost and/or to value, if specified
-- by options (-B/--cost/-V/-X/--value etc.). Strip prices if not needed. This
-- should be the main stop for performing costing and valuation. The exception is
-- whenever you need to perform valuation _after_ summing up amounts, as in a
-- historical balance report with --value=end. valuationAfterSum will check for this
-- condition.
journalApplyValuationFromOpts :: ReportSpec -> Journal -> Journal
journalApplyValuationFromOpts rspec j =
journalApplyValuationFromOptsWith rspec j priceoracle
where priceoracle = journalPriceOracle (infer_value_ $ _rsReportOpts rspec) j
-- | Like journalApplyValuationFromOpts, but takes PriceOracle as an argument.
journalApplyValuationFromOptsWith :: ReportSpec -> Journal -> PriceOracle -> Journal
journalApplyValuationFromOptsWith rspec@ReportSpec{_rsReportOpts=ropts} j priceoracle =
journalMapPostings valuation $ costing j
where
valuation p = maybe id (postingApplyValuation priceoracle styles (periodEnd p) (_rsDay rspec)) (value_ ropts) p
costing = case cost_ ropts of
Cost -> journalToCost
NoCost -> id
-- Find the end of the period containing this posting
periodEnd = addDays (-1) . fromMaybe err . mPeriodEnd . postingDate
mPeriodEnd = spanEnd <=< latestSpanContaining (historical : spans)
historical = DateSpan Nothing $ spanStart =<< headMay spans
spans = splitSpan (interval_ ropts) $ reportSpanBothDates j rspec
styles = journalCommodityStyles j
err = error "journalApplyValuationFromOpts: expected all spans to have an end date"
-- | Select the Account valuation functions required for performing valuation after summing
-- amounts. Used in MultiBalanceReport to value historical and similar reports.
mixedAmountApplyValuationAfterSumFromOptsWith :: ReportOpts -> Journal -> PriceOracle
-> (DateSpan -> MixedAmount -> MixedAmount)
mixedAmountApplyValuationAfterSumFromOptsWith ropts j priceoracle =
case valuationAfterSum ropts of
Just mc -> \span -> valuation mc span . costing
Nothing -> const id
where
valuation mc span = mixedAmountValueAtDate priceoracle styles mc (maybe err (addDays (-1)) $ spanEnd span)
where err = error "mixedAmountApplyValuationAfterSumFromOptsWith: expected all spans to have an end date"
costing = case cost_ ropts of
Cost -> styleMixedAmount styles . mixedAmountCost
NoCost -> id
styles = journalCommodityStyles j
-- | If the ReportOpts specify that we are performing valuation after summing amounts,
-- return Just the commodity symbol we're converting to, otherwise return Nothing.
-- Used for example with historical reports with --value=end.
valuationAfterSum :: ReportOpts -> Maybe (Maybe CommoditySymbol)
valuationAfterSum ropts = case value_ ropts of
Just (AtEnd mc) | valueAfterSum -> Just mc
_ -> Nothing
where valueAfterSum = balancecalc_ ropts == CalcValueChange
|| balanceaccum_ ropts /= PerPeriod
-- | Convert report options to a query, ignoring any non-flag command line arguments.
queryFromFlags :: ReportOpts -> Query
queryFromFlags ReportOpts{..} = simplifyQuery $ And flagsq
where
flagsq = consIf Real real_
. consJust Depth depth_
$ [ (if date2_ then Date2 else Date) $ periodAsDateSpan period_
, Or $ map StatusQ statuses_
]
consIf f b = if b then (f True:) else id
consJust f = maybe id ((:) . f)
-- Report dates.
-- | The effective report span is the start and end dates specified by
-- options or queries, or otherwise the earliest and latest transaction or
-- posting dates in the journal. If no dates are specified by options/queries
-- and the journal is empty, returns the null date span.
reportSpan :: Journal -> ReportSpec -> DateSpan
reportSpan = reportSpanHelper False
-- | Like reportSpan, but uses both primary and secondary dates when calculating
-- the span.
reportSpanBothDates :: Journal -> ReportSpec -> DateSpan
reportSpanBothDates = reportSpanHelper True
-- | A helper for reportSpan, which takes a Bool indicating whether to use both
-- primary and secondary dates.
reportSpanHelper :: Bool -> Journal -> ReportSpec -> DateSpan
reportSpanHelper bothdates j ReportSpec{_rsQuery=query, _rsReportOpts=ropts} = reportspan
where
-- The date span specified by -b/-e/-p options and query args if any.
requestedspan = dbg3 "requestedspan" $ if bothdates then queryDateSpan' query else queryDateSpan (date2_ ropts) query
-- If we are requesting period-end valuation, the journal date span should
-- include price directives after the last transaction
journalspan = dbg3 "journalspan" $ if bothdates then journalDateSpanBothDates j else journalDateSpan (date2_ ropts) j
pricespan = dbg3 "pricespan" . DateSpan Nothing $ case value_ ropts of
Just (AtEnd _) -> fmap (addDays 1) . maximumMay . map pddate $ jpricedirectives j
_ -> Nothing
-- If the requested span is open-ended, close it using the journal's start and end dates.
-- This can still be the null (open) span if the journal is empty.
requestedspan' = dbg3 "requestedspan'" $ requestedspan `spanDefaultsFrom` (journalspan `spanUnion` pricespan)
-- The list of interval spans enclosing the requested span.
-- This list can be empty if the journal was empty,
-- or if hledger-ui has added its special date:-tomorrow to the query
-- and all txns are in the future.
intervalspans = dbg3 "intervalspans" $ splitSpan (interval_ ropts) requestedspan'
-- The requested span enlarged to enclose a whole number of intervals.
-- This can be the null span if there were no intervals.
reportspan = dbg3 "reportspan" $ DateSpan (spanStart =<< headMay intervalspans)
(spanEnd =<< lastMay intervalspans)
reportStartDate :: Journal -> ReportSpec -> Maybe Day
reportStartDate j = spanStart . reportSpan j
reportEndDate :: Journal -> ReportSpec -> Maybe Day
reportEndDate j = spanEnd . reportSpan j
-- Some pure alternatives to the above. XXX review/clean up
-- Get the report's start date.
-- If no report period is specified, will be Nothing.
reportPeriodStart :: ReportSpec -> Maybe Day
reportPeriodStart = queryStartDate False . _rsQuery
-- Get the report's start date, or if no report period is specified,
-- the journal's start date (the earliest posting date). If there's no
-- report period and nothing in the journal, will be Nothing.
reportPeriodOrJournalStart :: ReportSpec -> Journal -> Maybe Day
reportPeriodOrJournalStart rspec j =
reportPeriodStart rspec <|> journalStartDate False j
-- Get the last day of the overall report period.
-- This the inclusive end date (one day before the
-- more commonly used, exclusive, report end date).
-- If no report period is specified, will be Nothing.
reportPeriodLastDay :: ReportSpec -> Maybe Day
reportPeriodLastDay = fmap (addDays (-1)) . queryEndDate False . _rsQuery
-- Get the last day of the overall report period, or if no report
-- period is specified, the last day of the journal (ie the latest
-- posting date). If we're doing period-end valuation, include price
-- directive dates. If there's no report period and nothing in the
-- journal, will be Nothing.
reportPeriodOrJournalLastDay :: ReportSpec -> Journal -> Maybe Day
reportPeriodOrJournalLastDay rspec j = reportPeriodLastDay rspec <|> journalOrPriceEnd
where
journalOrPriceEnd = case value_ $ _rsReportOpts rspec of
Just (AtEnd _) -> max (journalLastDay False j) lastPriceDirective
_ -> journalLastDay False j
lastPriceDirective = fmap (addDays 1) . maximumMay . map pddate $ jpricedirectives j
-- | Make a name for the given period in a multiperiod report, given
-- the type of balance being reported and the full set of report
-- periods. This will be used as a column heading (or row heading, in
-- a register summary report). We try to pick a useful name as follows:
--
-- - ending-balance reports: the period's end date
--
-- - balance change reports where the periods are months and all in the same year:
-- the short month name in the current locale
--
-- - all other balance change reports: a description of the datespan,
-- abbreviated to compact form if possible (see showDateSpan).
reportPeriodName :: BalanceAccumulation -> [DateSpan] -> DateSpan -> T.Text
reportPeriodName balanceaccumulation spans =
case balanceaccumulation of
PerPeriod -> if multiyear then showDateSpan else showDateSpanMonthAbbrev
where
multiyear = (>1) $ length $ nubSort $ map spanStartYear spans
_ -> maybe "" (showDate . prevday) . spanEnd
-- lenses
-- Reportable functors are so that we can create special lenses which can fail
-- and report on their failure.
class Functor f => Reportable f e where
report :: a -> f (Either e a) -> f a
instance Reportable (Const r) e where
report _ (Const x) = Const x
instance Reportable Identity e where
report a (Identity i) = Identity $ either (const a) id i
instance Reportable Maybe e where
report _ = join . fmap eitherToMaybe
instance (e ~ a) => Reportable (Either a) e where
report _ = join
-- | Apply a function over a lens, but report on failure.
overWithReport :: ((a -> Either e b) -> s -> Either e t) -> (a -> b) -> s -> Either e t
overWithReport l f s = l (pure . f) s
-- | Set a field using a lens, but report on failure.
setWithReport :: ((a -> Either e b) -> s -> Either e t) -> b -> s -> Either e t
setWithReport l = overWithReport l . const
type ReportableLens' s a = forall f. Reportable f String => (a -> f a) -> s -> f s
-- | Lenses for ReportOpts.
-- Note that some of these are not true lenses, as they have a further restriction on
-- the functor. This will work as a normal lens for all common uses, but since they
-- don't obey the lens laws for some fancy cases, they may fail in some exotic circumstances.
-- The special type is so that updating querystring_, real_, depth_, date2_, period_, or
-- statuses_ can automatically update the Query and QueryOpts in ReportSpec.
class HasReportOpts a where
reportOpts :: ReportableLens' a ReportOpts
reportOptsNoUpdate :: Lens' a ReportOpts
period :: ReportableLens' a Period
period = reportOpts.period
{-# INLINE period #-}
statuses :: ReportableLens' a [Status]
statuses = reportOpts.statuses
{-# INLINE statuses #-}
depth :: ReportableLens' a (Maybe Int)
depth = reportOpts.depth
{-# INLINE depth #-}
date2 :: ReportableLens' a Bool
date2 = reportOpts.date2
{-# INLINE date2 #-}
real :: ReportableLens' a Bool
real = reportOpts.real
{-# INLINE real #-}
querystring :: ReportableLens' a [T.Text]
querystring = reportOpts.querystring
{-# INLINE querystring #-}
interval :: Lens' a Interval
interval = reportOptsNoUpdate.interval
{-# INLINE interval #-}
cost :: Lens' a Costing
cost = reportOptsNoUpdate.cost
{-# INLINE cost #-}
value :: Lens' a (Maybe ValuationType)
value = reportOptsNoUpdate.value
{-# INLINE value #-}
infer_value :: Lens' a Bool
infer_value = reportOptsNoUpdate.infer_value
{-# INLINE infer_value #-}
empty__ :: Lens' a Bool
empty__ = reportOptsNoUpdate.empty__
{-# INLINE empty__ #-}
no_elide :: Lens' a Bool
no_elide = reportOptsNoUpdate.no_elide
{-# INLINE no_elide #-}
format :: Lens' a StringFormat
format = reportOptsNoUpdate.format
{-# INLINE format #-}
average :: Lens' a Bool
average = reportOptsNoUpdate.average
{-# INLINE average #-}
related :: Lens' a Bool
related = reportOptsNoUpdate.related
{-# INLINE related #-}
txn_dates :: Lens' a Bool
txn_dates = reportOptsNoUpdate.txn_dates
{-# INLINE txn_dates #-}
balancecalc :: Lens' a BalanceCalculation
balancecalc = reportOptsNoUpdate.balancecalc
{-# INLINE balancecalc #-}
balanceaccum :: Lens' a BalanceAccumulation
balanceaccum = reportOptsNoUpdate.balanceaccum
{-# INLINE balanceaccum #-}
accountlistmode :: Lens' a AccountListMode
accountlistmode = reportOptsNoUpdate.accountlistmode
{-# INLINE accountlistmode #-}
drop__ :: Lens' a Int
drop__ = reportOptsNoUpdate.drop__
{-# INLINE drop__ #-}
row_total :: Lens' a Bool
row_total = reportOptsNoUpdate.row_total
{-# INLINE row_total #-}
no_total :: Lens' a Bool
no_total = reportOptsNoUpdate.no_total
{-# INLINE no_total #-}
show_costs :: Lens' a Bool
show_costs = reportOptsNoUpdate.show_costs
{-# INLINE show_costs #-}
pretty_tables :: Lens' a Bool
pretty_tables = reportOptsNoUpdate.pretty_tables
{-# INLINE pretty_tables #-}
sort_amount :: Lens' a Bool
sort_amount = reportOptsNoUpdate.sort_amount
{-# INLINE sort_amount #-}
percent :: Lens' a Bool
percent = reportOptsNoUpdate.percent
{-# INLINE percent #-}
invert :: Lens' a Bool
invert = reportOptsNoUpdate.invert
{-# INLINE invert #-}
normalbalance :: Lens' a (Maybe NormalSign)
normalbalance = reportOptsNoUpdate.normalbalance
{-# INLINE normalbalance #-}
color__ :: Lens' a Bool
color__ = reportOptsNoUpdate.color__
{-# INLINE color__ #-}
transpose__ :: Lens' a Bool
transpose__ = reportOptsNoUpdate.transpose__
{-# INLINE transpose__ #-}
instance HasReportOpts ReportOpts where
reportOpts = id
reportOptsNoUpdate = id
period f ropts = (\x -> ropts{period_=x}) <$> f (period_ ropts)
{-# INLINE period #-}
interval f ropts = (\x -> ropts{interval_=x}) <$> f (interval_ ropts)
{-# INLINE interval #-}
statuses f ropts = (\x -> ropts{statuses_=x}) <$> f (statuses_ ropts)
{-# INLINE statuses #-}
cost f ropts = (\x -> ropts{cost_=x}) <$> f (cost_ ropts)
{-# INLINE cost #-}
value f ropts = (\x -> ropts{value_=x}) <$> f (value_ ropts)
{-# INLINE value #-}
infer_value f ropts = (\x -> ropts{infer_value_=x}) <$> f (infer_value_ ropts)
{-# INLINE infer_value #-}
depth f ropts = (\x -> ropts{depth_=x}) <$> f (depth_ ropts)
{-# INLINE depth #-}
date2 f ropts = (\x -> ropts{date2_=x}) <$> f (date2_ ropts)
{-# INLINE date2 #-}
empty__ f ropts = (\x -> ropts{empty_=x}) <$> f (empty_ ropts)
{-# INLINE empty__ #-}
no_elide f ropts = (\x -> ropts{no_elide_=x}) <$> f (no_elide_ ropts)
{-# INLINE no_elide #-}
real f ropts = (\x -> ropts{real_=x}) <$> f (real_ ropts)
{-# INLINE real #-}
format f ropts = (\x -> ropts{format_=x}) <$> f (format_ ropts)
{-# INLINE format #-}
querystring f ropts = (\x -> ropts{querystring_=x}) <$> f (querystring_ ropts)
{-# INLINE querystring #-}
average f ropts = (\x -> ropts{average_=x}) <$> f (average_ ropts)
{-# INLINE average #-}
related f ropts = (\x -> ropts{related_=x}) <$> f (related_ ropts)
{-# INLINE related #-}
txn_dates f ropts = (\x -> ropts{txn_dates_=x}) <$> f (txn_dates_ ropts)
{-# INLINE txn_dates #-}
balancecalc f ropts = (\x -> ropts{balancecalc_=x}) <$> f (balancecalc_ ropts)
{-# INLINE balancecalc #-}
balanceaccum f ropts = (\x -> ropts{balanceaccum_=x}) <$> f (balanceaccum_ ropts)
{-# INLINE balanceaccum #-}
accountlistmode f ropts = (\x -> ropts{accountlistmode_=x}) <$> f (accountlistmode_ ropts)
{-# INLINE accountlistmode #-}
drop__ f ropts = (\x -> ropts{drop_=x}) <$> f (drop_ ropts)
{-# INLINE drop__ #-}
row_total f ropts = (\x -> ropts{row_total_=x}) <$> f (row_total_ ropts)
{-# INLINE row_total #-}
no_total f ropts = (\x -> ropts{no_total_=x}) <$> f (no_total_ ropts)
{-# INLINE no_total #-}
show_costs f ropts = (\x -> ropts{show_costs_=x}) <$> f (show_costs_ ropts)
{-# INLINE show_costs #-}
pretty_tables f ropts = (\x -> ropts{pretty_tables_=x}) <$> f (pretty_tables_ ropts)
{-# INLINE pretty_tables #-}
sort_amount f ropts = (\x -> ropts{sort_amount_=x}) <$> f (sort_amount_ ropts)
{-# INLINE sort_amount #-}
percent f ropts = (\x -> ropts{percent_=x}) <$> f (percent_ ropts)
{-# INLINE percent #-}
invert f ropts = (\x -> ropts{invert_=x}) <$> f (invert_ ropts)
{-# INLINE invert #-}
normalbalance f ropts = (\x -> ropts{normalbalance_=x}) <$> f (normalbalance_ ropts)
{-# INLINE normalbalance #-}
color__ f ropts = (\x -> ropts{color_=x}) <$> f (color_ ropts)
{-# INLINE color__ #-}
transpose__ f ropts = (\x -> ropts{transpose_=x}) <$> f (transpose_ ropts)
{-# INLINE transpose__ #-}
class HasReportSpec a where
reportSpec :: Lens' a ReportSpec
rsDay :: Lens' a Day
rsDay = reportSpec.rsDay
{-# INLINE rsDay #-}
rsQuery :: Lens' a Query
rsQuery = reportSpec.rsQuery
{-# INLINE rsQuery #-}
rsQueryOpts :: Lens' a [QueryOpt]
rsQueryOpts = reportSpec.rsQueryOpts
{-# INLINE rsQueryOpts #-}
instance HasReportSpec ReportSpec where
reportSpec = id
rsDay f rspec = (\d -> either (error "Updating _rsDay should never fail") id . -- PARTIAL:
reportOptsToSpec d $ _rsReportOpts rspec) <$> f (_rsDay rspec)
{-# INLINE rsDay #-}
rsQuery f rspec = (\x -> rspec{_rsQuery=x}) <$> f (_rsQuery rspec)
{-# INLINE rsQuery #-}
rsQueryOpts f rspec = (\x -> rspec{_rsQueryOpts=x}) <$> f (_rsQueryOpts rspec)
{-# INLINE rsQueryOpts #-}
instance HasReportOpts ReportSpec where
reportOpts f rspec = report (error "Updating ReportSpec failed: try using overWithReport instead of over") $ -- PARTIAL:
reportOptsToSpec (_rsDay rspec) <$> f (_rsReportOpts rspec)
{-# INLINE reportOpts #-}
reportOptsNoUpdate f rspec = (\x -> rspec{_rsReportOpts=x}) <$> f (_rsReportOpts rspec)
{-# INLINE reportOptsNoUpdate #-}