简体   繁体   中英

Sort array list by numbers then by letters

I have array list of strings:

"1A", "12A", "12B", "6", "A", "5B", "B", "13" .

If I do myList.Sort(); then I get:

"1A", "12A", "12B", "13", "5B", "6", "A", "B" .

But what I need is first sort by numbers in front, then by letter:

"1A", "5B", "6", "12A", "12B", "13", "A", "B" .

I could use

public class CustomComparer : IComparer
{
    Comparer _comparer = new Comparer(System.Globalization.CultureInfo.CurrentCulture);

    public int Compare(object x, object y)
    {
        // Convert string comparisons to int
        return _comparer.Compare(Convert.ToInt32(x), Convert.ToInt32(y));
    }
}

But it throws exception. How do I get what I need?

Your comparer is too simplistic. Your comparison needs to split each value into the number and the rest, then compare the numbers first, then the strings if they're equal. So it would be something like:

public sealed class NumberStringComparer : IComparer<string>
{
    public int Compare(string x, string y)
    {
        return NumberString.Parse(x).CompareTo(NumberString.Parse(y));
    }


    private struct NumberString : IComparable<NumberString>
    {
        private readonly int? number;
        private readonly string text;

        private NumberString(int? number, string text)
        {
            this.number = number;
            this.text = text;
        }

        internal static NumberString Parse(string text)
        {
            // TODO: Find where the digits stop, parse the number
            // (if there is one), call the constructor and return a value.
            // (You could use a regular expression to separate the parts...)
        }

        public int CompareTo(NumberString other)
        {
            // TODO: Compare numbers; if they're equal, compare
            // strings
        }
    }
}

If you have problems with either of the TODOs ( after spending some time trying), you can ask for more specific help - but this is the general approach I'd use.

You cannot simply pass a string "1A" or "5B" to Convert.ToInt32(x) , because it has a portion that is not part of an Int32 .

Instead, you should first split the string into two parts - the digits and everything else, and do the comparison with tie breaks.

One way of doing it would be writing two helper methods, and then using LINQ's OrderBy and ThenBy :

static int ExtractPrefix(string s) {
    // Parse the digits and stop; return the number
}
static string ExtractSuffix(string s) {
    // Skip digits, and return everything else
}
...
var sorted = unsorted.OrderBy(ExtractPrefix).ThenBy(ExtractSuffix).ToList();

Try this:

public class CustomComparer : IComparer<string> {
  Comparer _comparer = new Comparer(System.Globalization.CultureInfo.CurrentCulture);

  public int Compare(string x, string y) {
     string numxs = string.Concat(x.TakeWhile(c => char.IsDigit(c)).ToArray());
     string numys = string.Concat(y.TakeWhile(c => char.IsDigit(c)).ToArray());

     int xnum;
     int ynum;
     if (!int.TryParse(numxs, out xnum) || !int.TryParse(numys, out ynum)) {
        return _comparer.Compare(x, y);
     }
     int compareNums = xnum.CompareTo(ynum);
     if (compareNums != 0) {
        return compareNums;
     }
     return _comparer.Compare(x, y);
  }
}

Give this implementation a try?

http://www.dotnetperls.com/alphanumeric-sorting

From the link above:

public class AlphanumComparatorFast : 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)
    {
        char ch1 = s1[marker1];
        char ch2 = s2[marker2];

        // Some buffers we can build up characters in for each chunk.
        char[] space1 = new char[len1];
        int loc1 = 0;
        char[] space2 = new char[len2];
        int 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.
        string str1 = new string(space1);
        string str2 = new string(space2);

        int result;

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

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

Below implementation of comparer should match your requirements:

public int Compare(string x, string y)
{
    var rx = new Regex("^(d+)");

    var xRes = rx .Match(x);
    var yRes = rx .Match(y);

    if (xRes.Success 
         && yRes.Success)
    {
        return int.Parse(xRes.Groups[1].Value).
               CompareTo(int.Parse(yRes.Groups[1].Value));
    }

    return x.CompareTo(y);
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM