Implementing IComparer combining multiple Linq OrderBy's
Asked Answered
C

4

5

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?

Curriculum answered 20/2, 2013 at 9:44 Comment(3)
I don't quite understand what the problem is here. Do you have a dynamic collection that should always produce its contents in some specific order? Use e.g. a SortedSet with a custom comparer and everything will happen automatically (caveat: SortedSet does not allow duplicates).Neo
Not to confuse the issue with the dynamic keyword, but yes the items in the collection are probably unique/different every time. Im looking at SortedSet now...Curriculum
By "dynamic" I mean "stuff is getting into and out of the collection all the time". If it was something you build once and then don't modify you could simply sort once after building and call it a day.Neo
C
2

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();
Chrysanthemum answered 20/2, 2013 at 10:14 Comment(0)
C
4

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;
}
Complicity answered 20/2, 2013 at 10:1 Comment(2)
will throw on x.name == nullBethune
@Bethune Im probably wrong here, but wouldnt trying to look at x.name when x is null do the same in your example?Curriculum
C
2

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();
Chrysanthemum answered 20/2, 2013 at 10:14 Comment(0)
U
1

what's wrong with an extension method?

Unclog answered 20/2, 2013 at 9:57 Comment(1)
Whilst not a huge fan of extension methods, it totally did not occur to me to use one of these, looking into it now...Curriculum
B
1

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;
}
Bethune answered 20/2, 2013 at 10:2 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.