From ca714f88becd09e2a4fbfad74169a692be7ce723 Mon Sep 17 00:00:00 2001 From: vga91 Date: Wed, 21 Feb 2024 12:23:42 +0100 Subject: [PATCH] Fixes #1464: Create apoc.temporal.overlap(start1, end1, start2, end2, acceptAdjacentSpans:true) function --- .../apoc.temporal/apoc.temporal.overlap.adoc | 43 ++++++ .../pages/overview/apoc.temporal/index.adoc | 10 ++ .../documentation.adoc | 9 ++ .../partials/generated-documentation/nav.adoc | 2 + .../partials/usage/apoc.temporal.overlap.adoc | 74 ++++++++++ .../java/apoc/temporal/TemporalExtended.java | 44 ++++++ .../apoc/temporal/TemporalExtendedTest.java | 139 ++++++++++++++++++ 7 files changed, 321 insertions(+) create mode 100644 docs/asciidoc/modules/ROOT/pages/overview/apoc.temporal/apoc.temporal.overlap.adoc create mode 100644 docs/asciidoc/modules/ROOT/pages/overview/apoc.temporal/index.adoc create mode 100644 docs/asciidoc/modules/ROOT/partials/usage/apoc.temporal.overlap.adoc create mode 100644 extended/src/main/java/apoc/temporal/TemporalExtended.java create mode 100644 extended/src/test/java/apoc/temporal/TemporalExtendedTest.java diff --git a/docs/asciidoc/modules/ROOT/pages/overview/apoc.temporal/apoc.temporal.overlap.adoc b/docs/asciidoc/modules/ROOT/pages/overview/apoc.temporal/apoc.temporal.overlap.adoc new file mode 100644 index 0000000000..76b8767cec --- /dev/null +++ b/docs/asciidoc/modules/ROOT/pages/overview/apoc.temporal/apoc.temporal.overlap.adoc @@ -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[] + + diff --git a/docs/asciidoc/modules/ROOT/pages/overview/apoc.temporal/index.adoc b/docs/asciidoc/modules/ROOT/pages/overview/apoc.temporal/index.adoc new file mode 100644 index 0000000000..f0ec07db87 --- /dev/null +++ b/docs/asciidoc/modules/ROOT/pages/overview/apoc.temporal/index.adoc @@ -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[] +|=== diff --git a/docs/asciidoc/modules/ROOT/partials/generated-documentation/documentation.adoc b/docs/asciidoc/modules/ROOT/partials/generated-documentation/documentation.adoc index eda04706c1..626000d647 100644 --- a/docs/asciidoc/modules/ROOT/partials/generated-documentation/documentation.adoc +++ b/docs/asciidoc/modules/ROOT/partials/generated-documentation/documentation.adoc @@ -777,6 +777,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'] diff --git a/docs/asciidoc/modules/ROOT/partials/generated-documentation/nav.adoc b/docs/asciidoc/modules/ROOT/partials/generated-documentation/nav.adoc index 879240ef65..8177ab689b 100644 --- a/docs/asciidoc/modules/ROOT/partials/generated-documentation/nav.adoc +++ b/docs/asciidoc/modules/ROOT/partials/generated-documentation/nav.adoc @@ -157,6 +157,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[] diff --git a/docs/asciidoc/modules/ROOT/partials/usage/apoc.temporal.overlap.adoc b/docs/asciidoc/modules/ROOT/partials/usage/apoc.temporal.overlap.adoc new file mode 100644 index 0000000000..9c566fbc0f --- /dev/null +++ b/docs/asciidoc/modules/ROOT/partials/usage/apoc.temporal.overlap.adoc @@ -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 +|=== \ No newline at end of file diff --git a/extended/src/main/java/apoc/temporal/TemporalExtended.java b/extended/src/main/java/apoc/temporal/TemporalExtended.java new file mode 100644 index 0000000000..7433287662 --- /dev/null +++ b/extended/src/main/java/apoc/temporal/TemporalExtended.java @@ -0,0 +1,44 @@ +package apoc.temporal; + +import apoc.Extended; +import apoc.util.Util; +import org.neo4j.graphdb.Result; +import org.neo4j.graphdb.Transaction; +import org.neo4j.procedure.Context; +import org.neo4j.procedure.Description; +import org.neo4j.procedure.Name; +import org.neo4j.procedure.UserFunction; + +import java.util.Map; + +@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 config) { + + boolean acceptAdjacentSpans = Util.toBoolean(config.get(ACCEPT_ADJACENT_KEY)); + + String operator = acceptAdjacentSpans ? "<=" : "<"; + String query = "RETURN ($start1 %1$s $end2) AND ($start2 %1$s $end1) AS value".formatted(operator); + Map params = Map.of("start1", start1, + "end1", end1, + "start2", start2, + "end2", end2); + + try (Result result = tx.execute(query, params)) { + Object value = result.next().get("value"); + return (Boolean) value; + } + } + +} diff --git a/extended/src/test/java/apoc/temporal/TemporalExtendedTest.java b/extended/src/test/java/apoc/temporal/TemporalExtendedTest.java new file mode 100644 index 0000000000..5fe824423c --- /dev/null +++ b/extended/src/test/java/apoc/temporal/TemporalExtendedTest.java @@ -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 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 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 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 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 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 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 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 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); + } + +}