{-# LANGUAGE OverloadedStrings, RecordWildCards, DeriveDataTypeable #-}
module Hledger.Reports.ReportOptions (
ReportOpts(..),
BalanceType(..),
AccountListMode(..),
FormatStr,
defreportopts,
rawOptsToReportOpts,
checkReportOpts,
flat_,
tree_,
reportOptsToggleStatus,
simplifyStatuses,
whichDateFromOpts,
journalSelectingAmountFromOpts,
intervalFromRawOpts,
queryFromOpts,
queryFromOptsOnly,
queryOptsFromOpts,
transactionDateFn,
postingDateFn,
reportSpan,
reportStartDate,
reportEndDate,
specifiedStartEndDates,
specifiedStartDate,
specifiedEndDate,
tests_ReportOptions
)
where
import Control.Applicative ((<|>))
import Data.Data (Data)
import Data.List
import Data.Maybe
import qualified Data.Text as T
import Data.Typeable (Typeable)
import Data.Time.Calendar
import Data.Default
import Safe
import System.Console.ANSI (hSupportsANSI)
import System.IO (stdout)
import Text.Megaparsec.Custom
import Hledger.Data
import Hledger.Query
import Hledger.Utils
type FormatStr = String
data BalanceType = PeriodChange
| CumulativeChange
| HistoricalBalance
deriving (Eq,Show,Data,Typeable)
instance Default BalanceType where def = PeriodChange
data AccountListMode = ALDefault | ALTree | ALFlat deriving (Eq, Show, Data, Typeable)
instance Default AccountListMode where def = ALDefault
data ReportOpts = ReportOpts {
period_ :: Period
,interval_ :: Interval
,statuses_ :: [Status]
,cost_ :: Bool
,depth_ :: Maybe Int
,display_ :: Maybe DisplayExp
,date2_ :: Bool
,empty_ :: Bool
,no_elide_ :: Bool
,real_ :: Bool
,format_ :: Maybe FormatStr
,query_ :: String
,average_ :: Bool
,related_ :: Bool
,balancetype_ :: BalanceType
,accountlistmode_ :: AccountListMode
,drop_ :: Int
,row_total_ :: Bool
,no_total_ :: Bool
,value_ :: Bool
,pretty_tables_ :: Bool
,sort_amount_ :: Bool
,invert_ :: Bool
,normalbalance_ :: Maybe NormalSign
,color_ :: Bool
,forecast_ :: Bool
} deriving (Show, Data, Typeable)
instance Default ReportOpts where def = defreportopts
defreportopts :: ReportOpts
defreportopts = ReportOpts
def
def
def
def
def
def
def
def
def
def
def
def
def
def
def
def
def
def
def
def
def
def
def
def
def
def
rawOptsToReportOpts :: RawOpts -> IO ReportOpts
rawOptsToReportOpts rawopts = checkReportOpts <$> do
let rawopts' = checkRawOpts rawopts
d <- getCurrentDay
color <- hSupportsANSI stdout
return defreportopts{
period_ = periodFromRawOpts d rawopts'
,interval_ = intervalFromRawOpts rawopts'
,statuses_ = statusesFromRawOpts rawopts'
,cost_ = boolopt "cost" rawopts'
,depth_ = maybeintopt "depth" rawopts'
,display_ = maybedisplayopt d rawopts'
,date2_ = boolopt "date2" rawopts'
,empty_ = boolopt "empty" rawopts'
,no_elide_ = boolopt "no-elide" rawopts'
,real_ = boolopt "real" rawopts'
,format_ = maybestringopt "format" rawopts'
,query_ = unwords $ listofstringopt "args" rawopts'
,average_ = boolopt "average" rawopts'
,related_ = boolopt "related" rawopts'
,balancetype_ = balancetypeopt rawopts'
,accountlistmode_ = accountlistmodeopt rawopts'
,drop_ = intopt "drop" rawopts'
,row_total_ = boolopt "row-total" rawopts'
,no_total_ = boolopt "no-total" rawopts'
,value_ = boolopt "value" rawopts'
,sort_amount_ = boolopt "sort-amount" rawopts'
,invert_ = boolopt "invert" rawopts'
,pretty_tables_ = boolopt "pretty-tables" rawopts'
,color_ = color
,forecast_ = boolopt "forecast" rawopts'
}
checkRawOpts :: RawOpts -> RawOpts
checkRawOpts rawopts
| otherwise = rawopts
checkReportOpts :: ReportOpts -> ReportOpts
checkReportOpts ropts@ReportOpts{..} =
either usageError (const ropts) $ do
case depth_ of
Just d | d < 0 -> Left "--depth should have a positive number"
_ -> Right ()
accountlistmodeopt :: RawOpts -> AccountListMode
accountlistmodeopt rawopts =
case reverse $ filter (`elem` ["tree","flat"]) $ map fst rawopts of
("tree":_) -> ALTree
("flat":_) -> ALFlat
_ -> ALDefault
balancetypeopt :: RawOpts -> BalanceType
balancetypeopt rawopts =
case reverse $ filter (`elem` ["change","cumulative","historical"]) $ map fst rawopts of
("historical":_) -> HistoricalBalance
("cumulative":_) -> CumulativeChange
_ -> PeriodChange
periodFromRawOpts :: Day -> RawOpts -> Period
periodFromRawOpts d rawopts =
case (mearliestb, mlateste) of
(Nothing, Nothing) -> PeriodAll
(Just b, Nothing) -> PeriodFrom b
(Nothing, Just e) -> PeriodTo e
(Just b, Just e) -> simplifyPeriod $
PeriodBetween b e
where
mearliestb = case beginDatesFromRawOpts d rawopts of
[] -> Nothing
bs -> Just $ minimum bs
mlateste = case endDatesFromRawOpts d rawopts of
[] -> Nothing
es -> Just $ maximum es
beginDatesFromRawOpts :: Day -> RawOpts -> [Day]
beginDatesFromRawOpts d = catMaybes . map (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
endDatesFromRawOpts :: Day -> RawOpts -> [Day]
endDatesFromRawOpts d = catMaybes . map (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
intervalFromRawOpts :: RawOpts -> Interval
intervalFromRawOpts = lastDef NoInterval . catMaybes . map intervalfromrawopt
where
intervalfromrawopt (n,v)
| n == "period" =
either (\e -> usageError $ "could not parse period option: "++customErrorBundlePretty e) (Just . fst) $
parsePeriodExpr nulldate (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
statusesFromRawOpts :: RawOpts -> [Status]
statusesFromRawOpts = simplifyStatuses . catMaybes . map statusfromrawopt
where
statusfromrawopt (n,_)
| n == "unmarked" = Just Unmarked
| n == "pending" = Just Pending
| n == "cleared" = Just Cleared
| otherwise = Nothing
simplifyStatuses l
| length l' >= numstatuses = []
| otherwise = l'
where
l' = nub $ sort l
numstatuses = length [minBound .. maxBound :: Status]
reportOptsToggleStatus s ropts@ReportOpts{statuses_=ss}
| s `elem` ss = ropts{statuses_=filter (/= s) ss}
| otherwise = ropts{statuses_=simplifyStatuses (s:ss)}
type DisplayExp = String
maybedisplayopt :: Day -> RawOpts -> Maybe DisplayExp
maybedisplayopt d rawopts =
maybe Nothing (Just . regexReplaceBy "\\[.+?\\]" fixbracketeddatestr) $ maybestringopt "display" rawopts
where
fixbracketeddatestr "" = ""
fixbracketeddatestr s = "[" ++ fixSmartDateStr d (T.pack $ init $ tail s) ++ "]"
transactionDateFn :: ReportOpts -> (Transaction -> Day)
transactionDateFn ReportOpts{..} = if date2_ then transactionDate2 else tdate
postingDateFn :: ReportOpts -> (Posting -> Day)
postingDateFn ReportOpts{..} = if date2_ then postingDate2 else postingDate
whichDateFromOpts :: ReportOpts -> WhichDate
whichDateFromOpts ReportOpts{..} = if date2_ then SecondaryDate else PrimaryDate
tree_ :: ReportOpts -> Bool
tree_ = (==ALTree) . accountlistmode_
flat_ :: ReportOpts -> Bool
flat_ = (==ALFlat) . accountlistmode_
journalSelectingAmountFromOpts :: ReportOpts -> Journal -> Journal
journalSelectingAmountFromOpts opts
| cost_ opts = journalConvertAmountsToCost
| otherwise = id
queryFromOpts :: Day -> ReportOpts -> Query
queryFromOpts d ReportOpts{..} = simplifyQuery $ And $ [flagsq, argsq]
where
flagsq = And $
[(if date2_ then Date2 else Date) $ periodAsDateSpan period_]
++ (if real_ then [Real True] else [])
++ (if empty_ then [Empty True] else [])
++ [Or $ map StatusQ statuses_]
++ (maybe [] ((:[]) . Depth) depth_)
argsq = fst $ parseQuery d (T.pack query_)
queryFromOptsOnly :: Day -> ReportOpts -> Query
queryFromOptsOnly _d ReportOpts{..} = simplifyQuery flagsq
where
flagsq = And $
[(if date2_ then Date2 else Date) $ periodAsDateSpan period_]
++ (if real_ then [Real True] else [])
++ (if empty_ then [Empty True] else [])
++ [Or $ map StatusQ statuses_]
++ (maybe [] ((:[]) . Depth) depth_)
queryOptsFromOpts :: Day -> ReportOpts -> [QueryOpt]
queryOptsFromOpts d ReportOpts{..} = flagsqopts ++ argsqopts
where
flagsqopts = []
argsqopts = snd $ parseQuery d (T.pack query_)
reportSpan :: Journal -> ReportOpts -> IO DateSpan
reportSpan j ropts = do
(mspecifiedstartdate, mspecifiedenddate) <-
dbg2 "specifieddates" <$> specifiedStartEndDates ropts
let
DateSpan mjournalstartdate mjournalenddate =
dbg2 "journalspan" $ journalDateSpan False j
mstartdate = mspecifiedstartdate <|> mjournalstartdate
menddate = mspecifiedenddate <|> mjournalenddate
return $ dbg1 "reportspan" $ DateSpan mstartdate menddate
reportStartDate :: Journal -> ReportOpts -> IO (Maybe Day)
reportStartDate j ropts = spanStart <$> reportSpan j ropts
reportEndDate :: Journal -> ReportOpts -> IO (Maybe Day)
reportEndDate j ropts = spanEnd <$> reportSpan j ropts
specifiedStartEndDates :: ReportOpts -> IO (Maybe Day, Maybe Day)
specifiedStartEndDates ropts = do
today <- getCurrentDay
let
q = queryFromOpts today ropts
mspecifiedstartdate = queryStartDate False q
mspecifiedenddate = queryEndDate False q
return (mspecifiedstartdate, mspecifiedenddate)
specifiedStartDate :: ReportOpts -> IO (Maybe Day)
specifiedStartDate ropts = fst <$> specifiedStartEndDates ropts
specifiedEndDate :: ReportOpts -> IO (Maybe Day)
specifiedEndDate ropts = snd <$> specifiedStartEndDates ropts
tests_ReportOptions = tests "ReportOptions" [
tests "queryFromOpts" [
(queryFromOpts nulldate defreportopts) `is` Any
,(queryFromOpts nulldate defreportopts{query_="a"}) `is` (Acct "a")
,(queryFromOpts nulldate defreportopts{query_="desc:'a a'"}) `is` (Desc "a a")
,(queryFromOpts nulldate defreportopts{period_=PeriodFrom (parsedate "2012/01/01"),query_="date:'to 2013'" })
`is` (Date $ mkdatespan "2012/01/01" "2013/01/01")
,(queryFromOpts nulldate defreportopts{query_="date2:'in 2012'"}) `is` (Date2 $ mkdatespan "2012/01/01" "2013/01/01")
,(queryFromOpts nulldate defreportopts{query_="'a a' 'b"}) `is` (Or [Acct "a a", Acct "'b"])
]
,tests "queryOptsFromOpts" [
(queryOptsFromOpts nulldate defreportopts) `is` []
,(queryOptsFromOpts nulldate defreportopts{query_="a"}) `is` []
,(queryOptsFromOpts nulldate defreportopts{period_=PeriodFrom (parsedate "2012/01/01")
,query_="date:'to 2013'"
})
`is` []
]
]