Making the Songlist sortable by using IComparer and IComparable

Yesterday we've looked at a way to access iTunes from C# to get a list of recently played tracks. Today, we want to extend this solution a little bit, by making the list sortable in any way we'd like.

If you have a look at the sidebar, you will find a "Recently Played Songs" near the bottom. Achieving this is the ultimate goal of this small article series, and I'll walk you through the steps needed to achieve this automatically.

The Series of Articles will include:

  1. Grab the contents of a Playlist from iTunes
  2. Making the Songlist sortable by using IComparer and IComparable
  3. Create a XML File
  4. Upload this file via FTP to this Server
  5. Write a PHP Script that performs an XSL Transformation (XSLT) on the Xml File and Call this file from Sidebar.php within WordPress

Of course, the last step applies to PHP and WordPress, but if you have an ASP.net, a Ruby on Rails, a Python or whatever Web Server, you can still follow the base concepts if you'd like.

Let's get started. Yesterday, we created a Class called "Song" which essentially represents all the data about a song that we're interested in and a custom Comparator to be able to sort it. Now, we extend this class a little:

[Serializable()]
public class Song : IComparable<Song>
{
    public string Title { get; set; }
    public string Artist { get; set; }
    public string Album { get; set; }
    public int Year { get; set; }
    public int Length { get; set; }
    public int PlayCount { get; set; }
    public DateTime LastPlayed { get; set; }

    public string LengthAsString
    {
        get
        {
            TimeSpan ts = TimeSpan.FromSeconds(this.Length);
            if (ts.Hours > 0)
            {
                return string.Format("{0}:{1:00}:{2:00}", ts.Hours,
                                              ts.Minutes, ts.Seconds);
            }
            else
            {
                return string.Format("{0:00}:{1:00}", ts.Minutes, ts.Seconds);
            }
        }
    }

    public override string ToString()
    {
        return
        string.Format("{0} - {1} ({2}, {3}) [{4}] {{Last Played: {5:dd/MM/yyyy HH:mm}, Total: {6}}}"
        , Artist, Title, Album, Year, LengthAsString,LastPlayed,PlayCount);
    }

    #region IComparable<Song> Members

    public int CompareTo(Song other)
    {
        return this.LastPlayed.CompareTo(other.LastPlayed);
    }
    #endregion

    public int CompareTo(Song other, SongComparer.SortField field, bool sortDescending)
    {
        int result = 0;
        switch (field)
        {
            case SongComparer.SortField.Album:
                result = this.Album.CompareTo(other.Album);
                break;
            case SongComparer.SortField.Artist:
                result = this.Album.CompareTo(other.Artist);
                break;
            case SongComparer.SortField.Title:
                result = this.Title.CompareTo(other.Title);
                break;
            case SongComparer.SortField.Year:
                result = this.Year.CompareTo(other.Year);
                break;
            case SongComparer.SortField.PlayCount:
                result = this.PlayCount.CompareTo(other.PlayCount);
                break;
            case SongComparer.SortField.Length:
                result = this.Length.CompareTo(other.Length);
                break;
            case SongComparer.SortField.LastPlayed:
                result = this.LastPlayed.CompareTo(other.LastPlayed);
                break;
        }
        if (sortDescending) result = result * -1;
        return result;
    }
}

Compared to yesterfay, the CompareTo function was modified and overloaded. Yesterday, the function was always trying to sort by LastPlayed date, descending. Now, I have added an Overload which takes two additional parameters to determine a field to sort by and whether or not to sort in descending order. Also note that we are not coding the actual comparison logic ourselfes. Yesterday i've done it to illustrate the 3 return values, but here we are just calling the CompareTo-function that many .net Types already implement, hence no need for extra logic on our side.

For our Overload to work, we need to create a class that implements IComparer. Notice the difference between IComparable and IComparer. IComparable indicates that the Object which implements it can be compared to other objects of it's type, whereas IComparer is a "stand-alone" class that can be used against different objects. Also have a look at two blog articles about IComparable and IComparer. So here is a new class, the SongComparer:

public class SongComparer : IComparer<Song>
{
    public enum SortField
    {
        Title,
        Artist,
        Album,
        Year,
        Length,
        PlayCount,
        LastPlayed
    }

    public SortField Field { get; set; }
    public bool SortDescending { get; set; }

    public SongComparer()
    {
        SortDescending = true;
        Field = SortField.LastPlayed;
    }

    #region IComparer<Song> Members

    public int Compare(Song x, Song y)
    {
        return x.CompareTo(y, Field, SortDescending);
    }

    #endregion
}

This class starts out with an enum that defines the various fields that one can sort by. Now, using an enum is simple but if you add a field to the Song class, you have to extend the enum as well. There are other solutions to that (i.e. using Reflections), but again, for the sake of simplicity an enum works. We have then defined a class property to hold the selected field, and a boolean that indicates whether or not we want to sort descending. Our constructor just sets some default values (LastPlayed, descending). The important function is the Compare function. This takes two objects, and is then supposed to return one of the 3 return values that IComparable.CompareTo also returns. So in this case, it calls our overloaded Song.CompareTo() function. If you have a look at this function again, you see that it will now just look at the field and if the sort should be descending, it will just invert the result (through * -1).

The concept of IComparable and IComparer is quite simple, yet a bit tricky to grasp at first. The biggest issue is the compare order: You always compare the first object to the second object (first.CompareTo(second) or this.CompareTo(other)), otherwise the result will be in the wrong order.

So now that we have our custom IComparer, how do we use it? You can create an instance of the SortComparer class and then just pass it into List.Sort():

comparer = new SongComparer();
comparer.Field = SongComparer.SortField.PlayCount;
comparer.SortDescending = true;
List<Song> result = new List<Song>();
// fill the List...
result.Sort(comparer);

The List will now be sorted descending by PlayCount instead of LastPlayed. So now that we have made the Song class generic enough to sort by whatever field we like, the next step is to turn it into Xml. We will tackle that in an upcoming blog post.

Comments (1)

ShubhaMarch 4th, 2009 at 17:13

Cool... thanks for this tip. very helpful.