tags:

views:

212

answers:

7

I'm reviewing a piece of code I wrote not too long ago, and I just hate the way I handled the sorting - I'm wondering if anyone might be able to show me a better way.

I have a class, Holding, which contains some information. I have another class, HoldingsList, which contains a List<Holding> member. I also have an enum, PortfolioSheetMapping, which has ~40 or so elements.

It sort of looks like this:

public class Holding
{
    public ProductInfo Product {get;set;} 
    // ... various properties & methods ...
}

public class ProductInfo
{
    // .. various properties, methods... 
}

public class HoldingsList
{
    public List<Holding> Holdings {get;set;}
    // ... more code ...
}

public enum PortfolioSheetMapping
{
    Unmapped = 0,
    Symbol,
    Quantitiy,
    Price,
    // ... more elements ...
}

I have a method which can invoke the List to be sorted depending on which enumeration the user selects. The method uses a mondo switch statement that has over 40 cases (ugh!).

A short snippet below illustrates the code:

if (frm.SelectedSortColumn.IsBaseColumn)
{
    switch (frm.SelectedSortColumn.BaseColumn)
    {
        case PortfolioSheetMapping.IssueId:
            if (frm.SortAscending)
            {
                // here I'm sorting the Holding instance's
                // Product.IssueId property values...
                // this is the pattern I'm using in the switch...
                pf.Holdings = pf.Holdings.OrderBy
                  (c => c.Product.IssueId).ToList();
            }
            else
            {
                pf.Holdings = pf.Holdings.OrderByDescending
                  (c => c.Product.IssueId).ToList();
            }
            break;
        case PortfolioSheetMapping.MarketId:
            if (frm.SortAscending)
            {
                pf.Holdings = pf.Holdings.OrderBy
                  (c => c.Product.MarketId).ToList();
            }
            else
            {
                pf.Holdings = pf.Holdings.OrderByDescending
                  (c => c.Product.MarketId).ToList();
            }
            break;
        case PortfolioSheetMapping.Symbol:
            if (frm.SortAscending)
            {
                pf.Holdings = pf.Holdings.OrderBy
                  (c => c.Symbol).ToList();
            }
            else
            {
                pf.Holdings = pf.Holdings.OrderByDescending
                  (c => c.Symbol).ToList();
            }
            break;
        // ... more code ....

My problem is with the switch statement. The switch is tightly bound to the PortfolioSheetMapping enum, which can change tomorrow or the next day. Each time it changes, I'm going to have to revisit this switch statement, and add yet another case block to it. I'm just afraid that eventually this switch statement will grow so big that it is utterly unmanageable.

Can someone tell me if there's a better way to sort my list?

+3  A: 

Have you looked into Dynamic LINQ

Specifically, you could simply do something like:

var column = PortFolioSheetMapping.MarketId.ToString();
if (frm.SelectedSortColumn.IsBaseColumn)
{
    if (frm.SortAscending)
         pf.Holdings = pf.Holdings.OrderBy(column).ToList();
    else
         pf.Holdings = pf.Holdings.OrderByDescending(column).ToList();
}

Note: This does have the constraint that your enum match your column names, if that suits you.

EDIT

Missed the Product property the first time. In these cases, DynamicLINQ is going to need to see, for example, "Product.ProductId". You could reflect the property name or simply use a 'well-known' value and concat with the enum .ToString(). At this point, I'm just really forcing my answer to your question so that it at least is a working solution.

Marc
This is a bad answer I think, it looks like "read this, may be it helps and I am to lazy to help you to find a solution" =)
Restuta
Links are good they avoid redundant information.
Michael Stoll
@Marc, thanks for the link (no pun intended), but how would this help reduce my huge switch statement?
code4life
Wow, I go to edit my post to put something specific and the itchy triggers come out. Heaven forbid I'm a slow typer people! ;)
Marc
@Restuta: code4life is working with data that seems like it would be most efficiently handled in a database representation (the part that stood out to me was the fact that the data has a set of named columns, each of which can be the sorting column for the data). Marc posted a link that explains how to do such queries dynamically, which is what code4life wants.
JAB
@Marc, I think I wasn't clear. pf.Holdings is a List<Holding>, and c.Product is actually referencing a Product instance. So a simple OrderBy(string) won't work, AFAIK... Sorry about that - I'll update my post.
code4life
@JAB Without any explanation his answer isn't usefull, this is proved by the fact that he has edited his answer.
Restuta
+1  A: 

You could implement a custom IComparer class which uses reflection. However this would be slower.

Here's a class, which I once used:

class ListComparer : IComparer
{
    private ComparerState State = ComparerState.Init;
    public string Field {get;set;}


    public int Compare(object x, object y) 
    {
        object cx;
        object cy;

        if (State == ComparerState.Init) 
        {
            if (x.GetType().GetProperty(pField) == null)
                State = ComparerState.Field;
            else
                State = ComparerState.Property;
        }

        if (State == ComparerState.Property) 
        {
            cx = x.GetType().GetProperty(Field).GetValue(x,null);
            cy = y.GetType().GetProperty(Field).GetValue(y,null);
        }
        else 
        {
            cx = x.GetType().GetField(Field).GetValue(x);
            cy = y.GetType().GetField(Field).GetValue(y);
        }


        if (cx == null) 
            if (cy == null)
                return 0;
            else 
                return -1;
        else if (cy == null)
            return 1;

        return ((IComparable) cx).CompareTo((IComparable) cy);

    }

    private enum ComparerState 
    {
        Init,
        Field,
        Property
    }
}

Then use it like this:

var comparer = new ListComparer() { 
    Field= frm.SelectedSortColumn.BaseColumn.ToString() };
if (frm.SortAscending)
    pf.Holding = pf.Holding.OrderBy(h=>h.Product, comparer).ToList();
else
    pf.Holding = pf.Holding.OrderByDescending(h=>h.Product, comparer).ToList();
Michael Stoll
@Michael, I'm willing to look at this - can you elaborate a bit more on how reflection would be used? Not sure how that fits in with my switch statement and the enumeration values...
code4life
However, if your framework version allows it, I'd prefer the Dynamic LINQ solution!
Michael Stoll
+1  A: 

how about:

Func<Holding, object> sortBy;

switch (frm.SelectedSortColumn.BaseColumn)
{
    case PortfolioSheetMapping.IssueId:
        sortBy = c => c.Product.IssueId;
        break;
    case PortfolioSheetMapping.MarketId:
        sortBy = c => c.Product.MarketId;
        break;
    /// etc.
}

/// EDIT: can't use var here or it'll try to use IQueryable<> which doesn't Reverse() properly
IEnumerable<Holding> sorted = pf.Holdings.OrderBy(sortBy);
if (!frm.SortAscending)
{
    sorted = sorted.Reverse();
}

?

Not exactly the fastest solution, but it's reasonably elegant, which is what you were asking for!

EDIT: Oh, and with the case statement, it probably needs refactoring to a seperate function that returns a Func, not really a nice way to get rid of it entirely, but you can at least hide it away from the middle of your procedure !

Ed Woodcock
@Michael fixed the original where I'd copied the wrong if, that's correct now yes?
Ed Woodcock
It's correct, but I'd prefer IEnumerable<Holding> sorted = (frm.SortAscending) ? pf.Holdings.OrderBy(sortBy) : pf.Holdings.OrderByDescending(sortBy). This avoids the Reverse()
Michael Stoll
@Michael I'm not sure it makes a huge difference, it seems that OrderByDescending just returns a reversed enumerator internally anyway judging by the output from Reflector, but I get your point.
Ed Woodcock
+4  A: 

You could try reducing the switch to something like this:

    private static readonly Dictionary<PortfolioSheetMapping, Func<Holding, object>> sortingOperations = new Dictionary<PortfolioSheetMapping, Func<Holding, object>>
    {
        {PortfolioSheetMapping.Symbol, h => h.Symbol},
        {PortfolioSheetMapping.Quantitiy, h => h.Quantitiy},
        // more....
    };

    public static List<Holding> SortHoldings(this List<Holding> holdings, SortOrder sortOrder, PortfolioSheetMapping sortField)
    {
        if (sortOrder == SortOrder.Decreasing)
        {
            return holdings.OrderByDescending(sortingOperations[sortField]).ToList();
        }
        else
        {
            return holdings.OrderBy(sortingOperations[sortField]).ToList();                
        }
    }

You could populate sortingOperations with reflection, or maintain it by hand. You could also make SortHoldings accept and return an IEnumerable and remove the ToList calls if you don't mind calling ToList in the caller later. I'm not 100% sure that OrderBy is happy receiving an object, but worth a shot.

Edit: See LukeH's solution to keep things strongly typed.

Douglas
I was about to suggest a dictionary of comparison classes, but this is MUCH neater!
Dr Herbie
Thanks - that helps!
code4life
@Douglas, thanks for the tip! This is definitely easier to code, but I think I'll go with @LukeH's suggestion to use .Sort() instead, because it's a bit faster. But conceptually the same approach as you (using a dictionary, etc etc).
code4life
+1  A: 

It seems to me that there are two immediate improvements we can make:

  • the logic that uses frm.SortAscending to decide between OrderBy and OrderByDesccending is duplicated in every case, and can be pulled out to after the switch if the cases are changed to do nothing more than establishing the sort key and putting it in a Func

  • that still leaves the switch itself of course - and this can be replaced by a static map (in a Dictionary, say) from PortfolioSheetMapping to a Func taking a Holding and returning the sort key. eg

AakashM
Thanks, these are good suggestions, definitely helps modularize the code better.
code4life
+5  A: 

You're re-assigning the sorted data straight back to your pf.Holdings property, so why not bypass the overhead of OrderBy and ToList and just use the list's Sort method directly instead?

You could use a map to hold Comparison<T> delegates for all the supported sortings and then call Sort(Comparison<T>) with the appropriate delegate:

if (frm.SelectedSortColumn.IsBaseColumn)
{
    Comparison<Holding> comparison;
    if (!_map.TryGetValue(frm.SelectedSortColumn.BaseColumn, out comparison))
        throw new InvalidOperationException("Can't sort on BaseColumn");

    if (frm.SortAscending)
        pf.Holdings.Sort(comparison);
    else
        pf.Holdings.Sort((x, y) => comparison(y, x));
}

// ...

private static readonly Dictionary<PortfolioSheetMapping, Comparison<Holding>>
    _map = new Dictionary<PortfolioSheetMapping, Comparison<Holding>>
    {
        { PortfolioSheetMapping.IssueId,  GetComp(x => x.Product.IssueId) },
        { PortfolioSheetMapping.MarketId, GetComp(x => x.Product.MarketId) },
        { PortfolioSheetMapping.Symbol,   GetComp(x => x.Symbol) },
        // ...
    };

private static Comparison<Holding> GetComp<T>(Func<Holding, T> selector)
{
    return (x, y) => Comparer<T>.Default.Compare(selector(x), selector(y));
}
LukeH
Good suggestion, definitely worth trying out.
code4life
@LukeH, thanks! Anecdotally the <code>.Sort</code> is slightly but definitely faster than .OrderBy().ToList().
code4life
Nice, I was wondering how to strongly type my solution - this, is better: keep it encapsulated in the delegate.
Douglas
+1  A: 

If the properties in the Holding class (symbol, price etc) are the same type you can do the following:

var holdingList = new List<Holding>()
{
      new Holding() { Quantity = 2, Price = 5 },
      new Holding() { Quantity = 7, Price = 2 },
      new Holding() { Quantity = 1, Price = 3 }
};

var lookup = new Dictionary<PortfolioSheetMapping, Func<Holding, int>>()
{
      { PortfolioSheetMapping.Price, new Func<Holding, int>(x => x.Price) },
      { PortfolioSheetMapping.Symbol, new Func<Holding, int>(x => x.Symbol) },
      { PortfolioSheetMapping.Quantitiy, new Func<Holding, int>(x => x.Quantity) }
};

Console.WriteLine("Original values:");
foreach (var sortedItem in holdingList)
{
    Console.WriteLine("Quantity = {0}, price = {1}", sortedItem.Quantity, sortedItem.Price);
}

var item = PortfolioSheetMapping.Price;
Func<Holding, int> action;
if (lookup.TryGetValue(item, out action))
{
    Console.WriteLine("Values sorted by {0}:", item);
    foreach (var sortedItem in holdingList.OrderBy(action))
    {
         Console.WriteLine("Quantity = {0}, price = {1}", sortedItem.Quantity, sortedItem.Price);
    }
}

which then displays:

Original values:
Quantity = 2, price = 5
Quantity = 7, price = 2
Quantity = 1, price = 3

Values sorted by Price:
Quantity = 7, price = 2
Quantity = 1, price = 3
Quantity = 2, price = 5

Matt Warren
@Matt, I like the Func idea. The properties in the Holding class are different types, so I would have to take a modified approach from what you are suggesting, but good idea nonetheless.
code4life
I've just seen the other answers that were posted when I was doing mine and you can change the lookup to lookup = new Dictionary<PortfolioSheetMapping, Func<Holding, object>> and this should work
Matt Warren