'How to validate Date Start and Finish Overlap from a list of items
What I have
A list of objects with Id
, DateStart
and DateFinish
.
[
{
Id: 1234567890,
DateStart: new DateTime(),
DateFinish: new DateTime(),
},
...
]
What I need to do
I need to validate if none of the dates overlap each other.
I'm not sure if overlap
is passing the right meaning here, so here is some examples:
Invalid Entry
[
{
Id: 1,
DateStart: new DateTime().AddHours(1),
DateFinish: new DateTime().AddHours(3),
},
{
Id: 2,
DateStart: new DateTime().AddHours(2),
DateFinish: new DateTime().AddHours(4),
}
]
This list have an overlap because the time of id 2 is in the middle of id 1
A table to show better:
-------------------------------------------------------------
| 1 | 2 | 3 | 4 |
| DateStart1 | | DateFinish1 | |
| | DateStart2 | | DateFinish2 |
-------------------------------------------------------------
*overlap* *overlap*
Other Invalid Examples
-------------------------------------------------------------
| 1 | 2 | 3 | 4 |
| DateStart1 | | | DateFinish1 |
| | DateStart2 | DateFinish2 | |
-------------------------------------------------------------
*overlap* *overlap*
-------------------------------------------------------------
| 1 | 2 | 3 | 4 |
| DateStart1 | | | DateFinish1 | // This would be a full overlap
| DateStart2 | | | DateFinish2 | // And it's also Invalid
-------------------------------------------------------------
*overlap* *overlap*
-------------------------------------------------------------
| 1 | 2 | 3 | 4 |
| | DateStart1 | | DateFinish1 | // Same as first example
| DateStart2 | | DateFinish2 | | // But "inverted"
-------------------------------------------------------------
*overlap* *overlap*
Valid Entry
[
{
Id: 1,
DateStart: new DateTime().AddHours(1),
DateFinish: new DateTime().AddHours(2),
},
{
Id: 2,
DateStart: new DateTime().AddHours(2),
DateFinish: new DateTime().AddHours(4),
}
]
A table to show better:
-------------------------------------------------------------
| 1 | 2 | 3 | 4 |
| DateStart1 | DateFinish1 | | |
| | DateStart2 | | DateFinish2 |
-------------------------------------------------------------
*not overlap*
And you can also have DateStart
and DateFinish
that are the same value, which means it can start and end at the same time.
-------------------------------------------------------------
| 1 | 2 | 3 | 4 |
| DateStart1 | | | |
| DateFinish1 | | | |
| DateStart2 | | | DateFinish2 |
-------------------------------------------------------------
*not overlap*
What I have done so far:
I'm making a foreach loop, where item
is each element, and using a where with the following expression:
myList.Any(
x => x.Id == item.Id
&&
(
(
item.DateStart <= x.DateStart
&&
item.DateFinish > x.DateStart
&&
item.DateFinish <= x.DateFinish
)
||
(
item.DateStart >= x.DateStart
&&
item.DateStart < x.DateFinish
&&
item.DateFinish > x.DateFinish
)
||
(
item.DateStart <= x.DateStart
&&
item.DateFinish >= x.DateFinish
)
)
)
My Question
Is this expression correct? I have tried it with a lot of data and it seems to be wrong sometimes.
I need to be certain that it will cover all edge cases.
If there is a better way of writing all this logic, it would help to, because this code looks to ugly and hard to understand for other people.
Solution 1:[1]
I would use the following code:
static bool IsOverlapping(IEnumerable<Range> list)
{
Range previousRange = null;
foreach (var currentRange in list.OrderBy(x => x.DateStart).ThenBy(x => x.DateFinish))
{
if (currentRange.DateStart > currentRange.DateFinish)
return true;
if (previousRange?.DateFinish > currentRange.DateStart)
return true;
previousRange = currentRange;
}
return false;
}
Solution 2:[2]
A quick and dirty version. Not very performant as is on large sets. But can be improved on.
https://dotnetfiddle.net/Widget/PEn2Lm
static void DetectOverlap(List<Range> l)
{
foreach(var r in l)
{
var overlap = l.Any(x => x.Id != r.Id
&& ((r.Start == x.Start && r.End == x.End)
|| (r.Start >= x.Start && r.Start < x.End)
|| (r.End > x.Start && r.End <= x.End)));
if(overlap)
{
Console.WriteLine("Overlap detected");
throw new Exception("Overlapping range detected");
}
}
Console.WriteLine("Clean ranges");
}
Solution 3:[3]
I tried to mirror your cases, but I'd suggest writing unit tests to full test all of your scenarios.
List<FooBar> bars = new List<FooBar>()
{
new FooBar() //end date is inside 3
{
Start = new DateTime(2001,12,1),
End = new DateTime(2002,5,15),
Id = 1
},
new FooBar() //fine
{
Start = new DateTime(2005,12,1),
End = new DateTime(2006,5,15),
Id = 2
},
new FooBar() //start date is inside 1
{
Start = new DateTime(2002,4,1),
End = new DateTime(2003,5,15),
Id = 3
},
new FooBar() //this one is fine
{
Start = new DateTime(2006,5,15),
End = new DateTime(2007,5,15),
Id = 4
},
new FooBar() //also fine
{
Start = new DateTime(2001,12,1),
End = new DateTime(2001,12,1),
Id = 5
},
};
And then a, to me at least, slightly easier to read / skim code snippet which seems to work perfectly:
var inside = bars.Where(w =>
bars.Where(outer => ((outer.Start < w.Start && outer.End > w.Start)
|| (outer.Start < w.End && outer.End > w.End)
|| (outer.Start == w.Start && outer.End == w.End)) && outer.Id != w.Id).Any()).ToList();
inside.ForEach(e => {
Console.WriteLine($"{e.Id}");
});
For actual use, I'd also test for Any or First, and not ToList, but this gives me the Ids for the console to check.
As for why I used this logic, it might prove faulty but my assumptions:
An overlapped start date is between another input's start and end, or an item's end date is between another input's start and end dates, or a start and end date matches another input's values exactly.
Additional tests (as with your provided code) I expect false positives due to the use of <= and >=.
For example, changing the method to include
bars.Where(outer => ((outer.Start <= w.Start && outer.End >= w.Start)
gives me false positives on 4 and 5
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | |
Solution 2 | Sai Puli |
Solution 3 | Austin T French |