Creating a Numeric String Comparer with .NET 9

By Tan Lee Published on Apr 16, 2025  26
While exploring the new features in .NET 10, I noticed it finally includes a numeric string comparer a much-needed addition for developers working with string data that includes numeric values.

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 of int.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 vs 1.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.