872 lines
		
	
	
		
			38 KiB
		
	
	
	
		
			Haskell
		
	
	
	
	
	
			
		
		
	
	
			872 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 TemplateHaskell       #-}
 | 
						|
{-# LANGUAGE TypeFamilies          #-}
 | 
						|
 | 
						|
module Hledger.Reports.ReportOptions (
 | 
						|
  ReportOpts(..),
 | 
						|
  HasReportOptsNoUpdate(..),
 | 
						|
  HasReportOpts(..),
 | 
						|
  ReportSpec(..),
 | 
						|
  HasReportSpec(..),
 | 
						|
  overEither,
 | 
						|
  setEither,
 | 
						|
  BalanceCalculation(..),
 | 
						|
  BalanceAccumulation(..),
 | 
						|
  AccountListMode(..),
 | 
						|
  ValuationType(..),
 | 
						|
  Layout(..),
 | 
						|
  defreportopts,
 | 
						|
  rawOptsToReportOpts,
 | 
						|
  defreportspec,
 | 
						|
  setDefaultConversionOp,
 | 
						|
  reportOptsToSpec,
 | 
						|
  updateReportSpec,
 | 
						|
  updateReportSpecWith,
 | 
						|
  rawOptsToReportSpec,
 | 
						|
  balanceAccumulationOverride,
 | 
						|
  flat_,
 | 
						|
  tree_,
 | 
						|
  reportOptsToggleStatus,
 | 
						|
  simplifyStatuses,
 | 
						|
  whichDate,
 | 
						|
  journalValueAndFilterPostings,
 | 
						|
  journalValueAndFilterPostingsWith,
 | 
						|
  journalApplyValuationFromOpts,
 | 
						|
  journalApplyValuationFromOptsWith,
 | 
						|
  mixedAmountApplyValuationAfterSumFromOptsWith,
 | 
						|
  valuationAfterSum,
 | 
						|
  intervalFromRawOpts,
 | 
						|
  queryFromFlags,
 | 
						|
  transactionDateFn,
 | 
						|
  postingDateFn,
 | 
						|
  reportSpan,
 | 
						|
  reportSpanBothDates,
 | 
						|
  reportStartDate,
 | 
						|
  reportEndDate,
 | 
						|
  reportPeriodStart,
 | 
						|
  reportPeriodOrJournalStart,
 | 
						|
  reportPeriodLastDay,
 | 
						|
  reportPeriodOrJournalLastDay,
 | 
						|
  reportPeriodName
 | 
						|
)
 | 
						|
where
 | 
						|
 | 
						|
import Control.Applicative (Const(..), (<|>), liftA2)
 | 
						|
import Control.Monad ((<=<), guard, join)
 | 
						|
import Data.Char (toLower)
 | 
						|
import Data.Either (fromRight)
 | 
						|
import Data.Either.Extra (eitherToMaybe)
 | 
						|
import Data.Functor.Identity (Identity(..))
 | 
						|
import Data.List.Extra (find, isPrefixOf, nubSort)
 | 
						|
import Data.Maybe (fromMaybe, isJust)
 | 
						|
import qualified Data.Text as T
 | 
						|
import Data.Time.Calendar (Day, addDays)
 | 
						|
import Data.Default (Default(..))
 | 
						|
import Safe (headMay, lastDef, lastMay, maximumMay, readMay)
 | 
						|
 | 
						|
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.
 | 
						|
  | CalcGain        -- ^ Change from previous period's gain, i.e. valuation minus cost basis.
 | 
						|
  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
 | 
						|
 | 
						|
data Layout = LayoutWide (Maybe Int)
 | 
						|
            | LayoutTall
 | 
						|
            | LayoutBare
 | 
						|
            | LayoutTidy
 | 
						|
  deriving (Eq, Show)
 | 
						|
 | 
						|
-- | 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
 | 
						|
    ,conversionop_     :: Maybe ConversionOp  -- ^ Which operation should we apply to conversion transactions?
 | 
						|
    ,value_            :: Maybe ValuationType  -- ^ What value should amounts be converted to ?
 | 
						|
    ,infer_prices_     :: Bool      -- ^ Infer market prices from transactions ?
 | 
						|
    ,depth_            :: Maybe Int
 | 
						|
    ,date2_            :: Bool
 | 
						|
    ,empty_            :: Bool
 | 
						|
    ,no_elide_         :: Bool
 | 
						|
    ,real_             :: Bool
 | 
						|
    ,format_           :: StringFormat
 | 
						|
    ,pretty_           :: Bool
 | 
						|
    ,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
 | 
						|
    ,declared_         :: Bool  -- ^ Include accounts declared but not yet posted to ?
 | 
						|
    ,row_total_        :: Bool
 | 
						|
    ,no_total_         :: Bool
 | 
						|
    ,show_costs_       :: Bool  -- ^ Show costs for reports which normally don't show them ?
 | 
						|
    ,sort_amount_      :: Bool
 | 
						|
    ,percent_          :: Bool
 | 
						|
    ,invert_           :: Bool  -- ^ 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
 | 
						|
    ,layout_           :: Layout
 | 
						|
 } deriving (Show)
 | 
						|
 | 
						|
instance Default ReportOpts where def = defreportopts
 | 
						|
 | 
						|
defreportopts :: ReportOpts
 | 
						|
defreportopts = ReportOpts
 | 
						|
    { period_           = PeriodAll
 | 
						|
    , interval_         = NoInterval
 | 
						|
    , statuses_         = []
 | 
						|
    , conversionop_     = Nothing
 | 
						|
    , value_            = Nothing
 | 
						|
    , infer_prices_     = False
 | 
						|
    , depth_            = Nothing
 | 
						|
    , date2_            = False
 | 
						|
    , empty_            = False
 | 
						|
    , no_elide_         = False
 | 
						|
    , real_             = False
 | 
						|
    , format_           = def
 | 
						|
    , pretty_           = False
 | 
						|
    , querystring_      = []
 | 
						|
    , average_          = False
 | 
						|
    , related_          = False
 | 
						|
    , txn_dates_        = False
 | 
						|
    , balancecalc_      = def
 | 
						|
    , balanceaccum_     = def
 | 
						|
    , budgetpat_        = Nothing
 | 
						|
    , accountlistmode_  = ALFlat
 | 
						|
    , drop_             = 0
 | 
						|
    , declared_         = False
 | 
						|
    , row_total_        = False
 | 
						|
    , no_total_         = False
 | 
						|
    , show_costs_       = False
 | 
						|
    , sort_amount_      = False
 | 
						|
    , percent_          = False
 | 
						|
    , invert_           = False
 | 
						|
    , normalbalance_    = Nothing
 | 
						|
    , color_            = False
 | 
						|
    , transpose_        = False
 | 
						|
    , layout_           = LayoutWide Nothing
 | 
						|
    }
 | 
						|
 | 
						|
-- | 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.
 | 
						|
-- - an invalid --pretty argument,
 | 
						|
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
 | 
						|
        pretty = fromMaybe False $ alwaysneveropt "pretty" 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
 | 
						|
          ,conversionop_     = conversionOpFromRawOpts rawopts
 | 
						|
          ,value_            = valuationTypeFromRawOpts rawopts
 | 
						|
          ,infer_prices_     = boolopt "infer-market-prices" 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
 | 
						|
          ,declared_         = boolopt "declared" 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_           = pretty
 | 
						|
          ,color_            = useColorOnStdout -- a lower-level helper
 | 
						|
          ,transpose_        = boolopt "transpose" rawopts
 | 
						|
          ,layout_           = layoutopt 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  = []
 | 
						|
    }
 | 
						|
 | 
						|
-- | Set the default ConversionOp.
 | 
						|
setDefaultConversionOp :: ConversionOp -> ReportSpec -> ReportSpec
 | 
						|
setDefaultConversionOp def rspec@ReportSpec{_rsReportOpts=ropts} =
 | 
						|
    rspec{_rsReportOpts=ropts{conversionop_=conversionop_ ropts <|> Just def}}
 | 
						|
 | 
						|
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
 | 
						|
      "gain"        -> Just CalcGain
 | 
						|
      "budget"      -> Just CalcBudget
 | 
						|
      _             -> Nothing
 | 
						|
 | 
						|
balanceaccumopt :: RawOpts -> BalanceAccumulation
 | 
						|
balanceaccumopt = fromMaybe PerPeriod . balanceAccumulationOverride
 | 
						|
 | 
						|
alwaysneveropt :: String -> RawOpts -> Maybe Bool
 | 
						|
alwaysneveropt opt rawopts = case maybestringopt opt rawopts of
 | 
						|
    Just "always" -> Just True
 | 
						|
    Just "yes"    -> Just True
 | 
						|
    Just "y"      -> Just True
 | 
						|
    Just "never"  -> Just False
 | 
						|
    Just "no"     -> Just False
 | 
						|
    Just "n"      -> Just False
 | 
						|
    Just _        -> usageError "--pretty's argument should be \"yes\" or \"no\" (or y, n, always, never)"
 | 
						|
    _             -> Nothing
 | 
						|
 | 
						|
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
 | 
						|
 | 
						|
layoutopt :: RawOpts -> Layout
 | 
						|
layoutopt rawopts = fromMaybe (LayoutWide Nothing) $ layout <|> column
 | 
						|
  where
 | 
						|
    layout = parse <$> maybestringopt "layout" rawopts
 | 
						|
    column = LayoutBare <$ guard (boolopt "commodity-column" rawopts)
 | 
						|
 | 
						|
    parse opt = maybe err snd $ guard (not $ null s) *> find (isPrefixOf s . fst) checkNames
 | 
						|
      where
 | 
						|
        checkNames = [ ("wide", LayoutWide w)
 | 
						|
                     , ("tall", LayoutTall)
 | 
						|
                     , ("bare", LayoutBare)
 | 
						|
                     , ("tidy", LayoutTidy)
 | 
						|
                     ]
 | 
						|
        -- For `--layout=elided,n`, elide to the given width
 | 
						|
        (s,n) = break (==',') $ map toLower opt
 | 
						|
        w = case drop 1 n of
 | 
						|
              "" -> Nothing
 | 
						|
              c | Just w <- readMay c -> Just w
 | 
						|
              _ -> usageError "width in --layout=wide,WIDTH must be an integer"
 | 
						|
 | 
						|
        err = usageError "--layout's argument should be \"wide[,WIDTH]\", \"tall\", \"bare\", or \"tidy\""
 | 
						|
 | 
						|
-- 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 to be performed, if any, specified by -V,
 | 
						|
-- -X/--exchange, or --value flags. 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 -> Maybe ValuationType
 | 
						|
valuationTypeFromRawOpts rawopts = case (balancecalcopt rawopts, directval) of
 | 
						|
    (CalcValueChange, Nothing       ) -> Just $ AtEnd Nothing  -- If no valuation requested for valuechange, use AtEnd
 | 
						|
    (CalcValueChange, Just (AtEnd _)) -> directval             -- If AtEnd valuation requested, use it
 | 
						|
    (CalcValueChange, _             ) -> usageError "--valuechange only produces sensible results with --value=end"
 | 
						|
    (CalcGain,        Nothing       ) -> Just $ AtEnd Nothing  -- If no valuation requested for gain, use AtEnd
 | 
						|
    (_,               _             ) -> directval             -- Otherwise, use requested valuation
 | 
						|
  where
 | 
						|
    directval = lastMay $ collectopts valuationfromrawopt rawopts
 | 
						|
    valuationfromrawopt (n,v)  -- option name, value
 | 
						|
      | n == "V"     = Just $ AtEnd Nothing
 | 
						|
      | n == "X"     = Just $ AtEnd (Just $ T.pack v)
 | 
						|
      | n == "value" = valueopt v
 | 
						|
      | otherwise    = Nothing
 | 
						|
    valueopt v
 | 
						|
      | t `elem` ["cost","c"]  = AtEnd . Just <$> mc  -- keep supporting --value=cost,COMM for now
 | 
						|
      | t `elem` ["then" ,"t"] = Just $ AtThen mc
 | 
						|
      | t `elem` ["end" ,"e"]  = Just $ AtEnd  mc
 | 
						|
      | t `elem` ["now" ,"n"]  = Just $ AtNow  mc
 | 
						|
      | otherwise = case parsedateM t of
 | 
						|
            Just d  -> 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
 | 
						|
 | 
						|
-- | Parse the type of costing to be performed, if any, specified by -B/--cost
 | 
						|
-- or --value flags. If there's more than one costing type, the rightmost flag
 | 
						|
-- wins. This will fail with a usage error if an invalid argument is passed to
 | 
						|
-- --cost or if a costing type is requested with --gain.
 | 
						|
conversionOpFromRawOpts :: RawOpts -> Maybe ConversionOp
 | 
						|
conversionOpFromRawOpts rawopts
 | 
						|
    | isJust costFlag && balancecalcopt rawopts == CalcGain = usageError "--gain cannot be combined with --cost"
 | 
						|
    | otherwise = costFlag
 | 
						|
  where
 | 
						|
    costFlag = lastMay $ collectopts conversionopfromrawopt rawopts
 | 
						|
    conversionopfromrawopt (n,v)  -- option name, value
 | 
						|
      | n == "B"                                    = Just ToCost
 | 
						|
      | n == "value", takeWhile (/=',') v `elem` ["cost", "c"] = Just ToCost  -- keep supporting --value=cost for now
 | 
						|
      | otherwise                                   = Nothing
 | 
						|
 | 
						|
-- | 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.
 | 
						|
whichDate :: ReportOpts -> WhichDate
 | 
						|
whichDate 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 a 'Journal''s amounts to cost and/or to value (see
 | 
						|
-- 'journalApplyValuationFromOpts'), and filter by the 'ReportSpec' 'Query'.
 | 
						|
--
 | 
						|
-- We make sure to first filter by amt: and cur: terms, then value the
 | 
						|
-- 'Journal', then filter by the remaining terms.
 | 
						|
journalValueAndFilterPostings :: ReportSpec -> Journal -> Journal
 | 
						|
journalValueAndFilterPostings rspec j = journalValueAndFilterPostingsWith rspec j priceoracle
 | 
						|
  where priceoracle = journalPriceOracle (infer_prices_ $ _rsReportOpts rspec) j
 | 
						|
 | 
						|
-- | Like 'journalValueAndFilterPostings', but takes a 'PriceOracle' as an argument.
 | 
						|
journalValueAndFilterPostingsWith :: ReportSpec -> Journal -> PriceOracle -> Journal
 | 
						|
journalValueAndFilterPostingsWith rspec@ReportSpec{_rsQuery=q, _rsReportOpts=ropts} j =
 | 
						|
    -- Filter by the remainder of the query
 | 
						|
      filterJournal reportq
 | 
						|
    -- Apply valuation and costing
 | 
						|
    . journalApplyValuationFromOptsWith rspec
 | 
						|
    -- Filter by amount and currency, so it matches pre-valuation/costing
 | 
						|
      (if queryIsNull amtsymq then j else filterJournalAmounts amtsymq j)
 | 
						|
  where
 | 
						|
    -- with -r, replace each posting with its sibling postings
 | 
						|
    filterJournal = if related_ ropts then filterJournalRelatedPostings else filterJournalPostings
 | 
						|
    amtsymq = dbg3 "amtsymq" $ filterQuery queryIsAmtOrSym q
 | 
						|
    reportq = dbg3 "reportq" $ filterQuery (not . queryIsAmtOrSym) q
 | 
						|
    queryIsAmtOrSym = liftA2 (||) queryIsAmt queryIsSym
 | 
						|
 | 
						|
-- | 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_prices_ $ _rsReportOpts rspec) j
 | 
						|
 | 
						|
-- | Like journalApplyValuationFromOpts, but takes PriceOracle as an argument.
 | 
						|
journalApplyValuationFromOptsWith :: ReportSpec -> Journal -> PriceOracle -> Journal
 | 
						|
journalApplyValuationFromOptsWith rspec@ReportSpec{_rsReportOpts=ropts} j priceoracle =
 | 
						|
    case balancecalc_ ropts of
 | 
						|
      CalcGain -> journalMapPostings (\p -> postingTransformAmount (gain p) p) j
 | 
						|
      _        -> journalMapPostings (\p -> postingTransformAmount (valuation p) p) $ costing j
 | 
						|
  where
 | 
						|
    valuation p = maybe id (mixedAmountApplyValuation priceoracle styles (periodEnd p) (_rsDay rspec) (postingDate p)) (value_ ropts)
 | 
						|
    gain      p = maybe id (mixedAmountApplyGain      priceoracle styles (periodEnd p) (_rsDay rspec) (postingDate p)) (value_ ropts)
 | 
						|
    costing     = journalToCost (fromMaybe NoConversionOp $ conversionop_ ropts)
 | 
						|
 | 
						|
    -- Find the end of the period containing this posting
 | 
						|
    periodEnd  = addDays (-1) . fromMaybe err . mPeriodEnd . postingDateOrDate2 (whichDate ropts)
 | 
						|
    mPeriodEnd = case interval_ ropts of
 | 
						|
        NoInterval -> const . spanEnd . fst $ reportSpan j rspec
 | 
						|
        _          -> spanEnd <=< latestSpanContaining (historical : spans)
 | 
						|
    historical = DateSpan Nothing $ spanStart =<< headMay spans
 | 
						|
    spans = snd $ 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 -> case balancecalc_ ropts of
 | 
						|
            CalcGain -> gain mc
 | 
						|
            _        -> \span -> valuation mc span . costing
 | 
						|
        Nothing      -> const id
 | 
						|
  where
 | 
						|
    valuation mc span = mixedAmountValueAtDate priceoracle styles mc (maybe err (addDays (-1)) $ spanEnd span)
 | 
						|
    gain mc span = mixedAmountGainAtDate priceoracle styles mc (maybe err (addDays (-1)) $ spanEnd span)
 | 
						|
    costing = case fromMaybe NoConversionOp $ conversionop_ ropts of
 | 
						|
        NoConversionOp -> id
 | 
						|
        ToCost         -> styleMixedAmount styles . mixedAmountCost
 | 
						|
    styles = journalCommodityStyles j
 | 
						|
    err = error "mixedAmountApplyValuationAfterSumFromOptsWith: expected all spans to have an end date"
 | 
						|
 | 
						|
-- | If the ReportOpts specify that we are performing valuation after summing amounts,
 | 
						|
-- return Just of the commodity symbol we're converting to, Just Nothing for the default,
 | 
						|
-- and 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
 | 
						|
                     || balancecalc_  ropts == CalcGain
 | 
						|
                     || 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.
 | 
						|
-- Also return the intervals if they are requested.
 | 
						|
reportSpan :: Journal -> ReportSpec -> (DateSpan, [DateSpan])
 | 
						|
reportSpan = reportSpanHelper False
 | 
						|
 | 
						|
-- | Like reportSpan, but uses both primary and secondary dates when calculating
 | 
						|
-- the span.
 | 
						|
reportSpanBothDates :: Journal -> ReportSpec -> (DateSpan, [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, [DateSpan])
 | 
						|
reportSpanHelper bothdates j ReportSpec{_rsQuery=query, _rsReportOpts=ropts} =
 | 
						|
    (reportspan, intervalspans)
 | 
						|
  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 . fst . reportSpan j
 | 
						|
 | 
						|
reportEndDate :: Journal -> ReportSpec -> Maybe Day
 | 
						|
reportEndDate j = spanEnd . fst . 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 $ fromRight a i
 | 
						|
 | 
						|
instance Reportable Maybe e where
 | 
						|
    report _ = (eitherToMaybe =<<)
 | 
						|
 | 
						|
instance (e ~ a) => Reportable (Either a) e where
 | 
						|
    report _ = join
 | 
						|
 | 
						|
-- | Apply a function over a lens, but report on failure.
 | 
						|
overEither :: ((a -> Either e b) -> s -> Either e t) -> (a -> b) -> s -> Either e t
 | 
						|
overEither l f = l (pure . f)
 | 
						|
 | 
						|
-- | Set a field using a lens, but report on failure.
 | 
						|
setEither :: ((a -> Either e b) -> s -> Either e t) -> b -> s -> Either e t
 | 
						|
setEither l = overEither l . const
 | 
						|
 | 
						|
type ReportableLens' s a = forall f. Reportable f String => (a -> f a) -> s -> f s
 | 
						|
 | 
						|
-- | Lenses for ReportOpts.
 | 
						|
 | 
						|
-- Implement HasReportOptsNoUpdate, the basic lenses for ReportOpts.
 | 
						|
makeHledgerClassyLenses ''ReportOpts
 | 
						|
makeHledgerClassyLenses ''ReportSpec
 | 
						|
 | 
						|
-- | Special lenses for ReportOpts which also update the Query and QueryOpts in ReportSpec.
 | 
						|
-- Note that 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.
 | 
						|
--
 | 
						|
-- Note that setEither/overEither should only be necessary with
 | 
						|
-- querystring and reportOpts: the other lenses should never fail.
 | 
						|
--
 | 
						|
-- === Examples:
 | 
						|
-- >>> import Lens.Micro (set)
 | 
						|
-- >>> _rsQuery <$> setEither querystring ["assets"] defreportspec
 | 
						|
-- Right (Acct (RegexpCI "assets"))
 | 
						|
-- >>> _rsQuery <$> setEither querystring ["(assets"] defreportspec
 | 
						|
-- Left "this regular expression could not be compiled: (assets"
 | 
						|
-- >>> _rsQuery $ set querystring ["assets"] defreportspec
 | 
						|
-- Acct (RegexpCI "assets")
 | 
						|
-- >>> _rsQuery $ set querystring ["(assets"] defreportspec
 | 
						|
-- *** Exception: Updating ReportSpec failed: try using overEither instead of over or setEither instead of set
 | 
						|
-- >>> _rsQuery $ set period (MonthPeriod 2021 08) defreportspec
 | 
						|
-- Date DateSpan 2021-08
 | 
						|
class HasReportOptsNoUpdate a => HasReportOpts a where
 | 
						|
    reportOpts :: ReportableLens' a ReportOpts
 | 
						|
    reportOpts = reportOptsNoUpdate
 | 
						|
    {-# INLINE reportOpts #-}
 | 
						|
 | 
						|
    period :: ReportableLens' a Period
 | 
						|
    period = reportOpts.periodNoUpdate
 | 
						|
    {-# INLINE period #-}
 | 
						|
 | 
						|
    statuses :: ReportableLens' a [Status]
 | 
						|
    statuses = reportOpts.statusesNoUpdate
 | 
						|
    {-# INLINE statuses #-}
 | 
						|
 | 
						|
    depth :: ReportableLens' a (Maybe Int)
 | 
						|
    depth = reportOpts.depthNoUpdate
 | 
						|
    {-# INLINE depth #-}
 | 
						|
 | 
						|
    date2 :: ReportableLens' a Bool
 | 
						|
    date2 = reportOpts.date2NoUpdate
 | 
						|
    {-# INLINE date2 #-}
 | 
						|
 | 
						|
    real :: ReportableLens' a Bool
 | 
						|
    real = reportOpts.realNoUpdate
 | 
						|
    {-# INLINE real #-}
 | 
						|
 | 
						|
    querystring :: ReportableLens' a [T.Text]
 | 
						|
    querystring = reportOpts.querystringNoUpdate
 | 
						|
    {-# INLINE querystring #-}
 | 
						|
 | 
						|
instance HasReportOpts ReportOpts
 | 
						|
 | 
						|
instance HasReportOptsNoUpdate ReportSpec where
 | 
						|
    reportOptsNoUpdate = rsReportOpts
 | 
						|
 | 
						|
instance HasReportOpts ReportSpec where
 | 
						|
    reportOpts f rspec = report (error' "Updating ReportSpec failed: try using overEither instead of over or setEither instead of set") $  -- PARTIAL:
 | 
						|
      reportOptsToSpec (_rsDay rspec) <$> f (_rsReportOpts rspec)
 | 
						|
    {-# INLINE reportOpts #-}
 | 
						|
 | 
						|
-- | 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 = setEither reportOpts
 | 
						|
 | 
						|
-- | Like updateReportSpec, but takes a ReportOpts-modifying function.
 | 
						|
updateReportSpecWith :: (ReportOpts -> ReportOpts) -> ReportSpec -> Either String ReportSpec
 | 
						|
updateReportSpecWith = overEither reportOpts
 | 
						|
 | 
						|
-- | Generate a ReportSpec from RawOpts and a provided day, or return an error
 | 
						|
-- string if there are regular expression errors.
 | 
						|
rawOptsToReportSpec :: Day -> RawOpts -> Either String ReportSpec
 | 
						|
rawOptsToReportSpec day = reportOptsToSpec day . rawOptsToReportOpts day
 |