In my current client project, there is a screen where the user must enter a series of date ranges. None of these date ranges can overlap at all. If one does, an error message must be shown on the screen and the selections have to be changed before they can be saved.
I looked for some quick and easy library method to use for this, but didn’t find anything readily available. So, I built my own and I want to share how it works and why I did it the way I did.
The High Level Overview
Given an array of these objects, I need to do a few things to check for overlaps:
- Sort the date ranges by “start” date
- Loop through the list and compare “previous” to “current” date range
- Capture a list of any overlapping ranges
- Return a boolean indicating any overlaps
Step 1: Sort The Date Ranges
I can’t assume the user will enter the date ranges in the correct order. In fact, there’s a high likelihood that they won’t as the system will be used to enter historic date ranges at various times based on business decisions.
With that in mind, I want to sort the date ranges before doing anything else. This makes it easier to check for overlaps because I don’t need to loop through the entire list, each time I look at a given range. I can assume, after sorting, that the previous range comes before the current one and the next range comes after the current one. The question, then, is how do I guarantee this ordering?
The easy way to do it is to base the sort on the “start” date. I don’t worry about the end date at all, during sorting.
Now I just need to use the array sort method to figure out which date comes first.
In this code, I’m taking the start date from both the previous and current Date objects and converting them to the date’s time code. This method produces a large integer object, representing the number of milliseconds since Jan 1, 1970. Using the integer version of the date comparison will make this a little more accurate / portable. I’ve often run into problems comparing Date objects directly to each other, and this is a simple workaround.
Now that the dates are sorted, I need to loop through the whole list and compare them to each other.
Step 2: Loop Through The List
Having the list sorted means I only have to loop through the complete list of ranges once. I can get a reference to the “previous” and “current” items in the iteration and make the comparison.
There are several ways to iterate the list and produce the results I want. For example, I could use a simple for-loop and start at index 1 to guarantee I always have a “previous” and “current” value:
This works, but there are several things I don’t like about it. For one, there are more variable declarations here than what I would like. Additionally, I need to capture a final boolean value that tells me whether or not anything overlaps, along with an array of the things that do overlap.
For my money, I would prefer to use an array reduce – something that can reduce the variable declarations, provide function scope the variables I need and give me the single return value all at once. The challenge with the reduce is how to get the previous value when looking at the current value.
In this version of the code, everything is encapsulated in the callback for the reduce method. I can check the index to see if we’re looking at the first item in the array and ignore it if we are. From that point forward, the “previous” value will always be the item in the array at the previous index location.
Honestly, the reduce version of this code doesn’t look very good right now. It adds an extra check for the index position each loop, but I’m willing to live with that because I think the reduce call is going to help out when we get to the comparison and the need to find a boolean value indicating any overlap.
Step 2.1: Compare The Previous And Current Range
Now that I have both the previous and current ranges, I need to compare them. To do that, I will check the previous end date against the current start date. This will tell me if there is any overlap at all.
Because the date ranges have been sorted already, it’s not possible for the previous range to start after the current range starts or ends. Therefore, I only need to check if the previous range ends on or after the date that the current range starts. If the boolean check returns true, then the date ranges do overlap.
Now that I know if two ranges overlap, I need to track this across the entire array and track which ranges overlap, if any.
Step 3 & 4: Capture Overlap, Return A Boolean
Capturing the overlapping date ranges, and the single boolean that says if anything overlaps at all, can be done with the array reduce function that was introduced earlier. Seed the reduce with an object that can hold an “overlap” value and an array of ranges, and use that to store the values as the array reduces. This object is passed as the first parameter and must be returned from the end of the reduce function.
In this completed reduce function, the “result” that is passed in to the callback function comes from the “seed” that is passed as the last parameter of the reduce call. Note that the see has an “overlap” and a “ranges” attribute on it. These will be the results that I want to check once the reduce call is completed.
Putting It All Together And Run It
With the sorting and reducing done, I can put the complete code together in a single function call and have it work with my array of date ranges.
To run the code, I need an array of my date range objects:
And the output that this produces will be an object that tells me if there is any overlap, and which date ranges – if any – do overlap.
The End Result
There’s probably some function in moment.js that would make the comparison easier for sorting and for checking overlap, but this version of the code prevented me from having to bring in another library.
The use of array reduce is probably a bit odd, here… it could have easily been done with just a for-loop or probably another version of the code using a different iteration technique. I stuck with this version because it make refactoring / cleaning up the code very simple. I was able to extract the reduce callback function along with the sort function. This reduced the lines of code within the parent “overlap” function and made things easier to read.
There are probably other areas that need improvement, like handling null dates or not assuming “start” and “end” are dates… but in the end, I’m pretty happy with this code. It works the way I need, even if it could be improved.