Assuming I have a simple structure that looks like this:
public class Range
{
public DateTime Start { get; set; }
public DateTime End { get; set; }
public Range(DateTime start, DateTime end)
{
this.Start = start;
this.End = end;
}
}
And I create an collection like so:
var dr1 = new Range(new DateTime(2011, 11, 1, 12, 0, 0),
new DateTime(2011, 11, 1, 13, 0, 0));
var dr2 = new Range(new DateTime(2011, 11, 1, 13, 0, 0),
new DateTime(2011, 11, 1, 14, 0, 0));
var dr3 = new Range(new DateTime(2011, 11, 1, 14, 0, 0),
new DateTime(2011, 11, 1, 15, 0, 0));
var dr4 = new Range(new DateTime(2011, 11, 1, 16, 0, 0),
new DateTime(2011, 11, 1, 17, 0, 0));
var ranges = new List<Range>() { dr1, dr2, dr3, dr4 };
What I want to do is group the ranges where they are continuous - i.e. they are continuous if the End value of the previous Range is the same as the Start of the next.
We can assume that there are no collisions/duplicates or overlaps in the Range values.
In the example posted, I would end up with two groups:
2011-11-1 12:00:00 - 2011-11-1 15:00:00
2011-11-1 16:00:00 - 2011-11-1 17:00:00
It's fairly easy to come up with an iterative solution for this. But is there some LINQ magic I can use to get this in a pretty one-liner?
A generic version of casperOne's extension method, used as such:
Implementation of extension method
The following works but it really is an abuse of LINQ:
The code does the following:
First select:
Where: Only select those elements that mark the start of a new continuous range
Second select:
Range
object with the start date of the saved start value and the end value of the previous item.Please note:
I would stick with the iterative solution, because the above code is unreadable, unmaintainable and it took me significantly more time than just typing a loop and an if...
The below code does it in query comprehension syntax.
I don't think I would actually do this in production code because I feel it violates the rule of least surprise. Where linq statements usually has no side effects, this works because it has side effects. However I thought it worthwhile to post to show that indeed it can be solved using query comprehension syntax in O(n)
Here is another LINQ solution. It finds the start of each continuous range with one query, the end of each continuous range with another, and then goes through the pairs to construct new ranges.
It could be rewritten into a one-liner, but the separate version is clearer and easier to maintain:
Your best bet is to use
yield
and an extension method:Granted, the call to
OrderBy
is going to cause a full iteration of theranges
sequence, but there's no avoiding that. Once you have it ordered, you can prevent having to materialize your results before returning them; you simplyyield
the results if the conditions dictate.If you know that the sequence is ordered, however, then you don't have to call
OrderBy
at all, and you canyield
items as you traverse the list and break on different collapsedRange
instances.Ultimately, if the sequence is unordered, then you have two options:
OrderBy
is deferred as well, but will have to use one full iteration to order the sequence), usingyield
to return the item when you have one to process