Skip to content

NaturalStringComparer is intransitive for non-ASCII digits (٥ < 10 < z < ٥), so sort output depends on input order #55

Closed

Description

@matt-edmondson

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:

  • Mathematical digits U+1D7CE–1D7FF
  • Adlam
  • Osmanya

Reproduction (ran): Compare("file𝟗", "file𝟏𝟎") returns positive (9 > 10), while Compare("file9", "file10") returns negative.

Suggested fix / acceptance criteria

  • 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.
    • "file𝟗" < "file𝟏𝟎".

Activity

  1. matt-edmondson commented on Sep 26, 2026

    @matt-edmondson
    ContributorAuthor

    Triage


    Generated by Claude Code

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

Metadata

Metadata

Labels

bugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions