2011-06-05 22:36:32 +04:00
{- |
2012-05-27 22:14:20 +04:00
A general query system for matching things ( accounts , postings ,
2017-07-27 14:59:55 +03:00
transactions .. ) by various criteria , and a SimpleTextParser for query expressions .
2011-06-05 22:36:32 +04:00
- }
2019-12-29 02:52:32 +03:00
-- Silence safe 0.3.18's deprecation warnings for (max|min)imum(By)?Def for now
-- (may hide other deprecation warnings too). https://github.com/ndmitchell/safe/issues/26
{- # OPTIONS_GHC - Wno - warnings - deprecations # -}
2020-08-15 12:14:27 +03:00
{- # LANGUAGE CPP # -}
{- # LANGUAGE FlexibleContexts # -}
{- # LANGUAGE OverloadedStrings # -}
{- # LANGUAGE ViewPatterns # -}
lib: textification begins! account names
The first of several conversions from String to (strict) Text, hopefully
reducing space and time usage.
This one shows a small improvement, with GHC 7.10.3 and text-1.2.2.1:
hledger -f data/100x100x10.journal stats
string: <<ghc: 39471064 bytes, 77 GCs, 198421/275048 avg/max bytes residency (3 samples), 2M in use, 0.000 INIT (0.001 elapsed), 0.015 MUT (0.020 elapsed), 0.010 GC (0.014 elapsed) :ghc>>
text: <<ghc: 39268024 bytes, 77 GCs, 197018/270840 avg/max bytes residency (3 samples), 2M in use, 0.000 INIT (0.002 elapsed), 0.016 MUT (0.022 elapsed), 0.009 GC (0.011 elapsed) :ghc>>
hledger -f data/1000x100x10.journal stats
string: <<ghc: 318555920 bytes, 617 GCs, 2178997/7134472 avg/max bytes residency (7 samples), 16M in use, 0.000 INIT (0.001 elapsed), 0.129 MUT (0.136 elapsed), 0.067 GC (0.077 elapsed) :ghc>>
text: <<ghc: 314248496 bytes, 612 GCs, 2074045/6617960 avg/max bytes residency (7 samples), 16M in use, 0.000 INIT (0.003 elapsed), 0.137 MUT (0.145 elapsed), 0.067 GC (0.079 elapsed) :ghc>>
hledger -f data/10000x100x10.journal stats
string: <<ghc: 3114763608 bytes, 6026 GCs, 18858950/75552024 avg/max bytes residency (11 samples), 201M in use, 0.000 INIT (0.000 elapsed), 1.331 MUT (1.372 elapsed), 0.699 GC (0.812 elapsed) :ghc>>
text: <<ghc: 3071468920 bytes, 5968 GCs, 14120344/62951360 avg/max bytes residency (9 samples), 124M in use, 0.000 INIT (0.003 elapsed), 1.272 MUT (1.349 elapsed), 0.513 GC (0.578 elapsed) :ghc>>
hledger -f data/100000x100x10.journal stats
string: <<ghc: 31186579432 bytes, 60278 GCs, 135332581/740228992 avg/max bytes residency (13 samples), 1697M in use, 0.000 INIT (0.008 elapsed), 14.677 MUT (15.508 elapsed), 7.081 GC (8.074 elapsed) :ghc>>
text: <<ghc: 30753427672 bytes, 59763 GCs, 117595958/666457240 avg/max bytes residency (14 samples), 1588M in use, 0.000 INIT (0.008 elapsed), 13.713 MUT (13.966 elapsed), 6.220 GC (7.108 elapsed) :ghc>>
2016-05-24 04:16:21 +03:00
2012-05-16 11:57:10 +04:00
module Hledger.Query (
2012-05-16 11:50:22 +04:00
-- * Query and QueryOpt
2012-05-16 11:12:49 +04:00
Query ( .. ) ,
2012-05-16 11:50:22 +04:00
QueryOpt ( .. ) ,
2012-05-16 12:28:02 +04:00
-- * parsing
parseQuery ,
2012-05-17 20:02:22 +04:00
simplifyQuery ,
2012-05-27 22:14:20 +04:00
filterQuery ,
2012-05-16 12:28:02 +04:00
-- * accessors
2012-05-16 11:37:24 +04:00
queryIsNull ,
2014-02-28 05:47:47 +04:00
queryIsAcct ,
2018-07-15 10:37:13 +03:00
queryIsAmt ,
2012-05-27 22:14:20 +04:00
queryIsDepth ,
queryIsDate ,
2014-12-25 03:11:30 +03:00
queryIsDate2 ,
queryIsDateOrDate2 ,
2012-05-16 11:50:22 +04:00
queryIsStartDateOnly ,
2014-02-28 05:47:47 +04:00
queryIsSym ,
2016-06-01 20:48:57 +03:00
queryIsReal ,
2016-06-04 03:51:10 +03:00
queryIsStatus ,
2016-08-09 03:24:37 +03:00
queryIsEmpty ,
2012-05-27 22:14:20 +04:00
queryStartDate ,
2014-07-15 18:01:01 +04:00
queryEndDate ,
2012-05-27 22:14:20 +04:00
queryDateSpan ,
2014-12-25 03:11:30 +03:00
queryDateSpan' ,
2012-05-27 22:14:20 +04:00
queryDepth ,
2012-05-16 10:43:13 +04:00
inAccount ,
2012-05-16 11:37:24 +04:00
inAccountQuery ,
2012-05-16 11:50:22 +04:00
-- * matching
2012-05-27 22:14:20 +04:00
matchesTransaction ,
2014-02-28 05:47:47 +04:00
matchesPosting ,
matchesAccount ,
2014-04-06 06:33:44 +04:00
matchesMixedAmount ,
2014-02-28 05:47:47 +04:00
matchesAmount ,
2018-07-15 10:37:13 +03:00
matchesCommodity ,
2020-08-07 17:53:00 +03:00
matchesTags ,
2019-06-04 03:26:27 +03:00
matchesPriceDirective ,
2014-04-15 22:45:30 +04:00
words'' ,
2020-05-25 11:49:34 +03:00
prefixes ,
2012-05-16 11:50:22 +04:00
-- * tests
2018-09-06 23:08:26 +03:00
tests_Query
2012-05-16 10:43:13 +04:00
)
2011-06-05 22:36:32 +04:00
where
2018-09-04 19:59:48 +03:00
2020-08-15 12:14:27 +03:00
import Control.Applicative ( ( <|> ) , liftA2 , many , optional )
2020-08-31 07:56:38 +03:00
import Data.Either ( partitionEithers )
import Data.List ( partition )
import Data.Maybe ( fromMaybe , isJust , mapMaybe )
2018-03-25 01:51:56 +03:00
# if ! ( MIN_VERSION_base ( 4 , 11 , 0 ) )
2016-07-29 18:57:10 +03:00
import Data.Monoid ( ( <> ) )
2018-03-25 01:51:56 +03:00
# endif
lib: textification begins! account names
The first of several conversions from String to (strict) Text, hopefully
reducing space and time usage.
This one shows a small improvement, with GHC 7.10.3 and text-1.2.2.1:
hledger -f data/100x100x10.journal stats
string: <<ghc: 39471064 bytes, 77 GCs, 198421/275048 avg/max bytes residency (3 samples), 2M in use, 0.000 INIT (0.001 elapsed), 0.015 MUT (0.020 elapsed), 0.010 GC (0.014 elapsed) :ghc>>
text: <<ghc: 39268024 bytes, 77 GCs, 197018/270840 avg/max bytes residency (3 samples), 2M in use, 0.000 INIT (0.002 elapsed), 0.016 MUT (0.022 elapsed), 0.009 GC (0.011 elapsed) :ghc>>
hledger -f data/1000x100x10.journal stats
string: <<ghc: 318555920 bytes, 617 GCs, 2178997/7134472 avg/max bytes residency (7 samples), 16M in use, 0.000 INIT (0.001 elapsed), 0.129 MUT (0.136 elapsed), 0.067 GC (0.077 elapsed) :ghc>>
text: <<ghc: 314248496 bytes, 612 GCs, 2074045/6617960 avg/max bytes residency (7 samples), 16M in use, 0.000 INIT (0.003 elapsed), 0.137 MUT (0.145 elapsed), 0.067 GC (0.079 elapsed) :ghc>>
hledger -f data/10000x100x10.journal stats
string: <<ghc: 3114763608 bytes, 6026 GCs, 18858950/75552024 avg/max bytes residency (11 samples), 201M in use, 0.000 INIT (0.000 elapsed), 1.331 MUT (1.372 elapsed), 0.699 GC (0.812 elapsed) :ghc>>
text: <<ghc: 3071468920 bytes, 5968 GCs, 14120344/62951360 avg/max bytes residency (9 samples), 124M in use, 0.000 INIT (0.003 elapsed), 1.272 MUT (1.349 elapsed), 0.513 GC (0.578 elapsed) :ghc>>
hledger -f data/100000x100x10.journal stats
string: <<ghc: 31186579432 bytes, 60278 GCs, 135332581/740228992 avg/max bytes residency (13 samples), 1697M in use, 0.000 INIT (0.008 elapsed), 14.677 MUT (15.508 elapsed), 7.081 GC (8.074 elapsed) :ghc>>
text: <<ghc: 30753427672 bytes, 59763 GCs, 117595958/666457240 avg/max bytes residency (14 samples), 1588M in use, 0.000 INIT (0.008 elapsed), 13.713 MUT (13.966 elapsed), 6.220 GC (7.108 elapsed) :ghc>>
2016-05-24 04:16:21 +03:00
import qualified Data.Text as T
2020-08-31 07:56:38 +03:00
import Data.Time.Calendar ( Day , fromGregorian )
2020-08-05 03:39:48 +03:00
import Safe ( readDef , readMay , maximumByMay , maximumMay , minimumMay )
2020-08-15 12:14:27 +03:00
import Text.Megaparsec ( between , noneOf , sepBy )
2020-08-31 07:56:38 +03:00
import Text.Megaparsec.Char ( char , string )
2011-06-05 22:36:32 +04:00
2018-09-04 23:52:36 +03:00
import Hledger.Utils hiding ( words' )
2011-06-05 22:36:32 +04:00
import Hledger.Data.Types
2011-06-14 18:29:31 +04:00
import Hledger.Data.AccountName
2018-09-04 17:29:48 +03:00
import Hledger.Data.Amount ( nullamt , usd )
2011-06-05 22:36:32 +04:00
import Hledger.Data.Dates
2011-06-29 03:18:36 +04:00
import Hledger.Data.Posting
import Hledger.Data.Transaction
2011-06-05 22:36:32 +04:00
2012-05-16 11:50:22 +04:00
2012-05-16 11:37:24 +04:00
-- | A query is a composition of search criteria, which can be used to
-- match postings, transactions, accounts and more.
2012-05-16 11:12:49 +04:00
data Query = Any -- ^ always match
| None -- ^ never match
| Not Query -- ^ negate this match
| Or [ Query ] -- ^ match if any of these match
| And [ Query ] -- ^ match if all of these match
2015-05-22 02:24:20 +03:00
| Code Regexp -- ^ match if code matches this regexp
| Desc Regexp -- ^ match if description matches this regexp
| Acct Regexp -- ^ match postings whose account matches this regexp
2012-12-06 08:43:41 +04:00
| Date DateSpan -- ^ match if primary date in this date span
| Date2 DateSpan -- ^ match if secondary date in this date span
2017-06-16 02:54:34 +03:00
| StatusQ Status -- ^ match txns/postings with this status
2012-05-16 11:12:49 +04:00
| Real Bool -- ^ match if "realness" (involves a real non-virtual account ?) has this value
2014-03-21 06:10:48 +04:00
| Amt OrdPlus Quantity -- ^ match if the amount's numeric quantity is less than/greater than/equal to/unsignedly equal to some value
2015-05-22 02:24:20 +03:00
| Sym Regexp -- ^ match if the entire commodity symbol is matched by this regexp
2012-05-27 22:14:20 +04:00
| Empty Bool -- ^ if true, show zero-amount postings/accounts which are usually not shown
-- more of a query option than a query criteria ?
2015-08-28 18:04:54 +03:00
| Depth Int -- ^ match if account depth is less than or equal to this value.
-- Depth is sometimes used like a query (for filtering report data)
-- and sometimes like a query option (for controlling display)
2015-05-22 02:24:20 +03:00
| Tag Regexp ( Maybe Regexp ) -- ^ match if a tag's name, and optionally its value, is matched by these respective regexps
2012-05-28 04:27:55 +04:00
-- matching the regexp if provided, exists
2020-08-31 07:56:38 +03:00
deriving ( Eq , Show )
2013-09-23 22:50:20 +04:00
2020-08-15 12:14:27 +03:00
-- | Construct a payee tag
payeeTag :: Maybe String -> Either RegexError Query
payeeTag = liftA2 Tag ( toRegexCI_ " payee " ) . maybe ( pure Nothing ) ( fmap Just . toRegexCI_ )
-- | Construct a note tag
noteTag :: Maybe String -> Either RegexError Query
noteTag = liftA2 Tag ( toRegexCI_ " note " ) . maybe ( pure Nothing ) ( fmap Just . toRegexCI_ )
2018-09-04 17:29:48 +03:00
-- | A more expressive Ord, used for amt: queries. The Abs* variants
-- compare with the absolute value of a number, ignoring sign.
data OrdPlus = Lt | LtEq | Gt | GtEq | Eq | AbsLt | AbsLtEq | AbsGt | AbsGtEq | AbsEq
2020-08-31 07:56:38 +03:00
deriving ( Show , Eq )
2018-09-04 17:29:48 +03:00
2011-06-13 23:46:35 +04:00
-- | A query option changes a query's/report's behaviour and output in some way.
2011-07-01 04:32:09 +04:00
data QueryOpt = QueryOptInAcctOnly AccountName -- ^ show an account register focussed on this account
| QueryOptInAcct AccountName -- ^ as above but include sub-accounts in the account register
2011-06-13 23:46:35 +04:00
-- | QueryOptCostBasis -- ^ show amounts converted to cost where possible
2012-12-06 08:43:41 +04:00
-- | QueryOptDate2 -- ^ show secondary dates instead of primary dates
2020-08-31 07:56:38 +03:00
deriving ( Show , Eq )
2011-06-13 23:46:35 +04:00
2012-05-16 12:28:02 +04:00
-- parsing
2012-05-16 11:50:22 +04:00
2012-05-16 11:37:24 +04:00
-- -- | A query restricting the account(s) to be shown in the sidebar, if any.
2011-07-01 04:32:09 +04:00
-- -- Just looks at the first query option.
2012-05-16 11:12:49 +04:00
-- showAccountMatcher :: [QueryOpt] -> Maybe Query
-- showAccountMatcher (QueryOptInAcctSubsOnly a:_) = Just $ Acct True $ accountNameToAccountRegex a
2011-07-01 04:32:09 +04:00
-- showAccountMatcher _ = Nothing
2011-06-13 23:46:35 +04:00
2012-05-27 22:14:20 +04:00
2020-08-05 03:39:48 +03:00
-- | Convert a query expression containing zero or more
-- space-separated terms to a query and zero or more query options; or
-- return an error message if query parsing fails.
--
-- A query term is either:
2011-06-13 02:35:54 +04:00
--
2012-05-27 22:14:20 +04:00
-- 1. a search pattern, which matches on one or more fields, eg:
--
-- acct:REGEXP - match the account name with a regular expression
-- desc:REGEXP - match the transaction description
-- date:PERIODEXP - match the date with a period expression
--
-- The prefix indicates the field to match, or if there is no prefix
-- account name is assumed.
--
-- 2. a query option, which modifies the reporting behaviour in some
-- way. There is currently one of these, which may appear only once:
2011-06-13 02:35:54 +04:00
--
2012-05-27 22:14:20 +04:00
-- inacct:FULLACCTNAME
2011-06-13 02:35:54 +04:00
--
2012-05-27 22:14:20 +04:00
-- The usual shell quoting rules are assumed. When a pattern contains
-- whitespace, it (or the whole term including prefix) should be enclosed
-- in single or double quotes.
2011-06-13 02:35:54 +04:00
--
2012-05-27 22:14:20 +04:00
-- Period expressions may contain relative dates, so a reference date is
-- required to fully parse these.
--
-- Multiple terms are combined as follows:
-- 1. multiple account patterns are OR'd together
-- 2. multiple description patterns are OR'd together
2017-06-10 23:31:43 +03:00
-- 3. multiple status patterns are OR'd together
-- 4. then all terms are AND'd together
2020-02-03 21:10:32 +03:00
--
-- >>> parseQuery nulldate "expenses:dining out"
2020-08-18 04:32:15 +03:00
-- Right (Or [Acct (RegexpCI "expenses:dining"),Acct (RegexpCI "out")],[])
2020-08-05 23:41:13 +03:00
--
2020-02-03 21:10:32 +03:00
-- >>> parseQuery nulldate "\"expenses:dining out\""
2020-08-18 04:32:15 +03:00
-- Right (Acct (RegexpCI "expenses:dining out"),[])
2020-08-05 23:41:13 +03:00
parseQuery :: Day -> T . Text -> Either String ( Query , [ QueryOpt ] )
parseQuery d s = do
let termstrs = words'' prefixes s
eterms <- sequence $ map ( parseQueryTerm d ) termstrs
let ( pats , opts ) = partitionEithers eterms
( descpats , pats' ) = partition queryIsDesc pats
( acctpats , pats'' ) = partition queryIsAcct pats'
( statuspats , otherpats ) = partition queryIsStatus pats''
q = simplifyQuery $ And $ [ Or acctpats , Or descpats , Or statuspats ] ++ otherpats
Right ( q , opts )
2011-06-13 23:46:35 +04:00
2012-05-29 21:02:18 +04:00
-- XXX
2011-06-14 18:27:48 +04:00
-- | Quote-and-prefix-aware version of words - don't split on spaces which
-- are inside quotes, including quotes which may have one of the specified
-- prefixes in front, and maybe an additional not: prefix in front of that.
2016-07-29 18:57:10 +03:00
words'' :: [ T . Text ] -> T . Text -> [ T . Text ]
2011-06-14 18:27:48 +04:00
words'' prefixes = fromparse . parsewith maybeprefixedquotedphrases -- XXX
where
2017-07-27 14:59:55 +03:00
maybeprefixedquotedphrases :: SimpleTextParser [ T . Text ]
2020-07-20 18:09:46 +03:00
maybeprefixedquotedphrases = choice' [ prefixedQuotedPattern , singleQuotedPattern , doubleQuotedPattern , pattern ] ` sepBy ` skipNonNewlineSpaces1
2017-07-27 14:59:55 +03:00
prefixedQuotedPattern :: SimpleTextParser T . Text
2011-06-14 18:27:48 +04:00
prefixedQuotedPattern = do
2018-05-22 01:47:56 +03:00
not' <- fromMaybe " " ` fmap ` ( optional $ string " not: " )
2017-07-27 14:59:55 +03:00
let allowednexts | T . null not' = prefixes
| otherwise = prefixes ++ [ " " ]
2018-05-22 01:47:56 +03:00
next <- choice' $ map string allowednexts
2016-07-29 18:57:10 +03:00
let prefix :: T . Text
2017-07-27 14:59:55 +03:00
prefix = not' <> next
2014-04-15 22:45:30 +04:00
p <- singleQuotedPattern <|> doubleQuotedPattern
2016-07-29 18:57:10 +03:00
return $ prefix <> stripquotes p
2017-07-27 14:59:55 +03:00
singleQuotedPattern :: SimpleTextParser T . Text
2016-07-29 18:57:10 +03:00
singleQuotedPattern = between ( char '\ ' ' ) ( char '\ ' ' ) ( many $ noneOf ( " ' " :: [ Char ] ) ) >>= return . stripquotes . T . pack
2017-07-27 14:59:55 +03:00
doubleQuotedPattern :: SimpleTextParser T . Text
2016-07-29 18:57:10 +03:00
doubleQuotedPattern = between ( char '"' ) ( char '"' ) ( many $ noneOf ( " \ " " :: [ Char ] ) ) >>= return . stripquotes . T . pack
2017-07-27 14:59:55 +03:00
pattern :: SimpleTextParser T . Text
2016-07-29 18:57:10 +03:00
pattern = fmap T . pack $ many ( noneOf ( " \ n \ r " :: [ Char ] ) )
2011-06-14 18:27:48 +04:00
2012-05-29 21:02:18 +04:00
-- XXX
-- keep synced with patterns below, excluding "not"
2016-07-29 18:57:10 +03:00
prefixes :: [ T . Text ]
prefixes = map ( <> " : " ) [
2012-05-29 21:02:18 +04:00
" inacctonly "
, " inacct "
2013-09-10 02:04:43 +04:00
, " amt "
2013-03-22 21:59:16 +04:00
, " code "
2012-05-29 21:02:18 +04:00
, " desc "
2017-08-31 00:21:01 +03:00
, " payee "
, " note "
2012-05-29 21:02:18 +04:00
, " acct "
, " date "
2014-12-16 22:06:21 +03:00
, " date2 "
2012-05-29 21:02:18 +04:00
, " status "
2014-03-16 20:43:15 +04:00
, " cur "
2012-05-29 21:02:18 +04:00
, " real "
, " empty "
, " depth "
, " tag "
]
2016-07-29 18:57:10 +03:00
defaultprefix :: T . Text
2012-05-29 21:02:18 +04:00
defaultprefix = " acct "
2011-06-14 18:27:48 +04:00
-- -- | Parse the query string as a boolean tree of match patterns.
2012-05-16 11:12:49 +04:00
-- parseQueryTerm :: String -> Query
-- parseQueryTerm s = either (const (Any)) id $ runParser query () "" $ lexmatcher s
2011-06-14 18:27:48 +04:00
-- lexmatcher :: String -> [String]
-- lexmatcher s = words' s
2012-05-16 11:12:49 +04:00
-- query :: GenParser String () Query
2012-05-16 11:37:24 +04:00
-- query = undefined
2011-06-14 18:27:48 +04:00
2014-08-08 04:26:08 +04:00
-- | Parse a single query term as either a query or a query option,
2020-08-05 23:41:13 +03:00
-- or return an error message if parsing fails.
parseQueryTerm :: Day -> T . Text -> Either String ( Either Query QueryOpt )
parseQueryTerm _ ( T . stripPrefix " inacctonly: " -> Just s ) = Right $ Right $ QueryOptInAcctOnly s
parseQueryTerm _ ( T . stripPrefix " inacct: " -> Just s ) = Right $ Right $ QueryOptInAcct s
2016-07-29 18:57:10 +03:00
parseQueryTerm d ( T . stripPrefix " not: " -> Just s ) =
case parseQueryTerm d s of
2020-08-05 23:41:13 +03:00
Right ( Left m ) -> Right $ Left $ Not m
Right ( Right _ ) -> Right $ Left Any -- not:somequeryoption will be ignored
Left err -> Left err
2020-08-15 12:14:27 +03:00
parseQueryTerm _ ( T . stripPrefix " code: " -> Just s ) = Left . Code <$> toRegexCI_ ( T . unpack s )
parseQueryTerm _ ( T . stripPrefix " desc: " -> Just s ) = Left . Desc <$> toRegexCI_ ( T . unpack s )
parseQueryTerm _ ( T . stripPrefix " payee: " -> Just s ) = Left <$> payeeTag ( Just $ T . unpack s )
parseQueryTerm _ ( T . stripPrefix " note: " -> Just s ) = Left <$> noteTag ( Just $ T . unpack s )
parseQueryTerm _ ( T . stripPrefix " acct: " -> Just s ) = Left . Acct <$> toRegexCI_ ( T . unpack s )
2016-07-29 18:57:10 +03:00
parseQueryTerm d ( T . stripPrefix " date2: " -> Just s ) =
2020-08-05 23:41:13 +03:00
case parsePeriodExpr d s of Left e -> Left $ " \ " date2: " ++ T . unpack s ++ " \ " gave a " ++ showDateParseError e
Right ( _ , span ) -> Right $ Left $ Date2 span
2016-07-29 18:57:10 +03:00
parseQueryTerm d ( T . stripPrefix " date: " -> Just s ) =
2020-08-05 23:41:13 +03:00
case parsePeriodExpr d s of Left e -> Left $ " \ " date: " ++ T . unpack s ++ " \ " gave a " ++ showDateParseError e
Right ( _ , span ) -> Right $ Left $ Date span
2016-07-29 18:57:10 +03:00
parseQueryTerm _ ( T . stripPrefix " status: " -> Just s ) =
2020-08-05 23:41:13 +03:00
case parseStatus s of Left e -> Left $ " \ " status: " ++ T . unpack s ++ " \ " gave a parse error: " ++ e
Right st -> Right $ Left $ StatusQ st
parseQueryTerm _ ( T . stripPrefix " real: " -> Just s ) = Right $ Left $ Real $ parseBool s || T . null s
2020-08-06 02:05:56 +03:00
parseQueryTerm _ ( T . stripPrefix " amt: " -> Just s ) = Right $ Left $ Amt ord q where ( ord , q ) = either error id $ parseAmountQueryTerm s -- PARTIAL:
2020-08-05 23:41:13 +03:00
parseQueryTerm _ ( T . stripPrefix " empty: " -> Just s ) = Right $ Left $ Empty $ parseBool s
2016-07-29 18:57:10 +03:00
parseQueryTerm _ ( T . stripPrefix " depth: " -> Just s )
2020-08-05 23:41:13 +03:00
| n >= 0 = Right $ Left $ Depth n
| otherwise = Left " depth: should have a positive number "
2016-07-29 18:57:10 +03:00
where n = readDef 0 ( T . unpack s )
2015-08-28 19:57:30 +03:00
2020-08-15 12:14:27 +03:00
parseQueryTerm _ ( T . stripPrefix " cur: " -> Just s ) = Left . Sym <$> toRegexCI_ ( '^' : T . unpack s ++ " $ " ) -- support cur: as an alias
parseQueryTerm _ ( T . stripPrefix " tag: " -> Just s ) = Left <$> parseTag s
2020-08-05 23:41:13 +03:00
parseQueryTerm _ " " = Right $ Left $ Any
2016-07-29 18:57:10 +03:00
parseQueryTerm d s = parseQueryTerm d $ defaultprefix <> " : " <> s
2011-06-13 23:46:35 +04:00
2020-08-05 03:39:48 +03:00
-- | Parse the argument of an amt query term ([OP][SIGN]NUM), to an
-- OrdPlus and a Quantity, or if parsing fails, an error message. OP
-- can be <=, <, >=, >, or = . NUM can be a simple integer or decimal.
-- If a decimal, the decimal mark must be period, and it must have
-- digits preceding it. Digit group marks are not allowed.
parseAmountQueryTerm :: T . Text -> Either String ( OrdPlus , Quantity )
parseAmountQueryTerm amtarg =
case amtarg of
-- number has a + sign, do a signed comparison
( parse " <=+ " -> Just q ) -> Right ( LtEq , q )
( parse " <+ " -> Just q ) -> Right ( Lt , q )
( parse " >=+ " -> Just q ) -> Right ( GtEq , q )
( parse " >+ " -> Just q ) -> Right ( Gt , q )
( parse " =+ " -> Just q ) -> Right ( Eq , q )
( parse " + " -> Just q ) -> Right ( Eq , q )
-- number has a - sign, do a signed comparison
( parse " <- " -> Just q ) -> Right ( Lt , - q )
( parse " <=- " -> Just q ) -> Right ( LtEq , - q )
( parse " >- " -> Just q ) -> Right ( Gt , - q )
( parse " >=- " -> Just q ) -> Right ( GtEq , - q )
( parse " =- " -> Just q ) -> Right ( Eq , - q )
( parse " - " -> Just q ) -> Right ( Eq , - q )
-- number is unsigned and zero, do a signed comparison (more useful)
( parse " <= " -> Just 0 ) -> Right ( LtEq , 0 )
( parse " < " -> Just 0 ) -> Right ( Lt , 0 )
( parse " >= " -> Just 0 ) -> Right ( GtEq , 0 )
( parse " > " -> Just 0 ) -> Right ( Gt , 0 )
-- number is unsigned and non-zero, do an absolute magnitude comparison
( parse " <= " -> Just q ) -> Right ( AbsLtEq , q )
( parse " < " -> Just q ) -> Right ( AbsLt , q )
( parse " >= " -> Just q ) -> Right ( AbsGtEq , q )
( parse " > " -> Just q ) -> Right ( AbsGt , q )
( parse " = " -> Just q ) -> Right ( AbsEq , q )
( parse " " -> Just q ) -> Right ( AbsEq , q )
_ -> Left $
" could not parse as a comparison operator followed by an optionally-signed number: "
++ T . unpack amtarg
2013-09-09 22:57:25 +04:00
where
2020-08-05 03:39:48 +03:00
-- Strip outer whitespace from the text, require and remove the
-- specified prefix, remove all whitespace from the remainder, and
-- read it as a simple integer or decimal if possible.
parse :: T . Text -> T . Text -> Maybe Quantity
2020-08-05 04:01:31 +03:00
parse p s = ( T . stripPrefix p . T . strip ) s >>= readMay . filter ( not . ( == ' ' ) ) . T . unpack
2013-03-20 20:36:00 +04:00
2020-08-15 12:14:27 +03:00
parseTag :: T . Text -> Either RegexError Query
parseTag s = do
tag <- toRegexCI_ . T . unpack $ if T . null v then s else n
body <- if T . null v then pure Nothing else Just <$> toRegexCI_ ( tail $ T . unpack v )
return $ Tag tag body
where ( n , v ) = T . break ( == '=' ) s
2012-05-28 04:27:55 +04:00
2015-05-16 22:21:50 +03:00
-- | Parse the value part of a "status:" query, or return an error.
2017-06-16 02:54:34 +03:00
parseStatus :: T . Text -> Either String Status
2015-05-16 22:21:50 +03:00
parseStatus s | s ` elem ` [ " * " , " 1 " ] = Right Cleared
| s ` elem ` [ " ! " ] = Right Pending
2017-06-16 02:25:37 +03:00
| s ` elem ` [ " " , " 0 " ] = Right Unmarked
2015-05-16 22:21:50 +03:00
| otherwise = Left $ " could not parse " ++ show s ++ " as a status (should be *, ! or empty) "
2011-06-13 23:46:35 +04:00
2014-12-25 10:48:23 +03:00
-- | Parse the boolean value part of a "status:" query. "1" means true,
-- anything else will be parsed as false without error.
2016-07-29 18:57:10 +03:00
parseBool :: T . Text -> Bool
2011-06-29 03:18:36 +04:00
parseBool s = s ` elem ` truestrings
2016-07-29 18:57:10 +03:00
truestrings :: [ T . Text ]
2014-12-25 10:48:23 +03:00
truestrings = [ " 1 " ]
2011-06-05 22:36:32 +04:00
2012-05-17 20:02:22 +04:00
simplifyQuery :: Query -> Query
2012-05-27 22:14:20 +04:00
simplifyQuery q =
let q' = simplify q
in if q' == q then q else simplifyQuery q'
where
simplify ( And [] ) = Any
simplify ( And [ q ] ) = simplify q
simplify ( And qs ) | same qs = simplify $ head qs
| any ( == None ) qs = None
| all queryIsDate qs = Date $ spansIntersect $ mapMaybe queryTermDateSpan qs
| otherwise = And $ concat $ [ map simplify dateqs , map simplify otherqs ]
where ( dateqs , otherqs ) = partition queryIsDate $ filter ( /= Any ) qs
simplify ( Or [] ) = Any
simplify ( Or [ q ] ) = simplifyQuery q
simplify ( Or qs ) | same qs = simplify $ head qs
| any ( == Any ) qs = Any
-- all queryIsDate qs = Date $ spansUnion $ mapMaybe queryTermDateSpan qs ?
| otherwise = Or $ map simplify $ filter ( /= None ) qs
simplify ( Date ( DateSpan Nothing Nothing ) ) = Any
2014-12-25 01:43:49 +03:00
simplify ( Date2 ( DateSpan Nothing Nothing ) ) = Any
2012-05-27 22:14:20 +04:00
simplify q = q
same [] = True
same ( a : as ) = all ( a == ) as
-- | Remove query terms (or whole sub-expressions) not matching the given
2016-05-07 03:19:23 +03:00
-- predicate from this query. XXX Semantics not completely clear.
2012-05-27 22:14:20 +04:00
filterQuery :: ( Query -> Bool ) -> Query -> Query
2012-06-30 02:36:30 +04:00
filterQuery p = simplifyQuery . filterQuery' p
filterQuery' :: ( Query -> Bool ) -> Query -> Query
filterQuery' p ( And qs ) = And $ map ( filterQuery p ) qs
filterQuery' p ( Or qs ) = Or $ map ( filterQuery p ) qs
-- filterQuery' p (Not q) = Not $ filterQuery p q
filterQuery' p q = if p q then q else Any
2012-05-27 22:14:20 +04:00
2012-05-16 12:28:02 +04:00
-- * accessors
-- | Does this query match everything ?
2012-05-27 22:14:20 +04:00
queryIsNull :: Query -> Bool
2012-05-16 12:28:02 +04:00
queryIsNull Any = True
queryIsNull ( And [] ) = True
queryIsNull ( Not ( Or [] ) ) = True
queryIsNull _ = False
2012-05-27 22:14:20 +04:00
queryIsDepth :: Query -> Bool
queryIsDepth ( Depth _ ) = True
queryIsDepth _ = False
queryIsDate :: Query -> Bool
queryIsDate ( Date _ ) = True
queryIsDate _ = False
2014-12-25 03:11:30 +03:00
queryIsDate2 :: Query -> Bool
queryIsDate2 ( Date2 _ ) = True
queryIsDate2 _ = False
queryIsDateOrDate2 :: Query -> Bool
queryIsDateOrDate2 ( Date _ ) = True
queryIsDateOrDate2 ( Date2 _ ) = True
queryIsDateOrDate2 _ = False
2012-05-27 22:14:20 +04:00
queryIsDesc :: Query -> Bool
queryIsDesc ( Desc _ ) = True
queryIsDesc _ = False
queryIsAcct :: Query -> Bool
queryIsAcct ( Acct _ ) = True
queryIsAcct _ = False
2012-05-16 12:28:02 +04:00
2018-07-15 10:37:13 +03:00
queryIsAmt :: Query -> Bool
queryIsAmt ( Amt _ _ ) = True
queryIsAmt _ = False
2014-02-28 05:47:47 +04:00
queryIsSym :: Query -> Bool
queryIsSym ( Sym _ ) = True
queryIsSym _ = False
2016-06-01 20:48:57 +03:00
queryIsReal :: Query -> Bool
queryIsReal ( Real _ ) = True
queryIsReal _ = False
2016-06-04 03:51:10 +03:00
queryIsStatus :: Query -> Bool
2017-06-16 02:52:58 +03:00
queryIsStatus ( StatusQ _ ) = True
2016-06-04 03:51:10 +03:00
queryIsStatus _ = False
2016-08-09 03:24:37 +03:00
queryIsEmpty :: Query -> Bool
queryIsEmpty ( Empty _ ) = True
queryIsEmpty _ = False
2012-05-16 12:28:02 +04:00
-- | Does this query specify a start date and nothing else (that would
-- filter postings prior to the date) ?
2012-12-06 08:43:41 +04:00
-- When the flag is true, look for a starting secondary date instead.
2012-05-16 12:28:02 +04:00
queryIsStartDateOnly :: Bool -> Query -> Bool
queryIsStartDateOnly _ Any = False
queryIsStartDateOnly _ None = False
2012-12-06 08:43:41 +04:00
queryIsStartDateOnly secondary ( Or ms ) = and $ map ( queryIsStartDateOnly secondary ) ms
queryIsStartDateOnly secondary ( And ms ) = and $ map ( queryIsStartDateOnly secondary ) ms
2012-05-16 12:28:02 +04:00
queryIsStartDateOnly False ( Date ( DateSpan ( Just _ ) _ ) ) = True
2012-12-06 08:43:41 +04:00
queryIsStartDateOnly True ( Date2 ( DateSpan ( Just _ ) _ ) ) = True
2012-05-16 12:28:02 +04:00
queryIsStartDateOnly _ _ = False
2012-12-06 08:43:41 +04:00
-- | What start date (or secondary date) does this query specify, if any ?
2012-05-27 22:14:20 +04:00
-- For OR expressions, use the earliest of the dates. NOT is ignored.
queryStartDate :: Bool -> Query -> Maybe Day
2012-12-06 08:43:41 +04:00
queryStartDate secondary ( Or ms ) = earliestMaybeDate $ map ( queryStartDate secondary ) ms
queryStartDate secondary ( And ms ) = latestMaybeDate $ map ( queryStartDate secondary ) ms
2012-05-27 22:14:20 +04:00
queryStartDate False ( Date ( DateSpan ( Just d ) _ ) ) = Just d
2012-12-06 08:43:41 +04:00
queryStartDate True ( Date2 ( DateSpan ( Just d ) _ ) ) = Just d
2012-05-27 22:14:20 +04:00
queryStartDate _ _ = Nothing
2014-07-15 18:01:01 +04:00
-- | What end date (or secondary date) does this query specify, if any ?
-- For OR expressions, use the latest of the dates. NOT is ignored.
queryEndDate :: Bool -> Query -> Maybe Day
queryEndDate secondary ( Or ms ) = latestMaybeDate' $ map ( queryEndDate secondary ) ms
queryEndDate secondary ( And ms ) = earliestMaybeDate' $ map ( queryEndDate secondary ) ms
queryEndDate False ( Date ( DateSpan _ ( Just d ) ) ) = Just d
queryEndDate True ( Date2 ( DateSpan _ ( Just d ) ) ) = Just d
queryEndDate _ _ = Nothing
2012-05-27 22:14:20 +04:00
queryTermDateSpan ( Date span ) = Just span
queryTermDateSpan _ = Nothing
2018-07-14 13:10:16 +03:00
-- | What date span (or with a true argument, what secondary date span) does this query specify ?
-- OR clauses specifying multiple spans return their union (the span enclosing all of them).
-- AND clauses specifying multiple spans return their intersection.
-- NOT clauses are ignored.
2012-05-27 22:14:20 +04:00
queryDateSpan :: Bool -> Query -> DateSpan
2018-07-14 13:10:16 +03:00
queryDateSpan secondary ( Or qs ) = spansUnion $ map ( queryDateSpan secondary ) qs
queryDateSpan secondary ( And qs ) = spansIntersect $ map ( queryDateSpan secondary ) qs
queryDateSpan False ( Date span ) = span
queryDateSpan True ( Date2 span ) = span
queryDateSpan _ _ = nulldatespan
-- | What date span does this query specify, treating primary and secondary dates as equivalent ?
-- OR clauses specifying multiple spans return their union (the span enclosing all of them).
-- AND clauses specifying multiple spans return their intersection.
-- NOT clauses are ignored.
2014-12-25 03:11:30 +03:00
queryDateSpan' :: Query -> DateSpan
2018-07-14 13:10:16 +03:00
queryDateSpan' ( Or qs ) = spansUnion $ map queryDateSpan' qs
queryDateSpan' ( And qs ) = spansIntersect $ map queryDateSpan' qs
queryDateSpan' ( Date span ) = span
queryDateSpan' ( Date2 span ) = span
queryDateSpan' _ = nulldatespan
2014-12-25 03:11:30 +03:00
2019-11-29 05:09:05 +03:00
-- | What is the earliest of these dates, where Nothing is earliest ?
2012-05-16 12:28:02 +04:00
earliestMaybeDate :: [ Maybe Day ] -> Maybe Day
2020-07-16 12:30:18 +03:00
earliestMaybeDate = fromMaybe Nothing . minimumMay
2012-05-16 12:28:02 +04:00
-- | What is the latest of these dates, where Nothing is earliest ?
latestMaybeDate :: [ Maybe Day ] -> Maybe Day
2020-07-16 12:30:18 +03:00
latestMaybeDate = fromMaybe Nothing . maximumMay
2012-05-16 12:28:02 +04:00
2019-11-29 05:09:05 +03:00
-- | What is the earliest of these dates, where Nothing is the latest ?
2014-07-15 18:01:01 +04:00
earliestMaybeDate' :: [ Maybe Day ] -> Maybe Day
2020-07-16 12:30:18 +03:00
earliestMaybeDate' = fromMaybe Nothing . minimumMay . filter isJust
2014-07-15 18:01:01 +04:00
2019-11-29 05:09:05 +03:00
-- | What is the latest of these dates, where Nothing is the latest ?
2014-07-15 18:01:01 +04:00
latestMaybeDate' :: [ Maybe Day ] -> Maybe Day
2020-07-16 12:30:18 +03:00
latestMaybeDate' = fromMaybe Nothing . maximumByMay compareNothingMax
2019-11-29 05:09:05 +03:00
where
compareNothingMax Nothing Nothing = EQ
compareNothingMax ( Just _ ) Nothing = LT
compareNothingMax Nothing ( Just _ ) = GT
compareNothingMax ( Just a ) ( Just b ) = compare a b
2012-05-16 12:28:02 +04:00
2020-07-16 12:30:18 +03:00
-- | The depth limit this query specifies, if it has one
queryDepth :: Query -> Maybe Int
queryDepth = minimumMay . queryDepth'
2012-05-27 22:14:20 +04:00
where
queryDepth' ( Depth d ) = [ d ]
2020-07-16 12:30:18 +03:00
queryDepth' ( Or qs ) = concatMap queryDepth' qs
queryDepth' ( And qs ) = concatMap queryDepth' qs
queryDepth' _ = []
2012-05-27 22:14:20 +04:00
2012-05-16 12:28:02 +04:00
-- | The account we are currently focussed on, if any, and whether subaccounts are included.
-- Just looks at the first query option.
inAccount :: [ QueryOpt ] -> Maybe ( AccountName , Bool )
inAccount [] = Nothing
inAccount ( QueryOptInAcctOnly a : _ ) = Just ( a , False )
inAccount ( QueryOptInAcct a : _ ) = Just ( a , True )
-- | A query for the account(s) we are currently focussed on, if any.
-- Just looks at the first query option.
inAccountQuery :: [ QueryOpt ] -> Maybe Query
inAccountQuery [] = Nothing
2020-08-15 12:14:27 +03:00
inAccountQuery ( QueryOptInAcctOnly a : _ ) = Just . Acct $ accountNameToAccountOnlyRegex a
inAccountQuery ( QueryOptInAcct a : _ ) = Just . Acct $ accountNameToAccountRegex a
2012-05-16 12:28:02 +04:00
-- -- | Convert a query to its inverse.
-- negateQuery :: Query -> Query
-- negateQuery = Not
-- matching
2011-06-11 20:00:00 +04:00
2012-05-27 22:14:20 +04:00
-- | Does the match expression match this account ?
-- A matching in: clause is also considered a match.
2020-08-07 17:53:00 +03:00
-- When matching by account name pattern, if there's a regular
-- expression error, this function calls error.
2012-05-27 22:14:20 +04:00
matchesAccount :: Query -> AccountName -> Bool
matchesAccount ( None ) _ = False
matchesAccount ( Not m ) a = not $ matchesAccount m a
matchesAccount ( Or ms ) a = any ( ` matchesAccount ` a ) ms
matchesAccount ( And ms ) a = all ( ` matchesAccount ` a ) ms
2020-08-15 12:14:27 +03:00
matchesAccount ( Acct r ) a = match r ( T . unpack a ) -- XXX pack
2012-05-27 22:14:20 +04:00
matchesAccount ( Depth d ) a = accountNameLevel a <= d
2012-05-28 04:27:55 +04:00
matchesAccount ( Tag _ _ ) _ = False
2012-05-27 22:14:20 +04:00
matchesAccount _ _ = True
2014-04-06 06:33:44 +04:00
matchesMixedAmount :: Query -> MixedAmount -> Bool
matchesMixedAmount q ( Mixed [] ) = q ` matchesAmount ` nullamt
matchesMixedAmount q ( Mixed as ) = any ( q ` matchesAmount ` ) as
2018-07-15 10:37:13 +03:00
matchesCommodity :: Query -> CommoditySymbol -> Bool
2020-08-15 12:14:27 +03:00
matchesCommodity ( Sym r ) = match r . T . unpack
matchesCommodity _ = const True
2018-07-15 10:37:13 +03:00
2014-02-28 05:47:47 +04:00
-- | Does the match expression match this (simple) amount ?
matchesAmount :: Query -> Amount -> Bool
matchesAmount ( Not q ) a = not $ q ` matchesAmount ` a
matchesAmount ( Any ) _ = True
matchesAmount ( None ) _ = False
matchesAmount ( Or qs ) a = any ( ` matchesAmount ` a ) qs
matchesAmount ( And qs ) a = all ( ` matchesAmount ` a ) qs
2014-04-06 06:33:44 +04:00
matchesAmount ( Amt ord n ) a = compareAmount ord n a
2018-07-15 10:37:13 +03:00
matchesAmount ( Sym r ) a = matchesCommodity ( Sym r ) ( acommodity a )
2014-02-28 05:47:47 +04:00
matchesAmount _ _ = True
2014-03-21 06:10:48 +04:00
-- | Is this simple (single-amount) mixed amount's quantity less than, greater than, equal to, or unsignedly equal to this number ?
-- For multi-amount (multiple commodities, or just unsimplified) mixed amounts this is always true.
2014-04-06 06:33:44 +04:00
-- | Is this amount's quantity less than, greater than, equal to, or unsignedly equal to this number ?
compareAmount :: OrdPlus -> Quantity -> Amount -> Bool
2014-06-29 22:09:13 +04:00
compareAmount ord q Amount { aquantity = aq } = case ord of Lt -> aq < q
LtEq -> aq <= q
Gt -> aq > q
GtEq -> aq >= q
Eq -> aq == q
AbsLt -> abs aq < abs q
AbsLtEq -> abs aq <= abs q
AbsGt -> abs aq > abs q
AbsGtEq -> abs aq >= abs q
AbsEq -> abs aq == abs q
2014-02-28 05:47:47 +04:00
2011-06-11 20:00:00 +04:00
-- | Does the match expression match this posting ?
2017-01-13 19:02:11 +03:00
--
-- Note that for account match we try both original and effective account
2012-05-16 11:12:49 +04:00
matchesPosting :: Query -> Posting -> Bool
2012-05-27 22:14:20 +04:00
matchesPosting ( Not q ) p = not $ q ` matchesPosting ` p
2012-05-16 11:12:49 +04:00
matchesPosting ( Any ) _ = True
matchesPosting ( None ) _ = False
2012-05-27 22:14:20 +04:00
matchesPosting ( Or qs ) p = any ( ` matchesPosting ` p ) qs
matchesPosting ( And qs ) p = all ( ` matchesPosting ` p ) qs
2020-08-15 12:14:27 +03:00
matchesPosting ( Code r ) p = match r $ maybe " " ( T . unpack . tcode ) $ ptransaction p
matchesPosting ( Desc r ) p = match r $ maybe " " ( T . unpack . tdescription ) $ ptransaction p
2020-08-07 17:53:00 +03:00
matchesPosting ( Acct r ) p = matches p || matches ( originalPosting p )
2020-08-15 12:14:27 +03:00
where matches p = match r . T . unpack $ paccount p -- XXX pack
2012-12-06 05:10:15 +04:00
matchesPosting ( Date span ) p = span ` spanContainsDate ` postingDate p
2012-12-06 08:43:41 +04:00
matchesPosting ( Date2 span ) p = span ` spanContainsDate ` postingDate2 p
2017-06-16 02:52:58 +03:00
matchesPosting ( StatusQ s ) p = postingStatus p == s
2012-05-16 11:12:49 +04:00
matchesPosting ( Real v ) p = v == isReal p
2014-04-06 06:33:44 +04:00
matchesPosting q @ ( Depth _ ) Posting { paccount = a } = q ` matchesAccount ` a
matchesPosting q @ ( Amt _ _ ) Posting { pamount = amt } = q ` matchesMixedAmount ` amt
-- matchesPosting q@(Amt _ _) Posting{pamount=amt} = q `matchesMixedAmount` amt
2020-05-30 04:57:22 +03:00
-- matchesPosting (Empty v) Posting{pamount=a} = v == mixedAmountLooksZero a
2012-05-27 22:14:20 +04:00
-- matchesPosting (Empty False) Posting{pamount=a} = True
2020-05-30 04:57:22 +03:00
-- matchesPosting (Empty True) Posting{pamount=a} = mixedAmountLooksZero a
2012-05-27 22:14:20 +04:00
matchesPosting ( Empty _ ) _ = True
2018-07-15 10:37:13 +03:00
matchesPosting ( Sym r ) Posting { pamount = Mixed as } = any ( matchesCommodity ( Sym r ) ) $ map acommodity as
2020-08-15 12:14:27 +03:00
matchesPosting ( Tag n v ) p = case ( reString n , v ) of
( " payee " , Just v ) -> maybe False ( match v . T . unpack . transactionPayee ) $ ptransaction p
( " note " , Just v ) -> maybe False ( match v . T . unpack . transactionNote ) $ ptransaction p
( _ , v ) -> matchesTags n v $ postingAllTags p
2011-06-05 22:36:32 +04:00
2011-06-11 20:00:00 +04:00
-- | Does the match expression match this transaction ?
2012-05-16 11:12:49 +04:00
matchesTransaction :: Query -> Transaction -> Bool
2012-05-27 22:14:20 +04:00
matchesTransaction ( Not q ) t = not $ q ` matchesTransaction ` t
2012-05-16 11:12:49 +04:00
matchesTransaction ( Any ) _ = True
matchesTransaction ( None ) _ = False
2012-05-27 22:14:20 +04:00
matchesTransaction ( Or qs ) t = any ( ` matchesTransaction ` t ) qs
matchesTransaction ( And qs ) t = all ( ` matchesTransaction ` t ) qs
2020-08-15 12:14:27 +03:00
matchesTransaction ( Code r ) t = match r $ T . unpack $ tcode t
matchesTransaction ( Desc r ) t = match r $ T . unpack $ tdescription t
2012-05-27 22:14:20 +04:00
matchesTransaction q @ ( Acct _ ) t = any ( q ` matchesPosting ` ) $ tpostings t
2012-05-16 11:12:49 +04:00
matchesTransaction ( Date span ) t = spanContainsDate span $ tdate t
2012-12-06 08:43:41 +04:00
matchesTransaction ( Date2 span ) t = spanContainsDate span $ transactionDate2 t
2017-06-16 02:52:58 +03:00
matchesTransaction ( StatusQ s ) t = tstatus t == s
2012-05-16 11:12:49 +04:00
matchesTransaction ( Real v ) t = v == hasRealPostings t
2013-03-20 20:36:00 +04:00
matchesTransaction q @ ( Amt _ _ ) t = any ( q ` matchesPosting ` ) $ tpostings t
2012-05-27 22:14:20 +04:00
matchesTransaction ( Empty _ ) _ = True
matchesTransaction ( Depth d ) t = any ( Depth d ` matchesPosting ` ) $ tpostings t
2013-09-10 02:26:45 +04:00
matchesTransaction q @ ( Sym _ ) t = any ( q ` matchesPosting ` ) $ tpostings t
2020-08-15 12:14:27 +03:00
matchesTransaction ( Tag n v ) t = case ( reString n , v ) of
( " payee " , Just v ) -> match v . T . unpack . transactionPayee $ t
( " note " , Just v ) -> match v . T . unpack . transactionNote $ t
( _ , v ) -> matchesTags n v $ transactionAllTags t
2012-05-27 22:14:20 +04:00
2020-08-07 17:53:00 +03:00
-- | Does the query match the name and optionally the value of any of these tags ?
2017-08-31 00:21:01 +03:00
matchesTags :: Regexp -> Maybe Regexp -> [ Tag ] -> Bool
2020-08-15 12:14:27 +03:00
matchesTags namepat valuepat = not . null . filter ( matches namepat valuepat )
2020-08-07 17:53:00 +03:00
where
2020-08-15 12:14:27 +03:00
matches npat vpat ( n , v ) = match npat ( T . unpack n ) && maybe ( const True ) match vpat ( T . unpack v )
2020-08-07 17:53:00 +03:00
2018-07-15 10:37:13 +03:00
-- | Does the query match this market price ?
2019-06-04 03:26:27 +03:00
matchesPriceDirective :: Query -> PriceDirective -> Bool
matchesPriceDirective ( None ) _ = False
matchesPriceDirective ( Not q ) p = not $ matchesPriceDirective q p
matchesPriceDirective ( Or qs ) p = any ( ` matchesPriceDirective ` p ) qs
matchesPriceDirective ( And qs ) p = all ( ` matchesPriceDirective ` p ) qs
matchesPriceDirective q @ ( Amt _ _ ) p = matchesAmount q ( pdamount p )
matchesPriceDirective q @ ( Sym _ ) p = matchesCommodity q ( pdcommodity p )
matchesPriceDirective ( Date span ) p = spanContainsDate span ( pddate p )
matchesPriceDirective _ _ = True
2018-07-15 10:37:13 +03:00
2012-05-17 18:59:38 +04:00
-- tests
2011-06-29 03:18:36 +04:00
2018-09-06 23:08:26 +03:00
tests_Query = tests " Query " [
2019-11-29 02:29:03 +03:00
test " simplifyQuery " $ do
2020-08-15 12:14:27 +03:00
( simplifyQuery $ Or [ Acct $ toRegex' " a " ] ) @?= ( Acct $ toRegex' " a " )
2019-11-27 23:46:29 +03:00
( simplifyQuery $ Or [ Any , None ] ) @?= ( Any )
( simplifyQuery $ And [ Any , None ] ) @?= ( None )
( simplifyQuery $ And [ Any , Any ] ) @?= ( Any )
2020-08-15 12:14:27 +03:00
( simplifyQuery $ And [ Acct $ toRegex' " b " , Any ] ) @?= ( Acct $ toRegex' " b " )
2019-11-27 23:46:29 +03:00
( simplifyQuery $ And [ Any , And [ Date ( DateSpan Nothing Nothing ) ] ] ) @?= ( Any )
2020-08-26 11:11:20 +03:00
( simplifyQuery $ And [ Date ( DateSpan Nothing ( Just $ fromGregorian 2013 01 01 ) ) , Date ( DateSpan ( Just $ fromGregorian 2012 01 01 ) Nothing ) ] )
@?= ( Date ( DateSpan ( Just $ fromGregorian 2012 01 01 ) ( Just $ fromGregorian 2013 01 01 ) ) )
2020-08-15 12:14:27 +03:00
( simplifyQuery $ And [ Or [] , Or [ Desc $ toRegex' " b b " ] ] ) @?= ( Desc $ toRegex' " b b " )
2019-11-27 23:46:29 +03:00
2019-11-29 02:29:03 +03:00
, test " parseQuery " $ do
2020-08-15 12:14:27 +03:00
( parseQuery nulldate " acct:'expenses:autres d \ 233 penses' desc:b " ) @?= Right ( And [ Acct $ toRegexCI' " expenses:autres d \ 233 penses " , Desc $ toRegexCI' " b " ] , [] )
parseQuery nulldate " inacct:a desc: \ " b b \ " " @?= Right ( Desc $ toRegexCI' " b b " , [ QueryOptInAcct " a " ] )
2020-08-05 23:41:13 +03:00
parseQuery nulldate " inacct:a inacct:b " @?= Right ( Any , [ QueryOptInAcct " a " , QueryOptInAcct " b " ] )
2020-08-15 12:14:27 +03:00
parseQuery nulldate " desc:'x x' " @?= Right ( Desc $ toRegexCI' " x x " , [] )
parseQuery nulldate " 'a a' 'b " @?= Right ( Or [ Acct $ toRegexCI' " a a " , Acct $ toRegexCI' " 'b " ] , [] )
parseQuery nulldate " \ " " @?= Right ( Acct $ toRegexCI' " \ " " , [] )
2019-11-27 23:46:29 +03:00
2019-11-29 02:29:03 +03:00
, test " words'' " $ do
2019-11-27 23:46:29 +03:00
( words'' [] " a b " ) @?= [ " a " , " b " ]
( words'' [] " 'a b' " ) @?= [ " a b " ]
( words'' [] " not:a b " ) @?= [ " not:a " , " b " ]
( words'' [] " not:'a b' " ) @?= [ " not:a b " ]
( words'' [] " 'not:a b' " ) @?= [ " not:a b " ]
( words'' [ " desc: " ] " not:desc:'a b' " ) @?= [ " not:desc:a b " ]
( words'' prefixes " \ " acct:expenses:autres d \ 233 penses \ " " ) @?= [ " acct:expenses:autres d \ 233 penses " ]
( words'' prefixes " \ " " ) @?= [ " \ " " ]
2019-11-29 02:29:03 +03:00
, test " filterQuery " $ do
2019-11-27 23:46:29 +03:00
filterQuery queryIsDepth Any @?= Any
filterQuery queryIsDepth ( Depth 1 ) @?= Depth 1
filterQuery ( not . queryIsDepth ) ( And [ And [ StatusQ Cleared , Depth 1 ] ] ) @?= StatusQ Cleared
filterQuery queryIsDepth ( And [ Date nulldatespan , Not ( Or [ Any , Depth 1 ] ) ] ) @?= Any -- XXX unclear
2019-11-29 02:29:03 +03:00
, test " parseQueryTerm " $ do
2020-08-15 12:14:27 +03:00
parseQueryTerm nulldate " a " @?= Right ( Left $ Acct $ toRegexCI' " a " )
parseQueryTerm nulldate " acct:expenses:autres d \ 233 penses " @?= Right ( Left $ Acct $ toRegexCI' " expenses:autres d \ 233 penses " )
parseQueryTerm nulldate " not:desc:a b " @?= Right ( Left $ Not $ Desc $ toRegexCI' " a b " )
2020-08-05 23:41:13 +03:00
parseQueryTerm nulldate " status:1 " @?= Right ( Left $ StatusQ Cleared )
parseQueryTerm nulldate " status:* " @?= Right ( Left $ StatusQ Cleared )
parseQueryTerm nulldate " status:! " @?= Right ( Left $ StatusQ Pending )
parseQueryTerm nulldate " status:0 " @?= Right ( Left $ StatusQ Unmarked )
parseQueryTerm nulldate " status: " @?= Right ( Left $ StatusQ Unmarked )
2020-08-15 12:14:27 +03:00
parseQueryTerm nulldate " payee:x " @?= Left <$> payeeTag ( Just " x " )
parseQueryTerm nulldate " note:x " @?= Left <$> noteTag ( Just " x " )
2020-08-05 23:41:13 +03:00
parseQueryTerm nulldate " real:1 " @?= Right ( Left $ Real True )
2020-08-26 11:11:20 +03:00
parseQueryTerm nulldate " date:2008 " @?= Right ( Left $ Date $ DateSpan ( Just $ fromGregorian 2008 01 01 ) ( Just $ fromGregorian 2009 01 01 ) )
parseQueryTerm nulldate " date:from 2012/5/17 " @?= Right ( Left $ Date $ DateSpan ( Just $ fromGregorian 2012 05 17 ) Nothing )
parseQueryTerm nulldate " date:20180101-201804 " @?= Right ( Left $ Date $ DateSpan ( Just $ fromGregorian 2018 01 01 ) ( Just $ fromGregorian 2018 04 01 ) )
2020-08-05 23:41:13 +03:00
parseQueryTerm nulldate " inacct:a " @?= Right ( Right $ QueryOptInAcct " a " )
2020-08-15 12:14:27 +03:00
parseQueryTerm nulldate " tag:a " @?= Right ( Left $ Tag ( toRegexCI' " a " ) Nothing )
parseQueryTerm nulldate " tag:a=some value " @?= Right ( Left $ Tag ( toRegexCI' " a " ) ( Just $ toRegexCI' " some value " ) )
2020-08-05 23:41:13 +03:00
parseQueryTerm nulldate " amt:<0 " @?= Right ( Left $ Amt Lt 0 )
parseQueryTerm nulldate " amt:>10000.10 " @?= Right ( Left $ Amt AbsGt 10000.1 )
2019-11-27 23:46:29 +03:00
2019-11-29 02:29:03 +03:00
, test " parseAmountQueryTerm " $ do
2020-08-05 03:39:48 +03:00
parseAmountQueryTerm " <0 " @?= Right ( Lt , 0 ) -- special case for convenience, since AbsLt 0 would be always false
parseAmountQueryTerm " >0 " @?= Right ( Gt , 0 ) -- special case for convenience and consistency with above
2020-08-05 04:01:31 +03:00
parseAmountQueryTerm " > - 0 " @?= Right ( Gt , 0 ) -- accept whitespace around the argument parts
2020-08-05 03:39:48 +03:00
parseAmountQueryTerm " >10000.10 " @?= Right ( AbsGt , 10000.1 )
parseAmountQueryTerm " =0.23 " @?= Right ( AbsEq , 0.23 )
parseAmountQueryTerm " 0.23 " @?= Right ( AbsEq , 0.23 )
parseAmountQueryTerm " <=+0.23 " @?= Right ( LtEq , 0.23 )
parseAmountQueryTerm " -0.23 " @?= Right ( Eq , ( - 0.23 ) )
assertLeft $ parseAmountQueryTerm " -0,23 "
assertLeft $ parseAmountQueryTerm " =.23 "
2019-11-27 23:46:29 +03:00
2019-11-29 05:09:05 +03:00
, test " queryStartDate " $ do
let small = Just $ fromGregorian 2000 01 01
big = Just $ fromGregorian 2000 01 02
queryStartDate False ( And [ Date $ DateSpan small Nothing , Date $ DateSpan big Nothing ] ) @?= big
queryStartDate False ( And [ Date $ DateSpan small Nothing , Date $ DateSpan Nothing Nothing ] ) @?= small
queryStartDate False ( Or [ Date $ DateSpan small Nothing , Date $ DateSpan big Nothing ] ) @?= small
queryStartDate False ( Or [ Date $ DateSpan small Nothing , Date $ DateSpan Nothing Nothing ] ) @?= Nothing
, test " queryEndDate " $ do
let small = Just $ fromGregorian 2000 01 01
big = Just $ fromGregorian 2000 01 02
queryEndDate False ( And [ Date $ DateSpan Nothing small , Date $ DateSpan Nothing big ] ) @?= small
queryEndDate False ( And [ Date $ DateSpan Nothing small , Date $ DateSpan Nothing Nothing ] ) @?= small
queryEndDate False ( Or [ Date $ DateSpan Nothing small , Date $ DateSpan Nothing big ] ) @?= big
queryEndDate False ( Or [ Date $ DateSpan Nothing small , Date $ DateSpan Nothing Nothing ] ) @?= Nothing
2019-11-29 02:29:03 +03:00
, test " matchesAccount " $ do
2020-08-15 12:14:27 +03:00
assertBool " " $ ( Acct $ toRegex' " b:c " ) ` matchesAccount ` " a:bb:c:d "
assertBool " " $ not $ ( Acct $ toRegex' " ^a:b " ) ` matchesAccount ` " c:a:b "
2019-11-27 23:46:29 +03:00
assertBool " " $ Depth 2 ` matchesAccount ` " a "
assertBool " " $ Depth 2 ` matchesAccount ` " a:b "
assertBool " " $ not $ Depth 2 ` matchesAccount ` " a:b:c "
assertBool " " $ Date nulldatespan ` matchesAccount ` " a "
assertBool " " $ Date2 nulldatespan ` matchesAccount ` " a "
2020-08-15 12:14:27 +03:00
assertBool " " $ not $ Tag ( toRegex' " a " ) Nothing ` matchesAccount ` " a "
2019-07-15 13:28:52 +03:00
2018-09-04 17:29:48 +03:00
, tests " matchesPosting " [
2019-11-29 02:29:03 +03:00
test " positive match on cleared posting status " $
2019-11-27 23:46:29 +03:00
assertBool " " $ ( StatusQ Cleared ) ` matchesPosting ` nullposting { pstatus = Cleared }
2019-11-29 02:29:03 +03:00
, test " negative match on cleared posting status " $
2019-11-27 23:46:29 +03:00
assertBool " " $ not $ ( Not $ StatusQ Cleared ) ` matchesPosting ` nullposting { pstatus = Cleared }
2019-11-29 02:29:03 +03:00
, test " positive match on unmarked posting status " $
2019-11-27 23:46:29 +03:00
assertBool " " $ ( StatusQ Unmarked ) ` matchesPosting ` nullposting { pstatus = Unmarked }
2019-11-29 02:29:03 +03:00
, test " negative match on unmarked posting status " $
2019-11-27 23:46:29 +03:00
assertBool " " $ not $ ( Not $ StatusQ Unmarked ) ` matchesPosting ` nullposting { pstatus = Unmarked }
2019-11-29 02:29:03 +03:00
, test " positive match on true posting status acquired from transaction " $
2019-11-27 23:46:29 +03:00
assertBool " " $ ( StatusQ Cleared ) ` matchesPosting ` nullposting { pstatus = Unmarked , ptransaction = Just nulltransaction { tstatus = Cleared } }
2019-11-29 02:29:03 +03:00
, test " real:1 on real posting " $ assertBool " " $ ( Real True ) ` matchesPosting ` nullposting { ptype = RegularPosting }
, test " real:1 on virtual posting fails " $ assertBool " " $ not $ ( Real True ) ` matchesPosting ` nullposting { ptype = VirtualPosting }
, test " real:1 on balanced virtual posting fails " $ assertBool " " $ not $ ( Real True ) ` matchesPosting ` nullposting { ptype = BalancedVirtualPosting }
2020-08-15 12:14:27 +03:00
, test " acct: " $ assertBool " " $ ( Acct $ toRegex' " 'b " ) ` matchesPosting ` nullposting { paccount = " 'b " }
2019-11-29 02:29:03 +03:00
, test " tag: " $ do
2020-08-15 12:14:27 +03:00
assertBool " " $ not $ ( Tag ( toRegex' " a " ) ( Just $ toRegex' " r$ " ) ) ` matchesPosting ` nullposting
assertBool " " $ ( Tag ( toRegex' " foo " ) Nothing ) ` matchesPosting ` nullposting { ptags = [ ( " foo " , " " ) ] }
assertBool " " $ ( Tag ( toRegex' " foo " ) Nothing ) ` matchesPosting ` nullposting { ptags = [ ( " foo " , " baz " ) ] }
assertBool " " $ ( Tag ( toRegex' " foo " ) ( Just $ toRegex' " a " ) ) ` matchesPosting ` nullposting { ptags = [ ( " foo " , " bar " ) ] }
assertBool " " $ not $ ( Tag ( toRegex' " foo " ) ( Just $ toRegex' " a$ " ) ) ` matchesPosting ` nullposting { ptags = [ ( " foo " , " bar " ) ] }
assertBool " " $ not $ ( Tag ( toRegex' " foo " ) ( Just $ toRegex' " a " ) ) ` matchesPosting ` nullposting { ptags = [ ( " foo " , " bar " ) ] }
assertBool " " $ not $ ( Tag ( toRegex' " foo foo " ) ( Just $ toRegex' " ar ba " ) ) ` matchesPosting ` nullposting { ptags = [ ( " foo foo " , " bar bar " ) ] }
, test " a tag match on a posting also sees inherited tags " $ assertBool " " $ ( Tag ( toRegex' " txntag " ) Nothing ) ` matchesPosting ` nullposting { ptransaction = Just nulltransaction { ttags = [ ( " txntag " , " " ) ] } }
2019-11-29 02:29:03 +03:00
, test " cur: " $ do
2020-08-15 12:14:27 +03:00
let toSym = either id ( const $ error ' " N o q u e r y o p t s " ) . e i t h e r e r r o r' id . parseQueryTerm ( fromGregorian 2000 01 01 ) . ( " cur: " <> )
assertBool " " $ not $ toSym " $ " ` matchesPosting ` nullposting { pamount = Mixed [ usd 1 ] } -- becomes "^$$", ie testing for null symbol
assertBool " " $ ( toSym " \ \ $ " ) ` matchesPosting ` nullposting { pamount = Mixed [ usd 1 ] } -- have to quote $ for regexpr
assertBool " " $ ( toSym " shekels " ) ` matchesPosting ` nullposting { pamount = Mixed [ nullamt { acommodity = " shekels " } ] }
assertBool " " $ not $ ( toSym " shek " ) ` matchesPosting ` nullposting { pamount = Mixed [ nullamt { acommodity = " shekels " } ] }
2018-09-04 17:29:48 +03:00
]
2019-07-15 13:28:52 +03:00
2019-11-29 02:29:03 +03:00
, test " matchesTransaction " $ do
2019-11-27 23:46:29 +03:00
assertBool " " $ Any ` matchesTransaction ` nulltransaction
2020-08-15 12:14:27 +03:00
assertBool " " $ not $ ( Desc $ toRegex' " x x " ) ` matchesTransaction ` nulltransaction { tdescription = " x " }
assertBool " " $ ( Desc $ toRegex' " x x " ) ` matchesTransaction ` nulltransaction { tdescription = " x x " }
2018-09-04 17:29:48 +03:00
-- see posting for more tag tests
2020-08-15 12:14:27 +03:00
assertBool " " $ ( Tag ( toRegex' " foo " ) ( Just $ toRegex' " a " ) ) ` matchesTransaction ` nulltransaction { ttags = [ ( " foo " , " bar " ) ] }
assertBool " " $ ( Tag ( toRegex' " payee " ) ( Just $ toRegex' " payee " ) ) ` matchesTransaction ` nulltransaction { tdescription = " payee|note " }
assertBool " " $ ( Tag ( toRegex' " note " ) ( Just $ toRegex' " note " ) ) ` matchesTransaction ` nulltransaction { tdescription = " payee|note " }
2018-09-04 17:29:48 +03:00
-- a tag match on a transaction also matches posting tags
2020-08-15 12:14:27 +03:00
assertBool " " $ ( Tag ( toRegex' " postingtag " ) Nothing ) ` matchesTransaction ` nulltransaction { tpostings = [ nullposting { ptags = [ ( " postingtag " , " " ) ] } ] }
2011-09-21 04:28:32 +04:00
2018-09-04 17:29:48 +03:00
]