diff --git a/Sorting.Test/NaturalStringComparerIgnoreCaseTests.cs b/Sorting.Test/NaturalStringComparerIgnoreCaseTests.cs
index 7eff66f..46add13 100644
--- a/Sorting.Test/NaturalStringComparerIgnoreCaseTests.cs
+++ b/Sorting.Test/NaturalStringComparerIgnoreCaseTests.cs
@@ -117,6 +117,54 @@ public void Compare_IgnoringCase_IsTransitiveOverMixedCaseDigitsAndText()
}
}
+ [TestMethod]
+ public void Compare_IgnoringCase_LeavesLongSUnfoldedAsOrdinalIgnoreCaseDoes()
+ {
+ // char.ToUpperInvariant('ſ') is 'S', but OrdinalIgnoreCase leaves U+017F as it is.
+ Assert.IsTrue(_comparer.Compare("ſ", "t") > 0, "ſ sorts after t");
+ Assert.IsTrue(_comparer.Compare("t", "ſ") < 0, "t sorts before ſ");
+
+ foreach (string other in new[] { "s", "S", "t", "T" })
+ {
+ int expected = Math.Sign(string.Compare("ſ", other, StringComparison.OrdinalIgnoreCase));
+ Assert.AreEqual(expected, Math.Sign(_comparer.Compare("ſ", other)), $"Compare(\"ſ\", \"{other}\")");
+ Assert.AreEqual(-expected, Math.Sign(_comparer.Compare(other, "ſ")), $"Compare(\"{other}\", \"ſ\")");
+ }
+
+ string[] files = ["Tag1.txt", "ſtraße2.txt", "Rose1.txt"];
+ string[] expectedOrder = [.. files];
+ Array.Sort(files, _comparer);
+ Array.Sort(expectedOrder, StringComparer.OrdinalIgnoreCase);
+ CollectionAssert.AreEqual(expectedOrder, files);
+ }
+
+ [TestMethod]
+ public void Compare_IgnoringCase_FoldsEveryBmpLetterAsOrdinalIgnoreCaseDoes()
+ {
+ string[] probes = ["a", "s", "S", "t", "T", "z", "ß", "é", "Ω", "я"];
+
+ for (int code = 0; code <= char.MaxValue; code++)
+ {
+ char c = (char)code;
+ if (char.IsSurrogate(c) || !char.IsLetter(c))
+ {
+ continue;
+ }
+
+ string value = c.ToString();
+ foreach (string probe in probes)
+ {
+ int expected = Math.Sign(string.Compare(value, probe, StringComparison.OrdinalIgnoreCase));
+ if (expected == 0)
+ {
+ continue;
+ }
+
+ Assert.AreEqual(expected, Math.Sign(_comparer.Compare(value, probe)), $"Compare(U+{code:X4}, \"{probe}\")");
+ }
+ }
+ }
+
[TestMethod]
public void Compare_IgnoringCase_AllocatesNothing()
{
diff --git a/Sorting.Test/Sorting.Test.csproj b/Sorting.Test/Sorting.Test.csproj
index 1b37988..eb0c079 100644
--- a/Sorting.Test/Sorting.Test.csproj
+++ b/Sorting.Test/Sorting.Test.csproj
@@ -6,6 +6,9 @@
true
net10.0
+
+ false
diff --git a/Sorting/NaturalStringComparer.cs b/Sorting/NaturalStringComparer.cs
index 4d8e893..acfa190 100644
--- a/Sorting/NaturalStringComparer.cs
+++ b/Sorting/NaturalStringComparer.cs
@@ -256,7 +256,7 @@ private static int CompareFolded(string x, int xStart, string y, int yStart, int
{
for (int offset = 0; offset < length; offset++)
{
- int comparison = char.ToUpperInvariant(x[xStart + offset]).CompareTo(char.ToUpperInvariant(y[yStart + offset]));
+ int comparison = FoldCase(x[xStart + offset]).CompareTo(FoldCase(y[yStart + offset]));
if (comparison != 0)
{
return comparison;
@@ -273,7 +273,23 @@ private static int CompareFolded(string x, int xStart, string y, int yStart, int
///
/// A character from a text chunk.
/// The character to compare.
- private char Fold(char value) => ignoreCase ? char.ToUpperInvariant(value) : value;
+ private char Fold(char value) => ignoreCase ? FoldCase(value) : value;
+
+ ///
+ /// Folds to upper case the way
+ /// does. That is for every character but 'ſ' (U+017F,
+ /// LATIN SMALL LETTER LONG S): maps it to 'S', while
+ /// leaves it as it is.
+ ///
+ /// The character to fold.
+ /// The folded character.
+ private static char FoldCase(char value) => value == LatinSmallLetterLongS ? value : char.ToUpperInvariant(value);
+
+ ///
+ /// U+017F LATIN SMALL LETTER LONG S, the one BMP character whose
+ /// differs from the fold applies.
+ ///
+ private const char LatinSmallLetterLongS = '\u017F';
///
/// Compares two runs of decimal digits by the numeric value they spell, rather than by their