Try the most specific rule first, not the topmost
File order decided precedence, so a narrow rule had to be written above the broad one it carves an exception out of -- an ordering constraint the file cannot show and the user has to remember. *NIKOLA* below *NIK* silently matched nothing, and a catch-all * could only ever be the last line. Engine.New now sorts once and MatchIndex walks that order: most literal characters first, then fewest *, then account-scoped over unscoped. Literals are what a rule commits to and a * is what it gives up, so a bare * is tried last wherever it sits. The sort is stable, so equally specific rules keep file order and the earlier one wins -- which is all position decides now, and why AppendRule can keep appending without displacing a rule written by hand. The two orders must not be confused: Rules(), Usage and MatchIndex still speak in file positions, because that is what the rules screen numbers and what DeleteRules deletes by. A shadowed rule still reports zero usage, but a zero no longer says anything about where the rule sits. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -180,33 +180,40 @@ the folders on disk and the index; tags from every tag in use plus any named in
|
||||
which is what stops `groceries` acquiring a `grocery` twin.
|
||||
|
||||
Leaving the account blank applies the rule everywhere; filling it in also
|
||||
narrows the preview to that account. Rules are appended, so anything already in
|
||||
`rules.toml` keeps precedence — the preview accounts for that automatically,
|
||||
since it only ever lists transactions no existing rule has already tagged. Legs
|
||||
of a matched transfer are left out too: they are already accounted for by the
|
||||
definition that paired them, and the report leaves them out anyway.
|
||||
narrows the preview to that account. The preview lists only transactions no
|
||||
existing rule has already tagged, so it answers "what is still waiting for a
|
||||
rule" rather than "what would this rule win". Those differ when the new rule is
|
||||
narrower than an existing one: a `*LIDL SOFIA*` written while `*LIDL*` is
|
||||
already tagging those rows previews as nothing and still takes them on save,
|
||||
because the more specific rule wins. Legs of a matched transfer are left out
|
||||
too: they are already accounted for by the definition that paired them, and the
|
||||
report leaves them out anyway.
|
||||
|
||||
### Rules (`5`)
|
||||
|
||||
Every rule in file order, with the number of transactions it actually claims.
|
||||
Rules that claim none are marked `✗`.
|
||||
Every rule in file order — the order they are written in, not the order they
|
||||
are tried in — with the number of transactions it actually claims. Rules that
|
||||
claim none are marked `✗`.
|
||||
|
||||
```
|
||||
money · rules · 5 rules · 2 match nothing
|
||||
|
||||
# Pattern Account Tag Txns Note
|
||||
1 *LIDL* (all) groceries 3 the weekly shop
|
||||
2 ✗ *LIDL SOFIA* (all) shadowed 0
|
||||
1 *LIDL SOFIA* (all) groceries 3 the weekly shop
|
||||
2 ✗ *LIDL* (all) shadowed 0
|
||||
3 ✗ *OLD BANK NAME* (all) dead 0 closed in 2025
|
||||
4 *ZARA* (all) clothes 1
|
||||
5 *КАУФЛАНД* checking groceries 1 4412 is the branch
|
||||
```
|
||||
|
||||
The count is how many transactions the rule *wins*, not how many its glob could
|
||||
match. Rule 2 above matches `LIDL SOFIA 4412` perfectly well, but rule 1 claims
|
||||
it first, so rule 2 is dead weight — which is the point of the screen. A rule
|
||||
can therefore reach zero either by matching nothing or by being shadowed, and
|
||||
both are worth deleting.
|
||||
match. Rule 2 above matches `LIDL SOFIA 4412` perfectly well, but rule 1 spells
|
||||
out more of it and so claims it first; with nothing else here that says `LIDL`,
|
||||
rule 2 is dead weight — which is the point of the screen. A rule can therefore
|
||||
reach zero either by matching nothing or by being shadowed, and both are worth
|
||||
deleting. Note that shadowing has nothing to do with the numbering: rule 2 would
|
||||
be just as dead written above rule 1, because precedence is decided by how
|
||||
specific a rule is and not by where it sits.
|
||||
|
||||
`d` deletes the selected rule, `p` deletes every rule marked `✗` at once, and
|
||||
both ask for a `y` first. `r` recounts against what is currently in the index,
|
||||
@@ -317,9 +324,24 @@ it. `r` re-pairs against what is currently in the index.
|
||||
|
||||
## rules.toml
|
||||
|
||||
Rules are evaluated in file order and the **first match wins**, so put specific
|
||||
rules above general ones. Patterns are globs (`*` and `?`) matched
|
||||
case-insensitively, with whitespace collapsed.
|
||||
**The most specific rule wins**, so `*NIKOLA*` claims what it names even with a
|
||||
broad `*NIK*` sitting above it, and a catch-all can be written anywhere without
|
||||
swallowing the file. Patterns are globs (`*` and `?`) matched case-insensitively,
|
||||
with whitespace collapsed.
|
||||
|
||||
Specificity is measured on what a rule spells out, in this order:
|
||||
|
||||
1. how many literal characters its patterns pin down — `*LIDL SOFIA*` (10)
|
||||
beats `*LIDL*` (4), and a bare `*` (0) is tried last of all. A rule setting
|
||||
both `match` and `type` has to satisfy both, so both count.
|
||||
2. how few `*` it uses, the only wildcard that swallows a run of any length:
|
||||
`LIDL` fixes both ends where `*LIDL*` does not, so it goes first. (`?` is
|
||||
not counted either way: it fixes a length, not a character.)
|
||||
3. whether it names an `account`, which is a narrowing the unscoped rule with
|
||||
the same patterns does not have.
|
||||
|
||||
Rules that tie on all three fall back to file order, and the earlier one wins —
|
||||
which is what keeps appending a rule from disturbing one already written.
|
||||
|
||||
A rule matches on `match` (the description) and `type` (the bank's own
|
||||
classification). Setting both is an "and": both must match.
|
||||
|
||||
Reference in New Issue
Block a user