知识库

如何在 Cypher 中检查时间范围重叠

Neo4j 3.4 在 Cypher 中引入了时间类型,因此我们现在有了日期、时间和它们的本地版本,以及持续时间。

虽然我们没有时间范围的类型,但我们可以使用两个时间瞬间作为范围的开始和结束。

虽然检查时间瞬间是否在时间范围内很容易,但计算两个时间范围是否重叠要复杂得多。

本文提供了一个简单的逻辑公式来检查两个时间范围是否重叠,并举例说明如何在 Cypher 中应用该公式。

我们将使用一个简单的图表作为示例

CREATE (:Event {id:1, start:date(), end:date() + duration({days:5})}),
       (:Event {id:2, start:date() + duration({days:3}), end:date() + duration({days:8})})

事件 2 的开始和结束日期分别比事件 1 的开始和结束日期晚 3 天。事件重叠。

检查时间范围重叠的简化公式

时间范围重叠有 4 种可能的方式,不包括它们是相同范围或相切相邻的情况,一个结束另一个开始(我们可以使用包含不等式来表示这些)。

虽然我们不会详细介绍,但重点是计算重叠通常不是微不足道的,很容易创建遗漏某些类型重叠的计算,从而导致结果不正确。

值得庆幸的是,这里证明了一个逻辑归约,它在最后相对简单

Max(StartA, StartB) <= Min(EndA, EndB)

为了使范围重叠,开始时间的最后一次出现必须发生在结束时间的最早一次出现之前(或同时)。

Cypher 没有我们可以使用的标量 min() 和 max() 函数(它们都是聚合函数,不是我们用来解决这个问题的函数),因此我们需要采用另一种方法。

使用 CASE 的纯 Cypher 方法

我们可以使用 CASE 功能来实现标量 min() 和 max() 操作。

MATCH (e1:Event {id:1}), (e2:Event {id:2})
WITH CASE WHEN e1.start >= e2.start THEN e1.start ELSE e2.start END as maxStart,
     CASE WHEN e1.end <= e2.end THEN e1.end ELSE e2.end END as minEnd
RETURN maxStart <= minEnd as rangesOverlap

以上返回 true,因此范围确实重叠。

我们可以在 RETURN 中使用 CASE 评估,但当我们从重叠公式的应用中分离出 min() 和 max() 计算时,它更容易理解和理解。

使用集合函数的 APOC 方法

APOC 过程有一些用于计算集合的最大值和最小值的函数,apoc.coll.max()apoc.coll.min()

虽然这些看起来像是合适的工具,但仍然存在一个问题:截至 2020 年 4 月,这些函数尚不支持时间类型,尽管修复即将推出。

以下是修复后的样子

MATCH (e1:Event {id:1}), (e2:Event {id:2})
RETURN apoc.coll.max([e1.start, e2.start]) <= apoc.coll.min([e1.end, e2.end]) as rangesOverlap

在此之前,还有另一种解决方法(除了上面的纯 Cypher case),通过比较 epochMillis 值,但这要求我们使用 dateTime 类型。如果我们只有日期,我们可以使用 dateTime({date:date()}) as dateTimeFromADate 从它们中推导出 dateTime。

由于前面创建的示例节点是日期类型,因此我们需要进行该转换

MATCH (e1:Event {id:1}), (e2:Event {id:2})
WITH apoc.coll.max([dateTime({date:e1.start}).epochMillis, dateTime({date:e2.start}).epochMillis]) as maxStart,
     apoc.coll.min([dateTime({date:e1.end}).epochMillis, dateTime({date:e2.end}).epochMillis]) as minEnd
RETURN maxStart <= minEnd as rangesOverlap

您可能会认为,如果我们有一个可以调用的 overlap() 函数,它可以接收范围的开始和结束,那么这将容易得多,我们同意这一点。

我们正在开发一个 APOC 函数,它应该可以显著简化这些检查。