高效的日期范围重叠计算?

107

我有两个日期范围,每个范围由开始和结束日期确定(显然,这是datetime.date的实例)。这两个范围可以重叠或不重叠。我需要知道它们重叠的天数。当然我可以使用所有日期填充两个集合,然后执行一个集合交集,但这可能是低效的...除了另一个解决方案(使用一个长的if-elif部分覆盖所有情况),是否还有更好的方法?


这个回答解决了你的问题吗?确定两个日期范围是否重叠 - quqa123
10个回答

215
  • 确定两个开始日期中的最新日期和两个结束日期中的最早日期。
  • 通过相减计算timedelta。
  • 如果delta为正,那么这就是重叠天数。

这是一个示例计算:

>>> 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

1
+1 非常好的解决方案。但是,对于完全包含在其他日期中的日期,这并不完全适用。为了简化整数:Range(1,4)和Range(2,3)返回1。 - darkless
3
实际上,它返回的是2,这是正确的。请尝试以下输入:r1 = Range(start=datetime(2012, 1, 1), end=datetime(2012, 1, 4)); r2 = Range(start=datetime(2012, 1, 2), end=datetime(2012, 1, 3))。我认为你在重叠计算中错过了+1(因为区间两端都是闭合的,所以这是必要的)。 - Raymond Hettinger
1
如果你想计算2倍而不是2个日期怎么办?@RaymondHettinger - Eric
3
如果您使用带有时间的datetime对象,您可以使用.total_seconds()而不是.days。 - ErikXIII

11
我实现了一个TimeRange类,如下所示。
在get_overlapped_range方法中,首先通过一个简单的条件将所有不重叠的选项取反,然后考虑所有可能的选项来计算重叠范围。
为了获取天数,您需要从get_overlapped_range返回的TimeRange值,并将其除以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]])

11

函数调用比算术运算更耗费资源。

最快的方法涉及两个减法和一个min()函数:

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

与需要1次减法、1次min()函数调用和1次max()函数调用的次优解相比:

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

当然,在这两个表达式中,你仍然需要检查是否存在正重叠。


1
这个方法不会总是返回正确的答案。例如,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 应该输出1,但实际上输出了4。 - tkyass
我使用第一个方程式时出现了模糊的系列错误。我需要特定的库吗? - Arthur D. Howland

10
你可以使用datetimerange软件包: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()

DateTimeRange()中的"2015-01-01T00:00:00+0900"也可以是日期时间格式,例如Timestamp('2017-08-30 20:36:25')。


2
谢谢,我刚刚查看了DateTimeRange包的文档,它似乎支持is_intersection函数,该函数本身返回一个布尔值(True或False),取决于两个日期范围之间是否存在交集。因此,对于您的示例:如果它们相交,则time_range1.is_intersection(time_range2)将返回True,否则返回False - Deep
intersection() 返回一个 DateTimeRange 对象,它的属性 NOT_A_TIME_STR 总是等于 'NaT',因此 if 条件语句总是为真。 更好的方法是使用 is_intersection 函数,它返回 True 或 False。 - Mazhar Ali

3

在 @Raymond Hettinger 的解决方案上进一步改进,自Python 3.6起,您现在可以使用来自 typing 模块的 NamedTuple

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

最好使用datetime.date而不是datetime.datetime - undefined

2
伪代码:
 1 + max( -1, (min( a.dateEnd, b.dateEnd) - max( a.dateStart, b.dateStart)).days )

0
另一个解决方案是首先按升序对源数组进行排序,然后通过循环比较日期来完成操作:
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'})

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

0

在我的一个应用程序中使用的方法是创建一个日期差异列表,并将其与所提供的日期范围内的日期差异进行查询以保存。

如果所有旧日期范围的日期差异值(以为单位)的列表如下:

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

而新日期范围的日期差异为:

dateDiffNew = 2920360

然后使用:

if dateDiffNew in dateDiffOld:
    # do something

我为寻找类似需求的可能解决方案而进行了探索,看到了许多在SO上提供的答案,但在我的使用情况下,我发现这个方法是有效的(到目前为止,可以处理大量记录)。我还没有在其他地方使用过它。

注意:我正在描述的即时应用是使用Django创建的。

注意2:成员们友情提示此方法可能存在的任何潜在问题(到目前为止我尚未遇到过)。


0

好的,我的解决方案有点奇怪,因为我的数据框使用了所有的系列 - 但是假设你有以下列,其中有2个是固定的,即你的“财政年度”。PoP是“执行期间”,即你的可变数据:

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

假设所有数据都是日期时间格式,即 -

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

尝试使用以下方程式来找到天数重叠的数量:

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)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接