views:

328

answers:

5

I have an IList of type Breadcrumb which is just a lightweight class that has NavigationTitle, NavigationUrl and IsCurrent properties. It is cached on the webserver. I have a method that builds out the current breadcrumb trail up until the first Breadcrumb that has IsCurrent set to true... using the code below. Its very ugly and definitely a quick dirtbag willie solution, but I was curious, can this be easily refactored into LINQ?

IList<Breadcrumb> crumbs = new List<Breadcrumb>();
bool foundCurrent = false;
for (int a = 0; a < cachedCrumbs.Count; a++)
{
    crumbs.Add(crumbs[a]);
    if (foundCurrent)
    {
      break;
    }
    foundCurrent = (crumbs[a + 1] != null && ((Breadcrumb)crumbs[a + 1]).IsCurrent);
}
+1  A: 

First of all, that code doesn't work. I'm gonna guess that some of those places where you used "crumbs" you meant "cachedCrumbs". If so, the code can be reduced to:

IList<Breadcrumb> crumbs = new List<Breadcrumb>();
for (int a = 0; a < cachedCrumbs.Count; a++)
{
    crumbs.Add(cachedCrumbs[a]);
    if (cachedCrumbs[a] != null && cachedCrumbs[a].IsCurrent)
    {
          break;
    }
}
James Curran
Hi James. The code does work, I just tried to give a high level overview. I did not mean crumbs where I have cachedCrumbs. The cachedCrumbs is ALL of the possible crumbs, in order. Think of it like steps in a workflow, and not actual web breadcrumbs. Thx for tip on removing foundCurrent though!
EvilSyn
No, you misread James' comment - your code had: "crumbs.Add(crumbs[a]);" where you actually meant "crumbs.Add(cachedCrumbs[a]);"
Jon Skeet
+4  A: 

I'm typing this as I think, so that it shows a train of thought as well as just an answer.

  • Your source is just cachedCrumbs
  • You want to add the first crumb which does have IsCurrent set, but nothing afterwards
  • TakeWhile sounds like the way to go, but getting the "previous value had IsCurrent" is a bit of a pain
  • We can use a closure to effectively keep a variable determining whether the last value had IsCurrent set
  • We can do a somewhat "no-op" select to keep the TakeWhile separate from the working out of whether to keep going

So, we end up with:

bool foundCurrent = false;

var crumbs = cachedCrumbs.TakeWhile(crumb => !foundCurrent)
                         .Select(crumb => { 
                                 foundCurrent = crumb == null || !crumb.IsCurrent; 
                                 return crumb; });

I haven't tried this, but I think it should work... there might be a simpler way though.

EDIT: I'd argue that actually a straight foreach loop is simpler in this case. Having said that, you could write another extension method which acted like TakeWhile except it also returned the element which caused the condition to fail. Then it would be as simple as:

var crumbs = cachedCrumbs.NewMethod(crumb => crumb == null || !crumb.IsCurrent);

(I can't think of a decent name for the method at the moment, hence NewMethod !)

Jon Skeet
Hi Jon, this worked! Thank you. I understand what the select is doing, but I'm a little lost on the .TakeWhile(crumb => foundCurrent) - what exactly is that doing? And, why do we see foundCurrent again in the select?
EvilSyn
The TakeWhile was actually wrong - it should have been "!foundCurrent" rather than "foundCurrent" - I've fixed it. The TakeWhile lambda is just "keep going while we haven't found current". The select lambda is basically to set foundCurrent *after* the TakeWhile.
Jon Skeet
+1  A: 

An alternative answer based on James Curran's - this can certainly be improved using a foreach statement:

IList<Breadcrumb> crumbs = new List<BreadCrumb>();
foreach (Breadcrumb crumb in cachedCrumbs)
{
    crumbs.Add(crumb);
    if (crumb != null && crumb.IsCurrent)
    {
        break;
    }
}
Jon Skeet
foreach loop doesn't guarantee that the crumbs are accessed in the order they actually appear.
chakrit
They do for all sensible collections that support indexing by number. Can you think of a common IList implementation which misbehaves in this respect?
Jon Skeet
Thx for the foreach.. I had that first, but for some reason took it out. :) and this is the mess that was left afterwards. :)
EvilSyn
I took it out because for some reason I thought I had to check the next crumb to determine if IsCurrent == true. That's why I removed the foreach. Which, turns out, isn't needed anymore, so I'll go back to foreach or the LINQ suggestion above.
EvilSyn
Ah.. I can think of none... I mean it isn't guaranteed in the docs, is all.
chakrit
I think it's assumed in *so* much code that any implementation which didn't abide by it would very rapidly get tossed aside.
Jon Skeet
+1  A: 

How about...

// find the current item
var currentItem = cachedCrumbs.First(c => c.IsCurrent);
var currentIdx = cachedCrumbs.IndexOf(currentItem);

// get all items upto current item
var crumbs = cachedCrumbs.Take(currentIdx + 2);

And you can turn this into a TakeUpto method which takes all items upto the one which matched the predicates you supply.

How about:

public static IEnumerable<T> TakeUpto<T>(this IList<T> theList, Func<T, bool> predicate)
{
    var targetItem = theList.First(predicate);
    var targetIdx = theList.IndexOf(targetItem);

    return theList.Take(targetIdx + 2);
}

Then you could use it this way:

var crumbs = cachedCrumbs.TakeUpto(c => c.IsCurrent);

Much cleaner!

Havn't check the nulls and off-by-one cases and IList/IEnumerable differences, but you should get the idea.

chakrit
Oh nice! I like this solution too! Thank you. :)
EvilSyn
Chakrit, I liked this solution alot and decided to use it. Thank you. :) I like the re-use candidacy of it.
EvilSyn
Jon's solution can be refactored into an extension method that is as reusable... but it's a little complicated... I like more readability :-) but Jon's solution might execute faster though
chakrit
Note that this solution relies on the ordering of the list's enumerator just as much as the foreach which you were critical of :)
Jon Skeet
(And yes, I think it's off by one - if it matches the first element, IndexOf will return 0, but you want to take one element.)My main concern with this solution is that it iterates over the sequence 3 times. In *some* cases that will be expensive.
Jon Skeet
Just thought - as it can only work on ILists, it's less likely to be expensive than if it were dealing with IEnumerable. ILists don't tend to be lazily evaluated, but instead are fully in-memory. It wouldn't be impossible to have a lazy IList, admittedly... just unlikely.
Jon Skeet
it was my 'Just thought' too dude, nothing critically serious
chakrit
+1  A: 

This answer is an alternative implementation of chakrit's TakeUpTo:

public static IEnumerable<T> TakeUpto<T>(this IEnumerable<T> theList, Func<T, bool> predicate)
{
    foreach (T element in theList)
    {
        yield return element;
        if (predicate(element))
        {
            break;
        }
    }
}

This only iterates through the list once, which could be relevant in various cases. (Suppose the upstream sequence is the result of an OrderBy clause - you really don't want it to have to sort the results multiple times for no good reason.)

It also allows any IEnumerable<T> as the source, which makes it more flexible.

One of the wonderful things about LINQ is the multiple ways of achieving the same goal.

Jon Skeet
Thanks Jon, I really like this solution too!
EvilSyn