views:

1657

answers:

5

I need a basic example of how to use the IComparable interface so that I can sort in ascending or descending order and by different fields of the object type I'm sorting.

+2  A: 

Here's a simple example:

public class SortableItem : IComparable<SortableItem>
{
    public int someNumber;

    #region IComparable<SortableItem> Members

    public int CompareTo(SortableItem other)
    {
        int ret = -1;
        if (someNumber < other.someNumber)
            ret = -1;
        else if (someNumber > other.someNumber)
            ret = 1;
        else if (someNumber == other.someNumber)
            ret = 0;
        return ret;
    }

    #endregion
}

"That's great, but what if I want to be able to control the sort order, or sort by another field?"

Simple. All we need to do is add few more fields to the object. First we'll add a string for a different sort type and then we'll add a boolean to denote whether we're sorting in descending or ascending order and then add a field which determines which field we want to search by.

public class SortableItem : IComparable<SortableItem>
{
    public enum SortFieldType { SortNumber, SortString }

    public int someNumber = -1;
    public string someString = "";
    public bool descending = true;    
    public SortFieldType sortField = SortableItem.SortFieldType.SortNumber;        

    #region IComparable<SortableItem> Members

    public int CompareTo(SortableItem other)
    {
        int ret = -1;
        if(sortField == SortableItem.SortFieldType.SortString)
        {
            // A lot of other objects implement IComparable as well.
            // Take advantage of this.
            ret = someString.CompareTo(other.someString);
        }
        else
        {
            if (someNumber < other.someNumber)
                ret = -1;
            else if (someNumber > other.someNumber)
                ret = 1;
            else if (someNumber == other.someNumber)
                ret = 0;
        }
        // A quick way to switch sort order:
        // -1 becomes 1, 1 becomes -1, 0 stays the same.
        if(!descending) ret = ret * -1; 

        return ret;
    }

    #endregion

    public override string ToString()
    {
       if(sortField == SortableItem.SortFieldType.SortString)
          return someString;
       else
          return someNumber.ToString();
    }
}

"Show me how!"

Well since you asked so nicely.

static class Program
{
    static void Main()
    {

        List<SortableItem> items = new List<SortableItem>();
        SortableItem temp = new SortableItem();
        temp.someString = "Hello";
        temp.someNumber = 1;
        items.Add(temp);
        temp = new SortableItem();
        temp.someString = "World";
        temp.someNumber = 2;
        items.Add(temp);
        SortByString(items);
        Output(items);
        SortAscending(items);
        Output(items);
        SortByNumber(items);
        Output(items);
        SortDescending(items);
        Output(items);
        Console.ReadKey();
    }

    public static void SortDescending(List<SortableItem> items)
    {
        foreach (SortableItem item in items)
            item.descending = true;
    }
    public static void SortAscending(List<SortableItem> items)
    {
        foreach (SortableItem item in items)
            item.descending = false;
    }
    public static void SortByNumber(List<SortableItem> items)
    {
        foreach (SortableItem item in items)
            item.sortField = SortableItem.SortFieldType.SortNumber;
    }
    public static void SortByString(List<SortableItem> items)
    {
        foreach (SortableItem item in items)
            item.sortField = SortableItem.SortFieldType.SortString;
    }
    public static void Output(List<SortableItem> items)
    {
        items.Sort();
        for (int i = 0; i < items.Count; i++)
            Console.WriteLine("Item " + i + ": " + items[i].ToString());
    }
}
Spencer Ruport
That is actually pretty yeucky, sorry to say it. There are much better approaches, especially using LINQ or a LINQ-inspired approach...
Marc Gravell
There isn't anything that says you can't answer the question too if you have a better idea.
Spencer Ruport
I have - I was just writing it ;-p
Marc Gravell
+2  A: 

If you want dynamic sort, you can use LINQ

var itemsOrderedByNumber = ( from item in GetClasses() orderby item.Number select item ).ToList();
var itemsOrderedByText = ( from item in GetClasses() orderby item.Text select item ).ToList();
var itemsOrderedByDate = ( from item in GetClasses() orderby item.Date select item ).ToList();

or "Sort" method of List class:

List<Class1> itemsOrderedByNumber2 = new List<Class1>( GetClasses() );
itemsOrderedByNumber2.Sort( ( a, b ) => Comparer<int>.Default.Compare( a.Number, b.Number ) );

List<Class1> itemsOrderedByText2 = new List<Class1>( GetClasses() );
itemsOrderedByText2.Sort( ( a, b ) => Comparer<string>.Default.Compare( a.Text, b.Text ) );

List<Class1> itemsOrderedByDate2 = new List<Class1>( GetClasses() );
itemsOrderedByDate2.Sort( ( a, b ) => Comparer<DateTime>.Default.Compare( a.Date, b.Date ) );
TcKs
+6  A: 

Well, since you are using List<T> it would be a lot simpler to just use a Comparison<T>, for example:

List<Foo> data = ...
// sort by name descending
data.Sort((x,y) => -x.Name.CompareTo(y.Name));

Of course, with LINQ you could just use:

var ordered = data.OrderByDescending(x=>x.Name);

But you can re-introduce this in List<T> (for in-place re-ordering) quite easily; Here's an example that allows Sort on List<T> with lambda syntax:

using System;
using System.Collections.Generic;  

class Foo { // formatted for vertical space
    public string Bar{get;set;}
}
static class Program {
    static void Main() {
        List<Foo> data = new List<Foo> {
            new Foo {Bar = "abc"}, new Foo {Bar = "jkl"},
            new Foo {Bar = "def"}, new Foo {Bar = "ghi"}
        };
        data.SortDescending(x => x.Bar);
        foreach (var row in data) {
            Console.WriteLine(row.Bar);
        }
    }

    static void Sort<TSource, TValue>(this List<TSource> source,
            Func<TSource, TValue> selector) {
        var comparer = Comparer<TValue>.Default;
        source.Sort((x,y)=>comparer.Compare(selector(x),selector(y)));
    }
    static void SortDescending<TSource, TValue>(this List<TSource> source,
            Func<TSource, TValue> selector) {
        var comparer = Comparer<TValue>.Default;
        source.Sort((x,y)=>comparer.Compare(selector(y),selector(x)));
    }
}
Marc Gravell
A: 

YOu could do worse than the MSDN page on iComparable.

With due respect to my fellow answerers who've done good jobs themselves already, Microsoft does actually do documentation commendably thoroughly and it's generally a decent place to start with the entry level questions like this.

Unsliced
A: 

This might not be in relation to sorting order, but it is still - I think - an interesting use of IComparable:

public static void MustBeInRange<T>(this T x, T minimum, T maximum, string paramName)
where T : IComparable<T>
{
    bool underMinimum = (x.CompareTo(minimum) < 0);
    bool overMaximum = (x.CompareTo(maximum) > 0);
    if (underMinimum || overMaximum)
    {
     string message = string.Format(
      System.Globalization.CultureInfo.InvariantCulture,
      "Value outside of [{0},{1}] not allowed/expected",
      minimum, maximum
     );
     if (string.IsNullOrEmpty(paramName))
     {
      Exception noInner = null;
      throw new ArgumentOutOfRangeException(message, noInner);
     }
     else
     {
      throw new ArgumentOutOfRangeException(paramName, x, message);
     }
    }
}

public static void MustBeInRange<T>(this T x, T minimum, T maximum)
where T : IComparable<T> { x.MustBeInRange(minimum, maximum, null); }

These simple extension methods allow you to do parameter range checking for any type that implements IComparable like this:

public void SomeMethod(int percentage, string file) {
    percentage.MustBeInRange(0, 100, "percentage");
    file.MustBeInRange("file000", "file999", "file");
    // do something with percentage and file
    // (caller will have gotten ArgumentOutOfRangeExceptions when applicable)
}
peSHIr
To be honest, you're unlikely to use it on a type that isn't `IComparable<T>` or `IComparable`, so I'd drop the constraint and use Comparer<T>.Default. This is, for example, how LINQ-to-Objects does Min/Max.
Marc Gravell
Hadn't though of that. Thanks for the tip. However, wouldn't that show the extension method everything (=on types I'd be "unlikely to use it on") while with the type constraint it'll only show on types that actually are IComparable?
peSHIr