-1

I have an List of strings that I will pull from an enum and I need to sort in alphabetical order then by number and some strings won't have numbers, those strings should be at the end of the list.

List =     
    "Incl 11
     Incl 12
     Excl 4
     Incl 3
     Other
     Incl 4
     Incl 10
     Incl 11
     Excl 10
     Incl 1
     Incl 2
     Withdrew Col
     Excl 1
     Excl 2
     Excl 3         
     Follow Up
    "

So far I have this but it's only sorted by number and not alphabetically first, any help would be appreciated.

    var test =  Enum.GetValues(typeof(Reason))
        .Cast<Reason>()
        .Select(sc => new SelectListItem { Value = ((int)sc).ToString(), Text = sc.GetDisplayName() })
        .OrderBy(s => s.Text, new MyNumberComparer())
        .ToList();

    class MyNumberComparer : IComparer<string>
    {
        public int Compare(string x, string y)
        {
            var xResultString = Regex.Match(x, @"\d+").Value;
            var yResultString = Regex.Match(y, @"\d+").Value;

            int xVal, yVal;
            var xIsVal = int.TryParse(xResultString, out xVal);
            var yIsVal = int.TryParse(yResultString, out yVal);

            if (xIsVal && yIsVal)   
                return xVal.CompareTo(yVal);
            if (!xIsVal && !yIsVal) 
                return x.CompareTo(y);
            if (xIsVal)             
                return -1;
            return 1;              
        }
    }

Edit with what the final output should be:

List =     
    "Excl 1
     Excl 2
     Excl 3 
     Excl 4
     Excl 10
     Incl 1
     Incl 2
     Incl 3
     Incl 4
     Incl 10
     Incl 11
     Incl 12
     Follow Up       
     Other                         
     Withdrew Col                        
    "
AxV
  • 171
  • 1
  • 2
  • 9
  • Did you thought about simply splitting the list in two by grouping them by which has number and which don't and then simply sort both and merge the result ? – Franck Feb 20 '19 at 19:44
  • 1
    You may be able to make use of Natural Sorting. The NaturalStringComparer sample from: https://stackoverflow.com/questions/248603/natural-sort-order-in-c-sharp should work well. That will sort the numbers correctly (so that 10 does not come before 2 - which tends to happen with alphabetical sorts). Basically: If X and Y both contain numbers or both X and Y do not contain numbers, use natural sort. If only one of X or Y contains a number, the one with the number goes first. – Wiz Feb 20 '19 at 20:24
  • Possible duplicate of [Natural Sort Order in C#](https://stackoverflow.com/questions/248603/natural-sort-order-in-c-sharp) – Ňɏssa Pøngjǣrdenlarp Feb 20 '19 at 21:20

3 Answers3

1

You are almost there. If non-digit part of the values are not same then you need to compare only non-digit parts. Just put these lines in Compare method at the beginning.

        var xstring = Regex.Match(x, @".+?(?=\d+)").Value; //extract non-digit part
        var ystring = Regex.Match(y, @".+?(?=\d+)").Value; //extract non-digit part

        if (!string.IsNullOrEmpty(xstring) && !string.IsNullOrEmpty(ystring))
        {
            var comp = xstring.CompareTo(ystring);
            if (comp != 0)
            {
                return comp;
            }
        }
ozanmut
  • 2,898
  • 26
  • 22
0

Below is an implementation for your IComparer, which should get you what you want

class MyNumberComparer : IComparer<string> {
    public static Tuple<string, int?> SplitString(string s) {
        var x = s.LastIndexOf(' ');
        return x == -1 || x == s.Length - 1 || !int.TryParse(s.Substring(x + 1), out int n) ?
            Tuple.Create(s, (int?)null) : Tuple.Create(s.Substring(0, x), (int?)n);
    }

    public int Compare(string str1, string str2) {
        if (string.Compare(str1, str2) == 0)
            return 0;

        var str1Items = SplitString(str1);
        var str2Items = SplitString(str2);

        var prefixCompare = string.Compare(str1Items.Item1, str2Items.Item1);

        if (str1Items.Item2 == null && str1Items.Item2 == null)
            return prefixCompare;

        if (str1Items.Item2 == null)
            return 1;

        if (str2Items.Item2 == null)
            return -1;

        return prefixCompare == 0 ? (int)(str1Items.Item2 - str2Items.Item2) : prefixCompare;
    }
}

I have based the solution on what you have mentioned in your question (space separated number), but you can change the implementation of SplitString, to reflect breaking the incoming string into a string and a number or null.

If you want the string comparison to happen in a special way (like case insensitive way), then you can update the string.Compare calls in the MyNumberComparer.Compare code to reflect that.

Vikhram
  • 4,294
  • 1
  • 20
  • 32
0

Here's a somewhat simple solution based on regex groups. Tested it, it should give you the results you're looking for. Definitely not pretty though:

class Comparer : IComparer<string> {
    static Regex Matcher = new Regex(@"([^\d]+)(\d+)");

    public int Compare(string x, string y) {
        var xMatch = Matcher.Match(x);
        var yMatch = Matcher.Match(y);

        if (xMatch.Success != yMatch.Success)
            return xMatch.Success ? -1 : 1;

        if (!xMatch.Success)
            return string.Compare(x, y);

        if (xMatch.Groups[1].Value != yMatch.Groups[1].Value)
            return string.Compare(xMatch.Groups[1].Value, yMatch.Groups[1].Value);

        return (int.Parse(xMatch.Groups[2].Value) - int.Parse(yMatch.Groups[2].Value)) < 0 ? -1 : 1;
    }
}

God help you if you have multiple numbers throughout a string lol. In that case, you'll have to alter the regex pattern to look for only digits at the end of the string. Sorry, I got lazy on it.

AotN
  • 548
  • 2
  • 11