Use own IComparer<T> with Linq OrderBy

I have a generic

List<MyClass>

where MyClass has a property InvoiceNumber which contains values such as:

200906/1
200906/2
..
200906/10
200906/11
200906/12

My list is bound to a

BindingList<T>

which supports sorting with linq:

protected override void ApplySortCore(
           PropertyDescriptor property, ListSortDirection direction)
{

    _sortProperty = property;
    _sortDirection = direction;

    var items = this.Items;

    switch (direction)
    {
        case ListSortDirection.Ascending:
            items = items.OrderByDescending(x => property.GetValue(x)).ToList();
            break;
        case ListSortDirection.Descending:
            items = items.OrderByDescending(x => property.GetValue(x)).ToList();
            break;
    }

    this.Items = items;

}

However the default comparer sorts (as supposed) like this:

200906/1
200906/10
200906/11
200906/12
200906/2

which is nasty in this case.

Now I want to use my own IComparer<T> with this. It looks like this:

public class MyComparer : IComparer<Object>
{

    public int Compare(Object stringA, Object stringB)
    {
        String[] valueA = stringA.ToString().Split('/');
        String[] valueB = stringB.ToString().Split('/');

        if(valueA .Length != 2 || valueB .Length != 2)
             return String.Compare(stringA.ToString(), stringB.ToString());

        if (valueA[0] == valueB[0]) 
        {
          return String.Compare(valueA[1], valueB[1]);
        }
        else
        {
          return String.Compare(valueA[0], valueB[0]);
        }

    }

}

and changed the ApplySortCore code to use this IComparer:

case ListSortDirection.Ascending:
    MyComparer comparer = new MyComparer();
    items = items.OrderByDescending(
              x => property.GetValue(x), comparer).ToList();
    break;

When I debug my code, I see that MyComparer.Compare(object, object) is called multiple times and returns the right values (-1, 0, 1) for a compare method.

But my list is still sorted the "wrong" way. Am I missing something? I have no clue.


Your comparer looks wrong to me. You're still just sorting in the default text ordering. Surely you want to be parsing the two numbers and sorting based on that:

public int Compare(Object stringA, Object stringB)
{
    string[] valueA = stringA.ToString().Split('/');
    string[] valueB = stringB.ToString().Split('/');

    if (valueA.Length != 2 || valueB.Length != 2)
    {
        stringA.ToString().CompareTo(stringB.ToString());
    }

    // Note: do error checking and consider i18n issues too :)
    if (valueA[0] == valueB[0]) 
    {
        return int.Parse(valueA[1]).CompareTo(int.Parse(valueB[1]));
    }
    else
    {
        return int.Parse(valueA[0]).CompareTo(int.Parse(valueB[0]));
    }
}

(Note that this doesn't sit well with your question stating that you've debugged through and verified that Compare is returning the right value - but I'm afraid I suspect human error on that front.)

Additionally, Sven's right - changing the value of items doesn't change your bound list at all. You should add:

this.Items = items;

at the bottom of your method.


I encountered the issue of general natural sorting and blogged the solution here:

Natural Sort Compare with Linq OrderBy()

public class NaturalSortComparer<T> : IComparer<string>, IDisposable
{
    private bool isAscending;

    public NaturalSortComparer(bool inAscendingOrder = true)
    {
        this.isAscending = inAscendingOrder;
    }

    #region IComparer<string> Members

    public int Compare(string x, string y)
    {
        throw new NotImplementedException();
    }

    #endregion

    #region IComparer<string> Members

    int IComparer<string>.Compare(string x, string y)
    {
        if (x == y)
            return 0;

        string[] x1, y1;

        if (!table.TryGetValue(x, out x1))
        {
            x1 = Regex.Split(x.Replace(" ", ""), "([0-9]+)");
            table.Add(x, x1);
        }

        if (!table.TryGetValue(y, out y1))
        {
            y1 = Regex.Split(y.Replace(" ", ""), "([0-9]+)");
            table.Add(y, y1);
        }

        int returnVal;

        for (int i = 0; i < x1.Length && i < y1.Length; i++)
        {
            if (x1[i] != y1[i])
            {
                returnVal = PartCompare(x1[i], y1[i]);
                return isAscending ? returnVal : -returnVal;
            }
        }

        if (y1.Length > x1.Length)
        {
            returnVal = 1;
        }
        else if (x1.Length > y1.Length)
        { 
            returnVal = -1; 
        }
        else
        {
            returnVal = 0;
        }

        return isAscending ? returnVal : -returnVal;
    }

    private static int PartCompare(string left, string right)
    {
        int x, y;
        if (!int.TryParse(left, out x))
            return left.CompareTo(right);

        if (!int.TryParse(right, out y))
            return left.CompareTo(right);

        return x.CompareTo(y);
    }

    #endregion

    private Dictionary<string, string[]> table = new Dictionary<string, string[]>();

    public void Dispose()
    {
        table.Clear();
        table = null;
    }
}

Can't we do like this:

public class MyComparer : IComparer<string>
{

    public int Compare(string stringA, string stringB)
    {
        string small = stringA;
        string big = stringB;
        if (stringA.Length > stringB.Length)
        {
            small = stringB;
            big = stringA;
        }
        else if (stringA.Length < stringB.Length)
        {
            small = stringA;
            big = stringB;
        }
        for (int j = 0; j < small.Length; j++)
        {
            if (Convert.ToInt32(small[j]) > Convert.ToInt32(big[j])) return -1;
            if (Convert.ToInt32(small[j]) < Convert.ToInt32(big[j])) return 1;
        }

        //big is indeed bigger
        if (big.Length > small.Length) return 1;

        //finally they are smae
        return 0;
    }
}

Usage:

string[] inputStrings = {"_abc*&","#almnp","abc" };
//string[] inputStrings = { "#", "_", "_a", "@", "_" };
MyComparer computer = new MyComparer();
var kola = inputStrings.OrderBy(x => x, new MyComparer()).ToArray();

This is same as :

    Array.Sort(inputStrings, StringComparer.Ordinal);