1

I have sample codes below:

List<string> test = new List<string>();
test.Add("Hello2");
test.Add("Hello1");
test.Add("Welcome2");
test.Add("World");
test.Add("Hello11");
test.Add("Hello10");
test.Add("Welcome0");
test.Add("World3");
test.Add("Hello100");
test.Add("Hello20");
test.Add("Hello3");

test.Sort();

But what happen is, the test.Sort will sort the array to:

"Hello1", 
"Hello10", 
"Hello100", 
"Hello11", 
"Hello2", 
"Hello20", 
"Hello3", 
"Welcome0", 
"Welcome2", 
"World", 
"World3"

Is there any way to sort them so that the string will have the correct number order as well? (If there is no number at the end of the string, that string will always go first - after the alphabetical order)

Expected output:

"Hello1", 
"Hello2", 
"Hello3", 
"Hello10", 
"Hello11", 
"Hello20", 
"Hello100", 
"Welcome0", 
"Welcome2", 
"World", 
"World3"
Victor Sigler
  • 23,243
  • 14
  • 88
  • 105
C.J.
  • 3,409
  • 8
  • 34
  • 51
  • Just spitballing, but if you want "Hello1" to come before "Hello10", you'd be better off injecting a 0 so it'd be "Hello01", "Hello02" etc. Windows Explorer sorts files and folders in the same fashion, when done alphabetically. Might want to look at the last 2 characters of your string and determine if they're **both** numbers, if not, lead it with a 0. – sab669 Apr 01 '14 at 20:48
  • @sab669 I am NOT allowed to change the original data, they are extremely sensitive to the entire application. – C.J. Apr 01 '14 at 20:50

3 Answers3

8

Here is a one possible way using LINQ:

 var orderedList = test
            .OrderBy(x => new string(x.Where(char.IsLetter).ToArray()))
            .ThenBy(x =>
            {
                int number;
                if (int.TryParse(new string(x.Where(char.IsDigit).ToArray()), out number))
                    return number;
                return -1;
            }).ToList();
Selman Genç
  • 100,147
  • 13
  • 119
  • 184
  • @C.J. your welcome.note that this will not work as expected if your string contains numbers in the beginning or between the letters.this only works if digits come after all letters. if you want to handle different cases like this let me know, I can update my answer. – Selman Genç Apr 01 '14 at 20:57
  • 1
    +1 for not using Regex to split string and number and purely relying on LINQ. – Habib Apr 01 '14 at 20:58
  • @Habib: What's your problem with regex? – spender Apr 01 '14 at 21:06
  • @spender, I didn't see your answer before posting this comment :), actually I was trying a similar answer based on Regex and it was getting messy. That is just me and REGEX.... – Habib Apr 01 '14 at 21:12
3

Create an IComparer<string> implementation. The advantage of doing it this way over the LINQ suggestions is you now have a class that can be passed to anything that needs to sort in this fashion rather that recreating that linq query in other locations.

This is specific to your calling a sort from a LIST. If you want to call it as Array.Sort() please see version two:

List Version:

public class AlphaNumericComparer : IComparer<string>
    {
        public int Compare(string lhs, string rhs)
        {
            if (lhs == null)
            {
                return 0;
            }

            if (rhs == null)
            {
                return 0;
            }

            var s1Length = lhs.Length;
            var s2Length = rhs.Length;
            var s1Marker = 0;
            var s2Marker = 0;

            // Walk through two the strings with two markers.
            while (s1Marker < s1Length && s2Marker < s2Length)
            { 
                var ch1 = lhs[s1Marker];
                var ch2 = rhs[s2Marker];

                var s1Buffer = new char[s1Length];
                var loc1 = 0;
                var s2Buffer = new char[s2Length];
                var loc2 = 0;

                // Walk through all following characters that are digits or
                // characters in BOTH strings starting at the appropriate marker.
                // Collect char arrays.
                do
                {
                    s1Buffer[loc1++] = ch1;
                    s1Marker++;

                    if (s1Marker < s1Length)
                    {
                        ch1 = lhs[s1Marker];
                    }
                    else
                    {
                        break;
                    }
                } while (char.IsDigit(ch1) == char.IsDigit(s1Buffer[0]));

                do
                {
                    s2Buffer[loc2++] = ch2;
                    s2Marker++;

                    if (s2Marker < s2Length)
                    {
                        ch2 = rhs[s2Marker];
                    }
                    else
                    {
                        break;
                    }
                } while (char.IsDigit(ch2) == char.IsDigit(s2Buffer[0]));

                // If we have collected numbers, compare them numerically.
                // Otherwise, if we have strings, compare them alphabetically.
                string str1 = new string(s1Buffer);
                string str2 = new string(s2Buffer);

                int result;

                if (char.IsDigit(s1Buffer[0]) && char.IsDigit(s2Buffer[0]))
                {
                    var thisNumericChunk = int.Parse(str1);
                    var thatNumericChunk = int.Parse(str2);
                    result = thisNumericChunk.CompareTo(thatNumericChunk);
                }
                else
                {
                    result = str1.CompareTo(str2);
                }

                if (result != 0)
                {
                    return result;
                }
            }
            return s1Length - s2Length;
        }
    }

call like so:

test.sort(new AlphaNumericComparer());

//RESULT
Hello1 
Hello2 
Hello3 
Hello10 
Hello11 
Hello20 
Hello100 
Welcome0 
Welcome2 
World 
World3 

Array.sort version:

Create class:

public class AlphaNumericComparer : IComparer
{
    public int Compare(object x, object y)
    {
        string s1 = x as string;
        if (s1 == null)
        {
            return 0;
        }
        string s2 = y as string;
        if (s2 == null)
        {
            return 0;
        }

        int len1 = s1.Length;
        int len2 = s2.Length;
        int marker1 = 0;
        int marker2 = 0;

        // Walk through two the strings with two markers.
        while (marker1 < len1 && marker2 < len2)
        {
            var ch1 = s1[marker1];
            var ch2 = s2[marker2];

            // Some buffers we can build up characters in for each chunk.
            var space1 = new char[len1];
            var loc1 = 0;
            var space2 = new char[len2];
            var loc2 = 0;

            // Walk through all following characters that are digits or
            // characters in BOTH strings starting at the appropriate marker.
            // Collect char arrays.
            do
            {
                space1[loc1++] = ch1;
                marker1++;

                if (marker1 < len1)
                {
                    ch1 = s1[marker1];
                }
                else
                {
                    break;
                }
            } while (char.IsDigit(ch1) == char.IsDigit(space1[0]));

            do
            {
                space2[loc2++] = ch2;
                marker2++;

                if (marker2 < len2)
                {
                    ch2 = s2[marker2];
                }
                else
                {
                    break;
                }
            } while (char.IsDigit(ch2) == char.IsDigit(space2[0]));

            // If we have collected numbers, compare them numerically.
            // Otherwise, if we have strings, compare them alphabetically.
            var str1 = new string(space1);
            var str2 = new string(space2);

            var result = 0;

            if (char.IsDigit(space1[0]) && char.IsDigit(space2[0]))
            {
                var thisNumericChunk = int.Parse(str1);
                var thatNumericChunk = int.Parse(str2);
                result = thisNumericChunk.CompareTo(thatNumericChunk);
            }
            else
            {
                result = str1.CompareTo(str2);
            }

            if (result != 0)
            {
                return result;
            }
        }
        return len1 - len2;
    }
}

Call like so:

This time test is an array instead of a list.
Array.sort(test, new AlphaNumericComparer())
Victor Sigler
  • 23,243
  • 14
  • 88
  • 105
Cubicle.Jockey
  • 3,288
  • 1
  • 19
  • 31
0

You can use LINQ combined with regex to ensure that you use only numbers that occur at the end of the string for your secondary ordering

test
  .Select(t => new{match = Regex.Match(t, @"\d+$"), val = t})
  .Select(x => new{sortVal = x.match.Success
                                ?int.Parse(x.match.Value)
                                :-1,
                   val = x.val})
  .OrderBy(x => x.val)
  .ThenBy(x => x.sortVal)
  .Select(x => x.val)
  .ToList()
spender
  • 117,338
  • 33
  • 229
  • 351