feat: csv: allow multiple matchers on the same line
`If blocks` and `If tables` now allow multiple matchers on the same line separated by `&&` (AND) or `&& !` (AND NOT). Example `if block` with two matchers on the same line: if %description amazon && %date 2025-02-22 account2 expenses:books Example `if table` with two matchers on the same line: if,account2 %description amazon && %date 2025-02-22, expenses:books
This commit is contained in:
parent
0d98dd1351
commit
2faceb8e1b
@ -637,18 +637,18 @@ conditionaltablep = do
|
|||||||
])
|
])
|
||||||
when (null body) $
|
when (null body) $
|
||||||
customFailure $ parseErrorAt start $ "start of conditional table found, but no assignment rules afterward"
|
customFailure $ parseErrorAt start $ "start of conditional table found, but no assignment rules afterward"
|
||||||
return $ flip map body $ \(m,vs) ->
|
return $ flip map body $ \(ms,vs) ->
|
||||||
CB{cbMatchers=[m], cbAssignments=zip fields vs}
|
CB{cbMatchers=ms, cbAssignments=zip fields vs}
|
||||||
<?> "conditional table"
|
<?> "conditional table"
|
||||||
where
|
where
|
||||||
bodylinep :: Char -> [Text] -> CsvRulesParser (Matcher,[FieldTemplate])
|
bodylinep :: Char -> [Text] -> CsvRulesParser ([Matcher],[FieldTemplate])
|
||||||
bodylinep sep fields = do
|
bodylinep sep fields = do
|
||||||
off <- getOffset
|
off <- getOffset
|
||||||
m <- matcherp' $ void $ char sep
|
ms <- matcherp' (lookAhead . void . char $ sep) `manyTill` char sep
|
||||||
vs <- T.split (==sep) . T.pack <$> lift restofline
|
vs <- T.split (==sep) . T.pack <$> lift restofline
|
||||||
if (length vs /= length fields)
|
if (length vs /= length fields)
|
||||||
then customFailure $ parseErrorAt off $ ((printf "line of conditional table should have %d values, but this one has only %d" (length fields) (length vs)) :: String)
|
then customFailure $ parseErrorAt off $ ((printf "line of conditional table should have %d values, but this one has only %d" (length fields) (length vs)) :: String)
|
||||||
else return (m,vs)
|
else return (ms,vs)
|
||||||
|
|
||||||
|
|
||||||
-- A single matcher, on one line.
|
-- A single matcher, on one line.
|
||||||
@ -700,7 +700,7 @@ matcherprefixp :: CsvRulesParser MatcherPrefix
|
|||||||
matcherprefixp = do
|
matcherprefixp = do
|
||||||
lift $ dbgparse 8 "trying matcherprefixp"
|
lift $ dbgparse 8 "trying matcherprefixp"
|
||||||
(do
|
(do
|
||||||
char '&' >> lift skipNonNewlineSpaces
|
char '&' >> optional (char '&') >> lift skipNonNewlineSpaces
|
||||||
fromMaybe And <$> optional (char '!' >> lift skipNonNewlineSpaces >> return AndNot))
|
fromMaybe And <$> optional (char '!' >> lift skipNonNewlineSpaces >> return AndNot))
|
||||||
<|> (char '!' >> lift skipNonNewlineSpaces >> return Not)
|
<|> (char '!' >> lift skipNonNewlineSpaces >> return Not)
|
||||||
<|> return Or
|
<|> return Or
|
||||||
@ -718,10 +718,12 @@ 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` (double_ampersand <|> end)
|
||||||
case toRegexCI . T.strip . T.pack $ c:cs of
|
case toRegexCI . T.strip . T.pack $ c:cs of
|
||||||
Left x -> Fail.fail $ "CSV parser: " ++ x
|
Left x -> Fail.fail $ "CSV parser: " ++ x
|
||||||
Right x -> return x
|
Right x -> return x
|
||||||
|
where
|
||||||
|
double_ampersand = lookAhead . void $ char '&' >> char '&'
|
||||||
|
|
||||||
-- -- 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.
|
||||||
@ -1559,7 +1561,7 @@ tests_RulesReader = testGroup "RulesReader" [
|
|||||||
,testCase "quoted name" $ parseWithState' defrules csvfieldreferencep "%\"csv date\"" @?= (Right "%\"csv date\"")
|
,testCase "quoted name" $ parseWithState' defrules csvfieldreferencep "%\"csv date\"" @?= (Right "%\"csv date\"")
|
||||||
]
|
]
|
||||||
|
|
||||||
,testGroup "matcherp" [
|
,testGroup "recordmatcherp" [
|
||||||
|
|
||||||
testCase "recordmatcherp" $
|
testCase "recordmatcherp" $
|
||||||
parseWithState' defrules matcherp "A A\n" @?= (Right $ RecordMatcher Or $ toRegexCI' "A A")
|
parseWithState' defrules matcherp "A A\n" @?= (Right $ RecordMatcher Or $ toRegexCI' "A A")
|
||||||
@ -1567,18 +1569,47 @@ tests_RulesReader = testGroup "RulesReader" [
|
|||||||
,testCase "recordmatcherp.starts-with-&" $
|
,testCase "recordmatcherp.starts-with-&" $
|
||||||
parseWithState' defrules matcherp "& A A\n" @?= (Right $ RecordMatcher And $ toRegexCI' "A A")
|
parseWithState' defrules matcherp "& A A\n" @?= (Right $ RecordMatcher And $ toRegexCI' "A A")
|
||||||
|
|
||||||
,testCase "fieldmatcherp.starts-with-%" $
|
,testCase "recordmatcherp.starts-with-&&" $
|
||||||
parseWithState' defrules matcherp "description A A\n" @?= (Right $ RecordMatcher Or $ toRegexCI' "description A A")
|
parseWithState' defrules matcherp "&& A A\n" @?= (Right $ RecordMatcher And $ toRegexCI' "A A")
|
||||||
|
|
||||||
,testCase "fieldmatcherp" $
|
,testCase "recordmatcherp.starts-with-&&-!" $
|
||||||
|
parseWithState' defrules matcherp "&& ! A A\n" @?= (Right $ RecordMatcher AndNot $ toRegexCI' "A A")
|
||||||
|
|
||||||
|
,testCase "recordmatcherp.does-not-start-with-%" $
|
||||||
|
parseWithState' defrules matcherp "description A A\n" @?= (Right $ RecordMatcher Or $ toRegexCI' "description A A")
|
||||||
|
]
|
||||||
|
|
||||||
|
,testGroup "fieldmatcherp" [
|
||||||
|
testCase "fieldmatcherp" $
|
||||||
parseWithState' defrules matcherp "%description A A\n" @?= (Right $ FieldMatcher Or "%description" $ toRegexCI' "A A")
|
parseWithState' defrules matcherp "%description A A\n" @?= (Right $ FieldMatcher Or "%description" $ toRegexCI' "A A")
|
||||||
|
|
||||||
,testCase "fieldmatcherp.starts-with-&" $
|
,testCase "fieldmatcherp.starts-with-&" $
|
||||||
parseWithState' defrules matcherp "& %description A A\n" @?= (Right $ FieldMatcher And "%description" $ toRegexCI' "A A")
|
parseWithState' defrules matcherp "& %description A A\n" @?= (Right $ FieldMatcher And "%description" $ toRegexCI' "A A")
|
||||||
|
|
||||||
|
,testCase "fieldmatcherp.starts-with-&&" $
|
||||||
|
parseWithState' defrules matcherp "&& %description A A\n" @?= (Right $ FieldMatcher And "%description" $ toRegexCI' "A A")
|
||||||
|
|
||||||
|
,testCase "fieldmatcherp.starts-with-&&-!" $
|
||||||
|
parseWithState' defrules matcherp "&& ! %description A A\n" @?= (Right $ FieldMatcher AndNot "%description" $ toRegexCI' "A A")
|
||||||
|
|
||||||
-- ,testCase "fieldmatcherp with operator" $
|
-- ,testCase "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")
|
||||||
|
]
|
||||||
|
|
||||||
|
,testGroup "regexp" [
|
||||||
|
testCase "regexp.ends-before-&&" $
|
||||||
|
parseWithState' defrules (regexp empty) "A A && xxx" @?= (Right $ toRegexCI' "A A")
|
||||||
|
]
|
||||||
|
|
||||||
|
, let matchers = [RecordMatcher Or (toRegexCI' "A"), RecordMatcher And (toRegexCI' "B")]
|
||||||
|
assignments = [("account2", "foo"), ("comment2", "bar")]
|
||||||
|
block = CB matchers assignments
|
||||||
|
in
|
||||||
|
testGroup "Combine multiple matchers on the same line" [
|
||||||
|
testCase "conditionalblockp" $
|
||||||
|
parseWithState' defrules conditionalblockp "if A && B\n account2 foo\n comment2 bar" @?= (Right block)
|
||||||
|
,testCase "conditionaltablep" $
|
||||||
|
parseWithState' defrules conditionaltablep "if,account2,comment2\nA && B,foo,bar" @?= (Right [block])
|
||||||
]
|
]
|
||||||
|
|
||||||
,testGroup "hledgerField" [
|
,testGroup "hledgerField" [
|
||||||
|
|||||||
@ -3741,10 +3741,13 @@ If you have trouble with it, see "Regular expressions" in the hledger manual (<h
|
|||||||
When an if block has multiple matchers, each on its own line,
|
When an if block has multiple matchers, each on its own line,
|
||||||
|
|
||||||
- By default they are OR'd (any of them can match).
|
- By default they are OR'd (any of them can match).
|
||||||
- Matcher lines beginning with `&` (and optional space) are AND'ed with the matcher above (all in the AND'ed group must match).
|
- Matcher lines beginning with `&` (or `&&`, *since 1.42*) are AND'ed with the matcher above (all in the AND'ed group must match).
|
||||||
|
- Matcher lines beginning with `& !` (*since 1.41*, or `&& !`, *since 1.42*) are first negated and then AND'ed with the matcher above.
|
||||||
|
|
||||||
*(Since 1.41:)*
|
You can also combine multiple matchers one the same line separated by `&&` (AND) or `&& !` (AND NOT).
|
||||||
You can use a negated `!` matcher on a `&` line, meaning AND NOT.
|
Eg `%description amazon && %date 2025-01-01` will match only when the
|
||||||
|
description field contains "amazon" and the date field equals 2025-01-01.
|
||||||
|
*Added in 1.42.*
|
||||||
|
|
||||||
### Match groups
|
### Match groups
|
||||||
|
|
||||||
@ -3779,9 +3782,9 @@ they can express many matchers and field assignments in a more compact tabular f
|
|||||||
```rules
|
```rules
|
||||||
if,HLEDGERFIELD1,HLEDGERFIELD2,...
|
if,HLEDGERFIELD1,HLEDGERFIELD2,...
|
||||||
MATCHERA,VALUE1,VALUE2,...
|
MATCHERA,VALUE1,VALUE2,...
|
||||||
MATCHERB,VALUE1,VALUE2,...
|
MATCHERB && MATCHERC,VALUE1,VALUE2,... (*since 1.42*)
|
||||||
; Comment line that explains MATCHERC
|
; Comment line that explains MATCHERD
|
||||||
MATCHERC,VALUE1,VALUE2,...
|
MATCHERD,VALUE1,VALUE2,...
|
||||||
<empty line>
|
<empty line>
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -3796,8 +3799,8 @@ You can use the comment lines in the table body.
|
|||||||
The table must be terminated by an empty line (or end of file).
|
The table must be terminated by an empty line (or end of file).
|
||||||
|
|
||||||
An if table like the above is interpreted as follows:
|
An if table like the above is interpreted as follows:
|
||||||
try all of the matchers;
|
try all of the lines with matchers;
|
||||||
whenever a matcher succeeds, assign all of the values on that line to the corresponding hledger fields;
|
whenever a line with matchers succeeds, assign all of the values on that line to the corresponding hledger fields;
|
||||||
If multiple lines match, later lines will override fields assigned by the earlier ones - just like the sequence of `if` blocks would behave.
|
If multiple lines match, later lines will override fields assigned by the earlier ones - just like the sequence of `if` blocks would behave.
|
||||||
|
|
||||||
If table presented above is equivalent to this sequence of if blocks:
|
If table presented above is equivalent to this sequence of if blocks:
|
||||||
@ -3808,13 +3811,13 @@ if MATCHERA
|
|||||||
HLEDGERFIELD2 VALUE2
|
HLEDGERFIELD2 VALUE2
|
||||||
...
|
...
|
||||||
|
|
||||||
if MATCHERB
|
if MATCHERB && MATCHERC
|
||||||
HLEDGERFIELD1 VALUE1
|
HLEDGERFIELD1 VALUE1
|
||||||
HLEDGERFIELD2 VALUE2
|
HLEDGERFIELD2 VALUE2
|
||||||
...
|
...
|
||||||
|
|
||||||
; Comment line which explains MATCHERC
|
; Comment line which explains MATCHERD
|
||||||
if MATCHERC
|
if MATCHERD
|
||||||
HLEDGERFIELD1 VALUE1
|
HLEDGERFIELD1 VALUE1
|
||||||
HLEDGERFIELD2 VALUE2
|
HLEDGERFIELD2 VALUE2
|
||||||
...
|
...
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user