Creating a Numeric String Comparer with .NET 9
By Tan Lee Published on Apr 16, 2025 26
Think software versions, device models, or sequels to games. Honestly, I was surprised this wasn't already built in. But then I tried writing my own... and yeah, I get it now. Handling edge cases in string-based numeric comparisons is no joke.
s Version 1.10
greater than Version 1.2
? Should Pro Max 2
come before Pro Max 10
? What about something like Galaxy S20 Ultra
vs Galaxy S9
? These decisions are surprisingly subjective, and even a small variation in format can change the entire outcome.
So, I went ahead and wrote a numeric comparer using .NET 9 features especially leveraging Span<T>
. Here's a look at it, and I hope it helps as a foundation for your own use case.
Sorting Versions and Devices
Let’s take a list of software versions, smartphone models, and game titles. These all include numeric elements some at the end, some embedded, some decimal, and others as whole numbers.
var items = new List<string> { "Version 1.0", "Version 1.10", "Version 1.2", "Version 1.1", "Game 2", "Game 1", "Game 10", "Game 3", "Pro Max", "Pro Max 2", "Pro Max 10", "Galaxy S9", "Galaxy S20", "Galaxy S10", "Galaxy S21 Ultra", "Galaxy S21", "Version 2.0", "Version 10.0", "Game", };
Here’s how I sort them using a custom comparer:
var numericOrderer = new NumericOrderer(); var sorted = items .OrderBy(x => x, numericOrderer) .ToList(); foreach (var item in sorted) { Console.WriteLine(item); }
Expected Output
We want a natural sort order something that aligns with how a human would expect to see the list:
Game Game 1 Game 2 Game 3 Game 10 Galaxy S9 Galaxy S10 Galaxy S20 Galaxy S21 Galaxy S21 Ultra Pro Max Pro Max 2 Pro Max 10 Version 1.0 Version 1.1 Version 1.2 Version 1.10 Version 2.0 Version 10.0
The NumericOrderer Implementation
Here’s the comparer I built for this:
public sealed class NumericOrderer : IComparer<string> { public int Compare(string? x, string? y) { if (x == null && y == null) return 0; if (x == null) return -1; if (y == null) return 1; var xSpan = x.AsSpan(); var ySpan = y.AsSpan(); var commonPrefixLength = xSpan.CommonPrefixLength(ySpan); while (commonPrefixLength > 0) { xSpan = xSpan[commonPrefixLength..]; ySpan = ySpan[commonPrefixLength..]; commonPrefixLength = xSpan.CommonPrefixLength(ySpan); } if (double.TryParse(xSpan, out var xNumber) && double.TryParse(ySpan, out var yNumber)) { return xNumber.CompareTo(yNumber); } return xSpan.CompareTo(ySpan, StringComparison.OrdinalIgnoreCase); } }
Key Takeaways
CommonPrefixLength is a neat trick for identifying where strings diverge.
I'm using
double.TryParse()
here instead ofint.TryParse()
to better handle decimal values.StringComparison.OrdinalIgnoreCase
ensures the comparison is case-insensitive—tweak it as needed.This is still a pretty simplified numeric parser. Real-world cases may involve:
Roman numerals (
Version II
)Mixed formatting (
v1.2-alpha
)Multiple numeric parts (
1.0.0
vs1.0.10
)
Writing a general-purpose string numeric comparer is deceptively complex. In most real-world apps, you’ll likely be better off implementing something specific to your data structure, especially if you already have version fields as int
or double
.
That said, this was a fun exercise in working with Span<T>
and thinking through string parsing and comparison in a more memory-efficient way.