You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
NaturalStringComparer is intransitive for non-ASCII digits (٥ < 10 < z < ٥), so sort output depends on input order #55
Follow-up to #50 / #51. That fix made two numeric chunks compare by value, whatever script their digits are in. It left two gaps.
1. A digit chunk compared with a text chunk still uses raw code points, which creates a cycle
When exactly one chunk is numeric (one string starts with a digit, the other with a non-digit), Compare falls into the else branch (Sorting/NaturalStringComparer.cs:~76) and uses string.Compare(..., Ordinal).
ASCII digits (U+0030–0039) sort before letters ordinally.
Non-ASCII digits such as Arabic-Indic ٥ (U+0665) sort after them.
Combined with the numeric path, which says ٥ == 5 and ٥ < 10, this breaks transitivity.
Reproduction (ran):
Compare("٥", "10") < 0, Compare("10", "z") < 0, and Compare("z", "٥") < 0. That is a cycle.
Compare("٥", "5") == 0, yet "5" sorts before "z" while "٥" sorts after it.
List.Sort of {"٥","10","z"} gives z, ٥, 10, but {"z","10","٥"} gives ٥, 10, z.
Why it matters: An intransitive IComparer breaks the contract List.Sort, OrderBy and SortedSet rely on. Results become order-dependent, and SortedSet/SortedDictionary can fail lookups.
2. Digits outside the BMP are still compared as text
The class remarks say any run of category Nd digits is a number "whatever script it is written in". However, the \d+ regex and char.IsDigit(Value[0]) work on UTF-16 code units, and surrogate halves are category Cs. So these digits never form a numeric chunk:
Mixed numeric/text chunks: return a fixed order (for example, numeric before text, which matches the ASCII behaviour) instead of an ordinal comparison.
Chunking: chunk by code point (Rune enumeration, Rune.IsDigit, Rune.GetNumericValue) rather than by char. Alternatively, narrow the doc comment to BMP digits.
Tests:
A property test that Compare is transitive over a mixed set of ASCII/non-ASCII digit strings and letters.
Priority: Medium. An intransitive IComparer breaks List.Sort, OrderBy and SortedSet/SortedDictionary lookups. It only triggers with non-ASCII Nd digits mixed with text, or with digits outside the BMP, which is uncommon in typical inputs.
Area / suggested assignee:NaturalStringComparer.Compare, both the mixed numeric/text branch and the chunking. Maintainer: @matt-edmondson.
Next steps: Give mixed numeric/text chunks a fixed order (numeric first, which matches ASCII) instead of an ordinal comparison. Chunk by Rune so that supplementary-plane digits form numbers, or narrow the doc comment to BMP digits. Add a transitivity property test over mixed scripts, plus the "file𝟗" < "file𝟏𝟎" case.
Activity
matt-edmondson commentedon Sep 26, 2026
Triage
IComparerbreaksList.Sort,OrderByandSortedSet/SortedDictionarylookups. It only triggers with non-ASCIINddigits mixed with text, or with digits outside the BMP, which is uncommon in typical inputs.NaturalStringComparer.Compare, both the mixed numeric/text branch and the chunking. Maintainer: @matt-edmondson.Runeso that supplementary-plane digits form numbers, or narrow the doc comment to BMP digits. Add a transitivity property test over mixed scripts, plus the"file𝟗" < "file𝟏𝟎"case.Generated by Claude Code