diff --git a/FuzzySearch.Test/FuzzyTests.cs b/FuzzySearch.Test/FuzzyTests.cs index e31c606..4833168 100644 --- a/FuzzySearch.Test/FuzzyTests.cs +++ b/FuzzySearch.Test/FuzzyTests.cs @@ -709,6 +709,65 @@ public void Contains_WithScore_SupplementaryPlanePrefix_DoesNotExceedTheUncapped #endregion + #region Case Folding Tests + + [TestMethod] + [DataRow("ΟΔΟΣ", "οδος")] + [DataRow("bcaAσ", "ς")] + [DataRow("bcaAσ", "Σ")] + [DataRow("Μ", "µ")] + [DataRow("οδος", "ΟΔΟΣ")] + [DataRow("µ", "μ")] + public void Contains_LettersWithMoreThanOneLowercaseForm_MatchIgnoringCase(string subject, string pattern) + { + // Act + bool result = Fuzzy.Contains(subject, pattern); + bool scoredResult = Fuzzy.Contains(subject, pattern, out _); + + // Assert + Assert.IsTrue(result, $"\"{pattern}\" should match \"{subject}\" ignoring case."); + Assert.IsTrue(scoredResult, $"The scoring overload should agree that \"{pattern}\" matches \"{subject}\"."); + } + + [TestMethod] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Globalization", "CA1308:Normalize strings to uppercase", Justification = "Lower-casing the pattern is one of the two case changes under test.")] + public void Contains_ChangingThePatternsCase_NeverChangesTheResultOrScore() + { + // Arrange: an alphabet holding both sigma forms, the micro sign and the Greek mu alongside plain letters. + const string alphabet = "abAσςΣµΜμ "; + Random random = new(88); + + for (int i = 0; i < 20000; i++) + { + string subject = RandomString(random, alphabet, random.Next(0, 8)); + string pattern = RandomString(random, alphabet, random.Next(1, 4)); + + // Act + bool result = Fuzzy.Contains(subject, pattern, out int score); + bool upperResult = Fuzzy.Contains(subject, pattern.ToUpperInvariant(), out int upperScore); + bool lowerResult = Fuzzy.Contains(subject, pattern.ToLowerInvariant(), out int lowerScore); + + // Assert + Assert.AreEqual(result, upperResult, $"Upper-casing pattern \"{pattern}\" changed the match against \"{subject}\"."); + Assert.AreEqual(result, lowerResult, $"Lower-casing pattern \"{pattern}\" changed the match against \"{subject}\"."); + Assert.AreEqual(score, upperScore, $"Upper-casing pattern \"{pattern}\" changed the score against \"{subject}\"."); + Assert.AreEqual(score, lowerScore, $"Lower-casing pattern \"{pattern}\" changed the score against \"{subject}\"."); + } + } + + private static string RandomString(Random random, string alphabet, int length) + { + char[] chars = new char[length]; + for (int i = 0; i < length; i++) + { + chars[i] = alphabet[random.Next(alphabet.Length)]; + } + + return new string(chars); + } + + #endregion + #region Integration Tests [TestMethod] diff --git a/FuzzySearch/Fuzzy.cs b/FuzzySearch/Fuzzy.cs index b62af59..ab52741 100644 --- a/FuzzySearch/Fuzzy.cs +++ b/FuzzySearch/Fuzzy.cs @@ -332,9 +332,26 @@ internal static bool CodepointsEqual(ReadOnlySpan left, int leftIndex, int return leftLength == 2 ? left[leftIndex] == right[rightIndex] && left[leftIndex + 1] == right[rightIndex + 1] - : char.ToLowerInvariant(left[leftIndex]) == char.ToLowerInvariant(right[rightIndex]); + : CharsEqualIgnoringCase(left[leftIndex], right[rightIndex]); } + /// + /// Determines whether two characters are the same, ignoring case. + /// + /// The first character. + /// The second character. + /// true if the characters are equal ignoring case; otherwise, false. + /// + /// Comparing lowercase forms alone is not enough, because some letters have more than one lowercase form that + /// share an uppercase one. The Greek final sigma ς and σ both uppercase to Σ, but only + /// σ is what Σ lowercases to; likewise the micro sign µ uppercases to Μ, which + /// lowercases to μ. Matching on either the lowercase or the uppercase forms makes both pairs equal. + /// + private static bool CharsEqualIgnoringCase(char left, char right) => + left == right + || char.ToLowerInvariant(left) == char.ToLowerInvariant(right) + || char.ToUpperInvariant(left) == char.ToUpperInvariant(right); + /// /// Normalizes a span to so that canonically equivalent text compares equal. ///