C# simple natural sort (with max digits)

Date: 2020-02-05
void Main()
{
	var list = new List<string>() {
		"AB1235",
		"AB9999",
		"AB99",
		"AB999",
		"AB12",
		"AB123",
		"14",
		"12",
		"123",
	};
	var ordered = list.OrderBy(l => PrependDigits(l));
	foreach(var item in ordered)
		Console.WriteLine(item);
}

public static Regex regexPrependDigits = new Regex(@"\d+", RegexOptions.Compiled);
public static string PrependDigits(string s) 
{
    if (string.IsNullOrWhiteSpace(s))
        return s;
    return regexPrependDigits.Replace(s, m => m.Value.PadLeft(10, '0'));
}

Output:

12
14
123
AB12
AB99
AB123
AB999
AB1235
AB9999

Typescript prependdigits version

function prependDigits(s: string, len: number = 10): string
{
    return String(s).replace(/\d+/, (m) => m.padStart(len, "0"))
}

For a more advanced version:
https://stackoverflow.com/a/78874525/1052129

using System;
using System.Collections.Generic;
using System.Globalization;

public static class SpanExtensions
{
    private static ReadOnlySpan<char> TrimStart(this ReadOnlySpan<char> span, char trimChar)
    {
        int start = 0;
        while (start < span.Length && span[start] == trimChar)
            start++;
    
        return span.Slice(start);
    }
}

public class NaturalSort
{
    public static int CompareStrings(CultureInfo culture, CompareOptions options, string x, string y)
    {
        if (x == null || y == null) return string.Compare(x, y, StringComparison.Ordinal);

        var compareInfo = culture.CompareInfo;
        int xPos = 0, yPos = 0;

        while (xPos < x.Length && yPos < y.Length)
        {
            bool xIsDigit = char.IsDigit(x[xPos]);
            bool yIsDigit = char.IsDigit(y[yPos]);

            if (xIsDigit && yIsDigit)
            {
                int numberComparison = CompareNumbers(x, ref xPos, y, ref yPos);
                if (numberComparison != 0) return numberComparison;
            }
            else
            {
                int charComparison = compareInfo.Compare(x, xPos, 1, y, yPos, 1, options);
                if (charComparison != 0) return charComparison;
                xPos++;
                yPos++;
            }
        }

        // Handle remaining characters
        if (xPos < x.Length) return 1;
        if (yPos < y.Length) return -1;

        return 0;
    }
    
    private static int CompareNumbers(string x, ref int xPos, string y, ref int yPos)
    {
        int xStart = xPos, yStart = yPos;
    
        while (xPos < x.Length && char.IsDigit(x[xPos])) xPos++;
        while (yPos < y.Length && char.IsDigit(y[yPos])) yPos++;
    
        var xSpan = x.AsSpan(xStart, xPos - xStart).TrimStart('0');
        var ySpan = y.AsSpan(yStart, yPos - yStart).TrimStart('0');
    
        if (xSpan.Length != ySpan.Length)
            return xSpan.Length.CompareTo(ySpan.Length);
    
        for (int i = 0; i < xSpan.Length; i++)
        {
            if (xSpan[i] != ySpan[i])
                return xSpan[i].CompareTo(ySpan[i]);
        }
    
        return 0;
    }
}


public class NaturalSortComparer : IComparer<string>
{
    public static NaturalSortComparer Ordinal { get; } = new NaturalSortComparer(CultureInfo.InvariantCulture, CompareOptions.Ordinal);
    public static NaturalSortComparer OrdinalIgnoreCase { get; } = new NaturalSortComparer(CultureInfo.InvariantCulture, CompareOptions.OrdinalIgnoreCase);
    public static NaturalSortComparer CurrentCulture { get; } = new NaturalSortComparer(CultureInfo.CurrentCulture, CompareOptions.None);
    public static NaturalSortComparer CurrentCultureIgnoreCase { get; } = new NaturalSortComparer(CultureInfo.CurrentCulture, CompareOptions.IgnoreCase);
    public static NaturalSortComparer InvariantCulture { get; } = new NaturalSortComparer(CultureInfo.InvariantCulture, CompareOptions.None);
    public static NaturalSortComparer InvariantCultureIgnoreCase { get; } = new NaturalSortComparer(CultureInfo.InvariantCulture, CompareOptions.IgnoreCase);

    public NaturalSortComparer(CultureInfo culture, CompareOptions options)
    {
        Culture = culture;
        Options = options;
    }

    public CultureInfo Culture { get; }
    public CompareOptions Options { get; }

    public int Compare(string x, string y)
    {
        return NaturalSort.CompareStrings(Culture, Options, x, y);
    }
}

Example usage

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;

class Program
{
    static void Main()
    {
        // Voorbeeldlijst met strings
        var items = new List<string>
        {
            "file10.txt",
            "file2.txt",
            "file1.txt",
            "file20.txt",
            "file3.txt"
        };

        Console.WriteLine("Oorspronkelijke lijst:");
        foreach (var item in items)
        {
            Console.WriteLine(item);
        }

        // Sorteer met NaturalSortComparer
        Console.WriteLine("\nGesorteerd (Natural Sort):");
        var sorted = items.OrderBy(x => x, NaturalSortComparer.InvariantCulture);
        foreach (var item in sorted)
        {
            Console.WriteLine(item);
        }

        // Sorteer omgekeerd met NaturalSortComparer
        Console.WriteLine("\nOmgekeerd gesorteerd (Natural Sort):");
        var reverseSorted = items.OrderByDescending(x => x, NaturalSortComparer.InvariantCulture);
        foreach (var item in reverseSorted)
        {
            Console.WriteLine(item);
        }
    }
}

Typescript

function compareStrings(x: string, y: string): number {
    if (x === null || y === null) return x === y ? 0 : x === null ? -1 : 1;

    let xPos = 0, yPos = 0;

    while (xPos < x.length && yPos < y.length) {
        const xIsDigit = isDigit(x[xPos]);
        const yIsDigit = isDigit(y[yPos]);

        if (xIsDigit && yIsDigit) {
            const numberComparison = compareNumbers(x, xPos, y, yPos);
            if (numberComparison.result !== 0) return numberComparison.result;
            xPos = numberComparison.xPos;
            yPos = numberComparison.yPos;
        } else {
            const charComparison = x[xPos].localeCompare(y[yPos]);
            if (charComparison !== 0) return charComparison;
            xPos++;
            yPos++;
        }
    }

    // Handle remaining characters
    if (xPos < x.length) return 1;
    if (yPos < y.length) return -1;

    return 0;
}

function compareNumbers(
    x: string,
    xStart: number,
    y: string,
    yStart: number
): { result: number; xPos: number; yPos: number } {
    let xPos = xStart, yPos = yStart;

    // Find the end of the digit sequences
    while (xPos < x.length && isDigit(x[xPos])) xPos++;
    while (yPos < y.length && isDigit(y[yPos])) yPos++;

    // Extract number spans
    const xNumber = x.slice(xStart, xPos).replace(/^0+/, '');
    const yNumber = y.slice(yStart, yPos).replace(/^0+/, '');

    // Compare lengths first (natural sort behavior)
    if (xNumber.length !== yNumber.length) {
        return { result: xNumber.length - yNumber.length, xPos, yPos };
    }

    // Compare digit by digit
    const result = xNumber.localeCompare(yNumber);
    return { result, xPos, yPos };
}

function isDigit(char: string): boolean {
    return /\d/.test(char);
}

Example usage

const items = ["file10.txt", "file2.txt", "file1.txt", "file20.txt", "file3.txt"];

console.log("Original list:");
console.log(items);

// Natural sort
const sorted = items.sort(compareStrings);

console.log("\nSorted list (Natural Sort):");
console.log(sorted);

// Reversed sort
const reverseSorted = items.sort((a, b) => compareStrings(b, a));

console.log("\nReverse sorted list:");
console.log(reverseSorted);
33520cookie-checkC# simple natural sort (with max digits)