107

I have two date ranges where each range is determined by a start and end date (obviously, datetime.date instances). The two ranges can overlap or not. I need the number of days of the overlap. Of course I can pre-fill two sets with all dates within both ranges and the perform a set intersection but this is possibly inefficient...is there a better way apart from another solution using a long if-elif section covering all cases?

martineau
  • 119,623
  • 25
  • 170
  • 301
  • Does this answer your question? [Determine Whether Two Date Ranges Overlap](https://stackoverflow.com/questions/325933/determine-whether-two-date-ranges-overlap) – quqa123 Feb 12 '21 at 15:45

10 Answers10

214
  • Determine the latest of the two start dates and the earliest of the two end dates.
  • Compute the timedelta by subtracting them.
  • If the delta is positive, that is the number of days of overlap.

Here is an example calculation:

>>> from datetime import datetime
>>> from collections import namedtuple
>>> Range = namedtuple('Range', ['start', 'end'])

>>> r1 = Range(start=datetime(2012, 1, 15), end=datetime(2012, 5, 10))
>>> r2 = Range(start=datetime(2012, 3, 20), end=datetime(2012, 9, 15))
>>> latest_start = max(r1.start, r2.start)
>>> earliest_end = min(r1.end, r2.end)
>>> delta = (earliest_end - latest_start).days + 1
>>> overlap = max(0, delta)
>>> overlap
52
Raymond Hettinger
  • 216,523
  • 63
  • 388
  • 485
  • 1
    +1 very nice solution. Though, this doesn't quite work on dates that are fully contained in the other. For simplicity in integers: Range(1,4) and Range(2,3) returns 1 – darkless Sep 03 '13 at 05:19
  • 3
    @darkless Actually, it returns 2 which is **correct**. Try these inputs ``r1 = Range(start=datetime(2012, 1, 1), end=datetime(2012, 1, 4)); r2 = Range(start=datetime(2012, 1, 2), end=datetime(2012, 1, 3))``. I think you missed the ``+1`` in the overlap calculation (necessary because the interval is closed on both ends). – Raymond Hettinger Jan 19 '15 at 20:31
  • 1
    What if you want to calculate 2 times instead of 2 dates ? @RaymondHettinger – Eric Dec 08 '18 at 10:57
  • 3
    If you use datetime objects with times you could instead of .days write .total_seconds(). – ErikXIII Sep 14 '20 at 15:29
11

Function calls are more expensive than arithmetic operations.

The fastest way of doing this involves 2 subtractions and 1 min():

min(r1.end - r2.start, r2.end - r1.start).days + 1

compared with the next best which needs 1 subtraction, 1 min() and a max():

(min(r1.end, r2.end) - max(r1.start, r2.start)).days + 1

Of course with both expressions you still need to check for a positive overlap.

John Machin
  • 81,303
  • 11
  • 141
  • 189
  • 1
    This method will not return correct answer always. e.g. `Range = namedtuple('Range', ['start', 'end']) r1 = Range(start=datetime(2016, 6, 15), end=datetime(2016, 6, 15)) r2 = Range(start=datetime(2016, 6, 11), end=datetime(2016, 6, 18)) print min(r1.end - r2.start, r2.end - r1.start).days + 1` will print 4 where it supposded to print 1 – tkyass Jun 16 '16 at 16:52
  • I get an ambiguous series error using the first equation. Do I need a particular library? – Arthur D. Howland Oct 07 '19 at 18:43
10

I implemented a TimeRange class as you can see below.

The get_overlapped_range first negates all the non overlapped options by a simple condition, and then calculate the overlapped range by considering all the possible options.

To get the amount of days you'll need to take the TimeRange value that was returned from get_overlapped_range and divide the duration by 60*60*24.

class TimeRange(object):
    def __init__(self, start, end):
        self.start = start
        self.end = end
        self.duration = self.end - self.start

    def is_overlapped(self, time_range):
        if max(self.start, time_range.start) < min(self.end, time_range.end):
            return True
        else:
            return False

    def get_overlapped_range(self, time_range):
        if not self.is_overlapped(time_range):
            return

        if time_range.start >= self.start:
            if self.end >= time_range.end:
                return TimeRange(time_range.start, time_range.end)
            else:
                return TimeRange(time_range.start, self.end)
        elif time_range.start < self.start:
            if time_range.end >= self.end:
                return TimeRange(self.start, self.end)
            else:
                return TimeRange(self.start, time_range.end)

    def __repr__(self):
        return '{0} ------> {1}'.format(*[time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(d))
                                          for d in [self.start, self.end]])
Jerther
  • 5,558
  • 8
  • 40
  • 59
Elad Sofer
  • 121
  • 1
  • 7
10

You can use the datetimerange package: https://pypi.org/project/DateTimeRange/

from datetimerange import DateTimeRange
time_range1 = DateTimeRange("2015-01-01T00:00:00+0900", "2015-01-04T00:20:00+0900") 
time_range2 = DateTimeRange("2015-01-01T00:00:10+0900", "2015-01-04T00:20:00+0900")
tem3 = time_range1.intersection(time_range2)
if tem3.NOT_A_TIME_STR == 'NaT':  # No overlap
    S_Time = 0
else: # Output the overlap seconds
    S_Time = tem3.timedelta.total_seconds()

"2015-01-01T00:00:00+0900" inside the DateTimeRange() can also be datetime format, like Timestamp('2017-08-30 20:36:25').

Songhua Hu
  • 191
  • 2
  • 4
  • 2
    Thanks, Just had a look at the documentation for `DateTimeRange` package and it seems they support `is_intersection` which natively returns a boolean value (True or False) depending on whether or not there is an intersection between two date ranges. So, for your example: `time_range1.is_intersection(time_range2)` would return `True` if they intersect else `False` – Deep Jun 09 '20 at 15:39
  • intersection() returns a DateTimeRange object and its property NOT_A_TIME_STR always equals 'NaT', so the if condition will always be true. Better approach will be to use the is_intersection which returns True or False. – Mazhar Ali Jun 16 '21 at 10:40
3

Building on the solution of @Raymond Hettinger, since python 3.6 you can now use NamedTuple from the typing module.

from datetime import datetime
from typing import NamedTuple

class Range(NamedTuple):
    start: datetime
    end: datetime
>>> r1 = Range(start=datetime(2012, 1, 15), end=datetime(2012, 5, 10))
>>> r2 = Range(start=datetime(2012, 3, 20), end=datetime(2012, 9, 15))
>>> latest_start = max(r1.start, r2.start)
>>> earliest_end = min(r1.end, r2.end)
>>> delta = (earliest_end - latest_start).days + 1
>>> overlap = max(0, delta)
>>> overlap
52
loicgasser
  • 1,403
  • 12
  • 17
2

Pseudocode:

 1 + max( -1, (min( a.dateEnd, b.dateEnd) - max( a.dateStart, b.dateStart)).days )
Community
  • 1
  • 1
ypercubeᵀᴹ
  • 113,259
  • 19
  • 174
  • 235
0
def get_overlap(r1,r2):
    latest_start=max(r1[0],r2[0])
    earliest_end=min(r1[1],r2[1])
    delta=(earliest_end-latest_start).days
    if delta>0:
        return delta+1
    else:
        return 0
grael
  • 657
  • 2
  • 11
  • 26
andros1337
  • 369
  • 1
  • 3
  • 10
0

Ok my solution is a bit wonky because my df uses all series - but lets say you have the following columns, 2 of which are fixed which is your "Fiscal Year". PoP is "Period of performance" which is your variable data:

df['PoP_Start']
df['PoP_End']
df['FY19_Start'] = '10/1/2018'
df['FY19_End'] = '09/30/2019'

Assume all of the data is in datetime format ie -

df['FY19_Start'] = pd.to_datetime(df['FY19_Start'])
df['FY19_End'] = pd.to_datetime(df['FY19_End'])

Try the following equations to find the number of days overlap:

min1 = np.minimum(df['POP_End'], df['FY19_End'])
max2 = np.maximum(df['POP_Start'], df['FY19_Start'])

df['Overlap_2019'] = (min1 - max2) / np.timedelta64(1, 'D')
df['Overlap_2019'] = np.maximum(df['Overlap_2019']+1,0)
Arthur D. Howland
  • 4,363
  • 3
  • 21
  • 31
0

Another solution would be sorting a source array by ascending first and then looping through and comparing the dates like so:

date_ranges = sorted(
    date_ranges,
    key=lambda item: item['start_date'],
)
for i in range(len(date_ranges)-1):
    if date_ranges[i]['end_date'] > date_ranges[i+1]['start_date']:
        raise Exception('Overlap'})
kozlone
  • 111
  • 8
0

What I have used in one of my apps, is to create a list of the date differences and querying that aginst the date difference in the date range proposed for save.

If the list of date difference values (in days) of all old date ranges is:

dateDiffOld = [2920753, 2920746, 2920698, 2920387, 2920360, 2920296]

and that for the new date range is:

dateDiffNew = 2920360

Then using:

if dateDiffNew in dateDiffOld:
    # do something

My quest for a possible solution with a similar requirement took me to various answers provided on SO, but somehow (for my use case) I have found this to be working (so far, with a multitude of records). Have not had the opportunity to use it elsewhere.

Note: The instant application I am describing is created using Django.

Note2: Members are kindly requested to advise on any possible pitfalls (I have not come across so far) of this method.

carla
  • 141
  • 9