How do I sort strings alphabetically while accounting for value when a string is numeric?

Viewed 125818

I'm trying to sort an array of numbers that are strings and I'd like them to sort numerically.

The catch is that I cannot convert the numbers into int.

Here is the code:

string[] things= new string[] { "105", "101", "102", "103", "90" };

foreach (var thing in things.OrderBy(x => x))
{
    Console.WriteLine(thing);
}

Output:

101, 102, 103, 105, 90

I'd like:

90, 101, 102, 103, 105

EDIT: The output can't be 090, 101, 102...

Updated the code sample to say "things" instead of "sizes". The array can be something like this:

string[] things= new string[] { "paul", "bob", "lauren", "007", "90" };

That means it needs to be sorted alphabetically and by number:

007, 90, bob, lauren, paul
23 Answers

Expanding on Jeff Paulsen answer. I wanted to make sure it didn't matter how many number or char groups were in the strings:

public class SemiNumericComparer : IComparer<string>
{
    public int Compare(string s1, string s2)
    {
        if (int.TryParse(s1, out var i1) && int.TryParse(s2, out var i2))
        {
            if (i1 > i2)
            {
                return 1;
            }

            if (i1 < i2)
            {
                return -1;
            }

            if (i1 == i2)
            {
                return 0;
            }
        }

        var text1 = SplitCharsAndNums(s1);
        var text2 = SplitCharsAndNums(s2);

        if (text1.Length > 1 && text2.Length > 1)
        {

            for (var i = 0; i < Math.Max(text1.Length, text2.Length); i++)
            {

                if (text1[i] != null && text2[i] != null)
                {
                    var pos = Compare(text1[i], text2[i]);
                    if (pos != 0)
                    {
                        return pos;
                    }
                }
                else
                {
                    //text1[i] is null there for the string is shorter and comes before a longer string.
                    if (text1[i] == null)
                    {
                        return -1;
                    }
                    if (text2[i] == null)
                    {
                        return 1;
                    }
                }
            }
        }

        return string.Compare(s1, s2, true);
    }

    private string[] SplitCharsAndNums(string text)
    {
        var sb = new StringBuilder();
        for (var i = 0; i < text.Length - 1; i++)
        {
            if ((!char.IsDigit(text[i]) && char.IsDigit(text[i + 1])) ||
                (char.IsDigit(text[i]) && !char.IsDigit(text[i + 1])))
            {
                sb.Append(text[i]);
                sb.Append(" ");
            }
            else
            {
                sb.Append(text[i]);
            }
        }

        sb.Append(text[text.Length - 1]);

        return sb.ToString().Split(' ');
    }
}

I also took SplitCharsAndNums from an SO Page after amending it to deal with file names.

Example of short IComparer class.

  1. if both string arguments can be converted to integer then arguments are parsed to integers and compared
  2. if only one argument can be converted to integer, then integer is prioritized (has lower value) and are inserted before string.
  3. If no one of arguments can be converted into integer then ordinary string comparison is used.

Code:

public class CompareIntegerStrings : IComparer<string>
    {
        public int Compare(string x, string y)
        {
            if (int.TryParse(x, out int xOut) && int.TryParse(y, out int yOut))
                return xOut.CompareTo(yOut);
            else if (int.TryParse(x, out _))
                return -1;
            else if (int.TryParse(y, out _))
                return 1;
            else
                return x.CompareTo(y);
    
        }
    }

In this example

List<string> intStrings = new List<string> { "01","0022","abba", "11", "deep purple", "02", };
List<string> orderedIntStrings = intStrings.OrderBy(i=>i,new CompareIntegerStrings()).ToList();

ordered list orderedIntString are { "01","02","11","0022","abba","deep purple"}.

Using Regex.Replace is so simple yet efficient. Note that the number "3" just has to be a number equal-to or larger than your longest string, so for anyone else, increase as needed.

using System.Text.RegularExpressions;

string[] things = new string[] { "105", "101", "102", "103", "90" };

foreach (var thing in things.OrderBy(x => Regex.Replace(x, @"\d+", i => 
i.Value.PadLeft(3, '0'))))
{
    Console.WriteLine(thing);
}

I would have commented under recursive's answer, but my reputation is too low for that.

Because recursive's answer only works with numeric strings (if You have a string like "I am just a damn long string", it would be sorted after "Not so long string") and OP edited his answer, my Idea for the question would be to sort the strings by differentiating them into numbers and not numbers:

int maxlen = items.Max(x => x.Length);
var items  = items.OrderBy(x => long.TryParse(x, out _) == true ? x.PadLeft(maxlen, '0') : x);

The underscore is for discarding the output

Recommend using NaturalSort.Extension(nuget/github), as it is a reasonably difficult operation as you can see from the answer.

using NaturalSort.Extension;
var ordered = things.OrderBy(x => x, StringComparison.OrdinalIgnoreCase.WithNaturalSort());
namespace X
{
    public class Utils
    {
        public class StrCmpLogicalComparer : IComparer<Projects.Sample>
        {
            [DllImport("Shlwapi.dll", CharSet = CharSet.Unicode)]
            private static extern int StrCmpLogicalW(string x, string y);


            public int Compare(Projects.Sample x, Projects.Sample y)
            {
                string[] ls1 = x.sample_name.Split("_");
                string[] ls2 = y.sample_name.Split("_");
                string s1 = ls1[0];
                string s2 = ls2[0];
                return StrCmpLogicalW(s1, s2);
            }
        }

    }
}
Related