What's wrong
In CalculateScoreCore (FuzzySearch/Fuzzy.cs:195-259), a character can still count as a rematch after the whole pattern has matched (hasPatternChar == false), as long as it is another copy of the last pattern letter:
bool rematch = bestLetterIdx is not null && CodepointsEqual(subject, bestLetterIdx.Value, ..., subject, strIdx, ...);
When that later copy falls on a camelCase or separator boundary, its newScore beats the original letter's score, so it replaces the original:
- it earns +10 for the boundary
- it often earns +5 for adjacency as well, because
prevMatched was set by the very letter it is replacing, so this bonus is spurious
The replacement costs only -1, and each extra character costs -1. The net result is that trailing text raises the score above the exact match.
Reproduction
Observed scores from a scratch test:
| pattern |
subject |
score |
ab |
ab |
15 |
ab |
ab_b |
18 |
item |
item |
25 |
item |
itemMap |
32 |
list |
list |
25 |
list |
listTools |
30 |
test |
test |
25 |
test |
testTest |
32 |
Why it matters
Type-ahead lists built on Score, including TextFilter's Rank, show an exact name below longer names that start with it. For example, typing list ranks listTools above list.
Relation to #89
#89 touches the same rematch branch, but for the opposite case: a rematch that loses. As a check, I applied #89's proposed else { score += unmatchedLetterPenalty; } locally. itemMap stayed at 32 and testTest only went from 32 to 31, so fixing #89 does not fix this.
Suggested fix
- Allow a
rematch to replace the best letter only while the pattern is still being matched (hasPatternChar).
- At minimum, don't award the adjacency bonus when the previous match is the letter being replaced.
- Add regression tests:
Score("item","item") > Score("itemMap","item")
Score("list","list") > Score("listTools","list")
What's wrong
In
CalculateScoreCore(FuzzySearch/Fuzzy.cs:195-259), a character can still count as arematchafter the whole pattern has matched (hasPatternChar == false), as long as it is another copy of the last pattern letter:When that later copy falls on a camelCase or separator boundary, its
newScorebeats the original letter's score, so it replaces the original:prevMatchedwas set by the very letter it is replacing, so this bonus is spuriousThe replacement costs only -1, and each extra character costs -1. The net result is that trailing text raises the score above the exact match.
Reproduction
Observed scores from a scratch test:
abababab_bitemitemitemitemMaplistlistlistlistToolstesttesttesttestTestWhy it matters
Type-ahead lists built on
Score, including TextFilter'sRank, show an exact name below longer names that start with it. For example, typinglistrankslistToolsabovelist.Relation to #89
#89 touches the same
rematchbranch, but for the opposite case: a rematch that loses. As a check, I applied #89's proposedelse { score += unmatchedLetterPenalty; }locally.itemMapstayed at 32 andtestTestonly went from 32 to 31, so fixing #89 does not fix this.Suggested fix
rematchto replace the best letter only while the pattern is still being matched (hasPatternChar).Score("item","item") > Score("itemMap","item")Score("list","list") > Score("listTools","list")