score:6

Accepted answer

It's not very efficient nor very readable, but you can do it in single query (see efficient solution at the bottom):

var twoHours = list.Where(d => d < blackoutStartTime || blackoutEndTime < d)
                   .OrderBy(d => d) // if sequence is not ordered
                   .GroupBy(d => blackoutEndTime < d)
                   .OrderBy(g => g.Key)
                   .Select(g => g.Take(2))
                   .Where(g => g.Count() == 2)
                   .SelectMany(g => g)
                   .Take(2);

Output:

7/8/2014 04:00:00
7/8/2014 05:00:00

Explanation:

  1. Filter out dates which does not fall in range - we don't need them
  2. Group all dates in two groups - dates less than range and dates bigger than range
  3. Order two groups so that smaller dates group will be first
  4. Select only first two dates from each group
  5. Take those groups which have at least two dates
  6. Project filtered result into flat sequence of dates
  7. Select first two, if any

More efficient way (if sequence is sorted, otherwise you should sort it before querying) - a little improved suggestion by Jim Mischel (I would go two queries way for much better readability):

var twoHours = list.TakeWhile(d => d < blackoutStartTime).Take(2).ToList();

if (twoHours.Count < 2)
    twoHours = list.SkipWhile(d => d <= blackoutEndTime).Take(2).ToList();

What was improved - you don't need to save each query result into list. That will enumerate all items which match condition and create new list in memory. If you have many items before range, or if you have less than two items before range and many items after range - that is not what you want. So, take only first two items and save them to list. In ideal world you would enumerate only first two items an stop. If not, then you will enumerate all items till the range end + 2.

score:0

list.Where(d => d >= blackoutStartTime && d <= blackoutEndTime)
    .Sort((a, b) => b.CompareTo(a))
    .Take(2);

You might want to also sort to make sure you're indeed getting the last 2.

score:3

To get consecutive values, use Zip like this:-

        // Assume an ordered pair of date times
        // if the later is before the start or the earlier is after the end, 
        // then there is no overlap
        Func<DateTime, DateTime, bool> outOfRange = (DateTime a, DateTime b) => 
                  b < blackoutStartTime || a > blackoutEndTime;

        var result = list.Zip(list.Skip(1), (a, b) => new { a, b })
            .Where(x => outOfRange(x.a, x.b))
            .First();

Doing it this way also lets you simplify the test for having an overlap with the blackout range.

This does assume that he initial list is in order, if it isn't, sort it first.

This answer also excludes a range like 1AM to 4AM which totally encloses the blackout window whereas most other answers do not.

score:4

I'll guess your problem is that your code returns erroneous results when you have, for example, something like:

[out,in,in,in,out,out]

That is, one time outside the range followed by some in the range, and then more outside the range. You want two consecutive items. You'll also have a problem with:

[out1,out2,out3,in,in,in,out4,out5]

Because if I read your question right you want out2 and out3.

The simple-minded way to do this with LINQ is multiple queries. I assume that your list is in order:

var before = list.TakeWhile(d => d <= BlackoutStart).ToList();
if (before.Count >= 2)
{
    return before.Skip(before.Count-2);
}
var after = list.SkipWhile(d => d <= BlackoutEnd).ToList();
if (after.Count >= 2)
{
    return after.Take(2);
}
// Error here because you didn't have two consecutive items.

Offhand, I can't see a way to do it with a single LINQ query, although it might be possible to optimize what I have above.

You could do it with a single pass over the list with a loop, but the logic is kind of messy.


Related Query

More Query from same tag