Skip to content

Leave a long s unfolded in OrdinalIgnoreCase, as StringComparison does [patch] - #80

Open
matt-edmondson wants to merge 1 commit into
mainfrom
fix/79-long-s-ordinal-ignore-case
Open

matt-edmondson wants to merge 1 commit into
mainfrom
fix/79-long-s-ordinal-ignore-case

Conversation

@matt-edmondson

Copy link
Copy Markdown
Contributor

Fixes #79

What was wrong

NaturalStringComparer.OrdinalIgnoreCase says it folds text "as StringComparison.OrdinalIgnoreCase folds it", but Fold and CompareFolded both call char.ToUpperInvariant. Under ICU, char.ToUpperInvariant('ſ') (U+017F) returns 'S', while OrdinalIgnoreCase leaves U+017F alone. So Compare("ſ", "t") came out negative, and ["Tag1.txt", "ſtraße2.txt", "Rose1.txt"] sorted differently from StringComparer.OrdinalIgnoreCase.

Change

  • Added a new FoldCase(char) helper. It is char.ToUpperInvariant except for U+017F, which it leaves unchanged. Both Fold and CompareFolded now call it, so the two fold paths can't drift apart.
  • Sorting.Test.csproj sets <InvariantGlobalization>false</InvariantGlobalization>. ktsu.Sdk turns invariant globalization on, and invariant casing already leaves U+017F alone. Under it the bug can't reproduce, and the new tests passed with or without the fix. Apps run under ICU, so the tests now do too.

Tests

  • Compare_IgnoringCase_LeavesLongSUnfoldedAsOrdinalIgnoreCaseDoes checks that Compare("ſ", "t") > 0. For "s", "S", "t" and "T", both argument orders must agree in sign with string.Compare(..., OrdinalIgnoreCase). It also checks that the issue's three file names sort the way StringComparer.OrdinalIgnoreCase sorts them.
  • Compare_IgnoringCase_FoldsEveryBmpLetterAsOrdinalIgnoreCaseDoes compares every BMP letter against ten probes and requires the sign to match OrdinalIgnoreCase wherever that result is non-zero. This confirms that U+017F is the only letter where the two folds disagree.
  • With the fix reverted, both new tests fail. With it, all 41 pass locally on net10.0.

Note on #78

Open PR #78 (code-point fold for letters outside the BMP) touches the same fold paths, so whichever PR merges second will need a small rebase. If #78 lands first, its code-point fold should also go through FoldCase for BMP characters.


Generated by Claude Code

…s [patch]

char.ToUpperInvariant('ſ') is 'S', but StringComparison.OrdinalIgnoreCase
leaves U+017F alone, so NaturalStringComparer.OrdinalIgnoreCase sorted
"ſtraße2.txt" before "Tag1.txt" where OrdinalIgnoreCase puts it after. Fold
through one helper that keeps U+017F as it is, in both Fold and CompareFolded.

The test project now runs with invariant globalization off: ktsu.Sdk turns it
on, and invariant casing already leaves U+017F alone, which hid the bug from
the tests.

Fixes #79

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017vfJW66FJ9HR9vCd1x5h4H
@sonarqubecloud

sonarqubecloud Bot commented Nov 7, 2026

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

OrdinalIgnoreCase folds "ſ" (U+017F, long s) to "S", so "ſtraße2.txt" sorts before "Tag1.txt" where StringComparison.OrdinalIgnoreCase puts it after

2 participants