mirror of
https://github.com/simonmichael/hledger.git
synced 2024-11-08 07:09:28 +03:00
lib: condition tables in csv rules + tests
This commit is contained in:
parent
a7bb6b9207
commit
834e9ec104
@ -42,16 +42,16 @@ where
|
|||||||
--- ** imports
|
--- ** imports
|
||||||
import Prelude ()
|
import Prelude ()
|
||||||
import "base-compat-batteries" Prelude.Compat hiding (fail)
|
import "base-compat-batteries" Prelude.Compat hiding (fail)
|
||||||
import qualified "base-compat-batteries" Control.Monad.Fail.Compat as Fail (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 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)
|
||||||
import Data.Char (toLower, isDigit, isSpace, ord)
|
import Data.Char (toLower, isDigit, isSpace, isAlphaNum, ord)
|
||||||
import Data.Bifunctor (first)
|
import Data.Bifunctor (first)
|
||||||
import "base-compat-batteries" Data.List.Compat
|
import "base-compat-batteries" Data.List.Compat
|
||||||
|
import qualified Data.List.Split as LS (splitOn)
|
||||||
import Data.Maybe
|
import Data.Maybe
|
||||||
import Data.Ord
|
import Data.Ord
|
||||||
import qualified Data.Set as S
|
import qualified Data.Set as S
|
||||||
@ -186,6 +186,9 @@ addAssignmentsFromList fs r = foldl' maybeAddAssignment r journalfieldnames
|
|||||||
addConditionalBlock :: ConditionalBlock -> CsvRules -> CsvRules
|
addConditionalBlock :: ConditionalBlock -> CsvRules -> CsvRules
|
||||||
addConditionalBlock b r = r{rconditionalblocks=b:rconditionalblocks r}
|
addConditionalBlock b r = r{rconditionalblocks=b:rconditionalblocks r}
|
||||||
|
|
||||||
|
addConditionalBlocks :: [ConditionalBlock] -> CsvRules -> CsvRules
|
||||||
|
addConditionalBlocks bs r = r{rconditionalblocks=bs++rconditionalblocks r}
|
||||||
|
|
||||||
getDirective :: DirectiveName -> CsvRules -> Maybe FieldTemplate
|
getDirective :: DirectiveName -> CsvRules -> Maybe FieldTemplate
|
||||||
getDirective directivename = lookup directivename . rdirectives
|
getDirective directivename = lookup directivename . rdirectives
|
||||||
|
|
||||||
@ -367,12 +370,15 @@ DIGIT: 0-9
|
|||||||
|
|
||||||
rulesp :: CsvRulesParser CsvRules
|
rulesp :: CsvRulesParser CsvRules
|
||||||
rulesp = do
|
rulesp = do
|
||||||
_ <- many $ choiceInState
|
_ <- many $ choice
|
||||||
[blankorcommentlinep <?> "blank or comment line"
|
[blankorcommentlinep <?> "blank or comment line"
|
||||||
,(directivep >>= modify' . addDirective) <?> "directive"
|
,(directivep >>= modify' . addDirective) <?> "directive"
|
||||||
,(fieldnamelistp >>= modify' . setIndexesAndAssignmentsFromList) <?> "field name list"
|
,(fieldnamelistp >>= modify' . setIndexesAndAssignmentsFromList) <?> "field name list"
|
||||||
,(fieldassignmentp >>= modify' . addAssignment) <?> "field assignment"
|
,(fieldassignmentp >>= modify' . addAssignment) <?> "field assignment"
|
||||||
,(conditionalblockp >>= modify' . addConditionalBlock) <?> "conditional block"
|
-- conditionaltablep backtracks because it shares "if" prefix with conditionalblockp and the
|
||||||
|
-- reverse is there to ensure that conditions are added in the order they listed in the file
|
||||||
|
,try (conditionaltablep >>= modify' . addConditionalBlocks . reverse) <?> "conditional table"
|
||||||
|
,(conditionalblockp >>= modify' . addConditionalBlock) <?> "conditional block"
|
||||||
]
|
]
|
||||||
eof
|
eof
|
||||||
r <- get
|
r <- get
|
||||||
@ -504,26 +510,60 @@ fieldvalp = do
|
|||||||
conditionalblockp :: CsvRulesParser ConditionalBlock
|
conditionalblockp :: CsvRulesParser ConditionalBlock
|
||||||
conditionalblockp = do
|
conditionalblockp = do
|
||||||
lift $ dbgparse 8 "trying conditionalblockp"
|
lift $ dbgparse 8 "trying conditionalblockp"
|
||||||
string "if" >> lift (skipMany spacenonewline) >> optional newline
|
-- "if\nMATCHER" or "if \nMATCHER" or "if MATCHER"
|
||||||
|
start <- getOffset
|
||||||
|
string "if" >> ( (newline >> return Nothing)
|
||||||
|
<|> (lift (skipSome spacenonewline) >> optional newline))
|
||||||
ms <- some matcherp
|
ms <- some matcherp
|
||||||
as <- many (try $ lift (skipSome spacenonewline) >> fieldassignmentp)
|
as <- catMaybes <$>
|
||||||
|
many (lift (skipSome spacenonewline) >>
|
||||||
|
choice [ lift eolof >> return Nothing
|
||||||
|
, fmap Just fieldassignmentp
|
||||||
|
])
|
||||||
when (null as) $
|
when (null as) $
|
||||||
Fail.fail "start of conditional block found, but no assignment rules afterward\n(assignment rules in a conditional block should be indented)\n"
|
customFailure $ parseErrorAt start $ "start of conditional block found, but no assignment rules afterward\n(assignment rules in a conditional block should be indented)\n"
|
||||||
return $ CB{cbMatchers=ms, cbAssignments=as}
|
return $ CB{cbMatchers=ms, cbAssignments=as}
|
||||||
<?> "conditional block"
|
<?> "conditional block"
|
||||||
|
|
||||||
|
-- A conditional table: "if" followed by separator, followed by some field names,
|
||||||
|
-- followed by many lines, each of which has:
|
||||||
|
-- one matchers, followed by field assignments (as many as there were fields)
|
||||||
|
conditionaltablep :: CsvRulesParser [ConditionalBlock]
|
||||||
|
conditionaltablep = do
|
||||||
|
lift $ dbgparse 8 "trying conditionaltablep"
|
||||||
|
start <- getOffset
|
||||||
|
string "if"
|
||||||
|
sep <- lift $ satisfy (not.isAlphaNum)
|
||||||
|
fields <- journalfieldnamep `sepBy1` (char sep)
|
||||||
|
newline
|
||||||
|
body <- flip manyTill (lift eolof) $ do
|
||||||
|
off <- getOffset
|
||||||
|
m <- matcherp' (char sep >> return ())
|
||||||
|
vs <- LS.splitOn [sep] <$> lift restofline
|
||||||
|
if (length vs /= length fields)
|
||||||
|
then customFailure $ parseErrorAt off $ ((printf "line of conditional table should have %d values, but this one has only %d\n" (length fields) (length vs)) :: String)
|
||||||
|
else return (m,vs)
|
||||||
|
when (null body) $
|
||||||
|
customFailure $ parseErrorAt start $ "start of conditional table found, but no assignment rules afterward\n"
|
||||||
|
return $ flip map body $ \(m,vs) ->
|
||||||
|
CB{cbMatchers=[m], cbAssignments=zip fields vs}
|
||||||
|
<?> "conditional table"
|
||||||
|
|
||||||
-- A single matcher, on one line.
|
-- A single matcher, on one line.
|
||||||
|
matcherp' :: CsvRulesParser () -> CsvRulesParser Matcher
|
||||||
|
matcherp' end = try (fieldmatcherp end) <|> recordmatcherp end
|
||||||
|
|
||||||
matcherp :: CsvRulesParser Matcher
|
matcherp :: CsvRulesParser Matcher
|
||||||
matcherp = try fieldmatcherp <|> recordmatcherp
|
matcherp = matcherp' (lift eolof)
|
||||||
|
|
||||||
-- A single whole-record matcher.
|
-- A single whole-record matcher.
|
||||||
-- A pattern on the whole line, not beginning with a csv field reference.
|
-- A pattern on the whole line, not beginning with a csv field reference.
|
||||||
recordmatcherp :: CsvRulesParser Matcher
|
recordmatcherp :: CsvRulesParser () -> CsvRulesParser Matcher
|
||||||
recordmatcherp = do
|
recordmatcherp end = do
|
||||||
lift $ dbgparse 8 "trying matcherp"
|
lift $ dbgparse 8 "trying recordmatcherp"
|
||||||
-- pos <- currentPos
|
-- pos <- currentPos
|
||||||
-- _ <- optional (matchoperatorp >> lift (skipMany spacenonewline) >> optional newline)
|
-- _ <- optional (matchoperatorp >> lift (skipMany spacenonewline) >> optional newline)
|
||||||
r <- regexp
|
r <- regexp end
|
||||||
-- 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 r
|
return $ RecordMatcher r
|
||||||
@ -533,8 +573,8 @@ recordmatcherp = do
|
|||||||
-- (like %date or %1), and a pattern on the rest of the line,
|
-- (like %date or %1), and a pattern on the rest of the line,
|
||||||
-- optionally space-separated. Eg:
|
-- optionally space-separated. Eg:
|
||||||
-- %description chez jacques
|
-- %description chez jacques
|
||||||
fieldmatcherp :: CsvRulesParser Matcher
|
fieldmatcherp :: CsvRulesParser () -> CsvRulesParser Matcher
|
||||||
fieldmatcherp = do
|
fieldmatcherp end = do
|
||||||
lift $ dbgparse 8 "trying fieldmatcher"
|
lift $ dbgparse 8 "trying fieldmatcher"
|
||||||
-- An optional fieldname (default: "all")
|
-- An optional fieldname (default: "all")
|
||||||
-- f <- fromMaybe "all" `fmap` (optional $ do
|
-- f <- fromMaybe "all" `fmap` (optional $ do
|
||||||
@ -545,7 +585,7 @@ fieldmatcherp = do
|
|||||||
-- optional operator.. just ~ (case insensitive infix regex) for now
|
-- optional operator.. just ~ (case insensitive infix regex) for now
|
||||||
-- _op <- fromMaybe "~" <$> optional matchoperatorp
|
-- _op <- fromMaybe "~" <$> optional matchoperatorp
|
||||||
lift (skipMany spacenonewline)
|
lift (skipMany spacenonewline)
|
||||||
r <- regexp
|
r <- regexp end
|
||||||
return $ FieldMatcher f r
|
return $ FieldMatcher f r
|
||||||
<?> "field matcher"
|
<?> "field matcher"
|
||||||
|
|
||||||
@ -557,12 +597,12 @@ csvfieldreferencep = do
|
|||||||
return $ '%' : quoteIfNeeded f
|
return $ '%' : quoteIfNeeded f
|
||||||
|
|
||||||
-- A single regular expression
|
-- A single regular expression
|
||||||
regexp :: CsvRulesParser RegexpPattern
|
regexp :: CsvRulesParser () -> CsvRulesParser RegexpPattern
|
||||||
regexp = 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` lift eolof
|
cs <- anySingle `manyTill` end
|
||||||
return $ strip $ c:cs
|
return $ strip $ c:cs
|
||||||
|
|
||||||
-- -- A match operator, indicating the type of match to perform.
|
-- -- A match operator, indicating the type of match to perform.
|
||||||
|
@ -520,7 +520,7 @@ separator TAB
|
|||||||
|
|
||||||
See also: [File Extension](#file-extension).
|
See also: [File Extension](#file-extension).
|
||||||
|
|
||||||
## `if`
|
## `if` block
|
||||||
|
|
||||||
```rules
|
```rules
|
||||||
if MATCHER
|
if MATCHER
|
||||||
@ -590,6 +590,57 @@ banking thru software
|
|||||||
comment XXX deductible ? check it
|
comment XXX deductible ? check it
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## `if` table
|
||||||
|
|
||||||
|
```rules
|
||||||
|
if,CSVFIELDNAME1,CSVFIELDNAME2,...,CSVFIELDNAMEn
|
||||||
|
MATCHER1,VALUE11,VALUE12,...,VALUE1n
|
||||||
|
MATCHER2,VALUE21,VALUE22,...,VALUE2n
|
||||||
|
MATCHER3,VALUE31,VALUE32,...,VALUE3n
|
||||||
|
<empty line>
|
||||||
|
```
|
||||||
|
|
||||||
|
Conditional tables ("if tables") are a different syntax to specify
|
||||||
|
field assignments that will be applied only to CSV records which match certain patterns.
|
||||||
|
|
||||||
|
MATCHER could be either field or record matcher, as described above. When MATCHER matches,
|
||||||
|
values from that row would be assigned to the CSV fields named on the `if` line, in the same order.
|
||||||
|
|
||||||
|
Therefore `if` table is exactly equivalent to a sequence of of `if` blocks:
|
||||||
|
```rules
|
||||||
|
if MATCHER1
|
||||||
|
CSVFIELDNAME1 VALUE11
|
||||||
|
CSVFIELDNAME2 VALUE12
|
||||||
|
...
|
||||||
|
CSVFIELDNAMEn VALUE1n
|
||||||
|
|
||||||
|
if MATCHER2
|
||||||
|
CSVFIELDNAME1 VALUE21
|
||||||
|
CSVFIELDNAME2 VALUE22
|
||||||
|
...
|
||||||
|
CSVFIELDNAMEn VALUE2n
|
||||||
|
|
||||||
|
if MATCHER3
|
||||||
|
CSVFIELDNAME1 VALUE31
|
||||||
|
CSVFIELDNAME2 VALUE32
|
||||||
|
...
|
||||||
|
CSVFIELDNAMEn VALUE3n
|
||||||
|
```
|
||||||
|
|
||||||
|
Each line starting with MATCHER should contain enough (possibly empty) values for all the listed fields.
|
||||||
|
|
||||||
|
Rules would be checked and applied in the order they are listed in the table and, like with `if` blocks, later rules (in the same or another table) or `if` blocks could override the effect of any rule.
|
||||||
|
|
||||||
|
Instead of ',' you can use a variety of other non-alphanumeric characters as a separator. First character after `if` is taken to be the separator for the rest of the table. It is the responsibility of the user to ensure that separator does not occur inside MATCHERs and values - there is no way to escape separator.
|
||||||
|
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```rules
|
||||||
|
if,account2,comment
|
||||||
|
atm transaction fee,expenses:business:banking,deductible? check it
|
||||||
|
%description groceries,expenses:groceries,
|
||||||
|
2020/01/12.*Plumbing LLC,expenses:house:upkeep,emergency plumbing call-out
|
||||||
|
```
|
||||||
|
|
||||||
## `end`
|
## `end`
|
||||||
|
|
||||||
|
168
tests/csv.test
168
tests/csv.test
@ -665,6 +665,174 @@ $ ./csvtest.sh
|
|||||||
|
|
||||||
>=0
|
>=0
|
||||||
|
|
||||||
|
# 35. tabular rules assigning multiple fields
|
||||||
|
<
|
||||||
|
10/2009/09,Flubber Co,50
|
||||||
|
|
||||||
|
RULES
|
||||||
|
fields date, description, amount
|
||||||
|
date-format %d/%Y/%m
|
||||||
|
currency $
|
||||||
|
account1 assets:myacct
|
||||||
|
if,account2,comment
|
||||||
|
Flubber,acct,cmt
|
||||||
|
$ ./csvtest.sh
|
||||||
|
2009-09-10 Flubber Co ; cmt
|
||||||
|
assets:myacct $50
|
||||||
|
acct $-50
|
||||||
|
|
||||||
|
>=0
|
||||||
|
|
||||||
|
# 36. tabular rules assigning multiple fields followed by regular rules
|
||||||
|
<
|
||||||
|
10/2009/09,Flubber Co,50
|
||||||
|
10/2009/09,Blubber Co,150
|
||||||
|
|
||||||
|
RULES
|
||||||
|
fields date, description, amount
|
||||||
|
date-format %d/%Y/%m
|
||||||
|
currency $
|
||||||
|
account1 assets:myacct
|
||||||
|
if,account2,comment
|
||||||
|
Flubber,acct,cmt
|
||||||
|
|
||||||
|
if
|
||||||
|
Blubber
|
||||||
|
account2 acct2
|
||||||
|
comment cmt2
|
||||||
|
$ ./csvtest.sh
|
||||||
|
2009-09-10 Flubber Co ; cmt
|
||||||
|
assets:myacct $50
|
||||||
|
acct $-50
|
||||||
|
|
||||||
|
2009-09-10 Blubber Co ; cmt2
|
||||||
|
assets:myacct $150
|
||||||
|
acct2 $-150
|
||||||
|
|
||||||
|
>=0
|
||||||
|
|
||||||
|
# 37. tabular rules with empty values
|
||||||
|
<
|
||||||
|
10/2009/09,Flubber Co,50
|
||||||
|
10/2009/09,Blubber Co,150
|
||||||
|
|
||||||
|
RULES
|
||||||
|
fields date, description, amount
|
||||||
|
date-format %d/%Y/%m
|
||||||
|
currency $
|
||||||
|
account1 assets:myacct
|
||||||
|
if,account2,comment
|
||||||
|
Flubber,acct,
|
||||||
|
Blubber,acct2,
|
||||||
|
$ ./csvtest.sh
|
||||||
|
2009-09-10 Flubber Co
|
||||||
|
assets:myacct $50
|
||||||
|
acct $-50
|
||||||
|
|
||||||
|
2009-09-10 Blubber Co
|
||||||
|
assets:myacct $150
|
||||||
|
acct2 $-150
|
||||||
|
|
||||||
|
>=0
|
||||||
|
|
||||||
|
# 38. tabular rules with field matchers and '|' separator
|
||||||
|
<
|
||||||
|
10/2009/09,Flubber Co,50
|
||||||
|
10/2009/09,Blubber Co,150
|
||||||
|
|
||||||
|
RULES
|
||||||
|
fields date, description, amount
|
||||||
|
date-format %d/%Y/%m
|
||||||
|
currency $
|
||||||
|
account1 assets:myacct
|
||||||
|
if|account2|comment
|
||||||
|
%description Flubber|acct|
|
||||||
|
%amount 150|acct2|cmt2
|
||||||
|
$ ./csvtest.sh
|
||||||
|
2009-09-10 Flubber Co
|
||||||
|
assets:myacct $50
|
||||||
|
acct $-50
|
||||||
|
|
||||||
|
2009-09-10 Blubber Co ; cmt2
|
||||||
|
assets:myacct $150
|
||||||
|
acct2 $-150
|
||||||
|
|
||||||
|
>=0
|
||||||
|
|
||||||
|
# 39. Insfficient number of values in tabular rules error
|
||||||
|
<
|
||||||
|
10/2009/09,Flubber Co,50
|
||||||
|
10/2009/09,Blubber Co,150
|
||||||
|
|
||||||
|
RULES
|
||||||
|
fields date, description, amount
|
||||||
|
date-format %d/%Y/%m
|
||||||
|
currency $
|
||||||
|
account1 assets:myacct
|
||||||
|
if|account2|comment
|
||||||
|
%amount 150|acct2
|
||||||
|
%description Flubber|acct|
|
||||||
|
$ ./csvtest.sh
|
||||||
|
>2
|
||||||
|
hledger: user error (input.rules:6:1:
|
||||||
|
|
|
||||||
|
6 | %amount 150|acct2
|
||||||
|
| ^
|
||||||
|
line of conditional table should have 2 values, but this one has only 1
|
||||||
|
|
||||||
|
)
|
||||||
|
>=1
|
||||||
|
|
||||||
|
# 40. unindented condition block error
|
||||||
|
<
|
||||||
|
10/2009/09,Flubber Co,50
|
||||||
|
|
||||||
|
RULES
|
||||||
|
fields date, description, amount
|
||||||
|
date-format %d/%Y/%m
|
||||||
|
currency $
|
||||||
|
account1 assets:myacct
|
||||||
|
if Flubber
|
||||||
|
account2 acct
|
||||||
|
comment cmt
|
||||||
|
$ ./csvtest.sh
|
||||||
|
>2
|
||||||
|
hledger: user error (input.rules:5:1:
|
||||||
|
|
|
||||||
|
5 | if Flubber
|
||||||
|
| ^
|
||||||
|
start of conditional block found, but no assignment rules afterward
|
||||||
|
(assignment rules in a conditional block should be indented)
|
||||||
|
|
||||||
|
)
|
||||||
|
>=1
|
||||||
|
|
||||||
|
# 41. Assignment to custom field (#1264) + spaces after the if (#1120)
|
||||||
|
<
|
||||||
|
10/2009/09,Flubber Co,50
|
||||||
|
|
||||||
|
RULES
|
||||||
|
fields date, description, amount
|
||||||
|
date-format %d/%Y/%m
|
||||||
|
currency $
|
||||||
|
account1 assets:myacct
|
||||||
|
if Flubber
|
||||||
|
myaccount2 acct
|
||||||
|
comment cmt
|
||||||
|
|
||||||
|
|
||||||
|
account2 %myaccount2
|
||||||
|
$ ./csvtest.sh
|
||||||
|
>2
|
||||||
|
hledger: user error (input.rules:6:3:
|
||||||
|
|
|
||||||
|
6 | myaccount2 acct
|
||||||
|
| ^^^^^^^^^^^^
|
||||||
|
unexpected "myaccount2 a"
|
||||||
|
expecting end of input, field assignment, or newline
|
||||||
|
)
|
||||||
|
>=1
|
||||||
|
|
||||||
## .
|
## .
|
||||||
#<
|
#<
|
||||||
#$ ./csvtest.sh
|
#$ ./csvtest.sh
|
||||||
|
@ -12,4 +12,6 @@ BEGIN{output=CSV}
|
|||||||
|
|
||||||
trap "rm -f t.$$.csv t.$$.csv.rules" EXIT ERR
|
trap "rm -f t.$$.csv t.$$.csv.rules" EXIT ERR
|
||||||
|
|
||||||
hledger -f csv:t.$$.csv --rules-file t.$$.csv.rules print "$@"
|
# Remove variable file name from error messages
|
||||||
|
:; ( hledger -f csv:t.$$.csv --rules-file t.$$.csv.rules print "$@" ) \
|
||||||
|
2> >( sed -re "s/t.*.csv/input/" >&2 )
|
||||||
|
Loading…
Reference in New Issue
Block a user