Skip to content

NaturalStringComparer has no ignore-case option, so File2.txt and README.md sort ahead of build3.log and file1.txt #66

Description

@matt-edmondson

What's wrong

NaturalStringComparer compares text chunks with string.CompareOrdinal. Every uppercase letter therefore sorts before every lowercase one. The comparer exists mostly to sort file and folder names, and in those names case is usually an accident. A capitalised name drops into a separate block ahead of all the lowercase names, and its numbers stop interleaving with the rest.

The class has no constructor, option or static instance that changes this. To work around it, a caller has to normalise case themselves, which allocates on every comparison and loses the original strings' tie-break order.

Repro

using ktsu.Sorting;
string[] files = ["file10.txt", "File2.txt", "file1.txt", "README.md", "build3.log", "Build20.log"];
Array.Sort(files, new NaturalStringComparer());
Console.WriteLine(string.Join(", ", files));
  • Observed (main @ afc17e2, net10.0): Build20.log, File2.txt, README.md, build3.log, file1.txt, file10.txt
  • Expected with an ignore-case option: build3.log, Build20.log, file1.txt, File2.txt, file10.txt, README.md. This is the order Windows Explorer and Finder show.

The existing test Compare_SimpleStrings_SortsAlphabetically says "should be case-sensitive by default". That reads as a default, not a deliberate exclusion, so this proposal keeps it.

Why it matters

Natural sort is used almost entirely for names that people read: files, versions, tags and menu entries. When a stray capital letter moves an item to the top of one of those lists, the user sees it as a bug.

Suggested fix and acceptance criteria

  • Add an opt-in ignore-case mode and keep the current behaviour as the default. For example, a NaturalStringComparer(bool ignoreCase) constructor plus static Ordinal and OrdinalIgnoreCase instances.
  • In that mode, compare text chunks with StringComparison.OrdinalIgnoreCase, using either MemoryExtensions.CompareTo over spans or string.Compare(x, xi, y, yi, len, StringComparison.OrdinalIgnoreCase). Keep it allocation-free: Compare_AllocatesNothing must still pass with the option on.
  • Keep the comparer a total order:
    • Strings that are equal ignoring case tie-break consistently with an ordinal comparison, in line with the tie-break in [patch] Break natural-order ties ordinally so distinct strings never compare equal #65.
    • The number-vs-text branch stays consistent with the case-folded text comparison. Today it compares the digit against the raw first character, and folding can change where that character sits relative to digits.
    • The transitivity/antisymmetry fuzz is re-run with the option on.
  • Tests:
  • The README documents the option.

Activity

  1. matt-edmondson commented on Oct 6, 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

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions