Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions FuzzySearch.Test/FuzzyTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -606,6 +606,37 @@ public void Contains_SurrogatePairPattern_DoesNotMatchADifferentSurrogatePair()
Assert.IsFalse(result, "Two supplementary-plane characters sharing a high surrogate are still different characters.");
}

[TestMethod]
public void Contains_WithScore_SupplementaryPlanePrefix_PenalizedPerCharacterNotPerCodeUnit()
{
// Arrange: both subjects have exactly three unmatched characters before the match, but the emoji prefix
// occupies six UTF-16 code units rather than three.
string emojiSubject = $"{Emoji}{Emoji}{Emoji}y";
string asciiSubject = "abcy";

// Act
Fuzzy.Contains(emojiSubject, "y", out int emojiScore);
Fuzzy.Contains(asciiSubject, "y", out int asciiScore);

// Assert
Assert.AreEqual(asciiScore, emojiScore, "The prefix penalty should count skipped characters, not their UTF-16 width.");
}

[TestMethod]
public void Contains_WithScore_SupplementaryPlanePrefix_DoesNotExceedTheUncappedPenalty()
{
// Arrange: two skipped codepoints occupy four UTF-16 code units, which is still under the penalty cap,
// so a code-unit count would show through as a larger penalty rather than being hidden by the cap.
string emojiSubject = $"{Emoji}{Emoji}y";

// Act
Fuzzy.Contains(emojiSubject, "y", out int emojiScore);
Fuzzy.Contains("aby", "y", out int asciiScore);

// Assert
Assert.AreEqual(asciiScore, emojiScore, "Two skipped supplementary-plane characters should cost the same as two skipped ASCII characters.");
}

#endregion

#region Integration Tests
Expand Down
16 changes: 12 additions & 4 deletions FuzzySearch/Fuzzy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@
/// or the pattern is empty and the subject is not; otherwise, <c>false</c>.
/// </param>
/// <returns>A score representing the quality of the match. Higher scores indicate better matches.</returns>
internal static int CalculateScoreCore(ReadOnlySpan<char> subject, ReadOnlySpan<char> pattern, out bool wholePatternIsPresent)

Check warning on line 161 in FuzzySearch/Fuzzy.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 29 to the 15 allowed.

Check warning on line 161 in FuzzySearch/Fuzzy.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 29 to the 15 allowed.

Check warning on line 161 in FuzzySearch/Fuzzy.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 29 to the 15 allowed.

Check warning on line 161 in FuzzySearch/Fuzzy.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 29 to the 15 allowed.

Check warning on line 161 in FuzzySearch/Fuzzy.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 29 to the 15 allowed.

Check warning on line 161 in FuzzySearch/Fuzzy.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 29 to the 15 allowed.

Check warning on line 161 in FuzzySearch/Fuzzy.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 29 to the 15 allowed.

Check warning on line 161 in FuzzySearch/Fuzzy.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 29 to the 15 allowed.

Check warning on line 161 in FuzzySearch/Fuzzy.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 29 to the 15 allowed.

Check warning on line 161 in FuzzySearch/Fuzzy.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 29 to the 15 allowed.
{
if (pattern.IsEmpty)
{
Expand All @@ -171,6 +171,10 @@
int patternLength = pattern.Length;
int strIdx = 0;
int strLength = subject.Length;

// Counts codepoints rather than code units, so the prefix penalty reflects skipped characters and not the
// UTF-16 width of those characters.
int strCodepointIdx = 0;
bool prevMatched = false;
bool prevLower = false;
bool prevSeparator = true; // true if first letter match gets separator bonus
Expand Down Expand Up @@ -210,7 +214,7 @@
{
int newScore = 0;

score = PenalizeNonPatternCharacters(score, patternIdx, strIdx);
score = PenalizeNonPatternCharacters(score, patternIdx, strCodepointIdx);

newScore = ApplyBonuses(prevMatched, prevLower, prevSeparator, strChar, strLower, strUpper, newScore);

Expand Down Expand Up @@ -249,6 +253,7 @@
prevSeparator = strChar is '_' or ' ';

strIdx += strCharLength;
strCodepointIdx++;
}

// Apply score for last match
Expand Down Expand Up @@ -390,15 +395,18 @@
/// </summary>
/// <param name="score">The current score to apply penalties to.</param>
/// <param name="patternIdx">The current index in the pattern.</param>
/// <param name="strIdx">The current index in the subject span.</param>
/// <param name="precedingCodepointCount">
/// The number of subject codepoints preceding the match. This is a count of characters, not of UTF-16 code units,
/// so a supplementary-plane prefix is penalized the same as a basic-plane prefix of the same length.
/// </param>
/// <returns>The updated score after applying any applicable penalties.</returns>
internal static int PenalizeNonPatternCharacters(int score, int patternIdx, int strIdx)
internal static int PenalizeNonPatternCharacters(int score, int patternIdx, int precedingCodepointCount)
{
// Apply penalty for each letter before the first pattern match
// Note: Math.Max because penalties are negative values. So max is smallest penalty.
if (patternIdx == 0)
{
int penalty = Math.Max(strIdx * unmatchedPrefixLetterPenalty, maxPrefixPenalty);
int penalty = Math.Max(precedingCodepointCount * unmatchedPrefixLetterPenalty, maxPrefixPenalty);
score += penalty;
}

Expand Down