From d3bed6bec2508f529304fcd0317a5d9889956395 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 14:26:30 +0000 Subject: [PATCH] Give matches after -, ., /, \ and any whitespace the separator bonus [minor] Only '_' and a space counted as separators, so a match that starts a path segment, a kebab-case word or a dotted name scored like one buried mid-word: "domain.cs" outranked "src/main.cs" for "main". Treat whitespace plus the common identifier and path separators as word boundaries. Fixes ktsu-dev/FuzzySearch#92 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01JGKBe8wvU3P6jYj9MkNC69 --- CLAUDE.md | 2 +- FuzzySearch.Test/FuzzyTests.cs | 52 ++++++++++++++++++++++++++++++++++ FuzzySearch/Fuzzy.cs | 19 +++++++++++-- README.md | 2 +- 4 files changed, 71 insertions(+), 4 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 21a2838..072a9e5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -26,7 +26,7 @@ The library consists of a single static class `Fuzzy` in `FuzzySearch/Fuzzy.cs` The scoring algorithm rewards: - Adjacent/consecutive character matches (`adjacentMatchBonus`) -- Matches after separators like `_` or space (`matchAfterSeparatorBonus`) +- Matches after separators: whitespace, `_`, `-`, `.`, `/` or `\` (`matchAfterSeparatorBonus`) - Matches at camelCase boundaries (`camelCaseMatchBonus`) And penalizes: diff --git a/FuzzySearch.Test/FuzzyTests.cs b/FuzzySearch.Test/FuzzyTests.cs index 47c70bd..8a329a4 100644 --- a/FuzzySearch.Test/FuzzyTests.cs +++ b/FuzzySearch.Test/FuzzyTests.cs @@ -938,4 +938,56 @@ public void Score_RepeatedLettersAfterSeparatorMatch_AreCharged() } #endregion + + #region Separator Tests + + [TestMethod] + public void Score_PathSegmentMatch_OutranksMidWordMatch() + { + Assert.IsGreaterThan(ScoreOf("domain.cs", "main"), ScoreOf("src/main.cs", "main"), "A match starting a path segment should outrank one buried in a word."); + } + + [TestMethod] + public void Score_HyphenatedWordMatch_OutranksJoinedWordMatch() + { + Assert.IsGreaterThan(ScoreOf("getlist", "list"), ScoreOf("get-list", "list"), "A match after a hyphen should outrank one in the middle of a word."); + } + + [TestMethod] + [DataRow("src-main.cs")] + [DataRow("src.main.cs")] + [DataRow("src/main.cs")] + [DataRow("src\\main.cs")] + [DataRow("src\tmain.cs")] + [DataRow("src main.cs")] + public void Score_EverySeparator_EarnsTheSameBonusAsUnderscore(string subject) + { + Assert.AreEqual(ScoreOf("src_main.cs", "main"), ScoreOf(subject, "main"), $"'{subject}' should score like 'src_main.cs'."); + } + + [TestMethod] + [DataRow('_')] + [DataRow('-')] + [DataRow('.')] + [DataRow('/')] + [DataRow('\\')] + [DataRow(' ')] + [DataRow('\t')] + [DataRow('\n')] + public void IsSeparator_SeparatorCharacters_ReturnTrue(char value) + { + Assert.IsTrue(Fuzzy.IsSeparator(value)); + } + + [TestMethod] + [DataRow('a')] + [DataRow('Z')] + [DataRow('0')] + [DataRow(':')] + public void IsSeparator_OtherCharacters_ReturnFalse(char value) + { + Assert.IsFalse(Fuzzy.IsSeparator(value)); + } + + #endregion } diff --git a/FuzzySearch/Fuzzy.cs b/FuzzySearch/Fuzzy.cs index 763bfeb..2fdd1ce 100644 --- a/FuzzySearch/Fuzzy.cs +++ b/FuzzySearch/Fuzzy.cs @@ -33,7 +33,10 @@ public static class Fuzzy /// The bonus score awarded for adjacent character matches. internal const int adjacentMatchBonus = 5; - /// The bonus score awarded for matches that occur after a separator character ('_' or space). + /// + /// The bonus score awarded for matches that occur after a separator character: whitespace, '_', '-', '.', '/' or + /// '\'. + /// internal const int matchAfterSeparatorBonus = 10; /// The bonus score awarded for matches that occur at camelCase boundaries. @@ -283,7 +286,7 @@ internal static int CalculateScoreCore(ReadOnlySpan subject, ReadOnlySpan< bool isLetter = strLower != strUpper; prevLower = strChar == strLower && isLetter; - prevSeparator = strChar is '_' or ' '; + prevSeparator = IsSeparator(strChar); strIdx += strCharLength; strCodepointIdx++; @@ -299,6 +302,18 @@ internal static int CalculateScoreCore(ReadOnlySpan subject, ReadOnlySpan< return score; } + /// + /// Determines whether a character separates words, so that a match right after it earns + /// . + /// + /// The character to test. + /// + /// true for whitespace and for the identifier and path separators '_', '-', '.', '/' and '\'; otherwise, + /// false. + /// + internal static bool IsSeparator(char value) => + char.IsWhiteSpace(value) || value is '_' or '-' or '.' or '/' or '\\'; + /// /// Gets the number of UTF-16 code units occupied by the Unicode codepoint starting at the given index. /// diff --git a/README.md b/README.md index ae379bd..ca9884f 100644 --- a/README.md +++ b/README.md @@ -199,7 +199,7 @@ Both overloads take `ReadOnlySpan`, so a `string` argument is passed strai | Rule | Effect | |------|--------| | Match adjacent to the previous match | `+5` | -| Match after a `_` or space separator | `+10` | +| Match after whitespace or a `_`, `-`, `.`, `/` or `\` separator | `+10` | | Match at a camelCase boundary | `+10` | | Each unmatched character | `-1` | | Unmatched characters before the first match | `-1` each, capped at `-5` |