1

I want to this list

A
B
C
111
11
123
1
42
5

To be sorted

1
5
11
42
111
123
A
B
C

By default, it sorts numbers like strings (So, it goes 1,11,111,123,42,5), But I want to sort numbers like numbers, and than strings that are not numbers.

Is there clean solution to sort it like above?

It is a list of objects, and object has several properties, one of which is a this string.

7
  • 1
    What type is your list to start with? What have you tried already? Commented Mar 31, 2017 at 19:48
  • Are your numbers represented as strings, or are they integers? Commented Mar 31, 2017 at 19:49
  • stackoverflow.com/questions/119730/… Commented Mar 31, 2017 at 19:50
  • 2
    list.GroupBy(s => int.TryParse(s, out var ignore)).OrderByDescending(g => g.Key).SelectMany(g => g.Key ? g.OrderBy(s => int.Parse(s)) : g.OrderBy(s => s)); Commented Mar 31, 2017 at 19:50
  • 1
    that might technically work, but I feel sorry for the next programmer after you who has to figure out what that line of code is doing, lol Commented Mar 31, 2017 at 19:53

8 Answers 8

4

This will work for most use cases, but may have odd results if the string starts with control characters, string like "\tabc" will come before the integers:

list.OrderBy(x=>int.TryParse(x, out var dummy) ? dummy.ToString("D10") : x);

or for versions of C# prior to 7:

list.OrderBy(x=> { int dummy; return int.TryParse(x, out dummy) ? dummy.ToString("D10") : x;} );
Sign up to request clarification or add additional context in comments.

4 Comments

You know you could do dummy.ToString("D10") instead of another parse.
@juharr Updated answer that doesn't require two parses, and added a pre C# 7.0 version.
Why use D10? I don't understand
@RoadRunner I just picked 10 as the numbers he gave used less than 10 digits. .ToString("D10") makes a string 10 digits long, left padded with 0's, so 1.ToString("D10") creates a string "0000000001" which then will be sortable.
2

What you want is called Natural sort.

I once wrote some code for that:

public static class NaturalCompare
{
    public static int Compare(string first, string second, StringComparison comparison = StringComparison.Ordinal)
    {
        if (string.Compare(first, second, comparison) == 0)
        {
            return 0;
        }

        if (first == null)
        {
            return -1;
        }

        if (second == null)
        {
            return 1;
        }

        DateTime d1, d2;

        if (DateTime.TryParse(first, out d1) && DateTime.TryParse(second, out d2))
        {
            return d1.CompareTo(d2);
        }

        var pos1 = 0;
        var pos2 = 0;

        int result;
        do
        {
            bool isNum1, isNum2;

            var part1 = GetNext(first, ref pos1, out isNum1);
            var part2 = GetNext(second, ref pos2, out isNum2);

            if (isNum1 && isNum2)
            {
                result = long.Parse(part1).CompareTo(long.Parse(part2));
            }
            else
            {
                result = String.Compare(part1, part2, comparison);
            }
        } while (result == 0 && pos1 < first.Length && pos2 < second.Length);

        return result;
    }

    public static int CompareToNatural(this string first, string second, StringComparison comparison = StringComparison.Ordinal)
    {
        return Compare(first, second, comparison);
    }

    public static IOrderedEnumerable<TSource> OrderByNatural<TSource>(this IEnumerable<TSource> source, Func<TSource, string> keySelector)
    {
        return source.OrderBy(keySelector, new NatComparer());
    }

    public static IOrderedEnumerable<TSource> OrderByNaturalDescending<TSource>(this IEnumerable<TSource> source, Func<TSource, string> keySelector)
    {
        return source.OrderByDescending(keySelector, new NatComparer());
    }

    private sealed class NatComparer : IComparer<string>
    {
        public int Compare(string x, string y)
        {
            return NaturalCompare.Compare(x, y);
        }
    }

    private static string GetNext(string s, ref int index, out bool isNumber)
    {
        if (index >= s.Length)
        {
            isNumber = false;
            return "";
        }

        isNumber = char.IsDigit(s[index]);

        var start = index;
        while (index < s.Length && char.IsDigit(s[index]) == isNumber)
        {
            index++;
        }
        return s.Substring(start, index - start);
    }
}

Comments

2

I wrote this IComparer implementation a few months back to handle something like this. I think it will do what you want by default, though it is built to handle more complex cases where number/letter groups are separated by delimiters that also need to be sorted atomically. You should be able to adjust it to your needs.

public class SemanticComparer : IComparer<string>
{
    private static Regex _splitter = new Regex("\\W+");

    public int Compare(string x, string y)
    {
        string[] partsX = _splitter.Split(x);
        string[] partsY = _splitter.Split(y);

        int shortest = Math.Min(partsX.Length, partsY.Length);

        for (int index = 0; index < shortest; index++)
        {
            int intX, intY;
            int result;

            if (int.TryParse(partsX[index], out intX) && int.TryParse(partsY[index], out intY))
            {
                result = intX.CompareTo(intY);
            }
            else
            {
                result = string.Compare(partsX[index], partsY[index], StringComparison.Ordinal);
            }

            if (result != 0)
            {
                return result;
            }
        }

        return 0;
    }
}

You can sort your list with it like this:

MyList.Sort(new SemanticComparer());

Comments

1

I've created a solution for this. I've divided the list into two part then sort and concat. Please check below:

public List<ListItem> getSortedList()
{
    int dummy = 0;

    List<ListItem> list = new List<ListItem>();
    list.Add(new ListItem() { Item = "A" });
    list.Add(new ListItem() { Item = "B" });
    list.Add(new ListItem() { Item = "C" });
    list.Add(new ListItem() { Item = "111" });
    list.Add(new ListItem() { Item = "11" });
    list.Add(new ListItem() { Item = "123" });
    list.Add(new ListItem() { Item = "1" });
    list.Add(new ListItem() { Item = "42" });
    list.Add(new ListItem() { Item = "5" });

    var listNumber = list.Where(m => int.TryParse(m.Item, out dummy)).ToList().OrderBy(m => Convert.ToInt16(m.Item)).ToList();
    var listString = list.Where(m => !int.TryParse(m.Item, out dummy)).ToList().OrderBy(m => m.Item).ToList();

    var sortedList = listNumber.Concat(listString).ToList();

    return sortedList;
}

You can run this in DotNetFiddle.

Comments

0

You could loop through all the values once, and use int.TryParse to separate them into two separate lists: one for the values where int.TryParse returned true (aka the numbers), and another list for the ones where it returned false (the non-numbers). Then you could sort these two lists separately, and concatenate their sorted results together at the end.

Comments

0

I haven't tested this code for performance, but you can solve this with a Comparer

public class ArrayItemComparer : IComparer<string>
{
    public int Compare(string x, string y)
    {
        int xInt = 0, yInt = 0;

        bool parseX = int.TryParse(x, out xInt);
        bool parseY = int.TryParse(y, out yInt);

        if (parseX && parseY)
        {
            return xInt.CompareTo(yInt);
        }
        else if (parseX)
        {
            return -1;
        }
        else if (parseY)
        {
            return 1;
        }
        else
        {
            return x.CompareTo(y);
        }
    }
}

Comments

0

Assuming you start with a collection of strings, a simple comparer should do the job:

public class Comparer : IComparer<string>
{
    public int Compare(string a, string b) 
    {
        int ia = 0;
        int ib = 0;
        var aIsInt = int.TryParse(a,out ia);
        var bIsInt = int.TryParse(b,out ib);
        if (aIsInt == bIsInt)
        {
            if (aIsInt)
            {
                return ia.CompareTo(ib);
            }
            else
            {
                return a.CompareTo(b);
            }
        }
        return aIsInt ? -1 : 1;
    }
}

Here's a fiddle

Comments

0

With Regex.Replace in the "OrderBy" it's one (fairly) simple line of code. And 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[] yourStrings = new string[] { "A", "B", "C", "111", "11", "123", "1", "42", "5" };

foreach (var item in yourStrings.OrderBy(x => Regex.Replace(x, @"\d+", i => 
i.Value.PadLeft(3, '0'))))
{
    Response.Write(item + "\n");
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.