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
40 changes: 40 additions & 0 deletions FuzzySearch.Test/FuzzyTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -583,6 +583,46 @@ public void Contains_MalformedUnicode_ComparesAsGivenWithoutThrowing()
Assert.IsTrue(result, "Text that cannot be normalized should still be matched as it was given.");
}

[TestMethod]
public void Contains_DecomposedSubjectWithLoneSurrogate_StillMatchesPrecomposedPattern()
{
// Act
bool result = Fuzzy.Contains(DecomposedCafe + " \uD800", PrecomposedCafe);

// Assert
Assert.IsTrue(result, "An unrelated lone surrogate must not switch off normalization for the rest of the subject.");
}

[TestMethod]
public void Contains_WithScore_DecomposedSubjectWithLoneSurrogate_StillMatchesPrecomposedPattern()
{
// Act
bool result = Fuzzy.Contains(DecomposedCafe + " \uD800", PrecomposedCafe, out _);

// Assert
Assert.IsTrue(result, "The scoring overload must agree that the normalized text is present.");
}

[TestMethod]
public void Contains_PatternWithLoneSurrogate_IsStillNormalized()
{
// Act
bool result = Fuzzy.Contains(PrecomposedCafe + " \uDC00", DecomposedCafe + " \uDC00");

// Assert
Assert.IsTrue(result, "A lone surrogate in the pattern must not switch off normalization for the rest of it.");
}

[TestMethod]
public void NormalizeForComparison_LoneSurrogate_KeepsItInPlaceAndNormalizesAroundIt()
{
// Act
string result = Fuzzy.NormalizeForComparison(DecomposedCafe + "\uD800" + DecomposedCafe + "\uD83D\uDE01").ToString();

// Assert
Assert.AreEqual(PrecomposedCafe + "\uD800" + PrecomposedCafe + "\uD83D\uDE01", result);
}

#endregion

#region Surrogate Pair Tests
Expand Down
53 changes: 50 additions & 3 deletions FuzzySearch/Fuzzy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,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 165 in FuzzySearch/Fuzzy.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

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

Check warning on line 165 in FuzzySearch/Fuzzy.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

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

Check warning on line 165 in FuzzySearch/Fuzzy.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

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

Check warning on line 165 in FuzzySearch/Fuzzy.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

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

Check warning on line 165 in FuzzySearch/Fuzzy.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

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

Check warning on line 165 in FuzzySearch/Fuzzy.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 35 to the 15 allowed.
{
if (pattern.IsEmpty)
{
Expand Down Expand Up @@ -359,9 +359,56 @@
}
catch (ArgumentException)
{
// Text that is not well-formed Unicode (for example a lone surrogate) cannot be normalized.
// Compare it as it was given rather than failing the match outright.
return value;
// Text that is not well-formed Unicode (for example a lone surrogate) cannot be normalized as a
// whole. Normalize the well-formed text around it instead, so one stray surrogate does not
// switch off canonical equivalence for the rest of the string.
return NormalizeWellFormedRuns(value);
}
}

/// <summary>
/// Normalizes each well-formed run of <paramref name="value"/> to <see cref="NormalizationForm.FormC"/>, leaving
/// every unpaired surrogate where it was.
/// </summary>
/// <param name="value">Text containing at least one unpaired surrogate.</param>
/// <returns>The text with its well-formed runs normalized.</returns>
/// <remarks>
/// An unpaired surrogate has no canonical composition with its neighbours, so treating it as a run boundary gives
/// the same result as normalizing the valid text on either side of it.
/// </remarks>
private static ReadOnlySpan<char> NormalizeWellFormedRuns(ReadOnlySpan<char> value)
{
StringBuilder builder = new(value.Length);
int runStart = 0;

for (int i = 0; i < value.Length; i++)
{
char c = value[i];
if (!char.IsSurrogate(c))
{
continue;
}

if (char.IsHighSurrogate(c) && i + 1 < value.Length && char.IsLowSurrogate(value[i + 1]))
{
i++;

Check warning on line 394 in FuzzySearch/Fuzzy.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Do not update the stop condition variable 'i' in the body of the for loop.

Check warning on line 394 in FuzzySearch/Fuzzy.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Do not update the stop condition variable 'i' in the body of the for loop.

Check warning on line 394 in FuzzySearch/Fuzzy.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Do not update the stop condition variable 'i' in the body of the for loop.

Check warning on line 394 in FuzzySearch/Fuzzy.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Do not update the stop condition variable 'i' in the body of the for loop.

Check warning on line 394 in FuzzySearch/Fuzzy.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Do not update the stop condition variable 'i' in the body of the for loop.

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_FuzzySearch&issues=AaDlqtpx-OaGbdNoQeTR&open=AaDlqtpx-OaGbdNoQeTR&pullRequest=95
continue;
}

AppendNormalized(builder, value[runStart..i]);
builder.Append(c);
runStart = i + 1;
}

AppendNormalized(builder, value[runStart..]);
return builder.ToString().AsSpan();

static void AppendNormalized(StringBuilder builder, ReadOnlySpan<char> run)
{
if (!run.IsEmpty)
{
builder.Append(run.ToString().Normalize(NormalizationForm.FormC));
}
}
}

Expand Down
Loading