Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Implementing IComparer combining multiple Linq OrderBy's

My problem is that I always want to order a collection of objects in a certain fashion.

For example:

class foo{
public string name {get;set;}
public DateTime date {get;set;}
public int counter {get;set;}
}

...

IEnumerable<foo> dosomething(foo[] bar){ 
return bar.OrderBy(a=>a.name).ThenBy(a=>a.date).ThenBy(a=>a.counter);
}

The issue I have is its quite longwinded tacking-on the sort order all the time. A neat solution appears to just create a class that implements IComparer<foo>, meaning I can do:

IEnumerable<foo> dosomething(foo[] bar){ 
return bar.OrderBy(a=>a, new fooIComparer())
}

.

The problem is, the order method this implements is as follows

...

public int Compare(foo x, foo y){ }

Meaning it compares on a very granular basis.

The currently implementation (which will probably work, although im writing pseudocode)

public int Compare(foo x, foo y){
if (x==y)
  return 0;
var order = new []{x,y}.OrderBy(a=>a.name).ThenBy(a=>a.date).ThenBy(a=>a.counter);
  return (order[0] == x) ? -1 : -1;//if x is first in array it is less than y, else it is greater
}

This is not exactly efficient, can another offer a neater solution? Ideally without a Compare(x,y) method altogether?

like image 688
maxp Avatar asked Feb 20 '13 09:02

maxp


4 Answers

Option 1 - The Comparer

As you're ordering by multiple conditions, you'll to check them individually within each case; for example, if x.name and y.name are equal, then you would check x.date and y.date, and so on.

public class FooComparer : IComparer<Foo>
{
    public int Compare(Foo x, Foo y)
    {
       // nasty null checks!
        if (x == null || y == null)
        {
            return x == y ? 0
                : x == null ? -1
                : 1;
        }

        // if the names are different, compare by name
        if (!string.Equals(x.Name, y.Name))
        {
            return string.Compare(x.Name, y.Name);
        }

        // if the dates are different, compare by date
        if (!DateTime.Equals(x.Date, y.Date))
        {
            return DateTime.Compare(x.Date, y.Date);
        }

        // finally compare by the counter
        return x.Counter.CompareTo(y.Counter);
    }
}

Option 2 - The extension method

An alternative, not so appealing approach, could be an extension method. Sadly as the TKey for each ThenBy can be different, we lose the power of generics, but can safely replace it with the type object in this case.

public static IOrderedEnumerable<T> OrderByThen<T>(this IEnumerable<T> source, Func<T, object> selector, params Func<T, object>[] thenBySelectors)
{
    IOrderedEnumerable<T> ordered = source.OrderBy(selector);
    foreach (Func<T, object> thenBy in thenBySelectors)
    {
        ordered = ordered.ThenBy(thenBy);
    }

    return ordered;
}
like image 185
Richard Avatar answered Oct 06 '22 10:10

Richard


You have to implement IComparable<foo> and compare all properties:

class foo: IComparable<foo>, IComparer<foo>
{
    public string name { get; set; }
    public DateTime date { get; set; }
    public int counter { get; set; }

    public int Compare(foo x, foo y)
    {
        if (x == null || y == null) return int.MinValue;
        if (x.name != y.name)
            return StringComparer.CurrentCulture.Compare(x.name, y.name);
        else if (x.date != y.date)
            return x.date.CompareTo(y.date);
        else if (x.counter != y.counter)
            return x.counter.CompareTo(y.counter);
        else
            return 0;
    }

    public int CompareTo(foo other)
    {
        return Compare(this, other);
    }
}

Then you can use OrderBy in this way:

var ordered = foos.OrderBy(f => f).ToList();
like image 44
Tim Schmelter Avatar answered Oct 06 '22 10:10

Tim Schmelter


what's wrong with an extension method?

like image 1
happygilmore Avatar answered Oct 06 '22 08:10

happygilmore


Why wont you simply compare your values:

int Compare(foo x, foo y)
{

    if (x== null && y == null)
        return 0;
    else if (x == null)
        return -1;
    else if (y == null)
        return 1;

    var nameComparision = string.Compare(x.name,y.name);
    if (nameComparision != 0)
        return nameComparision;
    var dateComparision = x.date.CompareTo(y.date);
    if (dateComparision != 0)
        return dateComparision;
    var counterComparision  = x.counter.CompareTo(y.counter);
    return counterComparision;
}
like image 1
Rafal Avatar answered Oct 06 '22 09:10

Rafal