When I display generated filenames, .NET 10 numeric string sorting gives me the order people usually expect: file9.txt before file10.txt. The new CompareOptions.NumericOrdering flag handles digit runs without a handwritten natural-sort parser.
That sounds like a presentation-only change. It is not. The same comparer also decides whether two strings are equal, which can quietly collapse file2.txt and file02.txt inside a set or dictionary. I want both parts of that contract visible before I reuse the comparer across an application.
Why ordinary string order looks wrong
Ordinal comparison reads the characters from left to right. After the shared file prefix, the first digit in file10.txt is 1, so it sorts before the 9 in file9.txt. The comparer is behaving correctly; it simply does not interpret a run of digits as a number.
string[] fileNames =
[
"file10.txt",
"file2.txt",
"file02.txt",
"file9.txt"
];
var ordinal = fileNames.Order(StringComparer.Ordinal);
That produces this sequence:
file02.txt | file10.txt | file2.txt | file9.txt
Padding every number can work when I control the naming format, but it is brittle for imported files, user-facing labels, and old data. A regex that splits text and numbers is another option, although it creates parsing, allocation, overflow, and edge-case decisions that a simple sort should not need.
.NET 10 numeric string sorting with one comparer
.NET 10 added NumericOrdering as a stable string-comparison option. The official .NET 10 library notes show that digit sequences are compared by numeric value, so 2 comes before 10.
I can package the culture and comparison rule in one StringComparer:
using System.Globalization;
StringComparer numericComparer = StringComparer.Create(
CultureInfo.InvariantCulture,
CompareOptions.NumericOrdering);
var numeric = fileNames.Order(numericComparer);
Console.WriteLine(string.Join(" | ", numeric));
The result is:
file2.txt | file02.txt | file9.txt | file10.txt
The sample uses InvariantCulture for reproducible output across machines. For labels shown directly to a person, CurrentCulture may be the better choice. The important part is to choose deliberately rather than inherit an implicit comparison rule.
I also keep this comparer close to the query that needs it. Passing it explicitly to Order makes the display rule reviewable and prevents a natural-sort policy from leaking into unrelated keys. If case should be ignored for a particular UI, CompareOptions.IgnoreCase can be combined with NumericOrdering, but that is another equality decision worth making at the call site.
Natural order also changes equality
StringComparer supplies ordering, equality, and hash codes. With NumericOrdering, leading zeroes do not change the numeric value of a digit run:
Console.WriteLine(
numericComparer.Equals("file2.txt", "file02.txt"));
// True
var names = new HashSet<string>(numericComparer)
{
"file2.txt",
"file02.txt"
};
Console.WriteLine(names.Count);
// 1
That equality is useful when chapter2 and chapter02 are alternate spellings of the same display label. It is destructive when both are real file names that must remain distinct. I use the numeric comparer only at the sorting boundary in that case and keep exact identity under StringComparer.Ordinal or the platform-appropriate file-name rule.
Equal items retain their incoming relative order in the sample, which is why file2.txt stays ahead of file02.txt. I do not treat input order as a meaningful tie-breaker. If the output must be reproducible after inputs arrive in a different order, I add an explicit ordinal secondary key while leaving identity comparisons separate.
Punctuation deserves the same care. The CompareOptions reference says a decimal point, minus sign, plus sign, or any other non-digit ends the digit sequence. As a result, v1.5 and v1.05 compare as equal: each digit run is compared independently. This is natural collation, not decimal or semantic-version parsing.
Verify the contract, not just the screen
The runnable sample on main turns these details into six deterministic checks. It has no package dependencies, credentials, network calls, or generated test data.
dotnet restore
dotnet format NumericStringSorting.csproj --verify-no-changes --no-restore
dotnet build NumericStringSorting.csproj -c Release --no-restore
dotnet run --project NumericStringSorting.csproj \
-c Release --no-build -- --verify
The verifier checks ordinal and numeric order, leading-zero equality, HashSet deduplication, punctuation behavior, and the rejection of NumericOrdering by index-based operations. A successful run ends with PASS 6/6. The merged sample pull request also records the exact validation commands.
When I would not use NumericOrdering
I would not use this comparer to parse signed numbers, decimals, dates, or semantic versions. Those domains need parsers that understand their grammar. I also would not use it for IndexOf, StartsWith, EndsWith, and related search operations; NumericOrdering is not valid for those APIs.
Most importantly, culture-aware collation should not decide authentication, authorization, protocol identifiers, or other security-sensitive equality. Microsoft's string comparison guidance recommends ordinal rules for non-linguistic and security comparisons.
For a visible list containing embedded positive integers, though, the new comparer removes a surprising amount of custom code. I just keep its equality semantics close enough that nobody mistakes natural order for exact identity.
What would you sort with NumericOrdering first: filenames, build labels, or something else?
Happy coding!
Top comments (0)