Skip to content
Open
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
48 changes: 48 additions & 0 deletions Sorting.Test/NaturalStringComparerIgnoreCaseTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,54 @@
}
}

[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");

Check warning on line 124 in Sorting.Test/NaturalStringComparerIgnoreCaseTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.IsGreaterThan' instead of 'Assert.IsTrue'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_Sorting&issues=AaG1lzoxMOgPm5XekFKc&open=AaG1lzoxMOgPm5XekFKc&pullRequest=80
Assert.IsTrue(_comparer.Compare("t", "ſ") < 0, "t sorts before ſ");

Check warning on line 125 in Sorting.Test/NaturalStringComparerIgnoreCaseTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.IsLessThan' instead of 'Assert.IsTrue'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_Sorting&issues=AaG1lzoxMOgPm5XekFKd&open=AaG1lzoxMOgPm5XekFKd&pullRequest=80

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);

Check warning on line 138 in Sorting.Test/NaturalStringComparerIgnoreCaseTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.AreSequenceEqual' instead of 'CollectionAssert.AreEqual'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_Sorting&issues=AaG1lzoxMOgPm5XekFKb&open=AaG1lzoxMOgPm5XekFKb&pullRequest=80
}

[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()
{
Expand Down
3 changes: 3 additions & 0 deletions Sorting.Test/Sorting.Test.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
<IsTestProject>true</IsTestProject>
<TargetFramework>net10.0</TargetFramework>
<TargetFrameworks></TargetFrameworks>
<!-- ktsu.Sdk turns invariant globalization on, and its casing leaves 'ſ' (U+017F) alone. Apps use ICU,
where char.ToUpperInvariant('ſ') is 'S', so the case-folding tests have to run against ICU too (#79). -->
<InvariantGlobalization>false</InvariantGlobalization>
</PropertyGroup>

<ItemGroup>
Expand Down
20 changes: 18 additions & 2 deletions Sorting/NaturalStringComparer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
/// ordinally.
/// Greater than zero if x is greater than y.
/// </returns>
public int Compare(string? x, string? y)

Check warning on line 62 in Sorting/NaturalStringComparer.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

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

Check warning on line 62 in Sorting/NaturalStringComparer.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

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

Check warning on line 62 in Sorting/NaturalStringComparer.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

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

Check warning on line 62 in Sorting/NaturalStringComparer.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

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

Check warning on line 62 in Sorting/NaturalStringComparer.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

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

Check warning on line 62 in Sorting/NaturalStringComparer.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

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

Check warning on line 62 in Sorting/NaturalStringComparer.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

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

Check warning on line 62 in Sorting/NaturalStringComparer.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

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

Check warning on line 62 in Sorting/NaturalStringComparer.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

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

Check warning on line 62 in Sorting/NaturalStringComparer.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 22 to the 15 allowed.
{
if (x == null && y == null)
{
Expand Down Expand Up @@ -256,7 +256,7 @@
{
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;
Expand All @@ -273,7 +273,23 @@
/// </summary>
/// <param name="value">A character from a text chunk.</param>
/// <returns>The character to compare.</returns>
private char Fold(char value) => ignoreCase ? char.ToUpperInvariant(value) : value;
private char Fold(char value) => ignoreCase ? FoldCase(value) : value;

/// <summary>
/// Folds <paramref name="value"/> to upper case the way <see cref="StringComparison.OrdinalIgnoreCase"/>
/// does. That is <see cref="char.ToUpperInvariant(char)"/> for every character but <c>'ſ'</c> (U+017F,
/// LATIN SMALL LETTER LONG S): <see cref="char.ToUpperInvariant(char)"/> maps it to <c>'S'</c>, while
/// <see cref="StringComparison.OrdinalIgnoreCase"/> leaves it as it is.
/// </summary>
/// <param name="value">The character to fold.</param>
/// <returns>The folded character.</returns>
private static char FoldCase(char value) => value == LatinSmallLetterLongS ? value : char.ToUpperInvariant(value);

/// <summary>
/// U+017F LATIN SMALL LETTER LONG S, the one BMP character whose <see cref="char.ToUpperInvariant(char)"/>
/// differs from the fold <see cref="StringComparison.OrdinalIgnoreCase"/> applies.
/// </summary>
private const char LatinSmallLetterLongS = '\u017F';

/// <summary>
/// Compares two runs of decimal digits by the numeric value they spell, rather than by their
Expand Down
Loading