lib,cli,ui,web: Make Regexp a wrapper for Regex.
This commit is contained in:
parent
ccd6fdd7b9
commit
e5371d5a6a
@ -14,7 +14,7 @@ import Data.List.Extra (groupSort, groupOn)
|
|||||||
import Data.Maybe (fromMaybe)
|
import Data.Maybe (fromMaybe)
|
||||||
import Data.Ord (Down(..))
|
import Data.Ord (Down(..))
|
||||||
import qualified Data.Map as M
|
import qualified Data.Map as M
|
||||||
import Data.Text (pack,unpack)
|
import qualified Data.Text as T
|
||||||
import Safe (headMay, lookupJustDef)
|
import Safe (headMay, lookupJustDef)
|
||||||
import Text.Printf
|
import Text.Printf
|
||||||
|
|
||||||
@ -28,11 +28,12 @@ import Hledger.Utils
|
|||||||
-- deriving instance Show Account
|
-- deriving instance Show Account
|
||||||
instance Show Account where
|
instance Show Account where
|
||||||
show Account{..} = printf "Account %s (boring:%s, postings:%d, ebalance:%s, ibalance:%s)"
|
show Account{..} = printf "Account %s (boring:%s, postings:%d, ebalance:%s, ibalance:%s)"
|
||||||
(pack $ regexReplace ":" "_" $ unpack aname) -- hide : so pretty-show doesn't break line
|
(T.map colonToUnderscore aname) -- hide : so pretty-show doesn't break line
|
||||||
(if aboring then "y" else "n" :: String)
|
(if aboring then "y" else "n" :: String)
|
||||||
anumpostings
|
anumpostings
|
||||||
(showMixedAmount aebalance)
|
(showMixedAmount aebalance)
|
||||||
(showMixedAmount aibalance)
|
(showMixedAmount aibalance)
|
||||||
|
where colonToUnderscore x = if x == ':' then '_' else x
|
||||||
|
|
||||||
instance Eq Account where
|
instance Eq Account where
|
||||||
(==) a b = aname a == aname b -- quick equality test for speed
|
(==) a b = aname a == aname b -- quick equality test for speed
|
||||||
|
|||||||
@ -18,7 +18,6 @@ module Hledger.Data.AccountName (
|
|||||||
,accountNameToAccountOnlyRegex
|
,accountNameToAccountOnlyRegex
|
||||||
,accountNameToAccountRegex
|
,accountNameToAccountRegex
|
||||||
,accountNameTreeFrom
|
,accountNameTreeFrom
|
||||||
,accountRegexToAccountName
|
|
||||||
,accountSummarisedName
|
,accountSummarisedName
|
||||||
,acctsep
|
,acctsep
|
||||||
,acctsepchar
|
,acctsepchar
|
||||||
@ -48,7 +47,6 @@ import Data.Monoid
|
|||||||
import Data.Text (Text)
|
import Data.Text (Text)
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
import Data.Tree
|
import Data.Tree
|
||||||
import Text.Printf
|
|
||||||
|
|
||||||
import Hledger.Data.Types
|
import Hledger.Data.Types
|
||||||
import Hledger.Utils
|
import Hledger.Utils
|
||||||
@ -210,23 +208,17 @@ clipOrEllipsifyAccountName n = clipAccountName n
|
|||||||
-- | Escape an AccountName for use within a regular expression.
|
-- | Escape an AccountName for use within a regular expression.
|
||||||
-- >>> putStr $ escapeName "First?!#$*?$(*) !@^#*? %)*!@#"
|
-- >>> putStr $ escapeName "First?!#$*?$(*) !@^#*? %)*!@#"
|
||||||
-- First\?!#\$\*\?\$\(\*\) !@\^#\*\? %\)\*!@#
|
-- First\?!#\$\*\?\$\(\*\) !@\^#\*\? %\)\*!@#
|
||||||
escapeName :: AccountName -> Regexp
|
escapeName :: AccountName -> String
|
||||||
escapeName = regexReplaceBy "[[?+|()*\\\\^$]" ("\\" <>)
|
escapeName = replaceAllBy (toRegex' "[[?+|()*\\\\^$]") ("\\" <>) -- PARTIAL: should not happen
|
||||||
. T.unpack
|
. T.unpack
|
||||||
|
|
||||||
-- | Convert an account name to a regular expression matching it and its subaccounts.
|
-- | Convert an account name to a regular expression matching it and its subaccounts.
|
||||||
accountNameToAccountRegex :: AccountName -> Regexp
|
accountNameToAccountRegex :: AccountName -> Regexp
|
||||||
accountNameToAccountRegex "" = ""
|
accountNameToAccountRegex a = toRegex' $ '^' : escapeName a ++ "(:|$)" -- PARTIAL: Is this safe after escapeName?
|
||||||
accountNameToAccountRegex a = printf "^%s(:|$)" (escapeName a)
|
|
||||||
|
|
||||||
-- | Convert an account name to a regular expression matching it but not its subaccounts.
|
-- | Convert an account name to a regular expression matching it but not its subaccounts.
|
||||||
accountNameToAccountOnlyRegex :: AccountName -> Regexp
|
accountNameToAccountOnlyRegex :: AccountName -> Regexp
|
||||||
accountNameToAccountOnlyRegex "" = ""
|
accountNameToAccountOnlyRegex a = toRegex' $ '^' : escapeName a ++ "$" -- PARTIAL: Is this safe after escapeName?
|
||||||
accountNameToAccountOnlyRegex a = printf "^%s$" $ escapeName a -- XXX pack
|
|
||||||
|
|
||||||
-- | Convert an exact account-matching regular expression to a plain account name.
|
|
||||||
accountRegexToAccountName :: Regexp -> AccountName
|
|
||||||
accountRegexToAccountName = T.pack . regexReplace "^\\^(.*?)\\(:\\|\\$\\)$" "\\1" -- XXX pack
|
|
||||||
|
|
||||||
-- -- | Does this string look like an exact account-matching regular expression ?
|
-- -- | Does this string look like an exact account-matching regular expression ?
|
||||||
--isAccountRegex :: String -> Bool
|
--isAccountRegex :: String -> Bool
|
||||||
|
|||||||
@ -74,7 +74,6 @@ module Hledger.Data.Journal (
|
|||||||
journalCashAccountQuery,
|
journalCashAccountQuery,
|
||||||
-- * Misc
|
-- * Misc
|
||||||
canonicalStyleFrom,
|
canonicalStyleFrom,
|
||||||
matchpats,
|
|
||||||
nulljournal,
|
nulljournal,
|
||||||
journalCheckBalanceAssertions,
|
journalCheckBalanceAssertions,
|
||||||
journalNumberAndTieTransactions,
|
journalNumberAndTieTransactions,
|
||||||
@ -301,7 +300,7 @@ journalAccountNameTree = accountNameTreeFrom . journalAccountNames
|
|||||||
-- or otherwise for accounts with names matched by the case-insensitive
|
-- or otherwise for accounts with names matched by the case-insensitive
|
||||||
-- regular expression @^assets?(:|$)@.
|
-- regular expression @^assets?(:|$)@.
|
||||||
journalAssetAccountQuery :: Journal -> Query
|
journalAssetAccountQuery :: Journal -> Query
|
||||||
journalAssetAccountQuery j = journalAccountTypeQuery [Asset,Cash] "^assets?(:|$)" j
|
journalAssetAccountQuery = journalAccountTypeQuery [Asset,Cash] (toRegex' "^assets?(:|$)")
|
||||||
|
|
||||||
-- | A query for "Cash" (liquid asset) accounts in this journal, ie accounts
|
-- | A query for "Cash" (liquid asset) accounts in this journal, ie accounts
|
||||||
-- declared as Cash by account directives, or otherwise with names matched by the
|
-- declared as Cash by account directives, or otherwise with names matched by the
|
||||||
@ -310,39 +309,37 @@ journalAssetAccountQuery j = journalAccountTypeQuery [Asset,Cash] "^assets?(:|$)
|
|||||||
journalCashAccountQuery :: Journal -> Query
|
journalCashAccountQuery :: Journal -> Query
|
||||||
journalCashAccountQuery j =
|
journalCashAccountQuery j =
|
||||||
case M.lookup Cash (jdeclaredaccounttypes j) of
|
case M.lookup Cash (jdeclaredaccounttypes j) of
|
||||||
|
Nothing -> And [ journalAssetAccountQuery j, Not . Acct $ toRegex' "(investment|receivable|:A/R|:fixed)" ]
|
||||||
Just _ -> journalAccountTypeQuery [Cash] notused j
|
Just _ -> journalAccountTypeQuery [Cash] notused j
|
||||||
where notused = error' "journalCashAccountQuery: this should not have happened!" -- PARTIAL:
|
where notused = error' "journalCashAccountQuery: this should not have happened!" -- PARTIAL:
|
||||||
Nothing -> And [journalAssetAccountQuery j
|
|
||||||
,Not $ Acct "(investment|receivable|:A/R|:fixed)"
|
|
||||||
]
|
|
||||||
|
|
||||||
-- | A query for accounts in this journal which have been
|
-- | A query for accounts in this journal which have been
|
||||||
-- declared as Liability by account directives, or otherwise for
|
-- declared as Liability by account directives, or otherwise for
|
||||||
-- accounts with names matched by the case-insensitive regular expression
|
-- accounts with names matched by the case-insensitive regular expression
|
||||||
-- @^(debts?|liabilit(y|ies))(:|$)@.
|
-- @^(debts?|liabilit(y|ies))(:|$)@.
|
||||||
journalLiabilityAccountQuery :: Journal -> Query
|
journalLiabilityAccountQuery :: Journal -> Query
|
||||||
journalLiabilityAccountQuery = journalAccountTypeQuery [Liability] "^(debts?|liabilit(y|ies))(:|$)"
|
journalLiabilityAccountQuery = journalAccountTypeQuery [Liability] (toRegex' "^(debts?|liabilit(y|ies))(:|$)")
|
||||||
|
|
||||||
-- | A query for accounts in this journal which have been
|
-- | A query for accounts in this journal which have been
|
||||||
-- declared as Equity by account directives, or otherwise for
|
-- declared as Equity by account directives, or otherwise for
|
||||||
-- accounts with names matched by the case-insensitive regular expression
|
-- accounts with names matched by the case-insensitive regular expression
|
||||||
-- @^equity(:|$)@.
|
-- @^equity(:|$)@.
|
||||||
journalEquityAccountQuery :: Journal -> Query
|
journalEquityAccountQuery :: Journal -> Query
|
||||||
journalEquityAccountQuery = journalAccountTypeQuery [Equity] "^equity(:|$)"
|
journalEquityAccountQuery = journalAccountTypeQuery [Equity] (toRegex' "^equity(:|$)")
|
||||||
|
|
||||||
-- | A query for accounts in this journal which have been
|
-- | A query for accounts in this journal which have been
|
||||||
-- declared as Revenue by account directives, or otherwise for
|
-- declared as Revenue by account directives, or otherwise for
|
||||||
-- accounts with names matched by the case-insensitive regular expression
|
-- accounts with names matched by the case-insensitive regular expression
|
||||||
-- @^(income|revenue)s?(:|$)@.
|
-- @^(income|revenue)s?(:|$)@.
|
||||||
journalRevenueAccountQuery :: Journal -> Query
|
journalRevenueAccountQuery :: Journal -> Query
|
||||||
journalRevenueAccountQuery = journalAccountTypeQuery [Revenue] "^(income|revenue)s?(:|$)"
|
journalRevenueAccountQuery = journalAccountTypeQuery [Revenue] (toRegex' "^(income|revenue)s?(:|$)")
|
||||||
|
|
||||||
-- | A query for accounts in this journal which have been
|
-- | A query for accounts in this journal which have been
|
||||||
-- declared as Expense by account directives, or otherwise for
|
-- declared as Expense by account directives, or otherwise for
|
||||||
-- accounts with names matched by the case-insensitive regular expression
|
-- accounts with names matched by the case-insensitive regular expression
|
||||||
-- @^expenses?(:|$)@.
|
-- @^expenses?(:|$)@.
|
||||||
journalExpenseAccountQuery :: Journal -> Query
|
journalExpenseAccountQuery :: Journal -> Query
|
||||||
journalExpenseAccountQuery = journalAccountTypeQuery [Expense] "^expenses?(:|$)"
|
journalExpenseAccountQuery = journalAccountTypeQuery [Expense] (toRegex' "^expenses?(:|$)")
|
||||||
|
|
||||||
-- | A query for Asset, Liability & Equity accounts in this journal.
|
-- | A query for Asset, Liability & Equity accounts in this journal.
|
||||||
-- Cf <http://en.wikipedia.org/wiki/Chart_of_accounts#Balance_Sheet_Accounts>.
|
-- Cf <http://en.wikipedia.org/wiki/Chart_of_accounts#Balance_Sheet_Accounts>.
|
||||||
@ -370,17 +367,16 @@ journalAccountTypeQuery :: [AccountType] -> Regexp -> Journal -> Query
|
|||||||
journalAccountTypeQuery atypes fallbackregex Journal{jdeclaredaccounttypes} =
|
journalAccountTypeQuery atypes fallbackregex Journal{jdeclaredaccounttypes} =
|
||||||
let
|
let
|
||||||
declaredacctsoftype :: [AccountName] =
|
declaredacctsoftype :: [AccountName] =
|
||||||
concat $ catMaybes [M.lookup t jdeclaredaccounttypes | t <- atypes]
|
concat $ mapMaybe (`M.lookup` jdeclaredaccounttypes) atypes
|
||||||
in case declaredacctsoftype of
|
in case declaredacctsoftype of
|
||||||
[] -> Acct fallbackregex
|
[] -> Acct fallbackregex
|
||||||
as ->
|
as -> And [ Or acctnameRegexes, Not $ Or differentlyTypedRegexes ]
|
||||||
|
where
|
||||||
-- XXX Query isn't able to match account type since that requires extra info from the journal.
|
-- XXX Query isn't able to match account type since that requires extra info from the journal.
|
||||||
-- So we do a hacky search by name instead.
|
-- So we do a hacky search by name instead.
|
||||||
And [
|
acctnameRegexes = map (Acct . accountNameToAccountRegex) as
|
||||||
Or $ map (Acct . accountNameToAccountRegex) as
|
differentlyTypedRegexes = map (Acct . accountNameToAccountRegex) differentlytypedsubs
|
||||||
,Not $ Or $ map (Acct . accountNameToAccountRegex) differentlytypedsubs
|
|
||||||
]
|
|
||||||
where
|
|
||||||
differentlytypedsubs = concat
|
differentlytypedsubs = concat
|
||||||
[subs | (t,bs) <- M.toList jdeclaredaccounttypes
|
[subs | (t,bs) <- M.toList jdeclaredaccounttypes
|
||||||
, not $ t `elem` atypes
|
, not $ t `elem` atypes
|
||||||
@ -1237,25 +1233,6 @@ postingFindTag tagname p = find ((tagname==) . fst) $ postingAllTags p
|
|||||||
-- )
|
-- )
|
||||||
-- ]
|
-- ]
|
||||||
|
|
||||||
-- Misc helpers
|
|
||||||
|
|
||||||
-- | Check if a set of hledger account/description filter patterns matches the
|
|
||||||
-- given account name or entry description. Patterns are case-insensitive
|
|
||||||
-- regular expressions. Prefixed with not:, they become anti-patterns.
|
|
||||||
matchpats :: [String] -> String -> Bool
|
|
||||||
matchpats pats str =
|
|
||||||
(null positives || any match positives) && (null negatives || not (any match negatives))
|
|
||||||
where
|
|
||||||
(negatives,positives) = partition isnegativepat pats
|
|
||||||
match "" = True
|
|
||||||
match pat = regexMatchesCI (abspat pat) str
|
|
||||||
|
|
||||||
negateprefix = "not:"
|
|
||||||
|
|
||||||
isnegativepat = (negateprefix `isPrefixOf`)
|
|
||||||
|
|
||||||
abspat pat = if isnegativepat pat then drop (length negateprefix) pat else pat
|
|
||||||
|
|
||||||
-- debug helpers
|
-- debug helpers
|
||||||
-- traceAmountPrecision a = trace (show $ map (precision . acommodity) $ amounts a) a
|
-- traceAmountPrecision a = trace (show $ map (precision . acommodity) $ amounts a) a
|
||||||
-- tracePostingsCommodities ps = trace (show $ map ((map (precision . acommodity) . amounts) . pamount) ps) ps
|
-- tracePostingsCommodities ps = trace (show $ map ((map (precision . acommodity) . amounts) . pamount) ps) ps
|
||||||
|
|||||||
@ -17,7 +17,6 @@ module Hledger.Data.Ledger (
|
|||||||
,ledgerRootAccount
|
,ledgerRootAccount
|
||||||
,ledgerTopAccounts
|
,ledgerTopAccounts
|
||||||
,ledgerLeafAccounts
|
,ledgerLeafAccounts
|
||||||
,ledgerAccountsMatching
|
|
||||||
,ledgerPostings
|
,ledgerPostings
|
||||||
,ledgerDateSpan
|
,ledgerDateSpan
|
||||||
,ledgerCommodities
|
,ledgerCommodities
|
||||||
@ -26,8 +25,6 @@ module Hledger.Data.Ledger (
|
|||||||
where
|
where
|
||||||
|
|
||||||
import qualified Data.Map as M
|
import qualified Data.Map as M
|
||||||
-- import Data.Text (Text)
|
|
||||||
import qualified Data.Text as T
|
|
||||||
import Safe (headDef)
|
import Safe (headDef)
|
||||||
import Text.Printf
|
import Text.Printf
|
||||||
|
|
||||||
@ -90,10 +87,6 @@ ledgerTopAccounts = asubs . head . laccounts
|
|||||||
ledgerLeafAccounts :: Ledger -> [Account]
|
ledgerLeafAccounts :: Ledger -> [Account]
|
||||||
ledgerLeafAccounts = filter (null.asubs) . laccounts
|
ledgerLeafAccounts = filter (null.asubs) . laccounts
|
||||||
|
|
||||||
-- | Accounts in ledger whose name matches the pattern, in tree order.
|
|
||||||
ledgerAccountsMatching :: [String] -> Ledger -> [Account]
|
|
||||||
ledgerAccountsMatching pats = filter (matchpats pats . T.unpack . aname) . laccounts -- XXX pack
|
|
||||||
|
|
||||||
-- | List a ledger's postings, in the order parsed.
|
-- | List a ledger's postings, in the order parsed.
|
||||||
ledgerPostings :: Ledger -> [Posting]
|
ledgerPostings :: Ledger -> [Posting]
|
||||||
ledgerPostings = journalPostings . ljournal
|
ledgerPostings = journalPostings . ljournal
|
||||||
|
|||||||
@ -315,7 +315,7 @@ aliasReplace (BasicAlias old new) a
|
|||||||
Right $ new <> T.drop (T.length old) a
|
Right $ new <> T.drop (T.length old) a
|
||||||
| otherwise = Right a
|
| otherwise = Right a
|
||||||
aliasReplace (RegexAlias re repl) a =
|
aliasReplace (RegexAlias re repl) a =
|
||||||
fmap T.pack $ regexReplaceCIMemo_ re repl $ T.unpack a -- XXX
|
fmap T.pack $ regexReplaceMemo_ re repl $ T.unpack a -- XXX
|
||||||
|
|
||||||
-- | Apply a specified valuation to this posting's amount, using the
|
-- | Apply a specified valuation to this posting's amount, using the
|
||||||
-- provided price oracle, commodity styles, reference dates, and
|
-- provided price oracle, commodity styles, reference dates, and
|
||||||
|
|||||||
@ -166,7 +166,7 @@ data AccountAlias = BasicAlias AccountName AccountName
|
|||||||
| RegexAlias Regexp Replacement
|
| RegexAlias Regexp Replacement
|
||||||
deriving (Eq, Read, Show, Ord, Data, Generic, Typeable)
|
deriving (Eq, Read, Show, Ord, Data, Generic, Typeable)
|
||||||
|
|
||||||
instance NFData AccountAlias
|
-- instance NFData AccountAlias
|
||||||
|
|
||||||
data Side = L | R deriving (Eq,Show,Read,Ord,Typeable,Data,Generic)
|
data Side = L | R deriving (Eq,Show,Read,Ord,Typeable,Data,Generic)
|
||||||
|
|
||||||
@ -512,13 +512,13 @@ data Journal = Journal {
|
|||||||
-- any included journal files. The main file is first,
|
-- any included journal files. The main file is first,
|
||||||
-- followed by any included files in the order encountered.
|
-- followed by any included files in the order encountered.
|
||||||
,jlastreadtime :: ClockTime -- ^ when this journal was last read from its file(s)
|
,jlastreadtime :: ClockTime -- ^ when this journal was last read from its file(s)
|
||||||
} deriving (Eq, Typeable, Data, Generic)
|
} deriving (Eq, Generic)
|
||||||
|
|
||||||
deriving instance Data ClockTime
|
deriving instance Data ClockTime
|
||||||
deriving instance Typeable ClockTime
|
deriving instance Typeable ClockTime
|
||||||
deriving instance Generic ClockTime
|
deriving instance Generic ClockTime
|
||||||
instance NFData ClockTime
|
instance NFData ClockTime
|
||||||
instance NFData Journal
|
-- instance NFData Journal
|
||||||
|
|
||||||
-- | A journal in the process of being parsed, not yet finalised.
|
-- | A journal in the process of being parsed, not yet finalised.
|
||||||
-- The data is partial, and list fields are in reverse order.
|
-- The data is partial, and list fields are in reverse order.
|
||||||
|
|||||||
@ -9,8 +9,11 @@ transactions..) by various criteria, and a SimpleTextParser for query expressio
|
|||||||
-- (may hide other deprecation warnings too). https://github.com/ndmitchell/safe/issues/26
|
-- (may hide other deprecation warnings too). https://github.com/ndmitchell/safe/issues/26
|
||||||
{-# OPTIONS_GHC -Wno-warnings-deprecations #-}
|
{-# OPTIONS_GHC -Wno-warnings-deprecations #-}
|
||||||
|
|
||||||
{-# LANGUAGE DeriveDataTypeable, OverloadedStrings, ViewPatterns #-}
|
|
||||||
{-# LANGUAGE CPP #-}
|
{-# LANGUAGE CPP #-}
|
||||||
|
{-# LANGUAGE DeriveDataTypeable #-}
|
||||||
|
{-# LANGUAGE FlexibleContexts #-}
|
||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
{-# LANGUAGE ViewPatterns #-}
|
||||||
|
|
||||||
module Hledger.Query (
|
module Hledger.Query (
|
||||||
-- * Query and QueryOpt
|
-- * Query and QueryOpt
|
||||||
@ -42,20 +45,13 @@ module Hledger.Query (
|
|||||||
inAccountQuery,
|
inAccountQuery,
|
||||||
-- * matching
|
-- * matching
|
||||||
matchesTransaction,
|
matchesTransaction,
|
||||||
matchesTransaction_,
|
|
||||||
matchesPosting,
|
matchesPosting,
|
||||||
matchesPosting_,
|
|
||||||
matchesAccount,
|
matchesAccount,
|
||||||
matchesAccount_,
|
|
||||||
matchesMixedAmount,
|
matchesMixedAmount,
|
||||||
matchesAmount,
|
matchesAmount,
|
||||||
matchesAmount_,
|
|
||||||
matchesCommodity,
|
matchesCommodity,
|
||||||
matchesCommodity_,
|
|
||||||
matchesTags,
|
matchesTags,
|
||||||
matchesTags_,
|
|
||||||
matchesPriceDirective,
|
matchesPriceDirective,
|
||||||
matchesPriceDirective_,
|
|
||||||
words'',
|
words'',
|
||||||
prefixes,
|
prefixes,
|
||||||
-- * tests
|
-- * tests
|
||||||
@ -63,7 +59,7 @@ module Hledger.Query (
|
|||||||
)
|
)
|
||||||
where
|
where
|
||||||
|
|
||||||
import Control.Arrow ((>>>))
|
import Control.Applicative ((<|>), liftA2, many, optional)
|
||||||
import Data.Data
|
import Data.Data
|
||||||
import Data.Either
|
import Data.Either
|
||||||
import Data.List
|
import Data.List
|
||||||
@ -74,7 +70,7 @@ import Data.Monoid ((<>))
|
|||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
import Data.Time.Calendar
|
import Data.Time.Calendar
|
||||||
import Safe (readDef, readMay, maximumByMay, maximumMay, minimumMay)
|
import Safe (readDef, readMay, maximumByMay, maximumMay, minimumMay)
|
||||||
import Text.Megaparsec
|
import Text.Megaparsec (between, noneOf, sepBy)
|
||||||
import Text.Megaparsec.Char
|
import Text.Megaparsec.Char
|
||||||
|
|
||||||
import Hledger.Utils hiding (words')
|
import Hledger.Utils hiding (words')
|
||||||
@ -111,6 +107,14 @@ data Query = Any -- ^ always match
|
|||||||
-- matching the regexp if provided, exists
|
-- matching the regexp if provided, exists
|
||||||
deriving (Eq,Data,Typeable)
|
deriving (Eq,Data,Typeable)
|
||||||
|
|
||||||
|
-- | 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_)
|
||||||
|
|
||||||
-- custom Show implementation to show strings more accurately, eg for debugging regexps
|
-- custom Show implementation to show strings more accurately, eg for debugging regexps
|
||||||
instance Show Query where
|
instance Show Query where
|
||||||
show Any = "Any"
|
show Any = "Any"
|
||||||
@ -273,11 +277,11 @@ parseQueryTerm d (T.stripPrefix "not:" -> Just s) =
|
|||||||
Right (Left m) -> Right $ Left $ Not m
|
Right (Left m) -> Right $ Left $ Not m
|
||||||
Right (Right _) -> Right $ Left Any -- not:somequeryoption will be ignored
|
Right (Right _) -> Right $ Left Any -- not:somequeryoption will be ignored
|
||||||
Left err -> Left err
|
Left err -> Left err
|
||||||
parseQueryTerm _ (T.stripPrefix "code:" -> Just s) = Right $ Left $ Code $ T.unpack s
|
parseQueryTerm _ (T.stripPrefix "code:" -> Just s) = Left . Code <$> toRegexCI_ (T.unpack s)
|
||||||
parseQueryTerm _ (T.stripPrefix "desc:" -> Just s) = Right $ Left $ Desc $ T.unpack s
|
parseQueryTerm _ (T.stripPrefix "desc:" -> Just s) = Left . Desc <$> toRegexCI_ (T.unpack s)
|
||||||
parseQueryTerm _ (T.stripPrefix "payee:" -> Just s) = Right $ Left $ Tag "payee" $ Just $ T.unpack s
|
parseQueryTerm _ (T.stripPrefix "payee:" -> Just s) = Left <$> payeeTag (Just $ T.unpack s)
|
||||||
parseQueryTerm _ (T.stripPrefix "note:" -> Just s) = Right $ Left $ Tag "note" $ Just $ T.unpack s
|
parseQueryTerm _ (T.stripPrefix "note:" -> Just s) = Left <$> noteTag (Just $ T.unpack s)
|
||||||
parseQueryTerm _ (T.stripPrefix "acct:" -> Just s) = Right $ Left $ Acct $ T.unpack s
|
parseQueryTerm _ (T.stripPrefix "acct:" -> Just s) = Left . Acct <$> toRegexCI_ (T.unpack s)
|
||||||
parseQueryTerm d (T.stripPrefix "date2:" -> Just s) =
|
parseQueryTerm d (T.stripPrefix "date2:" -> Just s) =
|
||||||
case parsePeriodExpr d s of Left e -> Left $ "\"date2:"++T.unpack s++"\" gave a "++showDateParseError e
|
case parsePeriodExpr d s of Left e -> Left $ "\"date2:"++T.unpack s++"\" gave a "++showDateParseError e
|
||||||
Right (_,span) -> Right $ Left $ Date2 span
|
Right (_,span) -> Right $ Left $ Date2 span
|
||||||
@ -295,8 +299,8 @@ parseQueryTerm _ (T.stripPrefix "depth:" -> Just s)
|
|||||||
| otherwise = Left "depth: should have a positive number"
|
| otherwise = Left "depth: should have a positive number"
|
||||||
where n = readDef 0 (T.unpack s)
|
where n = readDef 0 (T.unpack s)
|
||||||
|
|
||||||
parseQueryTerm _ (T.stripPrefix "cur:" -> Just s) = Right $ Left $ Sym (T.unpack s) -- support cur: as an alias
|
parseQueryTerm _ (T.stripPrefix "cur:" -> Just s) = Left . Sym <$> toRegexCI_ ('^' : T.unpack s ++ "$") -- support cur: as an alias
|
||||||
parseQueryTerm _ (T.stripPrefix "tag:" -> Just s) = Right $ Left $ Tag n v where (n,v) = parseTag s
|
parseQueryTerm _ (T.stripPrefix "tag:" -> Just s) = Left <$> parseTag s
|
||||||
parseQueryTerm _ "" = Right $ Left $ Any
|
parseQueryTerm _ "" = Right $ Left $ Any
|
||||||
parseQueryTerm d s = parseQueryTerm d $ defaultprefix<>":"<>s
|
parseQueryTerm d s = parseQueryTerm d $ defaultprefix<>":"<>s
|
||||||
|
|
||||||
@ -344,9 +348,11 @@ parseAmountQueryTerm amtarg =
|
|||||||
parse :: T.Text -> T.Text -> Maybe Quantity
|
parse :: T.Text -> T.Text -> Maybe Quantity
|
||||||
parse p s = (T.stripPrefix p . T.strip) s >>= readMay . filter (not.(==' ')) . T.unpack
|
parse p s = (T.stripPrefix p . T.strip) s >>= readMay . filter (not.(==' ')) . T.unpack
|
||||||
|
|
||||||
parseTag :: T.Text -> (Regexp, Maybe Regexp)
|
parseTag :: T.Text -> Either RegexError Query
|
||||||
parseTag s | "=" `T.isInfixOf` s = (T.unpack n, Just $ tail $ T.unpack v)
|
parseTag s = do
|
||||||
| otherwise = (T.unpack s, Nothing)
|
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
|
where (n,v) = T.break (=='=') s
|
||||||
|
|
||||||
-- | Parse the value part of a "status:" query, or return an error.
|
-- | Parse the value part of a "status:" query, or return an error.
|
||||||
@ -550,8 +556,8 @@ inAccount (QueryOptInAcct a:_) = Just (a,True)
|
|||||||
-- Just looks at the first query option.
|
-- Just looks at the first query option.
|
||||||
inAccountQuery :: [QueryOpt] -> Maybe Query
|
inAccountQuery :: [QueryOpt] -> Maybe Query
|
||||||
inAccountQuery [] = Nothing
|
inAccountQuery [] = Nothing
|
||||||
inAccountQuery (QueryOptInAcctOnly a : _) = Just $ Acct $ accountNameToAccountOnlyRegex a
|
inAccountQuery (QueryOptInAcctOnly a : _) = Just . Acct $ accountNameToAccountOnlyRegex a
|
||||||
inAccountQuery (QueryOptInAcct a : _) = Just $ Acct $ accountNameToAccountRegex a
|
inAccountQuery (QueryOptInAcct a : _) = Just . Acct $ accountNameToAccountRegex a
|
||||||
|
|
||||||
-- -- | Convert a query to its inverse.
|
-- -- | Convert a query to its inverse.
|
||||||
-- negateQuery :: Query -> Query
|
-- negateQuery :: Query -> Query
|
||||||
@ -568,36 +574,38 @@ matchesAccount (None) _ = False
|
|||||||
matchesAccount (Not m) a = not $ matchesAccount m a
|
matchesAccount (Not m) a = not $ matchesAccount m a
|
||||||
matchesAccount (Or ms) a = any (`matchesAccount` a) ms
|
matchesAccount (Or ms) a = any (`matchesAccount` a) ms
|
||||||
matchesAccount (And ms) a = all (`matchesAccount` a) ms
|
matchesAccount (And ms) a = all (`matchesAccount` a) ms
|
||||||
matchesAccount (Acct r) a = regexMatchesCI r (T.unpack a) -- XXX pack
|
matchesAccount (Acct r) a = match r (T.unpack a) -- XXX pack
|
||||||
matchesAccount (Depth d) a = accountNameLevel a <= d
|
matchesAccount (Depth d) a = accountNameLevel a <= d
|
||||||
matchesAccount (Tag _ _) _ = False
|
matchesAccount (Tag _ _) _ = False
|
||||||
matchesAccount _ _ = True
|
matchesAccount _ _ = True
|
||||||
|
|
||||||
-- | Total version of matchesAccount, which will return any error
|
-- | Total version of matchesAccount, which will return any error
|
||||||
-- arising from a malformed regular expression in the query.
|
-- arising from a malformed regular expression in the query.
|
||||||
matchesAccount_ :: Query -> AccountName -> Either RegexError Bool
|
-- FIXME: unnecssary
|
||||||
matchesAccount_ (None) _ = Right False
|
-- matchesAccount_ :: Query -> AccountName -> Either RegexError Bool
|
||||||
matchesAccount_ (Not m) a = Right $ not $ matchesAccount m a
|
-- matchesAccount_ (None) _ = Right False
|
||||||
matchesAccount_ (Or ms) a = sequence (map (`matchesAccount_` a) ms) >>= pure . or
|
-- matchesAccount_ (Not m) a = Right $ not $ matchesAccount m a
|
||||||
matchesAccount_ (And ms) a = sequence (map (`matchesAccount_` a) ms) >>= pure . and
|
-- matchesAccount_ (Or ms) a = sequence (map (`matchesAccount_` a) ms) >>= pure . or
|
||||||
matchesAccount_ (Acct r) a = regexMatchesCI_ r (T.unpack a) -- XXX pack
|
-- matchesAccount_ (And ms) a = sequence (map (`matchesAccount_` a) ms) >>= pure . and
|
||||||
matchesAccount_ (Depth d) a = Right $ accountNameLevel a <= d
|
-- matchesAccount_ (Acct r) a = match r (T.unpack a) -- XXX pack
|
||||||
matchesAccount_ (Tag _ _) _ = Right False
|
-- matchesAccount_ (Depth d) a = Right $ accountNameLevel a <= d
|
||||||
matchesAccount_ _ _ = Right True
|
-- matchesAccount_ (Tag _ _) _ = Right False
|
||||||
|
-- matchesAccount_ _ _ = Right True
|
||||||
|
|
||||||
matchesMixedAmount :: Query -> MixedAmount -> Bool
|
matchesMixedAmount :: Query -> MixedAmount -> Bool
|
||||||
matchesMixedAmount q (Mixed []) = q `matchesAmount` nullamt
|
matchesMixedAmount q (Mixed []) = q `matchesAmount` nullamt
|
||||||
matchesMixedAmount q (Mixed as) = any (q `matchesAmount`) as
|
matchesMixedAmount q (Mixed as) = any (q `matchesAmount`) as
|
||||||
|
|
||||||
matchesCommodity :: Query -> CommoditySymbol -> Bool
|
matchesCommodity :: Query -> CommoditySymbol -> Bool
|
||||||
matchesCommodity (Sym r) s = regexMatchesCI ("^" ++ r ++ "$") (T.unpack s)
|
matchesCommodity (Sym r) = match r . T.unpack
|
||||||
matchesCommodity _ _ = True
|
matchesCommodity _ = const True
|
||||||
|
|
||||||
-- | Total version of matchesCommodity, which will return any error
|
-- | Total version of matchesCommodity, which will return any error
|
||||||
-- arising from a malformed regular expression in the query.
|
-- arising from a malformed regular expression in the query.
|
||||||
matchesCommodity_ :: Query -> CommoditySymbol -> Either RegexError Bool
|
-- FIXME unnecessary
|
||||||
matchesCommodity_ (Sym r) s = regexMatchesCI_ ("^" ++ r ++ "$") (T.unpack s)
|
-- matchesCommodity_ :: Query -> CommoditySymbol -> Bool
|
||||||
matchesCommodity_ _ _ = Right True
|
-- matchesCommodity_ (Sym r) = match r . T.unpack
|
||||||
|
-- matchesCommodity_ _ = const True
|
||||||
|
|
||||||
-- | Does the match expression match this (simple) amount ?
|
-- | Does the match expression match this (simple) amount ?
|
||||||
matchesAmount :: Query -> Amount -> Bool
|
matchesAmount :: Query -> Amount -> Bool
|
||||||
@ -612,15 +620,16 @@ matchesAmount _ _ = True
|
|||||||
|
|
||||||
-- | Total version of matchesAmount, returning any error from a
|
-- | Total version of matchesAmount, returning any error from a
|
||||||
-- malformed regular expression in the query.
|
-- malformed regular expression in the query.
|
||||||
matchesAmount_ :: Query -> Amount -> Either RegexError Bool
|
-- FIXME Unnecessary
|
||||||
matchesAmount_ (Not q) a = not <$> q `matchesAmount_` a
|
-- matchesAmount_ :: Query -> Amount -> Either RegexError Bool
|
||||||
matchesAmount_ (Any) _ = Right True
|
-- matchesAmount_ (Not q) a = not <$> q `matchesAmount_` a
|
||||||
matchesAmount_ (None) _ = Right False
|
-- matchesAmount_ (Any) _ = Right True
|
||||||
matchesAmount_ (Or qs) a = sequence (map (`matchesAmount_` a) qs) >>= pure . or
|
-- matchesAmount_ (None) _ = Right False
|
||||||
matchesAmount_ (And qs) a = sequence (map (`matchesAmount_` a) qs) >>= pure . and
|
-- matchesAmount_ (Or qs) a = sequence (map (`matchesAmount_` a) qs) >>= pure . or
|
||||||
matchesAmount_ (Amt ord n) a = Right $ compareAmount ord n a
|
-- matchesAmount_ (And qs) a = sequence (map (`matchesAmount_` a) qs) >>= pure . and
|
||||||
matchesAmount_ (Sym r) a = matchesCommodity_ (Sym r) (acommodity a)
|
-- matchesAmount_ (Amt ord n) a = Right $ compareAmount ord n a
|
||||||
matchesAmount_ _ _ = Right True
|
-- matchesAmount_ (Sym r) a = matchesCommodity_ (Sym r) (acommodity a)
|
||||||
|
-- matchesAmount_ _ _ = Right True
|
||||||
|
|
||||||
-- | Is this simple (single-amount) mixed amount's quantity less than, greater than, equal to, or unsignedly equal to this number ?
|
-- | 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.
|
-- For multi-amount (multiple commodities, or just unsimplified) mixed amounts this is always true.
|
||||||
@ -647,10 +656,10 @@ matchesPosting (Any) _ = True
|
|||||||
matchesPosting (None) _ = False
|
matchesPosting (None) _ = False
|
||||||
matchesPosting (Or qs) p = any (`matchesPosting` p) qs
|
matchesPosting (Or qs) p = any (`matchesPosting` p) qs
|
||||||
matchesPosting (And qs) p = all (`matchesPosting` p) qs
|
matchesPosting (And qs) p = all (`matchesPosting` p) qs
|
||||||
matchesPosting (Code r) p = regexMatchesCI r $ maybe "" (T.unpack . tcode) $ ptransaction p
|
matchesPosting (Code r) p = match r $ maybe "" (T.unpack . tcode) $ ptransaction p
|
||||||
matchesPosting (Desc r) p = regexMatchesCI r $ maybe "" (T.unpack . tdescription) $ ptransaction p
|
matchesPosting (Desc r) p = match r $ maybe "" (T.unpack . tdescription) $ ptransaction p
|
||||||
matchesPosting (Acct r) p = matches p || matches (originalPosting p)
|
matchesPosting (Acct r) p = matches p || matches (originalPosting p)
|
||||||
where matches p = regexMatchesCI r $ T.unpack $ paccount p -- XXX pack
|
where matches p = match r . T.unpack $ paccount p -- XXX pack
|
||||||
matchesPosting (Date span) p = span `spanContainsDate` postingDate p
|
matchesPosting (Date span) p = span `spanContainsDate` postingDate p
|
||||||
matchesPosting (Date2 span) p = span `spanContainsDate` postingDate2 p
|
matchesPosting (Date2 span) p = span `spanContainsDate` postingDate2 p
|
||||||
matchesPosting (StatusQ s) p = postingStatus p == s
|
matchesPosting (StatusQ s) p = postingStatus p == s
|
||||||
@ -663,35 +672,36 @@ matchesPosting q@(Amt _ _) Posting{pamount=amt} = q `matchesMixedAmount` amt
|
|||||||
-- matchesPosting (Empty True) Posting{pamount=a} = mixedAmountLooksZero a
|
-- matchesPosting (Empty True) Posting{pamount=a} = mixedAmountLooksZero a
|
||||||
matchesPosting (Empty _) _ = True
|
matchesPosting (Empty _) _ = True
|
||||||
matchesPosting (Sym r) Posting{pamount=Mixed as} = any (matchesCommodity (Sym r)) $ map acommodity as
|
matchesPosting (Sym r) Posting{pamount=Mixed as} = any (matchesCommodity (Sym r)) $ map acommodity as
|
||||||
matchesPosting (Tag n v) p = case (n, v) of
|
matchesPosting (Tag n v) p = case (reString n, v) of
|
||||||
("payee", Just v) -> maybe False (regexMatchesCI v . T.unpack . transactionPayee) $ ptransaction p
|
("payee", Just v) -> maybe False (match v . T.unpack . transactionPayee) $ ptransaction p
|
||||||
("note", Just v) -> maybe False (regexMatchesCI v . T.unpack . transactionNote) $ ptransaction p
|
("note", Just v) -> maybe False (match v . T.unpack . transactionNote) $ ptransaction p
|
||||||
(n, v) -> matchesTags n v $ postingAllTags p
|
(_, v) -> matchesTags n v $ postingAllTags p
|
||||||
|
|
||||||
-- | Total version of matchesPosting, returning any error from a
|
-- | Total version of matchesPosting, returning any error from a
|
||||||
-- malformed regular expression in the query.
|
-- malformed regular expression in the query.
|
||||||
matchesPosting_ :: Query -> Posting -> Either RegexError Bool
|
-- -- FIXME: unnecessary
|
||||||
matchesPosting_ (Not q) p = not <$> q `matchesPosting_` p
|
-- matchesPosting_ :: Query -> Posting -> Bool
|
||||||
matchesPosting_ (Any) _ = Right True
|
-- matchesPosting_ (Not q) p = not <$> q `matchesPosting_` p
|
||||||
matchesPosting_ (None) _ = Right False
|
-- matchesPosting_ (Any) _ = Right True
|
||||||
matchesPosting_ (Or qs) p = sequence (map (`matchesPosting_` p) qs) >>= pure.or
|
-- matchesPosting_ (None) _ = Right False
|
||||||
matchesPosting_ (And qs) p = sequence (map (`matchesPosting_` p) qs) >>= pure.and
|
-- matchesPosting_ (Or qs) p = sequence (map (`matchesPosting_` p) qs) >>= pure.or
|
||||||
matchesPosting_ (Code r) p = regexMatchesCI_ r $ maybe "" (T.unpack . tcode) $ ptransaction p
|
-- matchesPosting_ (And qs) p = sequence (map (`matchesPosting_` p) qs) >>= pure.and
|
||||||
matchesPosting_ (Desc r) p = regexMatchesCI_ r $ maybe "" (T.unpack . tdescription) $ ptransaction p
|
-- matchesPosting_ (Code r) p = match r $ maybe "" (T.unpack . tcode) $ ptransaction p
|
||||||
matchesPosting_ (Acct r) p = sequence [matches p, matches (originalPosting p)] >>= pure.or
|
-- matchesPosting_ (Desc r) p = match r $ maybe "" (T.unpack . tdescription) $ ptransaction p
|
||||||
where matches p = regexMatchesCI_ r $ T.unpack $ paccount p -- XXX pack
|
-- matchesPosting_ (Acct r) p = sequence [matches p, matches (originalPosting p)] >>= pure.or
|
||||||
matchesPosting_ (Date span) p = Right $ span `spanContainsDate` postingDate p
|
-- where matches p = match r $ T.unpack $ paccount p -- XXX pack
|
||||||
matchesPosting_ (Date2 span) p = Right $ span `spanContainsDate` postingDate2 p
|
-- matchesPosting_ (Date span) p = Right $ span `spanContainsDate` postingDate p
|
||||||
matchesPosting_ (StatusQ s) p = Right $ postingStatus p == s
|
-- matchesPosting_ (Date2 span) p = Right $ span `spanContainsDate` postingDate2 p
|
||||||
matchesPosting_ (Real v) p = Right $ v == isReal p
|
-- matchesPosting_ (StatusQ s) p = Right $ postingStatus p == s
|
||||||
matchesPosting_ q@(Depth _) Posting{paccount=a} = q `matchesAccount_` a
|
-- matchesPosting_ (Real v) p = Right $ v == isReal p
|
||||||
matchesPosting_ q@(Amt _ _) Posting{pamount=amt} = Right $ q `matchesMixedAmount` amt
|
-- matchesPosting_ q@(Depth _) Posting{paccount=a} = q `matchesAccount_` a
|
||||||
matchesPosting_ (Empty _) _ = Right True
|
-- matchesPosting_ q@(Amt _ _) Posting{pamount=amt} = Right $ q `matchesMixedAmount` amt
|
||||||
matchesPosting_ (Sym r) Posting{pamount=Mixed as} = sequence (map (matchesCommodity_ (Sym r)) $ map acommodity as) >>= pure.or
|
-- matchesPosting_ (Empty _) _ = Right True
|
||||||
matchesPosting_ (Tag n v) p = case (n, v) of
|
-- matchesPosting_ (Sym r) Posting{pamount=Mixed as} = sequence (map (matchesCommodity_ (Sym r)) $ map acommodity as) >>= pure.or
|
||||||
("payee", Just v) -> maybe (Right False) (T.unpack . transactionPayee >>> regexMatchesCI_ v) $ ptransaction p
|
-- matchesPosting_ (Tag n v) p = case (n, v) of
|
||||||
("note", Just v) -> maybe (Right False) (T.unpack . transactionNote >>> regexMatchesCI_ v) $ ptransaction p
|
-- ("payee", Just v) -> maybe (Right False) (T.unpack . transactionPayee >>> match v) $ ptransaction p
|
||||||
(n, v) -> matchesTags_ n v $ postingAllTags p
|
-- ("note", Just v) -> maybe (Right False) (T.unpack . transactionNote >>> match v) $ ptransaction p
|
||||||
|
-- (n, v) -> matchesTags_ n v $ postingAllTags p
|
||||||
|
|
||||||
-- | Does the match expression match this transaction ?
|
-- | Does the match expression match this transaction ?
|
||||||
matchesTransaction :: Query -> Transaction -> Bool
|
matchesTransaction :: Query -> Transaction -> Bool
|
||||||
@ -700,8 +710,8 @@ matchesTransaction (Any) _ = True
|
|||||||
matchesTransaction (None) _ = False
|
matchesTransaction (None) _ = False
|
||||||
matchesTransaction (Or qs) t = any (`matchesTransaction` t) qs
|
matchesTransaction (Or qs) t = any (`matchesTransaction` t) qs
|
||||||
matchesTransaction (And qs) t = all (`matchesTransaction` t) qs
|
matchesTransaction (And qs) t = all (`matchesTransaction` t) qs
|
||||||
matchesTransaction (Code r) t = regexMatchesCI r $ T.unpack $ tcode t
|
matchesTransaction (Code r) t = match r $ T.unpack $ tcode t
|
||||||
matchesTransaction (Desc r) t = regexMatchesCI r $ T.unpack $ tdescription t
|
matchesTransaction (Desc r) t = match r $ T.unpack $ tdescription t
|
||||||
matchesTransaction q@(Acct _) t = any (q `matchesPosting`) $ tpostings t
|
matchesTransaction q@(Acct _) t = any (q `matchesPosting`) $ tpostings t
|
||||||
matchesTransaction (Date span) t = spanContainsDate span $ tdate t
|
matchesTransaction (Date span) t = spanContainsDate span $ tdate t
|
||||||
matchesTransaction (Date2 span) t = spanContainsDate span $ transactionDate2 t
|
matchesTransaction (Date2 span) t = spanContainsDate span $ transactionDate2 t
|
||||||
@ -711,51 +721,41 @@ matchesTransaction q@(Amt _ _) t = any (q `matchesPosting`) $ tpostings t
|
|||||||
matchesTransaction (Empty _) _ = True
|
matchesTransaction (Empty _) _ = True
|
||||||
matchesTransaction (Depth d) t = any (Depth d `matchesPosting`) $ tpostings t
|
matchesTransaction (Depth d) t = any (Depth d `matchesPosting`) $ tpostings t
|
||||||
matchesTransaction q@(Sym _) t = any (q `matchesPosting`) $ tpostings t
|
matchesTransaction q@(Sym _) t = any (q `matchesPosting`) $ tpostings t
|
||||||
matchesTransaction (Tag n v) t = case (n, v) of
|
matchesTransaction (Tag n v) t = case (reString n, v) of
|
||||||
("payee", Just v) -> regexMatchesCI v . T.unpack . transactionPayee $ t
|
("payee", Just v) -> match v . T.unpack . transactionPayee $ t
|
||||||
("note", Just v) -> regexMatchesCI v . T.unpack . transactionNote $ t
|
("note", Just v) -> match v . T.unpack . transactionNote $ t
|
||||||
(n, v) -> matchesTags n v $ transactionAllTags t
|
(_, v) -> matchesTags n v $ transactionAllTags t
|
||||||
|
|
||||||
-- | Total version of matchesTransaction, returning any error from a
|
-- | Total version of matchesTransaction, returning any error from a
|
||||||
-- malformed regular expression in the query.
|
-- malformed regular expression in the query.
|
||||||
matchesTransaction_ :: Query -> Transaction -> Either RegexError Bool
|
-- FIXME: unnecessary
|
||||||
matchesTransaction_ (Not q) t = not <$> q `matchesTransaction_` t
|
-- matchesTransaction_ :: Query -> Transaction -> Either RegexError Bool
|
||||||
matchesTransaction_ (Any) _ = Right True
|
-- matchesTransaction_ (Not q) t = not <$> q `matchesTransaction_` t
|
||||||
matchesTransaction_ (None) _ = Right False
|
-- matchesTransaction_ (Any) _ = Right True
|
||||||
matchesTransaction_ (Or qs) t = sequence (map (`matchesTransaction_` t) qs) >>= pure.or
|
-- matchesTransaction_ (None) _ = Right False
|
||||||
matchesTransaction_ (And qs) t = sequence (map (`matchesTransaction_` t) qs) >>= pure.and
|
-- matchesTransaction_ (Or qs) t = sequence (map (`matchesTransaction_` t) qs) >>= pure.or
|
||||||
matchesTransaction_ (Code r) t = regexMatchesCI_ r $ T.unpack $ tcode t
|
-- matchesTransaction_ (And qs) t = sequence (map (`matchesTransaction_` t) qs) >>= pure.and
|
||||||
matchesTransaction_ (Desc r) t = regexMatchesCI_ r $ T.unpack $ tdescription t
|
-- matchesTransaction_ (Code r) t = match r $ T.unpack $ tcode t
|
||||||
matchesTransaction_ q@(Acct _) t = sequence (map (q `matchesPosting_`) $ tpostings t) >>= pure.or
|
-- matchesTransaction_ (Desc r) t = match r $ T.unpack $ tdescription t
|
||||||
matchesTransaction_ (Date span) t = Right $ spanContainsDate span $ tdate t
|
-- matchesTransaction_ q@(Acct _) t = sequence (map (q `matchesPosting_`) $ tpostings t) >>= pure.or
|
||||||
matchesTransaction_ (Date2 span) t = Right $ spanContainsDate span $ transactionDate2 t
|
-- matchesTransaction_ (Date span) t = Right $ spanContainsDate span $ tdate t
|
||||||
matchesTransaction_ (StatusQ s) t = Right $ tstatus t == s
|
-- matchesTransaction_ (Date2 span) t = Right $ spanContainsDate span $ transactionDate2 t
|
||||||
matchesTransaction_ (Real v) t = Right $ v == hasRealPostings t
|
-- matchesTransaction_ (StatusQ s) t = Right $ tstatus t == s
|
||||||
matchesTransaction_ q@(Amt _ _) t = sequence (map (q `matchesPosting_`) $ tpostings t) >>= pure.or
|
-- matchesTransaction_ (Real v) t = Right $ v == hasRealPostings t
|
||||||
matchesTransaction_ (Empty _) _ = Right True
|
-- matchesTransaction_ q@(Amt _ _) t = sequence (map (q `matchesPosting_`) $ tpostings t) >>= pure.or
|
||||||
matchesTransaction_ (Depth d) t = sequence (map (Depth d `matchesPosting_`) $ tpostings t) >>= pure.or
|
-- matchesTransaction_ (Empty _) _ = Right True
|
||||||
matchesTransaction_ q@(Sym _) t = sequence (map (q `matchesPosting_`) $ tpostings t) >>= pure.or
|
-- matchesTransaction_ (Depth d) t = sequence (map (Depth d `matchesPosting_`) $ tpostings t) >>= pure.or
|
||||||
matchesTransaction_ (Tag n v) t = case (n, v) of
|
-- matchesTransaction_ q@(Sym _) t = sequence (map (q `matchesPosting_`) $ tpostings t) >>= pure.or
|
||||||
("payee", Just v) -> regexMatchesCI_ v . T.unpack . transactionPayee $ t
|
-- matchesTransaction_ (Tag n v) t = case (n, v) of
|
||||||
("note", Just v) -> regexMatchesCI_ v . T.unpack . transactionNote $ t
|
-- ("payee", Just v) -> match v . T.unpack . transactionPayee $ t
|
||||||
(n, v) -> matchesTags_ n v $ transactionAllTags t
|
-- ("note", Just v) -> match v . T.unpack . transactionNote $ t
|
||||||
|
-- (n, v) -> matchesTags_ n v $ transactionAllTags t
|
||||||
|
|
||||||
-- | Does the query match the name and optionally the value of any of these tags ?
|
-- | Does the query match the name and optionally the value of any of these tags ?
|
||||||
matchesTags :: Regexp -> Maybe Regexp -> [Tag] -> Bool
|
matchesTags :: Regexp -> Maybe Regexp -> [Tag] -> Bool
|
||||||
matchesTags namepat valuepat = not . null . filter (match namepat valuepat)
|
matchesTags namepat valuepat = not . null . filter (matches namepat valuepat)
|
||||||
where
|
where
|
||||||
match npat Nothing (n,_) = regexMatchesCI npat (T.unpack n) -- XXX
|
matches npat vpat (n,v) = match npat (T.unpack n) && maybe (const True) match vpat (T.unpack v)
|
||||||
match npat (Just vpat) (n,v) = regexMatchesCI npat (T.unpack n) && regexMatchesCI vpat (T.unpack v)
|
|
||||||
|
|
||||||
-- | Total version of matchesTags, returning any error from a
|
|
||||||
-- malformed regular expression in the query.
|
|
||||||
matchesTags_ :: Regexp -> Maybe Regexp -> [Tag] -> Either RegexError Bool
|
|
||||||
matchesTags_ namepat valuepat tags =
|
|
||||||
sequence (map (match namepat valuepat) tags) >>= pure.or
|
|
||||||
where
|
|
||||||
match npat Nothing (n,_) = regexMatchesCI_ npat (T.unpack n) -- XXX
|
|
||||||
match npat (Just vpat) (n,v) =
|
|
||||||
sequence [regexMatchesCI_ npat (T.unpack n), regexMatchesCI_ vpat (T.unpack v)] >>= pure.and
|
|
||||||
|
|
||||||
-- | Does the query match this market price ?
|
-- | Does the query match this market price ?
|
||||||
matchesPriceDirective :: Query -> PriceDirective -> Bool
|
matchesPriceDirective :: Query -> PriceDirective -> Bool
|
||||||
@ -770,38 +770,39 @@ matchesPriceDirective _ _ = True
|
|||||||
|
|
||||||
-- | Total version of matchesPriceDirective, returning any error from
|
-- | Total version of matchesPriceDirective, returning any error from
|
||||||
-- a malformed regular expression in the query.
|
-- a malformed regular expression in the query.
|
||||||
matchesPriceDirective_ :: Query -> PriceDirective -> Either RegexError Bool
|
-- FIXME unnecessary
|
||||||
matchesPriceDirective_ (None) _ = Right False
|
-- matchesPriceDirective_ :: Query -> PriceDirective -> Either RegexError Bool
|
||||||
matchesPriceDirective_ (Not q) p = not <$> matchesPriceDirective_ q p
|
-- matchesPriceDirective_ (None) _ = Right False
|
||||||
matchesPriceDirective_ (Or qs) p = sequence (map (`matchesPriceDirective_` p) qs) >>= pure.or
|
-- matchesPriceDirective_ (Not q) p = not <$> matchesPriceDirective_ q p
|
||||||
matchesPriceDirective_ (And qs) p = sequence (map (`matchesPriceDirective_` p) qs) >>= pure.and
|
-- matchesPriceDirective_ (Or qs) p = sequence (map (`matchesPriceDirective_` p) qs) >>= pure.or
|
||||||
matchesPriceDirective_ q@(Amt _ _) p = matchesAmount_ q (pdamount p)
|
-- matchesPriceDirective_ (And qs) p = sequence (map (`matchesPriceDirective_` p) qs) >>= pure.and
|
||||||
matchesPriceDirective_ q@(Sym _) p = matchesCommodity_ q (pdcommodity p)
|
-- matchesPriceDirective_ q@(Amt _ _) p = matchesAmount_ q (pdamount p)
|
||||||
matchesPriceDirective_ (Date span) p = Right $ spanContainsDate span (pddate p)
|
-- matchesPriceDirective_ q@(Sym _) p = matchesCommodity_ q (pdcommodity p)
|
||||||
matchesPriceDirective_ _ _ = Right True
|
-- matchesPriceDirective_ (Date span) p = Right $ spanContainsDate span (pddate p)
|
||||||
|
-- matchesPriceDirective_ _ _ = Right True
|
||||||
|
|
||||||
|
|
||||||
-- tests
|
-- tests
|
||||||
|
|
||||||
tests_Query = tests "Query" [
|
tests_Query = tests "Query" [
|
||||||
test "simplifyQuery" $ do
|
test "simplifyQuery" $ do
|
||||||
(simplifyQuery $ Or [Acct "a"]) @?= (Acct "a")
|
(simplifyQuery $ Or [Acct $ toRegex' "a"]) @?= (Acct $ toRegex' "a")
|
||||||
(simplifyQuery $ Or [Any,None]) @?= (Any)
|
(simplifyQuery $ Or [Any,None]) @?= (Any)
|
||||||
(simplifyQuery $ And [Any,None]) @?= (None)
|
(simplifyQuery $ And [Any,None]) @?= (None)
|
||||||
(simplifyQuery $ And [Any,Any]) @?= (Any)
|
(simplifyQuery $ And [Any,Any]) @?= (Any)
|
||||||
(simplifyQuery $ And [Acct "b",Any]) @?= (Acct "b")
|
(simplifyQuery $ And [Acct $ toRegex' "b",Any]) @?= (Acct $ toRegex' "b")
|
||||||
(simplifyQuery $ And [Any,And [Date (DateSpan Nothing Nothing)]]) @?= (Any)
|
(simplifyQuery $ And [Any,And [Date (DateSpan Nothing Nothing)]]) @?= (Any)
|
||||||
(simplifyQuery $ And [Date (DateSpan Nothing (Just $ fromGregorian 2013 01 01)), Date (DateSpan (Just $ fromGregorian 2012 01 01) Nothing)])
|
(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)))
|
@?= (Date (DateSpan (Just $ fromGregorian 2012 01 01) (Just $ fromGregorian 2013 01 01)))
|
||||||
(simplifyQuery $ And [Or [],Or [Desc "b b"]]) @?= (Desc "b b")
|
(simplifyQuery $ And [Or [],Or [Desc $ toRegex' "b b"]]) @?= (Desc $ toRegex' "b b")
|
||||||
|
|
||||||
,test "parseQuery" $ do
|
,test "parseQuery" $ do
|
||||||
(parseQuery nulldate "acct:'expenses:autres d\233penses' desc:b") @?= Right (And [Acct "expenses:autres d\233penses", Desc "b"], [])
|
(parseQuery nulldate "acct:'expenses:autres d\233penses' desc:b") @?= Right (And [Acct $ toRegexCI' "expenses:autres d\233penses", Desc $ toRegexCI' "b"], [])
|
||||||
parseQuery nulldate "inacct:a desc:\"b b\"" @?= Right (Desc "b b", [QueryOptInAcct "a"])
|
parseQuery nulldate "inacct:a desc:\"b b\"" @?= Right (Desc $ toRegexCI' "b b", [QueryOptInAcct "a"])
|
||||||
parseQuery nulldate "inacct:a inacct:b" @?= Right (Any, [QueryOptInAcct "a", QueryOptInAcct "b"])
|
parseQuery nulldate "inacct:a inacct:b" @?= Right (Any, [QueryOptInAcct "a", QueryOptInAcct "b"])
|
||||||
parseQuery nulldate "desc:'x x'" @?= Right (Desc "x x", [])
|
parseQuery nulldate "desc:'x x'" @?= Right (Desc $ toRegexCI' "x x", [])
|
||||||
parseQuery nulldate "'a a' 'b" @?= Right (Or [Acct "a a",Acct "'b"], [])
|
parseQuery nulldate "'a a' 'b" @?= Right (Or [Acct $ toRegexCI' "a a",Acct $ toRegexCI' "'b"], [])
|
||||||
parseQuery nulldate "\"" @?= Right (Acct "\"", [])
|
parseQuery nulldate "\"" @?= Right (Acct $ toRegexCI' "\"", [])
|
||||||
|
|
||||||
,test "words''" $ do
|
,test "words''" $ do
|
||||||
(words'' [] "a b") @?= ["a","b"]
|
(words'' [] "a b") @?= ["a","b"]
|
||||||
@ -820,23 +821,23 @@ tests_Query = tests "Query" [
|
|||||||
filterQuery queryIsDepth (And [Date nulldatespan, Not (Or [Any, Depth 1])]) @?= Any -- XXX unclear
|
filterQuery queryIsDepth (And [Date nulldatespan, Not (Or [Any, Depth 1])]) @?= Any -- XXX unclear
|
||||||
|
|
||||||
,test "parseQueryTerm" $ do
|
,test "parseQueryTerm" $ do
|
||||||
parseQueryTerm nulldate "a" @?= Right (Left $ Acct "a")
|
parseQueryTerm nulldate "a" @?= Right (Left $ Acct $ toRegexCI' "a")
|
||||||
parseQueryTerm nulldate "acct:expenses:autres d\233penses" @?= Right (Left $ Acct "expenses:autres d\233penses")
|
parseQueryTerm nulldate "acct:expenses:autres d\233penses" @?= Right (Left $ Acct $ toRegexCI' "expenses:autres d\233penses")
|
||||||
parseQueryTerm nulldate "not:desc:a b" @?= Right (Left $ Not $ Desc "a b")
|
parseQueryTerm nulldate "not:desc:a b" @?= Right (Left $ Not $ Desc $ toRegexCI' "a b")
|
||||||
parseQueryTerm nulldate "status:1" @?= Right (Left $ StatusQ Cleared)
|
parseQueryTerm nulldate "status:1" @?= Right (Left $ StatusQ Cleared)
|
||||||
parseQueryTerm nulldate "status:*" @?= Right (Left $ StatusQ Cleared)
|
parseQueryTerm nulldate "status:*" @?= Right (Left $ StatusQ Cleared)
|
||||||
parseQueryTerm nulldate "status:!" @?= Right (Left $ StatusQ Pending)
|
parseQueryTerm nulldate "status:!" @?= Right (Left $ StatusQ Pending)
|
||||||
parseQueryTerm nulldate "status:0" @?= Right (Left $ StatusQ Unmarked)
|
parseQueryTerm nulldate "status:0" @?= Right (Left $ StatusQ Unmarked)
|
||||||
parseQueryTerm nulldate "status:" @?= Right (Left $ StatusQ Unmarked)
|
parseQueryTerm nulldate "status:" @?= Right (Left $ StatusQ Unmarked)
|
||||||
parseQueryTerm nulldate "payee:x" @?= Right (Left $ Tag "payee" (Just "x"))
|
parseQueryTerm nulldate "payee:x" @?= Left <$> payeeTag (Just "x")
|
||||||
parseQueryTerm nulldate "note:x" @?= Right (Left $ Tag "note" (Just "x"))
|
parseQueryTerm nulldate "note:x" @?= Left <$> noteTag (Just "x")
|
||||||
parseQueryTerm nulldate "real:1" @?= Right (Left $ Real True)
|
parseQueryTerm nulldate "real:1" @?= Right (Left $ Real True)
|
||||||
parseQueryTerm nulldate "date:2008" @?= Right (Left $ Date $ DateSpan (Just $ fromGregorian 2008 01 01) (Just $ fromGregorian 2009 01 01))
|
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: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))
|
parseQueryTerm nulldate "date:20180101-201804" @?= Right (Left $ Date $ DateSpan (Just $ fromGregorian 2018 01 01) (Just $ fromGregorian 2018 04 01))
|
||||||
parseQueryTerm nulldate "inacct:a" @?= Right (Right $ QueryOptInAcct "a")
|
parseQueryTerm nulldate "inacct:a" @?= Right (Right $ QueryOptInAcct "a")
|
||||||
parseQueryTerm nulldate "tag:a" @?= Right (Left $ Tag "a" Nothing)
|
parseQueryTerm nulldate "tag:a" @?= Right (Left $ Tag (toRegexCI' "a") Nothing)
|
||||||
parseQueryTerm nulldate "tag:a=some value" @?= Right (Left $ Tag "a" (Just "some value"))
|
parseQueryTerm nulldate "tag:a=some value" @?= Right (Left $ Tag (toRegexCI' "a") (Just $ toRegexCI' "some value"))
|
||||||
parseQueryTerm nulldate "amt:<0" @?= Right (Left $ Amt Lt 0)
|
parseQueryTerm nulldate "amt:<0" @?= Right (Left $ Amt Lt 0)
|
||||||
parseQueryTerm nulldate "amt:>10000.10" @?= Right (Left $ Amt AbsGt 10000.1)
|
parseQueryTerm nulldate "amt:>10000.10" @?= Right (Left $ Amt AbsGt 10000.1)
|
||||||
|
|
||||||
@ -869,14 +870,14 @@ tests_Query = tests "Query" [
|
|||||||
queryEndDate False (Or [Date $ DateSpan Nothing small, Date $ DateSpan Nothing Nothing]) @?= Nothing
|
queryEndDate False (Or [Date $ DateSpan Nothing small, Date $ DateSpan Nothing Nothing]) @?= Nothing
|
||||||
|
|
||||||
,test "matchesAccount" $ do
|
,test "matchesAccount" $ do
|
||||||
assertBool "" $ (Acct "b:c") `matchesAccount` "a:bb:c:d"
|
assertBool "" $ (Acct $ toRegex' "b:c") `matchesAccount` "a:bb:c:d"
|
||||||
assertBool "" $ not $ (Acct "^a:b") `matchesAccount` "c:a:b"
|
assertBool "" $ not $ (Acct $ toRegex' "^a:b") `matchesAccount` "c:a:b"
|
||||||
assertBool "" $ Depth 2 `matchesAccount` "a"
|
assertBool "" $ Depth 2 `matchesAccount` "a"
|
||||||
assertBool "" $ Depth 2 `matchesAccount` "a:b"
|
assertBool "" $ Depth 2 `matchesAccount` "a:b"
|
||||||
assertBool "" $ not $ Depth 2 `matchesAccount` "a:b:c"
|
assertBool "" $ not $ Depth 2 `matchesAccount` "a:b:c"
|
||||||
assertBool "" $ Date nulldatespan `matchesAccount` "a"
|
assertBool "" $ Date nulldatespan `matchesAccount` "a"
|
||||||
assertBool "" $ Date2 nulldatespan `matchesAccount` "a"
|
assertBool "" $ Date2 nulldatespan `matchesAccount` "a"
|
||||||
assertBool "" $ not $ (Tag "a" Nothing) `matchesAccount` "a"
|
assertBool "" $ not $ Tag (toRegex' "a") Nothing `matchesAccount` "a"
|
||||||
|
|
||||||
,tests "matchesPosting" [
|
,tests "matchesPosting" [
|
||||||
test "positive match on cleared posting status" $
|
test "positive match on cleared posting status" $
|
||||||
@ -892,32 +893,33 @@ tests_Query = tests "Query" [
|
|||||||
,test "real:1 on real posting" $ assertBool "" $ (Real True) `matchesPosting` nullposting{ptype=RegularPosting}
|
,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 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}
|
,test "real:1 on balanced virtual posting fails" $ assertBool "" $ not $ (Real True) `matchesPosting` nullposting{ptype=BalancedVirtualPosting}
|
||||||
,test "acct:" $ assertBool "" $ (Acct "'b") `matchesPosting` nullposting{paccount="'b"}
|
,test "acct:" $ assertBool "" $ (Acct $ toRegex' "'b") `matchesPosting` nullposting{paccount="'b"}
|
||||||
,test "tag:" $ do
|
,test "tag:" $ do
|
||||||
assertBool "" $ not $ (Tag "a" (Just "r$")) `matchesPosting` nullposting
|
assertBool "" $ not $ (Tag (toRegex' "a") (Just $ toRegex' "r$")) `matchesPosting` nullposting
|
||||||
assertBool "" $ (Tag "foo" Nothing) `matchesPosting` nullposting{ptags=[("foo","")]}
|
assertBool "" $ (Tag (toRegex' "foo") Nothing) `matchesPosting` nullposting{ptags=[("foo","")]}
|
||||||
assertBool "" $ (Tag "foo" Nothing) `matchesPosting` nullposting{ptags=[("foo","baz")]}
|
assertBool "" $ (Tag (toRegex' "foo") Nothing) `matchesPosting` nullposting{ptags=[("foo","baz")]}
|
||||||
assertBool "" $ (Tag "foo" (Just "a")) `matchesPosting` nullposting{ptags=[("foo","bar")]}
|
assertBool "" $ (Tag (toRegex' "foo") (Just $ toRegex' "a")) `matchesPosting` nullposting{ptags=[("foo","bar")]}
|
||||||
assertBool "" $ not $ (Tag "foo" (Just "a$")) `matchesPosting` nullposting{ptags=[("foo","bar")]}
|
assertBool "" $ not $ (Tag (toRegex' "foo") (Just $ toRegex' "a$")) `matchesPosting` nullposting{ptags=[("foo","bar")]}
|
||||||
assertBool "" $ not $ (Tag " foo " (Just "a")) `matchesPosting` nullposting{ptags=[("foo","bar")]}
|
assertBool "" $ not $ (Tag (toRegex' " foo ") (Just $ toRegex' "a")) `matchesPosting` nullposting{ptags=[("foo","bar")]}
|
||||||
assertBool "" $ not $ (Tag "foo foo" (Just " ar ba ")) `matchesPosting` nullposting{ptags=[("foo foo","bar 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 "txntag" Nothing) `matchesPosting` nullposting{ptransaction=Just nulltransaction{ttags=[("txntag","")]}}
|
,test "a tag match on a posting also sees inherited tags" $ assertBool "" $ (Tag (toRegex' "txntag") Nothing) `matchesPosting` nullposting{ptransaction=Just nulltransaction{ttags=[("txntag","")]}}
|
||||||
,test "cur:" $ do
|
,test "cur:" $ do
|
||||||
assertBool "" $ not $ (Sym "$") `matchesPosting` nullposting{pamount=Mixed [usd 1]} -- becomes "^$$", ie testing for null symbol
|
let toSym = either id (const $ error' "No query opts") . either error' id . parseQueryTerm (fromGregorian 2000 01 01) . ("cur:"<>)
|
||||||
assertBool "" $ (Sym "\\$") `matchesPosting` nullposting{pamount=Mixed [usd 1]} -- have to quote $ for regexpr
|
assertBool "" $ not $ toSym "$" `matchesPosting` nullposting{pamount=Mixed [usd 1]} -- becomes "^$$", ie testing for null symbol
|
||||||
assertBool "" $ (Sym "shekels") `matchesPosting` nullposting{pamount=Mixed [nullamt{acommodity="shekels"}]}
|
assertBool "" $ (toSym "\\$") `matchesPosting` nullposting{pamount=Mixed [usd 1]} -- have to quote $ for regexpr
|
||||||
assertBool "" $ not $ (Sym "shek") `matchesPosting` nullposting{pamount=Mixed [nullamt{acommodity="shekels"}]}
|
assertBool "" $ (toSym "shekels") `matchesPosting` nullposting{pamount=Mixed [nullamt{acommodity="shekels"}]}
|
||||||
|
assertBool "" $ not $ (toSym "shek") `matchesPosting` nullposting{pamount=Mixed [nullamt{acommodity="shekels"}]}
|
||||||
]
|
]
|
||||||
|
|
||||||
,test "matchesTransaction" $ do
|
,test "matchesTransaction" $ do
|
||||||
assertBool "" $ Any `matchesTransaction` nulltransaction
|
assertBool "" $ Any `matchesTransaction` nulltransaction
|
||||||
assertBool "" $ not $ (Desc "x x") `matchesTransaction` nulltransaction{tdescription="x"}
|
assertBool "" $ not $ (Desc $ toRegex' "x x") `matchesTransaction` nulltransaction{tdescription="x"}
|
||||||
assertBool "" $ (Desc "x x") `matchesTransaction` nulltransaction{tdescription="x x"}
|
assertBool "" $ (Desc $ toRegex' "x x") `matchesTransaction` nulltransaction{tdescription="x x"}
|
||||||
-- see posting for more tag tests
|
-- see posting for more tag tests
|
||||||
assertBool "" $ (Tag "foo" (Just "a")) `matchesTransaction` nulltransaction{ttags=[("foo","bar")]}
|
assertBool "" $ (Tag (toRegex' "foo") (Just $ toRegex' "a")) `matchesTransaction` nulltransaction{ttags=[("foo","bar")]}
|
||||||
assertBool "" $ (Tag "payee" (Just "payee")) `matchesTransaction` nulltransaction{tdescription="payee|note"}
|
assertBool "" $ (Tag (toRegex' "payee") (Just $ toRegex' "payee")) `matchesTransaction` nulltransaction{tdescription="payee|note"}
|
||||||
assertBool "" $ (Tag "note" (Just "note")) `matchesTransaction` nulltransaction{tdescription="payee|note"}
|
assertBool "" $ (Tag (toRegex' "note") (Just $ toRegex' "note")) `matchesTransaction` nulltransaction{tdescription="payee|note"}
|
||||||
-- a tag match on a transaction also matches posting tags
|
-- a tag match on a transaction also matches posting tags
|
||||||
assertBool "" $ (Tag "postingtag" Nothing) `matchesTransaction` nulltransaction{tpostings=[nullposting{ptags=[("postingtag","")]}]}
|
assertBool "" $ (Tag (toRegex' "postingtag") Nothing) `matchesTransaction` nulltransaction{tpostings=[nullposting{ptags=[("postingtag","")]}]}
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|||||||
@ -144,7 +144,7 @@ import Text.Megaparsec.Custom
|
|||||||
import Control.Applicative.Permutations
|
import Control.Applicative.Permutations
|
||||||
|
|
||||||
import Hledger.Data
|
import Hledger.Data
|
||||||
import Hledger.Utils
|
import Hledger.Utils hiding (match)
|
||||||
|
|
||||||
--- ** doctest setup
|
--- ** doctest setup
|
||||||
-- $setup
|
-- $setup
|
||||||
|
|||||||
@ -44,6 +44,7 @@ import "base-compat-batteries" Prelude.Compat hiding (fail)
|
|||||||
import Control.Exception (IOException, handle, throw)
|
import Control.Exception (IOException, handle, throw)
|
||||||
import Control.Monad (liftM, unless, when)
|
import Control.Monad (liftM, unless, when)
|
||||||
import Control.Monad.Except (ExceptT, throwError)
|
import Control.Monad.Except (ExceptT, throwError)
|
||||||
|
import qualified Control.Monad.Fail as Fail
|
||||||
import Control.Monad.IO.Class (MonadIO, liftIO)
|
import Control.Monad.IO.Class (MonadIO, liftIO)
|
||||||
import Control.Monad.State.Strict (StateT, get, modify', evalStateT)
|
import Control.Monad.State.Strict (StateT, get, modify', evalStateT)
|
||||||
import Control.Monad.Trans.Class (lift)
|
import Control.Monad.Trans.Class (lift)
|
||||||
@ -69,7 +70,7 @@ import qualified Data.Csv.Parser.Megaparsec as CassavaMP
|
|||||||
import qualified Data.ByteString as B
|
import qualified Data.ByteString as B
|
||||||
import qualified Data.ByteString.Lazy as BL
|
import qualified Data.ByteString.Lazy as BL
|
||||||
import Data.Foldable
|
import Data.Foldable
|
||||||
import Text.Megaparsec hiding (parse)
|
import Text.Megaparsec hiding (match, parse)
|
||||||
import Text.Megaparsec.Char
|
import Text.Megaparsec.Char
|
||||||
import Text.Megaparsec.Custom
|
import Text.Megaparsec.Custom
|
||||||
import Text.Printf (printf)
|
import Text.Printf (printf)
|
||||||
@ -294,17 +295,14 @@ type FieldTemplate = String
|
|||||||
-- | A strptime date parsing pattern, as supported by Data.Time.Format.
|
-- | A strptime date parsing pattern, as supported by Data.Time.Format.
|
||||||
type DateFormat = String
|
type DateFormat = String
|
||||||
|
|
||||||
-- | A regular expression.
|
|
||||||
type RegexpPattern = String
|
|
||||||
|
|
||||||
-- | A prefix for a matcher test, either & or none (implicit or).
|
-- | A prefix for a matcher test, either & or none (implicit or).
|
||||||
data MatcherPrefix = And | None
|
data MatcherPrefix = And | None
|
||||||
deriving (Show, Eq)
|
deriving (Show, Eq)
|
||||||
|
|
||||||
-- | A single test for matching a CSV record, in one way or another.
|
-- | A single test for matching a CSV record, in one way or another.
|
||||||
data Matcher =
|
data Matcher =
|
||||||
RecordMatcher MatcherPrefix RegexpPattern -- ^ match if this regexp matches the overall CSV record
|
RecordMatcher MatcherPrefix Regexp -- ^ match if this regexp matches the overall CSV record
|
||||||
| FieldMatcher MatcherPrefix CsvFieldReference RegexpPattern -- ^ match if this regexp matches the referenced CSV field's value
|
| FieldMatcher MatcherPrefix CsvFieldReference Regexp -- ^ match if this regexp matches the referenced CSV field's value
|
||||||
deriving (Show, Eq)
|
deriving (Show, Eq)
|
||||||
|
|
||||||
-- | A conditional block: a set of CSV record matchers, and a sequence
|
-- | A conditional block: a set of CSV record matchers, and a sequence
|
||||||
@ -617,9 +615,9 @@ recordmatcherp end = do
|
|||||||
-- _ <- optional (matchoperatorp >> lift skipNonNewlineSpaces >> optional newline)
|
-- _ <- optional (matchoperatorp >> lift skipNonNewlineSpaces >> optional newline)
|
||||||
p <- matcherprefixp
|
p <- matcherprefixp
|
||||||
r <- regexp end
|
r <- regexp end
|
||||||
|
return $ RecordMatcher p r
|
||||||
-- when (null ps) $
|
-- when (null ps) $
|
||||||
-- Fail.fail "start of record matcher found, but no patterns afterward\n(patterns should not be indented)\n"
|
-- Fail.fail "start of record matcher found, but no patterns afterward\n(patterns should not be indented)\n"
|
||||||
return $ RecordMatcher p r
|
|
||||||
<?> "record matcher"
|
<?> "record matcher"
|
||||||
|
|
||||||
-- | A single matcher for a specific field. A csv field reference
|
-- | A single matcher for a specific field. A csv field reference
|
||||||
@ -656,13 +654,15 @@ csvfieldreferencep = do
|
|||||||
return $ '%' : quoteIfNeeded f
|
return $ '%' : quoteIfNeeded f
|
||||||
|
|
||||||
-- A single regular expression
|
-- A single regular expression
|
||||||
regexp :: CsvRulesParser () -> CsvRulesParser RegexpPattern
|
regexp :: CsvRulesParser () -> CsvRulesParser Regexp
|
||||||
regexp end = do
|
regexp end = do
|
||||||
lift $ dbgparse 8 "trying regexp"
|
lift $ dbgparse 8 "trying regexp"
|
||||||
-- notFollowedBy matchoperatorp
|
-- notFollowedBy matchoperatorp
|
||||||
c <- lift nonspace
|
c <- lift nonspace
|
||||||
cs <- anySingle `manyTill` end
|
cs <- anySingle `manyTill` end
|
||||||
return $ strip $ c:cs
|
case toRegexCI_ . strip $ c:cs of
|
||||||
|
Left x -> Fail.fail $ "CSV parser: " ++ x
|
||||||
|
Right x -> return x
|
||||||
|
|
||||||
-- -- A match operator, indicating the type of match to perform.
|
-- -- A match operator, indicating the type of match to perform.
|
||||||
-- -- Currently just ~ meaning case insensitive infix regex match.
|
-- -- Currently just ~ meaning case insensitive infix regex match.
|
||||||
@ -1181,7 +1181,7 @@ getEffectiveAssignment rules record f = lastMay $ map snd $ assignments
|
|||||||
where
|
where
|
||||||
-- does this individual matcher match the current csv record ?
|
-- does this individual matcher match the current csv record ?
|
||||||
matcherMatches :: Matcher -> Bool
|
matcherMatches :: Matcher -> Bool
|
||||||
matcherMatches (RecordMatcher _ pat) = regexMatchesCI pat' wholecsvline
|
matcherMatches (RecordMatcher _ pat) = match pat' wholecsvline
|
||||||
where
|
where
|
||||||
pat' = dbg7 "regex" pat
|
pat' = dbg7 "regex" pat
|
||||||
-- A synthetic whole CSV record to match against. Note, this can be
|
-- A synthetic whole CSV record to match against. Note, this can be
|
||||||
@ -1191,7 +1191,7 @@ getEffectiveAssignment rules record f = lastMay $ map snd $ assignments
|
|||||||
-- - and the field separator is always comma
|
-- - and the field separator is always comma
|
||||||
-- which means that a field containing a comma will look like two fields.
|
-- which means that a field containing a comma will look like two fields.
|
||||||
wholecsvline = dbg7 "wholecsvline" $ intercalate "," record
|
wholecsvline = dbg7 "wholecsvline" $ intercalate "," record
|
||||||
matcherMatches (FieldMatcher _ csvfieldref pat) = regexMatchesCI pat csvfieldvalue
|
matcherMatches (FieldMatcher _ csvfieldref pat) = match pat csvfieldvalue
|
||||||
where
|
where
|
||||||
-- the value of the referenced CSV field to match against.
|
-- the value of the referenced CSV field to match against.
|
||||||
csvfieldvalue = dbg7 "csvfieldvalue" $ replaceCsvFieldReference rules record csvfieldref
|
csvfieldvalue = dbg7 "csvfieldvalue" $ replaceCsvFieldReference rules record csvfieldref
|
||||||
@ -1199,7 +1199,7 @@ getEffectiveAssignment rules record f = lastMay $ map snd $ assignments
|
|||||||
-- | Render a field assignment's template, possibly interpolating referenced
|
-- | Render a field assignment's template, possibly interpolating referenced
|
||||||
-- CSV field values. Outer whitespace is removed from interpolated values.
|
-- CSV field values. Outer whitespace is removed from interpolated values.
|
||||||
renderTemplate :: CsvRules -> CsvRecord -> FieldTemplate -> String
|
renderTemplate :: CsvRules -> CsvRecord -> FieldTemplate -> String
|
||||||
renderTemplate rules record t = regexReplaceBy "%[A-z0-9_-]+" (replaceCsvFieldReference rules record) t
|
renderTemplate rules record t = replaceAllBy (toRegex' "%[A-z0-9_-]+") (replaceCsvFieldReference rules record) t -- PARTIAL: should not happen
|
||||||
|
|
||||||
-- | Replace something that looks like a reference to a csv field ("%date" or "%1)
|
-- | Replace something that looks like a reference to a csv field ("%date" or "%1)
|
||||||
-- with that field's value. If it doesn't look like a field reference, or if we
|
-- with that field's value. If it doesn't look like a field reference, or if we
|
||||||
@ -1256,12 +1256,12 @@ tests_CsvReader = tests "CsvReader" [
|
|||||||
|
|
||||||
,test "assignment with empty value" $
|
,test "assignment with empty value" $
|
||||||
parseWithState' defrules rulesp "account1 \nif foo\n account2 foo\n" @?=
|
parseWithState' defrules rulesp "account1 \nif foo\n account2 foo\n" @?=
|
||||||
(Right (mkrules $ defrules{rassignments = [("account1","")], rconditionalblocks = [CB{cbMatchers=[RecordMatcher None "foo"],cbAssignments=[("account2","foo")]}]}))
|
(Right (mkrules $ defrules{rassignments = [("account1","")], rconditionalblocks = [CB{cbMatchers=[RecordMatcher None (toRegex' "foo")],cbAssignments=[("account2","foo")]}]}))
|
||||||
]
|
]
|
||||||
,tests "conditionalblockp" [
|
,tests "conditionalblockp" [
|
||||||
test "space after conditional" $ -- #1120
|
test "space after conditional" $ -- #1120
|
||||||
parseWithState' defrules conditionalblockp "if a\n account2 b\n \n" @?=
|
parseWithState' defrules conditionalblockp "if a\n account2 b\n \n" @?=
|
||||||
(Right $ CB{cbMatchers=[RecordMatcher None "a"],cbAssignments=[("account2","b")]})
|
(Right $ CB{cbMatchers=[RecordMatcher None $ toRegexCI' "a"],cbAssignments=[("account2","b")]})
|
||||||
|
|
||||||
,tests "csvfieldreferencep" [
|
,tests "csvfieldreferencep" [
|
||||||
test "number" $ parseWithState' defrules csvfieldreferencep "%1" @?= (Right "%1")
|
test "number" $ parseWithState' defrules csvfieldreferencep "%1" @?= (Right "%1")
|
||||||
@ -1272,19 +1272,19 @@ tests_CsvReader = tests "CsvReader" [
|
|||||||
,tests "matcherp" [
|
,tests "matcherp" [
|
||||||
|
|
||||||
test "recordmatcherp" $
|
test "recordmatcherp" $
|
||||||
parseWithState' defrules matcherp "A A\n" @?= (Right $ RecordMatcher None "A A")
|
parseWithState' defrules matcherp "A A\n" @?= (Right $ RecordMatcher None $ toRegexCI' "A A")
|
||||||
|
|
||||||
,test "recordmatcherp.starts-with-&" $
|
,test "recordmatcherp.starts-with-&" $
|
||||||
parseWithState' defrules matcherp "& A A\n" @?= (Right $ RecordMatcher And "A A")
|
parseWithState' defrules matcherp "& A A\n" @?= (Right $ RecordMatcher And $ toRegexCI' "A A")
|
||||||
|
|
||||||
,test "fieldmatcherp.starts-with-%" $
|
,test "fieldmatcherp.starts-with-%" $
|
||||||
parseWithState' defrules matcherp "description A A\n" @?= (Right $ RecordMatcher None "description A A")
|
parseWithState' defrules matcherp "description A A\n" @?= (Right $ RecordMatcher None $ toRegexCI' "description A A")
|
||||||
|
|
||||||
,test "fieldmatcherp" $
|
,test "fieldmatcherp" $
|
||||||
parseWithState' defrules matcherp "%description A A\n" @?= (Right $ FieldMatcher None "%description" "A A")
|
parseWithState' defrules matcherp "%description A A\n" @?= (Right $ FieldMatcher None "%description" $ toRegexCI' "A A")
|
||||||
|
|
||||||
,test "fieldmatcherp.starts-with-&" $
|
,test "fieldmatcherp.starts-with-&" $
|
||||||
parseWithState' defrules matcherp "& %description A A\n" @?= (Right $ FieldMatcher And "%description" "A A")
|
parseWithState' defrules matcherp "& %description A A\n" @?= (Right $ FieldMatcher And "%description" $ toRegexCI' "A A")
|
||||||
|
|
||||||
-- ,test "fieldmatcherp with operator" $
|
-- ,test "fieldmatcherp with operator" $
|
||||||
-- parseWithState' defrules matcherp "%description ~ A A\n" @?= (Right $ FieldMatcher "%description" "A A")
|
-- parseWithState' defrules matcherp "%description ~ A A\n" @?= (Right $ FieldMatcher "%description" "A A")
|
||||||
@ -1296,19 +1296,19 @@ tests_CsvReader = tests "CsvReader" [
|
|||||||
|
|
||||||
in test "toplevel" $ getEffectiveAssignment rules ["a","b"] "date" @?= (Just "%csvdate")
|
in test "toplevel" $ getEffectiveAssignment rules ["a","b"] "date" @?= (Just "%csvdate")
|
||||||
|
|
||||||
,let rules = mkrules $ defrules{rcsvfieldindexes=[("csvdate",1)], rconditionalblocks=[CB [FieldMatcher None "%csvdate" "a"] [("date","%csvdate")]]}
|
,let rules = mkrules $ defrules{rcsvfieldindexes=[("csvdate",1)], rconditionalblocks=[CB [FieldMatcher None "%csvdate" $ toRegex' "a"] [("date","%csvdate")]]}
|
||||||
in test "conditional" $ getEffectiveAssignment rules ["a","b"] "date" @?= (Just "%csvdate")
|
in test "conditional" $ getEffectiveAssignment rules ["a","b"] "date" @?= (Just "%csvdate")
|
||||||
|
|
||||||
,let rules = mkrules $ defrules{rcsvfieldindexes=[("csvdate",1),("description",2)], rconditionalblocks=[CB [FieldMatcher None "%csvdate" "a", FieldMatcher None "%description" "b"] [("date","%csvdate")]]}
|
,let rules = mkrules $ defrules{rcsvfieldindexes=[("csvdate",1),("description",2)], rconditionalblocks=[CB [FieldMatcher None "%csvdate" $ toRegex' "a", FieldMatcher None "%description" $ toRegex' "b"] [("date","%csvdate")]]}
|
||||||
in test "conditional-with-or-a" $ getEffectiveAssignment rules ["a"] "date" @?= (Just "%csvdate")
|
in test "conditional-with-or-a" $ getEffectiveAssignment rules ["a"] "date" @?= (Just "%csvdate")
|
||||||
|
|
||||||
,let rules = mkrules $ defrules{rcsvfieldindexes=[("csvdate",1),("description",2)], rconditionalblocks=[CB [FieldMatcher None "%csvdate" "a", FieldMatcher None "%description" "b"] [("date","%csvdate")]]}
|
,let rules = mkrules $ defrules{rcsvfieldindexes=[("csvdate",1),("description",2)], rconditionalblocks=[CB [FieldMatcher None "%csvdate" $ toRegex' "a", FieldMatcher None "%description" $ toRegex' "b"] [("date","%csvdate")]]}
|
||||||
in test "conditional-with-or-b" $ getEffectiveAssignment rules ["_", "b"] "date" @?= (Just "%csvdate")
|
in test "conditional-with-or-b" $ getEffectiveAssignment rules ["_", "b"] "date" @?= (Just "%csvdate")
|
||||||
|
|
||||||
,let rules = mkrules $ defrules{rcsvfieldindexes=[("csvdate",1),("description",2)], rconditionalblocks=[CB [FieldMatcher None "%csvdate" "a", FieldMatcher And "%description" "b"] [("date","%csvdate")]]}
|
,let rules = mkrules $ defrules{rcsvfieldindexes=[("csvdate",1),("description",2)], rconditionalblocks=[CB [FieldMatcher None "%csvdate" $ toRegex' "a", FieldMatcher And "%description" $ toRegex' "b"] [("date","%csvdate")]]}
|
||||||
in test "conditional.with-and" $ getEffectiveAssignment rules ["a", "b"] "date" @?= (Just "%csvdate")
|
in test "conditional.with-and" $ getEffectiveAssignment rules ["a", "b"] "date" @?= (Just "%csvdate")
|
||||||
|
|
||||||
,let rules = mkrules $ defrules{rcsvfieldindexes=[("csvdate",1),("description",2)], rconditionalblocks=[CB [FieldMatcher None "%csvdate" "a", FieldMatcher And "%description" "b", FieldMatcher None "%description" "c"] [("date","%csvdate")]]}
|
,let rules = mkrules $ defrules{rcsvfieldindexes=[("csvdate",1),("description",2)], rconditionalblocks=[CB [FieldMatcher None "%csvdate" $ toRegex' "a", FieldMatcher And "%description" $ toRegex' "b", FieldMatcher None "%description" $ toRegex' "c"] [("date","%csvdate")]]}
|
||||||
in test "conditional.with-and-or" $ getEffectiveAssignment rules ["_", "c"] "date" @?= (Just "%csvdate")
|
in test "conditional.with-and-or" $ getEffectiveAssignment rules ["_", "c"] "date" @?= (Just "%csvdate")
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|||||||
@ -529,8 +529,8 @@ regexaliasp = do
|
|||||||
char '='
|
char '='
|
||||||
skipNonNewlineSpaces
|
skipNonNewlineSpaces
|
||||||
repl <- anySingle `manyTill` eolof
|
repl <- anySingle `manyTill` eolof
|
||||||
case toRegex_ re of
|
case toRegexCI_ re of
|
||||||
Right _ -> return $! RegexAlias re repl
|
Right r -> return $! RegexAlias r repl
|
||||||
Left e -> customFailure $! parseErrorAtRegion off1 off2 e
|
Left e -> customFailure $! parseErrorAtRegion off1 off2 e
|
||||||
|
|
||||||
endaliasesdirectivep :: JournalParser m ()
|
endaliasesdirectivep :: JournalParser m ()
|
||||||
|
|||||||
@ -50,7 +50,7 @@ entriesReport ropts@ReportOpts{..} q j@Journal{..} =
|
|||||||
|
|
||||||
tests_EntriesReport = tests "EntriesReport" [
|
tests_EntriesReport = tests "EntriesReport" [
|
||||||
tests "entriesReport" [
|
tests "entriesReport" [
|
||||||
test "not acct" $ (length $ entriesReport defreportopts (Not $ Acct "bank") samplejournal) @?= 1
|
test "not acct" $ (length $ entriesReport defreportopts (Not . Acct $ toRegex' "bank") samplejournal) @?= 1
|
||||||
,test "date" $ (length $ entriesReport defreportopts (Date $ DateSpan (Just $ fromGregorian 2008 06 01) (Just $ fromGregorian 2008 07 01)) samplejournal) @?= 3
|
,test "date" $ (length $ entriesReport defreportopts (Date $ DateSpan (Just $ fromGregorian 2008 06 01) (Just $ fromGregorian 2008 07 01)) samplejournal) @?= 3
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|||||||
@ -277,13 +277,13 @@ tests_PostingsReport = tests "PostingsReport" [
|
|||||||
(Any, samplejournal) `gives` 13
|
(Any, samplejournal) `gives` 13
|
||||||
-- register --depth just clips account names
|
-- register --depth just clips account names
|
||||||
(Depth 2, samplejournal) `gives` 13
|
(Depth 2, samplejournal) `gives` 13
|
||||||
(And [Depth 1, StatusQ Cleared, Acct "expenses"], samplejournal) `gives` 2
|
(And [Depth 1, StatusQ Cleared, Acct (toRegex' "expenses")], samplejournal) `gives` 2
|
||||||
(And [And [Depth 1, StatusQ Cleared], Acct "expenses"], samplejournal) `gives` 2
|
(And [And [Depth 1, StatusQ Cleared], Acct (toRegex' "expenses")], samplejournal) `gives` 2
|
||||||
-- with query and/or command-line options
|
-- with query and/or command-line options
|
||||||
(length $ snd $ postingsReport defreportopts Any samplejournal) @?= 13
|
(length $ snd $ postingsReport defreportopts Any samplejournal) @?= 13
|
||||||
(length $ snd $ postingsReport defreportopts{interval_=Months 1} Any samplejournal) @?= 11
|
(length $ snd $ postingsReport defreportopts{interval_=Months 1} Any samplejournal) @?= 11
|
||||||
(length $ snd $ postingsReport defreportopts{interval_=Months 1, empty_=True} Any samplejournal) @?= 20
|
(length $ snd $ postingsReport defreportopts{interval_=Months 1, empty_=True} Any samplejournal) @?= 20
|
||||||
(length $ snd $ postingsReport defreportopts (Acct "assets:bank:checking") samplejournal) @?= 5
|
(length $ snd $ postingsReport defreportopts (Acct (toRegex' "assets:bank:checking")) samplejournal) @?= 5
|
||||||
|
|
||||||
-- (defreportopts, And [Acct "a a", Acct "'b"], samplejournal2) `gives` 0
|
-- (defreportopts, And [Acct "a a", Acct "'b"], samplejournal2) `gives` 0
|
||||||
-- [(Just (fromGregorian 2008 01 01,"income"),assets:bank:checking $1,$1)
|
-- [(Just (fromGregorian 2008 01 01,"income"),assets:bank:checking $1,$1)
|
||||||
|
|||||||
@ -423,7 +423,7 @@ type DisplayExp = String
|
|||||||
|
|
||||||
maybedisplayopt :: Day -> RawOpts -> Maybe DisplayExp
|
maybedisplayopt :: Day -> RawOpts -> Maybe DisplayExp
|
||||||
maybedisplayopt d rawopts =
|
maybedisplayopt d rawopts =
|
||||||
maybe Nothing (Just . regexReplaceBy "\\[.+?\\]" fixbracketeddatestr) $ maybestringopt "display" rawopts
|
maybe Nothing (Just . replaceAllBy (toRegex' "\\[.+?\\]") fixbracketeddatestr) $ maybestringopt "display" rawopts
|
||||||
where
|
where
|
||||||
fixbracketeddatestr "" = ""
|
fixbracketeddatestr "" = ""
|
||||||
fixbracketeddatestr s = "[" ++ fixSmartDateStr d (T.pack $ init $ tail s) ++ "]"
|
fixbracketeddatestr s = "[" ++ fixSmartDateStr d (T.pack $ init $ tail s) ++ "]"
|
||||||
@ -573,12 +573,12 @@ reportPeriodOrJournalLastDay ropts j =
|
|||||||
tests_ReportOptions = tests "ReportOptions" [
|
tests_ReportOptions = tests "ReportOptions" [
|
||||||
test "queryFromOpts" $ do
|
test "queryFromOpts" $ do
|
||||||
queryFromOpts nulldate defreportopts @?= Any
|
queryFromOpts nulldate defreportopts @?= Any
|
||||||
queryFromOpts nulldate defreportopts{query_="a"} @?= Acct "a"
|
queryFromOpts nulldate defreportopts{query_="a"} @?= Acct (toRegexCI' "a")
|
||||||
queryFromOpts nulldate defreportopts{query_="desc:'a a'"} @?= Desc "a a"
|
queryFromOpts nulldate defreportopts{query_="desc:'a a'"} @?= Desc (toRegexCI' "a a")
|
||||||
queryFromOpts nulldate defreportopts{period_=PeriodFrom (fromGregorian 2012 01 01),query_="date:'to 2013'" }
|
queryFromOpts nulldate defreportopts{period_=PeriodFrom (fromGregorian 2012 01 01),query_="date:'to 2013'" }
|
||||||
@?= (Date $ DateSpan (Just $ fromGregorian 2012 01 01) (Just $ fromGregorian 2013 01 01))
|
@?= (Date $ DateSpan (Just $ fromGregorian 2012 01 01) (Just $ fromGregorian 2013 01 01))
|
||||||
queryFromOpts nulldate defreportopts{query_="date2:'in 2012'"} @?= (Date2 $ DateSpan (Just $ fromGregorian 2012 01 01) (Just $ fromGregorian 2013 01 01))
|
queryFromOpts nulldate defreportopts{query_="date2:'in 2012'"} @?= (Date2 $ DateSpan (Just $ fromGregorian 2012 01 01) (Just $ fromGregorian 2013 01 01))
|
||||||
queryFromOpts nulldate defreportopts{query_="'a a' 'b"} @?= Or [Acct "a a", Acct "'b"]
|
queryFromOpts nulldate defreportopts{query_="'a a' 'b"} @?= Or [Acct $ toRegexCI' "a a", Acct $ toRegexCI' "'b"]
|
||||||
|
|
||||||
,test "queryOptsFromOpts" $ do
|
,test "queryOptsFromOpts" $ do
|
||||||
queryOptsFromOpts nulldate defreportopts @?= []
|
queryOptsFromOpts nulldate defreportopts @?= []
|
||||||
@ -586,4 +586,3 @@ tests_ReportOptions = tests "ReportOptions" [
|
|||||||
queryOptsFromOpts nulldate defreportopts{period_=PeriodFrom (fromGregorian 2012 01 01)
|
queryOptsFromOpts nulldate defreportopts{period_=PeriodFrom (fromGregorian 2012 01 01)
|
||||||
,query_="date:'to 2013'"} @?= []
|
,query_="date:'to 2013'"} @?= []
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,8 @@
|
|||||||
|
{-# LANGUAGE DeriveDataTypeable #-}
|
||||||
|
{-# LANGUAGE FlexibleInstances #-}
|
||||||
|
{-# LANGUAGE MultiParamTypeClasses #-}
|
||||||
{-# LANGUAGE ScopedTypeVariables #-}
|
{-# LANGUAGE ScopedTypeVariables #-}
|
||||||
|
{-# LANGUAGE TypeSynonymInstances #-}
|
||||||
{-|
|
{-|
|
||||||
|
|
||||||
Easy regular expression helpers, currently based on regex-tdfa. These should:
|
Easy regular expression helpers, currently based on regex-tdfa. These should:
|
||||||
@ -42,48 +46,120 @@ Current limitations:
|
|||||||
-}
|
-}
|
||||||
|
|
||||||
module Hledger.Utils.Regex (
|
module Hledger.Utils.Regex (
|
||||||
|
-- * Regexp type and constructors
|
||||||
|
Regexp(reString)
|
||||||
|
,toRegex_
|
||||||
|
,toRegexCI_
|
||||||
|
,toRegex'
|
||||||
|
,toRegexCI'
|
||||||
-- * type aliases
|
-- * type aliases
|
||||||
Regexp
|
|
||||||
,Replacement
|
,Replacement
|
||||||
,RegexError
|
,RegexError
|
||||||
-- * partial regex operations (may call error)
|
-- * partial regex operations (may call error)
|
||||||
,regexMatches
|
-- ,regexMatches
|
||||||
,regexMatchesCI
|
-- ,regexMatchesCI
|
||||||
,regexReplace
|
-- ,regexReplaceCI
|
||||||
,regexReplaceCI
|
-- ,regexReplaceCIMemo
|
||||||
,regexReplaceMemo
|
-- ,regexReplaceByCI
|
||||||
,regexReplaceCIMemo
|
|
||||||
,regexReplaceBy
|
|
||||||
,regexReplaceByCI
|
|
||||||
-- * total regex operations
|
-- * total regex operations
|
||||||
,regexMatches_
|
,match
|
||||||
,regexMatchesCI_
|
,regexReplace
|
||||||
,regexReplace_
|
|
||||||
,regexReplaceCI_
|
|
||||||
,regexReplaceMemo_
|
,regexReplaceMemo_
|
||||||
,regexReplaceCIMemo_
|
-- ,replaceAllBy
|
||||||
,regexReplaceBy_
|
-- ,regexMatches_
|
||||||
,regexReplaceByCI_
|
-- ,regexMatchesCI_
|
||||||
,toRegex_
|
-- ,regexReplace_
|
||||||
|
-- ,regexReplaceCI_
|
||||||
|
-- ,regexReplaceMemo_
|
||||||
|
-- ,regexReplaceCIMemo_
|
||||||
|
,replaceAllBy
|
||||||
)
|
)
|
||||||
where
|
where
|
||||||
|
|
||||||
|
import Control.Arrow (first)
|
||||||
import Control.Monad (foldM)
|
import Control.Monad (foldM)
|
||||||
import Data.Array
|
import Data.Aeson (ToJSON(..), Value(String))
|
||||||
import Data.Char
|
import Data.Array ((!), elems, indices)
|
||||||
|
import Data.Char (isDigit)
|
||||||
|
import Data.Data (Data(..), mkNoRepType)
|
||||||
import Data.List (foldl')
|
import Data.List (foldl')
|
||||||
import Data.Maybe (fromMaybe)
|
|
||||||
import Data.MemoUgly (memo)
|
import Data.MemoUgly (memo)
|
||||||
|
import qualified Data.Text as T
|
||||||
import Text.Regex.TDFA (
|
import Text.Regex.TDFA (
|
||||||
Regex, CompOption(..), ExecOption(..), defaultCompOpt, defaultExecOpt,
|
Regex, CompOption(..), defaultCompOpt, defaultExecOpt,
|
||||||
makeRegexOptsM, AllMatches(getAllMatches), match, (=~), MatchText
|
makeRegexOptsM, AllMatches(getAllMatches), match, MatchText,
|
||||||
|
RegexLike(..), RegexMaker(..), RegexOptions(..), RegexContext(..)
|
||||||
)
|
)
|
||||||
|
|
||||||
import Hledger.Utils.UTF8IOCompat (error')
|
import Hledger.Utils.UTF8IOCompat (error')
|
||||||
|
|
||||||
|
|
||||||
-- | Regular expression. Extended regular expression-ish syntax ? But does not support eg (?i) syntax.
|
-- | Regular expression. Extended regular expression-ish syntax ? But does not support eg (?i) syntax.
|
||||||
type Regexp = String
|
data Regexp
|
||||||
|
= Regexp { reString :: String, reCompiled :: Regex }
|
||||||
|
| RegexpCI { reString :: String, reCompiled :: Regex }
|
||||||
|
|
||||||
|
instance Eq Regexp where
|
||||||
|
Regexp s1 _ == Regexp s2 _ = s1 == s2
|
||||||
|
RegexpCI s1 _ == RegexpCI s2 _ = s1 == s2
|
||||||
|
_ == _ = False
|
||||||
|
|
||||||
|
instance Ord Regexp where
|
||||||
|
Regexp s1 _ `compare` Regexp s2 _ = s1 `compare` s2
|
||||||
|
RegexpCI s1 _ `compare` RegexpCI s2 _ = s1 `compare` s2
|
||||||
|
Regexp _ _ `compare` RegexpCI _ _ = LT
|
||||||
|
RegexpCI _ _ `compare` Regexp _ _ = GT
|
||||||
|
|
||||||
|
instance Show Regexp where
|
||||||
|
showsPrec d (Regexp s _) = showString "Regexp " . showsPrec d s
|
||||||
|
showsPrec d (RegexpCI s _) = showString "RegexpCI " . showsPrec d s
|
||||||
|
|
||||||
|
instance Read Regexp where
|
||||||
|
readsPrec d ('R':'e':'g':'e':'x':'p':' ':xs) = map (first toRegex') $ readsPrec d xs
|
||||||
|
readsPrec d ('R':'e':'g':'e':'x':'p':'C':'I':' ':xs) = map (first toRegexCI') $ readsPrec d xs
|
||||||
|
readsPrec _ s = error' $ "read: Not a valid regex " ++ s
|
||||||
|
|
||||||
|
instance Data Regexp where
|
||||||
|
toConstr _ = error' "No toConstr for Regex"
|
||||||
|
gunfold _ _ = error' "No gunfold for Regex"
|
||||||
|
dataTypeOf _ = mkNoRepType "Hledger.Utils.Regex"
|
||||||
|
|
||||||
|
instance ToJSON Regexp where
|
||||||
|
toJSON (Regexp s _) = String . T.pack $ "Regexp " ++ s
|
||||||
|
toJSON (RegexpCI s _) = String . T.pack $ "RegexpCI " ++ s
|
||||||
|
|
||||||
|
instance RegexLike Regexp String where
|
||||||
|
matchOnce = matchOnce . reCompiled
|
||||||
|
matchAll = matchAll . reCompiled
|
||||||
|
matchCount = matchCount . reCompiled
|
||||||
|
matchTest = matchTest . reCompiled
|
||||||
|
matchAllText = matchAllText . reCompiled
|
||||||
|
matchOnceText = matchOnceText . reCompiled
|
||||||
|
|
||||||
|
instance RegexContext Regexp String String where
|
||||||
|
match = match . reCompiled
|
||||||
|
matchM = matchM . reCompiled
|
||||||
|
|
||||||
|
-- Convert a Regexp string to a compiled Regex, or return an error message.
|
||||||
|
toRegex_ :: String -> Either RegexError Regexp
|
||||||
|
toRegex_ = memo $ \s -> mkRegexErr s (Regexp s <$> makeRegexM s)
|
||||||
|
|
||||||
|
-- Like toRegex_, but make a case-insensitive Regex.
|
||||||
|
toRegexCI_ :: String -> Either RegexError Regexp
|
||||||
|
toRegexCI_ = memo $ \s -> mkRegexErr s (RegexpCI s <$> makeRegexOptsM defaultCompOpt{caseSensitive=False} defaultExecOpt s)
|
||||||
|
|
||||||
|
-- | Make a nice error message for a regexp error.
|
||||||
|
mkRegexErr :: String -> Maybe a -> Either RegexError a
|
||||||
|
mkRegexErr s = maybe (Left errmsg) Right
|
||||||
|
where errmsg = "this regular expression could not be compiled: " ++ s
|
||||||
|
|
||||||
|
-- Convert a Regexp string to a compiled Regex, throw an error
|
||||||
|
toRegex' :: String -> Regexp
|
||||||
|
toRegex' = either error' id . toRegex_
|
||||||
|
|
||||||
|
-- Like toRegex', but make a case-insensitive Regex.
|
||||||
|
toRegexCI' :: String -> Regexp
|
||||||
|
toRegexCI' = either error' id . toRegexCI_
|
||||||
|
|
||||||
-- | A replacement pattern. May include numeric backreferences (\N).
|
-- | A replacement pattern. May include numeric backreferences (\N).
|
||||||
type Replacement = String
|
type Replacement = String
|
||||||
@ -91,61 +167,10 @@ type Replacement = String
|
|||||||
-- | An regular expression compilation/processing error message.
|
-- | An regular expression compilation/processing error message.
|
||||||
type RegexError = String
|
type RegexError = String
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
|
||||||
-- old partial functions -- PARTIAL:
|
|
||||||
|
|
||||||
-- regexMatch' :: RegexContext Regexp String a => Regexp -> String -> a
|
|
||||||
-- regexMatch' r s = s =~ (toRegex' r)
|
|
||||||
|
|
||||||
regexMatches :: Regexp -> String -> Bool
|
|
||||||
regexMatches = flip (=~)
|
|
||||||
|
|
||||||
regexMatchesCI :: Regexp -> String -> Bool
|
|
||||||
regexMatchesCI r = match (toRegexCI r)
|
|
||||||
|
|
||||||
-- | Replace all occurrences of the regexp with the replacement
|
|
||||||
-- pattern. The replacement pattern supports numeric backreferences
|
|
||||||
-- (\N) but no other RE syntax.
|
|
||||||
regexReplace :: Regexp -> Replacement -> String -> String
|
|
||||||
regexReplace re = replaceRegex (toRegex re)
|
|
||||||
|
|
||||||
regexReplaceCI :: Regexp -> Replacement -> String -> String
|
|
||||||
regexReplaceCI re = replaceRegex (toRegexCI re)
|
|
||||||
|
|
||||||
-- | A memoising version of regexReplace. Caches the result for each
|
|
||||||
-- search pattern, replacement pattern, target string tuple.
|
|
||||||
regexReplaceMemo :: Regexp -> Replacement -> String -> String
|
|
||||||
regexReplaceMemo re repl = memo (regexReplace re repl)
|
|
||||||
|
|
||||||
regexReplaceCIMemo :: Regexp -> Replacement -> String -> String
|
|
||||||
regexReplaceCIMemo re repl = memo (regexReplaceCI re repl)
|
|
||||||
|
|
||||||
-- | Replace all occurrences of the regexp, transforming each match with the given function.
|
|
||||||
regexReplaceBy :: Regexp -> (String -> String) -> String -> String
|
|
||||||
regexReplaceBy r = replaceAllBy (toRegex r)
|
|
||||||
|
|
||||||
regexReplaceByCI :: Regexp -> (String -> String) -> String -> String
|
|
||||||
regexReplaceByCI r = replaceAllBy (toRegexCI r)
|
|
||||||
|
|
||||||
-- helpers
|
-- helpers
|
||||||
|
|
||||||
-- | Convert our string-based Regexp to a real Regex.
|
regexReplace :: Regexp -> Replacement -> String -> String
|
||||||
-- Or if it's not well formed, call error with a "malformed regexp" message.
|
regexReplace re repl s = foldl (replaceMatch repl) s (reverse $ match (reCompiled re) s :: [MatchText String])
|
||||||
toRegex :: Regexp -> Regex
|
|
||||||
toRegex = memo (compileRegex defaultCompOpt defaultExecOpt) -- PARTIAL:
|
|
||||||
|
|
||||||
-- | Like toRegex but make a case-insensitive Regex.
|
|
||||||
toRegexCI :: Regexp -> Regex
|
|
||||||
toRegexCI = memo (compileRegex defaultCompOpt{caseSensitive=False} defaultExecOpt) -- PARTIAL:
|
|
||||||
|
|
||||||
compileRegex :: CompOption -> ExecOption -> Regexp -> Regex
|
|
||||||
compileRegex compopt execopt r =
|
|
||||||
fromMaybe
|
|
||||||
(error $ "this regular expression could not be compiled: " ++ show r) $ -- PARTIAL:
|
|
||||||
makeRegexOptsM compopt execopt r
|
|
||||||
|
|
||||||
replaceRegex :: Regex -> Replacement -> String -> String
|
|
||||||
replaceRegex re repl s = foldl (replaceMatch repl) s (reverse $ match re s :: [MatchText String])
|
|
||||||
where
|
where
|
||||||
replaceMatch :: Replacement -> String -> MatchText String -> String
|
replaceMatch :: Replacement -> String -> MatchText String -> String
|
||||||
replaceMatch replpat s matchgroups = pre ++ repl ++ post
|
replaceMatch replpat s matchgroups = pre ++ repl ++ post
|
||||||
@ -153,7 +178,7 @@ replaceRegex re repl s = foldl (replaceMatch repl) s (reverse $ match re s :: [M
|
|||||||
((_,(off,len)):_) = elems matchgroups -- groups should have 0-based indexes, and there should always be at least one, since this is a match
|
((_,(off,len)):_) = elems matchgroups -- groups should have 0-based indexes, and there should always be at least one, since this is a match
|
||||||
(pre, post') = splitAt off s
|
(pre, post') = splitAt off s
|
||||||
post = drop len post'
|
post = drop len post'
|
||||||
repl = replaceAllBy (toRegex "\\\\[0-9]+") (lookupMatchGroup matchgroups) replpat
|
repl = replaceAllBy backrefRegex (lookupMatchGroup matchgroups) replpat
|
||||||
where
|
where
|
||||||
lookupMatchGroup :: MatchText String -> String -> String
|
lookupMatchGroup :: MatchText String -> String -> String
|
||||||
lookupMatchGroup grps ('\\':s@(_:_)) | all isDigit s =
|
lookupMatchGroup grps ('\\':s@(_:_)) | all isDigit s =
|
||||||
@ -161,68 +186,22 @@ replaceRegex re repl s = foldl (replaceMatch repl) s (reverse $ match re s :: [M
|
|||||||
-- PARTIAL:
|
-- PARTIAL:
|
||||||
_ -> error' $ "no match group exists for backreference \"\\"++s++"\""
|
_ -> error' $ "no match group exists for backreference \"\\"++s++"\""
|
||||||
lookupMatchGroup _ s = error' $ "lookupMatchGroup called on non-numeric-backreference \""++s++"\", shouldn't happen"
|
lookupMatchGroup _ s = error' $ "lookupMatchGroup called on non-numeric-backreference \""++s++"\", shouldn't happen"
|
||||||
|
backrefRegex = toRegex' "\\\\[0-9]+" -- PARTIAL: should not error happen
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
-- new total functions
|
-- new total functions
|
||||||
|
|
||||||
-- | Does this regexp match the given string ?
|
|
||||||
-- Or return an error if the regexp is malformed.
|
|
||||||
regexMatches_ :: Regexp -> String -> Either RegexError Bool
|
|
||||||
regexMatches_ r s = (`match` s) <$> toRegex_ r
|
|
||||||
|
|
||||||
-- | Like regexMatches_ but match case-insensitively.
|
|
||||||
regexMatchesCI_ :: Regexp -> String -> Either RegexError Bool
|
|
||||||
regexMatchesCI_ r s = (`match` s) <$> toRegexCI_ r
|
|
||||||
|
|
||||||
-- | Replace all occurrences of the regexp with the replacement
|
|
||||||
-- pattern, or return an error message. The replacement pattern
|
|
||||||
-- supports numeric backreferences (\N) but no other RE syntax.
|
|
||||||
regexReplace_ :: Regexp -> Replacement -> String -> Either RegexError String
|
|
||||||
regexReplace_ re repl s = toRegex_ re >>= \rx -> replaceRegex_ rx repl s
|
|
||||||
|
|
||||||
-- | Like regexReplace_ but match occurrences case-insensitively.
|
|
||||||
regexReplaceCI_ :: Regexp -> Replacement -> String -> Either RegexError String
|
|
||||||
regexReplaceCI_ re repl s = toRegexCI_ re >>= \rx -> replaceRegex_ rx repl s
|
|
||||||
|
|
||||||
-- | A memoising version of regexReplace_. Caches the result for each
|
-- | A memoising version of regexReplace_. Caches the result for each
|
||||||
-- search pattern, replacement pattern, target string tuple.
|
-- search pattern, replacement pattern, target string tuple.
|
||||||
regexReplaceMemo_ :: Regexp -> Replacement -> String -> Either RegexError String
|
regexReplaceMemo_ :: Regexp -> Replacement -> String -> Either RegexError String
|
||||||
regexReplaceMemo_ re repl = memo (regexReplace_ re repl)
|
regexReplaceMemo_ re repl = memo (replaceRegexUnmemo_ re repl)
|
||||||
|
|
||||||
-- | Like regexReplaceMemo_ but match occurrences case-insensitively.
|
|
||||||
regexReplaceCIMemo_ :: Regexp -> Replacement -> String -> Either RegexError String
|
|
||||||
regexReplaceCIMemo_ re repl = memo (regexReplaceCI_ re repl)
|
|
||||||
|
|
||||||
-- | Replace all occurrences of the regexp, transforming each match
|
|
||||||
-- with the given function, or return an error message.
|
|
||||||
regexReplaceBy_ :: Regexp -> (String -> String) -> String -> Either RegexError String
|
|
||||||
regexReplaceBy_ r f s = toRegex_ r >>= \rx -> Right $ replaceAllBy rx f s
|
|
||||||
|
|
||||||
-- | Like regexReplaceBy_ but match occurrences case-insensitively.
|
|
||||||
regexReplaceByCI_ :: Regexp -> (String -> String) -> String -> Either RegexError String
|
|
||||||
regexReplaceByCI_ r f s = toRegexCI_ r >>= \rx -> Right $ replaceAllBy rx f s
|
|
||||||
|
|
||||||
-- helpers:
|
-- helpers:
|
||||||
|
|
||||||
-- Convert a Regexp string to a compiled Regex, or return an error message.
|
|
||||||
toRegex_ :: Regexp -> Either RegexError Regex
|
|
||||||
toRegex_ = memo (compileRegex_ defaultCompOpt defaultExecOpt)
|
|
||||||
|
|
||||||
-- Like toRegex, but make a case-insensitive Regex.
|
|
||||||
toRegexCI_ :: Regexp -> Either RegexError Regex
|
|
||||||
toRegexCI_ = memo (compileRegex_ defaultCompOpt{caseSensitive=False} defaultExecOpt)
|
|
||||||
|
|
||||||
-- Compile a Regexp string to a Regex with the given options, or return an
|
|
||||||
-- error message if this fails.
|
|
||||||
compileRegex_ :: CompOption -> ExecOption -> Regexp -> Either RegexError Regex
|
|
||||||
compileRegex_ compopt execopt r =
|
|
||||||
maybe (Left $ "this regular expression could not be compiled: " ++ show r) Right $
|
|
||||||
makeRegexOptsM compopt execopt r
|
|
||||||
|
|
||||||
-- Replace this regular expression with this replacement pattern in this
|
-- Replace this regular expression with this replacement pattern in this
|
||||||
-- string, or return an error message.
|
-- string, or return an error message.
|
||||||
replaceRegex_ :: Regex -> Replacement -> String -> Either RegexError String
|
replaceRegexUnmemo_ :: Regexp -> Replacement -> String -> Either RegexError String
|
||||||
replaceRegex_ re repl s = foldM (replaceMatch_ repl) s (reverse $ match re s :: [MatchText String])
|
replaceRegexUnmemo_ re repl s = foldM (replaceMatch_ repl) s (reverse $ match (reCompiled re) s :: [MatchText String])
|
||||||
where
|
where
|
||||||
-- Replace one match within the string with the replacement text
|
-- Replace one match within the string with the replacement text
|
||||||
-- appropriate for this match. Or return an error message.
|
-- appropriate for this match. Or return an error message.
|
||||||
@ -236,7 +215,8 @@ replaceRegex_ re repl s = foldM (replaceMatch_ repl) s (reverse $ match re s ::
|
|||||||
-- The replacement text: the replacement pattern with all
|
-- The replacement text: the replacement pattern with all
|
||||||
-- numeric backreferences replaced by the appropriate groups
|
-- numeric backreferences replaced by the appropriate groups
|
||||||
-- from this match. Or an error message.
|
-- from this match. Or an error message.
|
||||||
erepl = toRegex_ "\\\\[0-9]+" >>= \rx -> replaceAllByM rx (lookupMatchGroup_ matchgroups) replpat
|
-- FIXME: Use makeRegex instead of toRegex_
|
||||||
|
erepl = replaceAllByM backrefRegex (lookupMatchGroup_ matchgroups) replpat
|
||||||
where
|
where
|
||||||
-- Given some match groups and a numeric backreference,
|
-- Given some match groups and a numeric backreference,
|
||||||
-- return the referenced group text, or an error message.
|
-- return the referenced group text, or an error message.
|
||||||
@ -245,6 +225,7 @@ replaceRegex_ re repl s = foldM (replaceMatch_ repl) s (reverse $ match re s ::
|
|||||||
case read s of n | n `elem` indices grps -> Right $ fst (grps ! n)
|
case read s of n | n `elem` indices grps -> Right $ fst (grps ! n)
|
||||||
_ -> Left $ "no match group exists for backreference \"\\"++s++"\""
|
_ -> Left $ "no match group exists for backreference \"\\"++s++"\""
|
||||||
lookupMatchGroup_ _ s = Left $ "lookupMatchGroup called on non-numeric-backreference \""++s++"\", shouldn't happen"
|
lookupMatchGroup_ _ s = Left $ "lookupMatchGroup called on non-numeric-backreference \""++s++"\", shouldn't happen"
|
||||||
|
backrefRegex = toRegex' "\\\\[0-9]+" -- PARTIAL: should not happen
|
||||||
|
|
||||||
-- helpers
|
-- helpers
|
||||||
|
|
||||||
@ -252,12 +233,12 @@ replaceRegex_ re repl s = foldM (replaceMatch_ repl) s (reverse $ match re s ::
|
|||||||
|
|
||||||
-- Replace all occurrences of a regexp in a string, transforming each match
|
-- Replace all occurrences of a regexp in a string, transforming each match
|
||||||
-- with the given pure function.
|
-- with the given pure function.
|
||||||
replaceAllBy :: Regex -> (String -> String) -> String -> String
|
replaceAllBy :: Regexp -> (String -> String) -> String -> String
|
||||||
replaceAllBy re transform s = prependdone rest
|
replaceAllBy re transform s = prependdone rest
|
||||||
where
|
where
|
||||||
(_, rest, prependdone) = foldl' go (0, s, id) matches
|
(_, rest, prependdone) = foldl' go (0, s, id) matches
|
||||||
where
|
where
|
||||||
matches = getAllMatches $ match re s :: [(Int, Int)] -- offset and length
|
matches = getAllMatches $ match (reCompiled re) s :: [(Int, Int)] -- offset and length
|
||||||
go :: (Int,String,String->String) -> (Int,Int) -> (Int,String,String->String)
|
go :: (Int,String,String->String) -> (Int,Int) -> (Int,String,String->String)
|
||||||
go (pos,todo,prepend) (off,len) =
|
go (pos,todo,prepend) (off,len) =
|
||||||
let (prematch, matchandrest) = splitAt (off - pos) todo
|
let (prematch, matchandrest) = splitAt (off - pos) todo
|
||||||
@ -268,11 +249,11 @@ replaceAllBy re transform s = prependdone rest
|
|||||||
-- with the given monadic function. Eg if the monad is Either, a Left result
|
-- with the given monadic function. Eg if the monad is Either, a Left result
|
||||||
-- from the transform function short-circuits and is returned as the overall
|
-- from the transform function short-circuits and is returned as the overall
|
||||||
-- result.
|
-- result.
|
||||||
replaceAllByM :: forall m. Monad m => Regex -> (String -> m String) -> String -> m String
|
replaceAllByM :: forall m. Monad m => Regexp -> (String -> m String) -> String -> m String
|
||||||
replaceAllByM re transform s =
|
replaceAllByM re transform s =
|
||||||
foldM go (0, s, id) matches >>= \(_, rest, prependdone) -> pure $ prependdone rest
|
foldM go (0, s, id) matches >>= \(_, rest, prependdone) -> pure $ prependdone rest
|
||||||
where
|
where
|
||||||
matches = getAllMatches $ match re s :: [(Int, Int)] -- offset and length
|
matches = getAllMatches $ match (reCompiled re) s :: [(Int, Int)] -- offset and length
|
||||||
go :: (Int,String,String->String) -> (Int,Int) -> m (Int,String,String->String)
|
go :: (Int,String,String->String) -> (Int,Int) -> m (Int,String,String->String)
|
||||||
go (pos,todo,prepend) (off,len) =
|
go (pos,todo,prepend) (off,len) =
|
||||||
let (prematch, matchandrest) = splitAt (off - pos) todo
|
let (prematch, matchandrest) = splitAt (off - pos) todo
|
||||||
|
|||||||
@ -134,10 +134,10 @@ whitespacechars = " \t\n\r"
|
|||||||
redirectchars = "<>"
|
redirectchars = "<>"
|
||||||
|
|
||||||
escapeDoubleQuotes :: String -> String
|
escapeDoubleQuotes :: String -> String
|
||||||
escapeDoubleQuotes = regexReplace "\"" "\""
|
escapeDoubleQuotes = id -- regexReplace "\"" "\""
|
||||||
|
|
||||||
escapeQuotes :: String -> String
|
escapeQuotes :: String -> String
|
||||||
escapeQuotes = regexReplace "([\"'])" "\\1"
|
escapeQuotes = id -- regexReplace "([\"'])" "\\1"
|
||||||
|
|
||||||
-- | Quote-aware version of words - don't split on spaces which are inside quotes.
|
-- | Quote-aware version of words - don't split on spaces which are inside quotes.
|
||||||
-- NB correctly handles "a'b" but not "''a''". Can raise an error if parsing fails.
|
-- NB correctly handles "a'b" but not "''a''". Can raise an error if parsing fails.
|
||||||
@ -346,7 +346,7 @@ strWidth s = maximum $ map (foldr (\a b -> charWidth a + b) 0) $ lines s'
|
|||||||
where s' = stripAnsi s
|
where s' = stripAnsi s
|
||||||
|
|
||||||
stripAnsi :: String -> String
|
stripAnsi :: String -> String
|
||||||
stripAnsi = regexReplace "\ESC\\[([0-9]+;)*([0-9]+)?[ABCDHJKfmsu]" ""
|
stripAnsi = regexReplace (toRegex' "\ESC\\[([0-9]+;)*([0-9]+)?[ABCDHJKfmsu]") "" -- PARTIAL: should never happen, no backreferences
|
||||||
|
|
||||||
-- | Get the designated render width of a character: 0 for a combining
|
-- | Get the designated render width of a character: 0 for a combining
|
||||||
-- character, 1 for a regular character, 2 for a wide character.
|
-- character, 1 for a regular character, 2 for a wide character.
|
||||||
|
|||||||
@ -90,7 +90,7 @@ asInit d reset ui@UIState{
|
|||||||
excludeforecastq Nothing = -- not:date:tomorrow- not:tag:generated-transaction
|
excludeforecastq Nothing = -- not:date:tomorrow- not:tag:generated-transaction
|
||||||
And [
|
And [
|
||||||
Not (Date $ DateSpan (Just $ addDays 1 d) Nothing)
|
Not (Date $ DateSpan (Just $ addDays 1 d) Nothing)
|
||||||
,Not (Tag "generated-transaction" Nothing)
|
,Not (Tag (toRegexCI' "generated-transaction") Nothing)
|
||||||
]
|
]
|
||||||
|
|
||||||
-- run the report
|
-- run the report
|
||||||
|
|||||||
@ -122,7 +122,7 @@ runBrickUi uopts@UIOpts{cliopts_=copts@CliOpts{inputopts_=_iopts,reportopts_=rop
|
|||||||
where
|
where
|
||||||
acct = headDef
|
acct = headDef
|
||||||
(error' $ "--register "++apat++" did not match any account") -- PARTIAL:
|
(error' $ "--register "++apat++" did not match any account") -- PARTIAL:
|
||||||
$ filter (regexMatches apat . T.unpack) $ journalAccountNames j
|
$ filter (match (toRegexCI' apat) . T.unpack) $ journalAccountNames j
|
||||||
-- Initialising the accounts screen is awkward, requiring
|
-- Initialising the accounts screen is awkward, requiring
|
||||||
-- another temporary UIState value..
|
-- another temporary UIState value..
|
||||||
ascr' = aScreen $
|
ascr' = aScreen $
|
||||||
|
|||||||
@ -76,7 +76,7 @@ rsInit d reset ui@UIState{aopts=_uopts@UIOpts{cliopts_=CliOpts{reportopts_=ropts
|
|||||||
excludeforecastq Nothing = -- not:date:tomorrow- not:tag:generated-transaction
|
excludeforecastq Nothing = -- not:date:tomorrow- not:tag:generated-transaction
|
||||||
And [
|
And [
|
||||||
Not (Date $ DateSpan (Just $ addDays 1 d) Nothing)
|
Not (Date $ DateSpan (Just $ addDays 1 d) Nothing)
|
||||||
,Not (Tag "generated-transaction" Nothing)
|
,Not (Tag (toRegexCI' "generated-transaction") Nothing)
|
||||||
]
|
]
|
||||||
|
|
||||||
(_label,items) = accountTransactionsReport ropts' j q thisacctq
|
(_label,items) = accountTransactionsReport ropts' j q thisacctq
|
||||||
|
|||||||
@ -115,7 +115,7 @@ addForm j today = identifyForm "add" $ \extra -> do
|
|||||||
]
|
]
|
||||||
where
|
where
|
||||||
-- avoid https://github.com/simonmichael/hledger/issues/236
|
-- avoid https://github.com/simonmichael/hledger/issues/236
|
||||||
escapeJSSpecialChars = regexReplaceCI "</script>" "<\\/script>"
|
escapeJSSpecialChars = regexReplace (toRegexCI' "</script>") "<\\/script>"
|
||||||
|
|
||||||
validateTransaction ::
|
validateTransaction ::
|
||||||
FormResult Day
|
FormResult Day
|
||||||
|
|||||||
@ -72,7 +72,7 @@ writeJournalTextIfValidAndChanged f t = do
|
|||||||
-- Ensure unix line endings, since both readJournal (cf
|
-- Ensure unix line endings, since both readJournal (cf
|
||||||
-- formatdirectivep, #1194) writeFileWithBackupIfChanged require them.
|
-- formatdirectivep, #1194) writeFileWithBackupIfChanged require them.
|
||||||
-- XXX klunky. Any equivalent of "hSetNewlineMode h universalNewlineMode" for form posts ?
|
-- XXX klunky. Any equivalent of "hSetNewlineMode h universalNewlineMode" for form posts ?
|
||||||
let t' = T.pack $ regexReplace "\r" "" $ T.unpack t
|
let t' = T.pack $ regexReplace (toRegex' "\r") "" $ T.unpack t
|
||||||
liftIO (readJournal def (Just f) t') >>= \case
|
liftIO (readJournal def (Just f) t') >>= \case
|
||||||
Left e -> return (Left e)
|
Left e -> return (Left e)
|
||||||
Right _ -> do
|
Right _ -> do
|
||||||
|
|||||||
@ -137,7 +137,7 @@ builtinCommands = [
|
|||||||
-- | The commands list, showing command names, standard aliases,
|
-- | The commands list, showing command names, standard aliases,
|
||||||
-- and short descriptions. This is modified at runtime, as follows:
|
-- and short descriptions. This is modified at runtime, as follows:
|
||||||
--
|
--
|
||||||
-- PROGVERSION is replaced with the program name and version.
|
-- progversion is the program name and version.
|
||||||
--
|
--
|
||||||
-- Lines beginning with a space represent builtin commands, with format:
|
-- Lines beginning with a space represent builtin commands, with format:
|
||||||
-- COMMAND (ALIASES) DESCRIPTION
|
-- COMMAND (ALIASES) DESCRIPTION
|
||||||
@ -152,10 +152,10 @@ builtinCommands = [
|
|||||||
--
|
--
|
||||||
-- TODO: generate more of this automatically.
|
-- TODO: generate more of this automatically.
|
||||||
--
|
--
|
||||||
commandsList :: String
|
commandsList :: String -> [String] -> [String]
|
||||||
commandsList = unlines [
|
commandsList progversion othercmds = [
|
||||||
"-------------------------------------------------------------------------------"
|
"-------------------------------------------------------------------------------"
|
||||||
,"PROGVERSION"
|
,progversion
|
||||||
,"Usage: hledger COMMAND [OPTIONS] [-- ADDONCMDOPTIONS]"
|
,"Usage: hledger COMMAND [OPTIONS] [-- ADDONCMDOPTIONS]"
|
||||||
,"Commands (+ addons found in $PATH):"
|
,"Commands (+ addons found in $PATH):"
|
||||||
,""
|
,""
|
||||||
@ -208,8 +208,10 @@ commandsList = unlines [
|
|||||||
,"+api run http api server"
|
,"+api run http api server"
|
||||||
,""
|
,""
|
||||||
,"Other:"
|
,"Other:"
|
||||||
,"OTHER"
|
] ++
|
||||||
,"Help:"
|
othercmds
|
||||||
|
++
|
||||||
|
["Help:"
|
||||||
," (no arguments) show this commands list"
|
," (no arguments) show this commands list"
|
||||||
," -h show general flags"
|
," -h show general flags"
|
||||||
," COMMAND -h show flags & docs for COMMAND"
|
," COMMAND -h show flags & docs for COMMAND"
|
||||||
@ -231,25 +233,21 @@ findCommand cmdname = find (elem cmdname . modeNames . fst) builtinCommands
|
|||||||
|
|
||||||
-- | Extract the command names from commandsList: the first word
|
-- | Extract the command names from commandsList: the first word
|
||||||
-- of lines beginning with a space or + sign.
|
-- of lines beginning with a space or + sign.
|
||||||
commandsFromCommandsList :: String -> [String]
|
commandsFromCommandsList :: [String] -> [String]
|
||||||
commandsFromCommandsList s =
|
commandsFromCommandsList s =
|
||||||
[w | c:l <- lines s, c `elem` [' ','+'], let w:_ = words l]
|
[w | c:l <- s, c `elem` [' ','+'], let w:_ = words l]
|
||||||
|
|
||||||
knownCommands :: [String]
|
knownCommands :: [String]
|
||||||
knownCommands = sort $ commandsFromCommandsList commandsList
|
knownCommands = sort . commandsFromCommandsList $ commandsList prognameandversion []
|
||||||
|
|
||||||
-- | Print the commands list, modifying the template above based on
|
-- | Print the commands list, modifying the template above based on
|
||||||
-- the currently available addons. Missing addons will be removed, and
|
-- the currently available addons. Missing addons will be removed, and
|
||||||
-- extra addons will be added under Misc.
|
-- extra addons will be added under Misc.
|
||||||
printCommandsList :: [String] -> IO ()
|
printCommandsList :: [String] -> IO ()
|
||||||
printCommandsList addonsFound =
|
printCommandsList addonsFound =
|
||||||
putStr $
|
putStr . unlines . concatMap adjustline $
|
||||||
regexReplace "PROGVERSION" (prognameandversion) $
|
commandsList prognameandversion (map ('+':) unknownCommandsFound)
|
||||||
regexReplace "OTHER" (unlines $ (map ('+':) unknownCommandsFound)) $
|
|
||||||
unlines $ concatMap adjustline $ lines $
|
|
||||||
cmdlist
|
|
||||||
where
|
where
|
||||||
cmdlist = commandsList
|
|
||||||
commandsFound = map (' ':) builtinCommandNames ++ map ('+':) addonsFound
|
commandsFound = map (' ':) builtinCommandNames ++ map ('+':) addonsFound
|
||||||
unknownCommandsFound = addonsFound \\ knownCommands
|
unknownCommandsFound = addonsFound \\ knownCommands
|
||||||
|
|
||||||
|
|||||||
@ -24,7 +24,9 @@ import Data.Aeson (toJSON)
|
|||||||
import Data.Aeson.Text (encodeToLazyText)
|
import Data.Aeson.Text (encodeToLazyText)
|
||||||
import Data.List
|
import Data.List
|
||||||
import Data.Maybe
|
import Data.Maybe
|
||||||
-- import Data.Text (Text)
|
#if !(MIN_VERSION_base(4,11,0))
|
||||||
|
import Data.Semigroup ((<>))
|
||||||
|
#endif
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
import qualified Data.Text.Lazy as TL
|
import qualified Data.Text.Lazy as TL
|
||||||
import Data.Time (addDays)
|
import Data.Time (addDays)
|
||||||
@ -77,8 +79,9 @@ aregister opts@CliOpts{rawopts_=rawopts,reportopts_=ropts} j = do
|
|||||||
when (null args') $ error' "aregister needs an account, please provide an account name or pattern" -- PARTIAL:
|
when (null args') $ error' "aregister needs an account, please provide an account name or pattern" -- PARTIAL:
|
||||||
let
|
let
|
||||||
(apat:queryargs) = args'
|
(apat:queryargs) = args'
|
||||||
|
apatregex = toRegex' apat -- PARTIAL: do better
|
||||||
acct = headDef (error' $ show apat++" did not match any account") $ -- PARTIAL:
|
acct = headDef (error' $ show apat++" did not match any account") $ -- PARTIAL:
|
||||||
filter (regexMatches apat . T.unpack) $ journalAccountNames j
|
filter (match apatregex . T.unpack) $ journalAccountNames j
|
||||||
-- gather report options
|
-- gather report options
|
||||||
inclusive = True -- tree_ ropts
|
inclusive = True -- tree_ ropts
|
||||||
thisacctq = Acct $ (if inclusive then accountNameToAccountRegex else accountNameToAccountOnlyRegex) acct
|
thisacctq = Acct $ (if inclusive then accountNameToAccountRegex else accountNameToAccountOnlyRegex) acct
|
||||||
@ -97,7 +100,7 @@ aregister opts@CliOpts{rawopts_=rawopts,reportopts_=ropts} j = do
|
|||||||
excludeforecastq False = -- not:date:tomorrow- not:tag:generated-transaction
|
excludeforecastq False = -- not:date:tomorrow- not:tag:generated-transaction
|
||||||
And [
|
And [
|
||||||
Not (Date $ DateSpan (Just $ addDays 1 d) Nothing)
|
Not (Date $ DateSpan (Just $ addDays 1 d) Nothing)
|
||||||
,Not (Tag "generated-transaction" Nothing)
|
,Not (Tag (toRegex' "generated-transaction") Nothing)
|
||||||
]
|
]
|
||||||
-- run the report
|
-- run the report
|
||||||
-- TODO: need to also pass the queries so we can choose which date to render - move them into the report ?
|
-- TODO: need to also pass the queries so we can choose which date to render - move them into the report ?
|
||||||
@ -147,11 +150,11 @@ accountTransactionsReportAsText
|
|||||||
itemamt (_,_,_,_,a,_) = a
|
itemamt (_,_,_,_,a,_) = a
|
||||||
itembal (_,_,_,_,_,a) = a
|
itembal (_,_,_,_,_,a) = a
|
||||||
-- show a title indicating which account was picked, which can be confusing otherwise
|
-- show a title indicating which account was picked, which can be confusing otherwise
|
||||||
title = maybe "" (("Transactions in "++).(++" and subaccounts:")) macct
|
title = T.unpack $ maybe "" (("Transactions in "<>).(<>" and subaccounts:")) macct
|
||||||
where
|
where
|
||||||
-- XXX temporary hack ? recover the account name from the query
|
-- XXX temporary hack ? recover the account name from the query
|
||||||
macct = case filterQuery queryIsAcct thisacctq of
|
macct = case filterQuery queryIsAcct thisacctq of
|
||||||
Acct r -> Just $ init $ init $ init $ init $ init $ tail r -- Acct "^JS:expenses(:|$)"
|
Acct r -> Just . T.drop 1 . T.dropEnd 5 . T.pack $ reString r -- Acct "^JS:expenses(:|$)"
|
||||||
_ -> Nothing -- shouldn't happen
|
_ -> Nothing -- shouldn't happen
|
||||||
|
|
||||||
-- | Render one account register report line item as plain text. Layout is like so:
|
-- | Render one account register report line item as plain text. Layout is like so:
|
||||||
|
|||||||
@ -33,8 +33,8 @@ filesmode = hledgerCommandMode
|
|||||||
files :: CliOpts -> Journal -> IO ()
|
files :: CliOpts -> Journal -> IO ()
|
||||||
files CliOpts{rawopts_=rawopts} j = do
|
files CliOpts{rawopts_=rawopts} j = do
|
||||||
let args = listofstringopt "args" rawopts
|
let args = listofstringopt "args" rawopts
|
||||||
regex = headMay args
|
regex <- mapM (either fail pure . toRegex_) $ headMay args
|
||||||
files = maybe id (filter . regexMatches) regex
|
let files = maybe id (filter . match) regex
|
||||||
$ map fst
|
$ map fst
|
||||||
$ jfiles j
|
$ jfiles j
|
||||||
mapM_ putStrLn files
|
mapM_ putStrLn files
|
||||||
|
|||||||
@ -7,6 +7,7 @@ module Hledger.Cli.Commands.Tags (
|
|||||||
)
|
)
|
||||||
where
|
where
|
||||||
|
|
||||||
|
import qualified Control.Monad.Fail as Fail
|
||||||
import Data.List.Extra (nubSort)
|
import Data.List.Extra (nubSort)
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
import qualified Data.Text.IO as T
|
import qualified Data.Text.IO as T
|
||||||
@ -24,11 +25,13 @@ tagsmode = hledgerCommandMode
|
|||||||
hiddenflags
|
hiddenflags
|
||||||
([], Just $ argsFlag "[TAGREGEX [QUERY...]]")
|
([], Just $ argsFlag "[TAGREGEX [QUERY...]]")
|
||||||
|
|
||||||
|
tags :: CliOpts -> Journal -> IO ()
|
||||||
tags CliOpts{rawopts_=rawopts,reportopts_=ropts} j = do
|
tags CliOpts{rawopts_=rawopts,reportopts_=ropts} j = do
|
||||||
d <- getCurrentDay
|
d <- getCurrentDay
|
||||||
let
|
let
|
||||||
args = listofstringopt "args" rawopts
|
args = listofstringopt "args" rawopts
|
||||||
mtagpat = headMay args
|
mtagpat <- mapM (either Fail.fail pure . toRegexCI_) $ headMay args
|
||||||
|
let
|
||||||
queryargs = drop 1 args
|
queryargs = drop 1 args
|
||||||
values = boolopt "values" rawopts
|
values = boolopt "values" rawopts
|
||||||
parsed = boolopt "parsed" rawopts
|
parsed = boolopt "parsed" rawopts
|
||||||
@ -39,7 +42,7 @@ tags CliOpts{rawopts_=rawopts,reportopts_=ropts} j = do
|
|||||||
(if parsed then id else nubSort)
|
(if parsed then id else nubSort)
|
||||||
[ r
|
[ r
|
||||||
| (t,v) <- concatMap transactionAllTags txns
|
| (t,v) <- concatMap transactionAllTags txns
|
||||||
, maybe True (`regexMatchesCI` T.unpack t) mtagpat
|
, maybe True (`match` T.unpack t) mtagpat
|
||||||
, let r = if values then v else t
|
, let r = if values then v else t
|
||||||
, not (values && T.null v && not empty)
|
, not (values && T.null v && not empty)
|
||||||
]
|
]
|
||||||
|
|||||||
@ -82,14 +82,14 @@ mainmode addons = defMode {
|
|||||||
[detailedversionflag]
|
[detailedversionflag]
|
||||||
-- ++ inputflags -- included here so they'll not raise a confusing error if present with no COMMAND
|
-- ++ inputflags -- included here so they'll not raise a confusing error if present with no COMMAND
|
||||||
}
|
}
|
||||||
,modeHelpSuffix = map (regexReplace "PROGNAME" progname) [
|
,modeHelpSuffix = "Examples:" :
|
||||||
"Examples:"
|
map (progname ++) [
|
||||||
,"PROGNAME list commands"
|
" list commands"
|
||||||
,"PROGNAME CMD [--] [OPTS] [ARGS] run a command (use -- with addon commands)"
|
," CMD [--] [OPTS] [ARGS] run a command (use -- with addon commands)"
|
||||||
,"PROGNAME-CMD [OPTS] [ARGS] or run addon commands directly"
|
,"-CMD [OPTS] [ARGS] or run addon commands directly"
|
||||||
,"PROGNAME -h show general usage"
|
," -h show general usage"
|
||||||
,"PROGNAME CMD -h show command usage"
|
," CMD -h show command usage"
|
||||||
,"PROGNAME help [MANUAL] show any of the hledger manuals in various formats"
|
," help [MANUAL] show any of the hledger manuals in various formats"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user