Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes #1464: Create apoc.temporal.overlap(start1, end1, start2, end2, acceptAdjacentSpans:true) function #3994

Open
wants to merge 4 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
= apoc.temporal.overlap
:description: This section contains reference documentation for the apoc.temporal.overlap function.

label:function[] label:apoc-extended[]

[.emphasis]
apoc.temporal.overlap(start1,end1,start2,end2,$config) - Check whether the two temporal spans (start1-end1 and start2-end2) overlap or not

== Signature

[source]
----
apoc.temporal.overlap(start1 :: ANY?, end1 :: ANY?, start2 :: ANY?, end2 :: ANY?, config = {} :: MAP?) :: BOOLEAN?
----

== Input parameters
[.procedures, opts=header]
|===
| Name | Type | Default
|start1|ANY?|null
|end1|ANY?|null
|start2|ANY?|null
|end2|ANY?|null
|config|MAP?|{}
|===


== Config parameters

The function support the following config parameters:

.Config parameters
[opts=header]
|===
| name | type | default | description
| acceptAdjacentSpans | boolean | false | also considers adjacent spans
|===

[[usage-apoc.temporal.overlap]]
== Usage Examples
include::partial$usage/apoc.temporal.overlap.adoc[]


10 changes: 10 additions & 0 deletions docs/asciidoc/modules/ROOT/pages/overview/apoc.temporal/index.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
= apoc.temporal
:description: This section contains reference documentation for the apoc.temporal procedures.

[.procedures, opts=header, cols='5a,1a']
|===
| Qualified Name | Type
|xref::overview/apoc.temporal/apoc.temporal.overlap.adoc[apoc.temporal.overlap icon:book[]]

|label:function[]
|===
Original file line number Diff line number Diff line change
Expand Up @@ -809,6 +809,15 @@ apoc.static.getAll(prefix) - returns statically stored values from config (apoc.
|label:procedure[]
|===

== xref::overview/apoc.temporal/index.adoc[]
[.procedures, opts=header, cols='5a,1a']
|===
| Qualified Name | Type
|xref::overview/apoc.temporal/apoc.temporal.overlap.adoc[apoc.temporal.overlap icon:book[]]

|label:function[]
|===

== xref::overview/apoc.trigger/index.adoc[]

[.procedures, opts=header, cols='5a,1a']
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,8 @@ This file is generated by DocsTest, so don't change it!
*** xref::overview/apoc.systemdb/apoc.systemdb.execute.adoc[]
*** xref::overview/apoc.systemdb/apoc.systemdb.export.metadata.adoc[]
*** xref::overview/apoc.systemdb/apoc.systemdb.graph.adoc[]
** xref::overview/apoc.temporal/index.adoc[]
*** xref::overview/apoc.temporal/apoc.temporal.overlap.adoc[]
** xref::overview/apoc.trigger/index.adoc[]
*** xref::overview/apoc.trigger/apoc.trigger.nodesByLabel.adoc[]
*** xref::overview/apoc.trigger/apoc.trigger.propertiesByKey.adoc[]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
.adjacent spans with default config
[source,cypher]
----
RETURN apoc.temporal.overlap(
date("1999"),
date("2000"),
date("2000"),
date("2001"))
AS value
----

.Results
[opts="header"]
|===
| value
| false
|===


.adjacent spans with config acceptAdjacentSpans: true
[source,cypher]
----
RETURN apoc.temporal.overlap(
date("1999"),
date("2000"),
date("2000"),
date("2001"),
{acceptAdjacentSpans: true} )
AS value
----

.Results
[opts="header"]
|===
| value
| true
|===

.duration spans
[source,cypher]
----
RETURN apoc.temporal.overlap(
time("00:01"),
time("01:01"),
time("00:00"),
time("00:02") )
AS value
----

.Results
[opts="header"]
|===
| value
| true
|===


.non-comparable spans
[source,cypher]
----
RETURN apoc.temporal.overlap(
date("1998"),
date("1999"),
time("00:00"),
time("00:02") )
AS value
----

.Results
[opts="header"]
|===
| value
| null
|===
58 changes: 58 additions & 0 deletions extended/src/main/java/apoc/temporal/TemporalExtended.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package apoc.temporal;

import apoc.Extended;
import apoc.util.Util;
import org.neo4j.graphdb.Transaction;
import org.neo4j.kernel.impl.util.ValueUtils;
import org.neo4j.procedure.Context;
import org.neo4j.procedure.Description;
import org.neo4j.procedure.Name;
import org.neo4j.procedure.UserFunction;
import org.neo4j.values.AnyValue;
import org.neo4j.values.Comparison;

import java.util.Map;

import static org.neo4j.values.AnyValues.TERNARY_COMPARATOR;

@Extended
public class TemporalExtended {
public static final String ACCEPT_ADJACENT_KEY = "acceptAdjacentSpans";

@Context
public Transaction tx;

@UserFunction("apoc.temporal.overlap")
@Description("apoc.temporal.overlap(start1,end1,start2,end2,$config) - Check whether the two temporal spans (start1-end1 and start2-end2) overlap or not")
public Boolean overlap(@Name("start1") Object start1,
@Name("end1") Object end1,
@Name("start2") Object start2,
@Name("end2") Object end2,
@Name(value = "config", defaultValue = "{}") Map<String, Object> config) {

boolean acceptAdjacentSpans = Util.toBoolean(config.get(ACCEPT_ADJACENT_KEY));

AnyValue startValue1 = ValueUtils.of(start1);
AnyValue endValue2 = ValueUtils.of(end2);

AnyValue startValue2 = ValueUtils.of(start2);
AnyValue endValue1 = ValueUtils.of(end1);

// The overlap formula is: `(start1 <= end2) && (start2 <= end1)`
Comparison comparisonLeft = TERNARY_COMPARATOR.ternaryCompare(startValue1, endValue2);
Comparison comparisonRight = TERNARY_COMPARATOR.ternaryCompare(startValue2, endValue1);

// if object are incompatibles, the Comparison returns UNDEFINED
if (comparisonLeft.equals(Comparison.UNDEFINED) || comparisonRight.equals(Comparison.UNDEFINED)) {
return null;
}

int rangeLeft = comparisonLeft.value();
int rangeRight = comparisonRight.value();
if (acceptAdjacentSpans) {
return rangeLeft <= 0 && rangeRight <= 0;
}
return rangeLeft < 0 && rangeRight < 0;
}

}
1 change: 1 addition & 0 deletions extended/src/main/resources/extended.txt
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ apoc.static.set
apoc.systemdb.execute
apoc.systemdb.export.metadata
apoc.systemdb.graph
apoc.temporal.overlap
apoc.ttl.expire
apoc.ttl.expireIn
apoc.util.hashCode
Expand Down
139 changes: 139 additions & 0 deletions extended/src/test/java/apoc/temporal/TemporalExtendedTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package apoc.temporal;

import apoc.Extended;
import apoc.util.TestUtil;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Test;
import org.neo4j.test.rule.DbmsRule;
import org.neo4j.test.rule.ImpermanentDbmsRule;
import org.neo4j.values.storable.DateTimeValue;
import org.neo4j.values.storable.DateValue;
import org.neo4j.values.storable.LocalDateTimeValue;
import org.neo4j.values.storable.LocalTimeValue;
import org.neo4j.values.storable.TimeValue;

import java.time.ZoneId;
import java.util.Map;

import static apoc.temporal.TemporalExtended.ACCEPT_ADJACENT_KEY;
import static apoc.util.TestUtil.singleResultFirstColumn;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;

@Extended
public class TemporalExtendedTest {

public static final String RETURN_OVERLAP = "RETURN apoc.temporal.overlap($start1, $end1, $start2, $end2, $conf)";

@ClassRule
public static DbmsRule db = new ImpermanentDbmsRule();

@BeforeClass
public static void setUp() {
TestUtil.registerProcedure(db, TemporalExtended.class);
}

@Test
public void testOverlapDates() {
Map<String, Object> params = Map.of("start1", DateValue.parse("1999"),
"end1", DateValue.parse("2000"),
"start2", DateValue.parse("2000"),
"end2", DateValue.parse("2001"),
"conf", Map.of());

boolean output = singleResultFirstColumn(db, RETURN_OVERLAP, params);
assertFalse(output);
}

@Test
public void testOverlapDatesWithConfigAcceptAdjacentSpansConf() {
Map<String, Object> params = Map.of("start1", DateValue.parse("1999"),
"end1", DateValue.parse("2000"),
"start2", DateValue.parse("2000"),
"end2", DateValue.parse("2001"),
"conf", Map.of(ACCEPT_ADJACENT_KEY, true));

boolean output = singleResultFirstColumn(db, RETURN_OVERLAP, params);
assertTrue(output);
}

@Test
public void testOverlapWithDatetime() {
Map<String, Object> params = Map.of("start1", DateTimeValue.parse("1999", ZoneId::systemDefault),
"end1", DateTimeValue.parse("2000", ZoneId::systemDefault),
"start2", DateTimeValue.parse("2000", ZoneId::systemDefault),
"end2", DateTimeValue.parse("2001", ZoneId::systemDefault),
"conf", Map.of());

boolean output = singleResultFirstColumn(db, RETURN_OVERLAP, params);
assertFalse(output);
}

@Test
public void testOverlapWithDatetimeAndAcceptAdjacentSpansConf() {
Map<String, Object> params = Map.of("start1", DateTimeValue.parse("1999", ZoneId::systemDefault),
"end1", DateTimeValue.parse("2000", ZoneId::systemDefault),
"start2", DateTimeValue.parse("2000", ZoneId::systemDefault),
"end2", DateTimeValue.parse("2001", ZoneId::systemDefault),
"conf", Map.of(ACCEPT_ADJACENT_KEY, true));

boolean output = singleResultFirstColumn(db, RETURN_OVERLAP, params);
assertTrue(output);
}

@Test
public void testOverlapWithTime() {
Map<String, Object> params = Map.of("start1", TimeValue.parse("00:01", ZoneId::systemDefault),
"end1", TimeValue.parse("01:01", ZoneId::systemDefault),
"start2", TimeValue.parse("00:00", ZoneId::systemDefault),
"end2", TimeValue.parse("00:02", ZoneId::systemDefault),
"conf", Map.of());

boolean output = singleResultFirstColumn(db, RETURN_OVERLAP, params);
assertTrue(output);
}

@Test
public void testOverlapWithLocalTime() {
Map<String, Object> params = Map.of("start1", LocalTimeValue.parse("00:01"),
"end1", LocalTimeValue.parse("01:01"),
"start2", LocalTimeValue.parse("00:00"),
"end2", LocalTimeValue.parse("00:02"),
"conf", Map.of(ACCEPT_ADJACENT_KEY, true));

boolean output = singleResultFirstColumn(db, RETURN_OVERLAP, params);
assertTrue(output);
}

@Test
public void testOverlapWithLocalDateTime() {
Map<String, Object> params = Map.of("start1", LocalDateTimeValue.parse("1999"),
"end1", LocalDateTimeValue.parse("2000"),
"start2", LocalDateTimeValue.parse("2000"),
"end2", LocalDateTimeValue.parse("2001"),
"conf", Map.of());

boolean output = singleResultFirstColumn(db, RETURN_OVERLAP, params);
assertFalse(output);
}

/**
* In this test case the 2 ranges have different types (i.e. `date` and `time`),
* and we just return `null`,
* to be consistent with Cypher's behavior (e.g., `return date("1999") > time("19:00")` has result `null` )
*/
@Test
public void testOverlapWithWrongTypes() {
Map<String, Object> params = Map.of("start1", DateValue.parse("1999"),
"end1", DateValue.parse("2000"),
"start2", LocalTimeValue.parse("19:00"),
"end2", LocalTimeValue.parse("20:00"),
"conf", Map.of());

Object output = singleResultFirstColumn(db, RETURN_OVERLAP, params);
assertNull(output);
}

}