9

I have a list of events and now I want to find out which events do overlap. Below you find the code I currently have, but I've the problem that the item which is searched for is also included in the list.

List<SomeEventObject> overlappingEvents = new List<SomeEventObject>();
foreach (SomeEventObject eventItem in EventList)
{
    bool overlapping = false;
    foreach (SomeEventObject anotherEventItem in EventList)
    {
        if (eventItem.StartDate <= anotherEventItem.EndDate &&
            eventItem.EndDate >= anotherEventItem.StartDate)
        {
            overlapping = true;
            overlappingEvents.Add(anotherEventItem);
        }
    }

    if (overlapping)
        overlappingEvents.Add(eventItem);
}

I would need to create a new list without the item searched for. Therefore I'm asking if there is a nice LINQ expression which can handle that for me. This is some pseudo code I thought of:

EventList.Where(e => 
                eventItem.StartDate <= e.EndDate && 
                eventItem.EndDate >= e.StartDate);

In this context eventItem doesn't exist of course.

As a result I think I would need two lists: one with overlapping events and one with non-overlapping events. But that should be possible with .Except() if I have my overlapping event list.

Edit:

I've created a dotnetfiddle so that one can play with it. One important question is the overlapping algorithm.

Event 1:
StartDate: today, 10:00
EndDate: today, 10:05

Event 2:
StartDate: today, 10:05
EndDate: today, 10:10

If you present this to the user then this is NOT overlapping. So I've to revise my algorithm.

PiotrWolkowski
  • 8,408
  • 6
  • 48
  • 68
testing
  • 19,681
  • 50
  • 236
  • 417
  • Just simply exclude it by Id. An item will always fit within its own time frame. – Gert Arnold May 26 '15 at 13:22
  • What do you mean with *do overlap*. It is possible event *A* overlaps with *B* and *B* with *C*. But that doesn't imply *A* overlaps with *C*. Overlapping is a relative property. – Willem Van Onsem May 26 '15 at 13:24
  • How many events will you have in this list? Average/usual vs. worst case? – Lasse V. Karlsen May 26 '15 at 13:27
  • @CommuSoft: With overlapping I mean time overlapping. There can be all sorts of time intersection. E.g. event *A* overlaps with *B* and *C*, but *B* doesn't overlaps with *C* (only with *A*). So I have to be aware of all cases. Perhaps a multidimensional array would be a better idea, but currently I only thought about detecting the overlapping. – testing May 26 '15 at 13:32
  • @LasseV.Karlsen: I'd say average would be 5 and worst case 50 (only an assumption for the number of events a person would have on one day). – testing May 26 '15 at 13:34
  • @Bombinosh: Do you mean something like this? `overlappingEvents = EventList.Join(EventList, e1 => e1, e2 => e2, (e1,e2) => e1.StartDate <= e2.EndDate && e1.EndDate >= e2.StartDate);` That throws an error: *Cannot implicitely convert type 'System.Collections.Generic.IENumerable' to 'System.Collections.Generic.IENumerable'. An explicit conversion exists (are you missing a cast?).* – testing May 26 '15 at 14:52
  • @testing; nevermind, join is not adequate for your problem. it's just for an inner join based on a key. – Bombinosh May 26 '15 at 17:11

5 Answers5

7

I would do it this way:

var overlappingEvents =
(
    from e1 in EventList
    from e2 in EventList
    where e1 != e2
    where e1.StartDate <= e2.EndDate
    where e1.EndDate >= e2.StartDate
    from e in new [] { e1, e2 }
    select e
).ToList();

I think that should be quite straight forward.


As per the comment, this version only returns an EventList element once regardless of how many overlaps it participates in.

var overlappingEvents =
(
    from e1 in EventList
    where EventList
        .Where(e2 => e1 != e2)
        .Where(e2 => e1.StartDate <= e2.EndDate)
        .Where(e2 => e1.EndDate >= e2.StartDate)
        .Any()
    select e1
).ToList();

It can also be written as:

var overlappingEvents =
    EventList
        .Where(e1 =>
            EventList
                .Where(e2 => e1 != e2)
                .Where(e2 => e1.StartDate <= e2.EndDate)
                .Where(e2 => e1.EndDate >= e2.StartDate)
                .Any())
        .ToList();

Based on the further comment, here's how to pair the events:

var overlappingEvents =
(
    from e1 in EventList
    from e2 in EventList
    where e1 != e2
    where e1.StartDate <= e2.EndDate
    where e1.EndDate >= e2.StartDate
    select new [] { e1, e2 }
).ToList();

Now overlappingEvents is a list of arrays - List<SomeEventObject[]> and not List<SomeEventObject>. The array contains the overlapping events.

Enigmativity
  • 113,464
  • 11
  • 89
  • 172
  • I tried your code in [this dotnetfiddle](https://dotnetfiddle.net/Q216FM). But there are too many results (four results but only two events). – testing May 27 '15 at 13:49
  • @testing - This is a direct translation of your code. Both sides of each pair are returned. I can modify to return only two events. – Enigmativity May 28 '15 at 06:47
  • One follow-up question: I need to know for an event if it overlaps with another one. In the above case I get all events in my event list which do overlap, but I can't say which one is overlapping with another one. Simply distinguishing in overlapping and non overlapping events is not enough. I've demonstrated the issue in [this example](https://dotnetfiddle.net/HYENi7). The function does work but I don't have the information that event 1 does overlap with 2 and event 3 does overlap with 4. Do you have an idea how to solve this? Iterating through all events? – testing May 28 '15 at 10:45
  • @testing - Further solution added. – Enigmativity May 28 '15 at 12:02
  • superb answer! this has helped me no end and is the most elegant solution ive seen all day – Simon Price Apr 04 '17 at 18:58
2

I'd try something like this:

var searchedFor = EventList.First(); // Replace by the proper one

var overlapping = EventList.Where(e => e != searchedFor && 
    EventList.Any(ev => e != ev && ev.StartDate <= e.EndDate && ev.EndDate >= e.StartDate))

// Alternatively with Except

var overlapping = EventList.Except(new[] { searchFor }).Where(e => 
    EventList.Any(ev => e != ev && ev.StartDate <= e.EndDate && ev.EndDate >= e.StartDate))

For the non-overlapping you simply filter the overlapping events from your EventList using Except:

var nonOverlapping = EventList.Except(overlappingEvents);

I don't know your SomeEventObject but you may need to change the inequality comparison by some Id comparison.

There is a quite good Q&A on detecting overlapping periods in C# that you may want to look into. You can also factor out the check as follows for convenience:

Func<SomeEventObject, SomeEventObject, bool> areOverlapped = (e1, e2) => 
    e1.StartDate <= e2.EndDate && e1.EndDate >= e2.StartDate;

var overlapping = EventList.Where(e => e != searchedFor && 
    EventList.Any(ev => e != ev && areOverlapped(e, ev)));
Community
  • 1
  • 1
jnovo
  • 5,659
  • 2
  • 38
  • 56
  • I changed the condition to `ev.StartDate <= e.EndDate && ev.EndDate > e.StartDate` so that there is no intersection when *ev* ends on 10:05 and *e* starts on 10:05. You can try this [here](https://dotnetfiddle.net/cM0Ccn). If I do this I get one result - namely *ev* is in the result list. But why? – testing May 27 '15 at 08:01
  • @testing that's because I missed the check to exclude the search for event: I corrected it in the edit – jnovo May 27 '15 at 08:13
  • How is this factoring out called? Where should I put the *areOverlapped* function? Can I put this anywhere? – testing May 28 '15 at 10:53
  • @testing since your original question regarded the LINQ query - and not what is considered overlapped or not - I just pointed out that you can parameterize the function checking the overlap.. – jnovo May 28 '15 at 13:03
1

You can get the result with this:

var nonOverlappingEvents = EventList.Where(e1 => !EventList.Where(e2 => e2 != e1).Any(e2 => e1.StartDate <= e2.EndDate && e1.EndDate >= e2.StartDate));

But I do not understand your overlapping algorithm. I believe overlap means that StartDate of one event is between StartDate and EndDate of another one:

    var nonOverlappingEvents = EventList.Where(e1 => !EventList.Where(e2 => e2 != e1).Any(e2 => e2.StartDate >= e1.StartDate && e2.StartDate <= e1.EndDate));
Alex Sikilinda
  • 2,928
  • 18
  • 34
  • When I try to execute your code I get *Cannot convert lambda expression to type 'IENumerable' because it is not a delegate type*. Do you have an idea why? I think I have to reconsider the overlapping algorithm ... – testing May 26 '15 at 14:48
  • I should have used `Where` instead of `Except`. I've edited the answer. – Alex Sikilinda May 26 '15 at 15:29
  • Thanks. I tried [both](https://dotnetfiddle.net/y021Gh) of your conditions but they seem to fail. Perhaps the overlapping algorithm has to be reconsidered? – testing May 27 '15 at 07:38
  • 1
    What is your definition of "overlap"? Check [this answer](http://stackoverflow.com/questions/13513932/algorithm-to-detect-overlapping-periods) – Alex Sikilinda May 27 '15 at 13:02
  • I should have read the **non** in *nonOverlappingEvents* ... In general *OverlapsWith* in [this graphic](http://www.codeproject.com/KB/datetime/TimePeriod/PeriodRelations_small.png) of [Time Period Library for .NET](http://www.codeproject.com/Articles/168662/Time-Period-Library-for-NET) describes it very good. I have to make some checks and adapt the query to what I need. Especially what happens with multiple overlaps and so on. – testing May 27 '15 at 13:43
1

I know you said you want to use Linq and it is certainly reasonably straight forward. We used to do that - however we do a lot of date range work and found a much better way of doing it.

If you are doing a lot of this time of work I would recommend the Itenso Time Period Library (available on Nuget).

It has support for a very rich range of time based operations. Such as IsSamePeriod, HasInside, OverlapsWith, or IntersectsWith are available for period relations.

It also support time collections e.g. All events for a person. These are based on ITimePeriodCollection can hold arbitrary elements of type ITimePeriod and interprets the earliest start of all its elements as the start of the collection time period. Correspondingly, the latest end of all its elements serves as the end of the collection period.

Some code example for a flavour of how it works

  //Setup a time range
TimeRange timeRange1 = new TimeRange(
        new DateTime( 2011, 2, 22, 14, 0, 0 ),
        new DateTime( 2011, 2, 22, 18, 0, 0 ) );
      Console.WriteLine( "TimeRange1: " + timeRange1 );
      // > TimeRange1: 22.02.2011 14:00:00 - 18:00:00 | 04:00:00

  // --- Setu uptime range 2 ---
  TimeRange timeRange2 = new TimeRange(
    new DateTime( 2011, 2, 22, 15, 0, 0 ),
    new TimeSpan( 2, 0, 0 ) );
  Console.WriteLine( "TimeRange2: " + timeRange2 );
  // > TimeRange2: 22.02.2011 15:00:00 - 17:00:00 | 02:00:00


  // --- relation ---
  Console.WriteLine( "TimeRange1.GetRelation( TimeRange2 ): " +
                     timeRange1.GetRelation( timeRange2 ) );

  // --- intersection ---
  Console.WriteLine( "TimeRange1.GetIntersection( TimeRange2 ): " +
                     timeRange1.GetIntersection( timeRange2 ) );
  // > TimeRange1.GetIntersection( TimeRange2 ):
  //             22.02.2011 15:00:00 - 17:00:00 | 02:00:00
GraemeMiller
  • 11,973
  • 8
  • 57
  • 111
  • Thanks for your addition and your more detailed explanation. I've my custom object. If I see that correctly I've to put the data from my custom objects into `ITimePeriodCollection`. But how do I know that event with the id 1234 is exactly the `ITimePeriod`, which is overlapping? Events can have the same start and end time ... – testing May 27 '15 at 11:52
  • 1
    You can make your events implement an interface ITimeRange. You can load anything that implements ITimeRange into an ITimePeriodCollection. You could also use TimeRange as a base class. – GraemeMiller May 27 '15 at 16:26
0

Not with linq but groups overlaping periods into sets:

public class Range
{
    public DateTime Start { get; set; }
    public DateTime Stop { get; set; }

    public Range(DateTime start, DateTime stop)
    {
        Start = start;
        Stop = stop;
    }
}

void Main()
{
    List<Range> ranges = new List<Range>();
    ranges.Add(new Range(new DateTime(2019, 10, 1), new DateTime(2019, 10, 2)));
    ranges.Add(new Range(new DateTime(2019, 10, 2), new DateTime(2019, 10, 3)));
    ranges.Add(new Range(new DateTime(2019, 10, 1), new DateTime(2019, 10, 3)));
    ranges.Add(new Range(new DateTime(2019, 10, 4), new DateTime(2019, 10, 5)));
    ranges.Add(new Range(new DateTime(2019, 10, 6), new DateTime(2019, 10, 8)));
    ranges.Add(new Range(new DateTime(2019, 10, 5), new DateTime(2019, 10, 7)));
    ranges.Add(new Range(new DateTime(2019, 10, 9), new DateTime(2019, 10, 9)));

    var rangesOrdered = ranges.OrderBy(t => t.Start).ToList();

    var sets = new List<List<Range>>() { new List<Range>() {rangesOrdered[0]}};
    var IsOverlaping = new Func<Range, Range, bool>((a, b) => a.Start < b.Stop && b.Start < a.Stop);


    for (var i = 1; i < rangesOrdered.Count; i++)
    {
            if (IsOverlaping(sets.Last().OrderBy(s => s.Stop).Last(), rangesOrdered[i]))
            {
                sets.Last().Add(rangesOrdered[i]);
            }
            else
            {
                sets.Add(new List<Range>() {rangesOrdered[i]});
            }
    }
}
mdziadowiec
  • 396
  • 3
  • 10