Introduction
Recently, I encountered a problem at my work - a list of account objects needed to appear in a specific order in a report and the order had to look like:
- ISA Stock
- ISA Cash
- Wrap Cash
- Personal Portfolio
The order is by the Account
object's AccountType
property. Apparently, I cannot use the IEnumerable.OrderBy()
method as the order required is not alphabetical. I could add an extra property - Order
- to the Account
object so that ISA Stock accounts will have Order
= 1; ISA Cash accounts have Order
= 2, etc, then sort the accounts by Order
property. But I don't like the idea of bloating my class just for sorting. Besides, in a different scenario, the order may differ.
Another possible solution is to make the Account
class implement the IComparable
interface, but again, the order may be different for different scenarios, and implementing IComparable can only deal with one specific order. Besides, sometimes you may not have access to modify the class you want to sort. So a better solution would be to separate the sorting algorithm from the objects you want to sort. In this way, they both can vary independently - a cliche in the GoF Design Pattern book :)
So I ended up developing an IList
Extension Method and a Sorter
class which mimics the syntax of IEnumerable.OrderBy()
and lets you custom sort (rather than alphabetical) a list of objects by its string or enum properties. (Contrary to common belief, my Sorter
class doesn't implement the IComparer
interface for reasons I will discuss later.)
Download the Source Code
Thanks to Paw Jershauge for pointing out the source code download location:
http://www.codeproject.com/KB/106570/Collections/CustomSort.zip
How to Use CustomSort
To sort a list by a single property:
_listToBeSorted.Sort(x => x.AccountType, "ISA Stock,ISA Cash,Wrap Cash,Personal Portfolio");
you can specify a StringComparison
to control how strings are compared when doing the sort:
_listToBeSorted.Sort(x => x.AccountType, "ISA Stock,ISA Cash,Wrap Cash,Personal Portfolio",
StringComparison.InvariantCultureIgnoreCase);
If you want to sort by more than one property, you have to use the Sorter
class. I employed the Fluent Interface technique to make the code more readable:
var sorter = new Sorter<ClassToBeSorted, string>();
_listToBeSorted.Sort(
sorter.Add(x => x.AccountType, "ISA Stock,ISA Cash,Wrap Cash,Personal Portfolio")
.Add(x => x.AccountName, "Peter,Adam", StringComparison.InvariantCultureIgnoreCase)
.Add(x => x.AccountLocation, "London,Edinburgh")
);
If your object contains other objects as properties, for example:
public class CompositeClassToBeSorted {
public ClassToBeSorted ClassProperty;
public string ClassName;
}
you can drill down as deep as you want to use a certain property for sorting:
_listToBeSorted.Sort(x => x.ClassProperty.AccountType, "Wrap,Cash");
The IEnumerable.OrderBy()
method have the same capability but it doesn't sort correctly if the to-be-sorted object property is a collection of other objects and you want to sort by a certain element in the collection. For example, if you have a class like this:
public class ListClass
{
public List<ClassToBeSorted> ClassProperty;
}
the following won't work:
_listToBeSorted.OrderBy(x => x.ClassProperty[3].AccountType);
However, you can use my custom sorter to sort by certain elements in a property of List
type:
classList.Sort(x => x.ClassProperty[0].AccountType, "Wrap,Cash");
The Sorting Behaviour, Implementation, and Performance
If your whole list contains A, B, C, D, E, F, and you only specify partial order, i.e.:
_listToBeSorted.OrderBy(x => x.AccountType,"B,E");
then B, E will come on top of the list, and the rest of the elements will remain where they are.
Initially, I made CustomSort
to implement the IComparer
interface and use the IList.Sort(IComparer)
method to sort the list. It works like a charm if the custom sort order contains every element in the list. But when we only specify a partial order, the remaining elements don't stay where they are, but kind of randomised (or maybe, there is a pattern that I failed to see). So I ended up sorting the list myself.
The performance could be better. I tested it with a list of 1000 objects. If the sort is only by one property, sorting completes in a split second. But if you sort by two or three properties, the sort will take a few seconds. I have included a performance test in the attached source file. You can see for yourself whether the performance is satisfactory for you.
Possible Improvements
- At the moment, the custom sort only works for the generic
IList
. It is easy enough to do the same for IEnumerable
. It is just that I found IList
to be the most common collection, and you can convert IEnumerable
to List
easily by calling the ToList()
method. - The custom sort can sort by a property which is a
List
, classList.Sort(x => x.ClassProperty[0].AccountType, "Wrap,Cash")
, but it doesn't do so for Dictionary
type properties. Again, this can be easily achieved. - Performance can be improved, but I haven't been bothered to try out various sorting algorithms. I just did enough to satisfy the requirement of my own work situation.
About the Source Code and Unit Tests
The source code is in .NET 3.5. Some of the .NET 4.0 features, like optional parameters, dynamics, etc., will make the code more concise and maybe more efficient, but for better backwards compatibility, I stuck to .NET 3.5 for now. The project file should open and convert fine in VS2010 (I tested and it works). I will aim to provide a copy of the source code in .Net 4.0 soon.
The tests are written in xunit in a Behaviour Driven Development (BDD) style. When executed in ReSharper, it gives the nice documentation-like specs. To run the tests, your reSharper needs to have the xunit test runner plugin installed.