From 79243f684777ebc3881baf725f9f5861088637a2 Mon Sep 17 00:00:00 2001 From: Giuseppe Villani Date: Thu, 30 Jun 2022 12:56:42 +0200 Subject: [PATCH] Added support for Duration to apoc.coll.avg (#2987) * added coll avg duration * moved into full * added coll avg duration * changed implementation - added tests * small adoc change --- .../apoc.coll-lite.csv | 1 + .../apoc.coll.avgDuration-lite.csv | 2 + .../apoc.coll.avgDuration.adoc | 5 ++ .../generated-documentation/apoc.coll.csv | 5 ++ .../generated-documentation/documentation.csv | 1 + .../apoc.coll/apoc.coll.avgDuration.adoc | 30 ++++++++ .../ROOT/pages/overview/apoc.coll/index.adoc | 5 ++ .../documentation.adoc | 5 ++ .../partials/generated-documentation/nav.adoc | 1 + .../partials/usage/apoc.coll.avgDuration.adoc | 59 +++++++++++++++ full/src/main/java/apoc/coll/CollFull.java | 38 ++++++++++ full/src/main/resources/extended.txt | 1 + .../src/test/java/apoc/coll/CollFullTest.java | 74 +++++++++++++++++++ 13 files changed, 227 insertions(+) create mode 100644 docs/asciidoc/modules/ROOT/examples/generated-documentation/apoc.coll.avgDuration-lite.csv create mode 100644 docs/asciidoc/modules/ROOT/examples/generated-documentation/apoc.coll.avgDuration.adoc create mode 100644 docs/asciidoc/modules/ROOT/pages/overview/apoc.coll/apoc.coll.avgDuration.adoc create mode 100644 docs/asciidoc/modules/ROOT/partials/usage/apoc.coll.avgDuration.adoc create mode 100644 full/src/main/java/apoc/coll/CollFull.java create mode 100644 full/src/test/java/apoc/coll/CollFullTest.java diff --git a/docs/asciidoc/modules/ROOT/examples/generated-documentation/apoc.coll-lite.csv b/docs/asciidoc/modules/ROOT/examples/generated-documentation/apoc.coll-lite.csv index 0cd43c954c..d900245670 100644 --- a/docs/asciidoc/modules/ROOT/examples/generated-documentation/apoc.coll-lite.csv +++ b/docs/asciidoc/modules/ROOT/examples/generated-documentation/apoc.coll-lite.csv @@ -4,6 +4,7 @@ ¦apoc.coll.split(values :: LIST? OF ANY?, value :: ANY?) :: (value :: LIST? OF ANY?) ¦apoc.coll.zipToRows(list1 :: LIST? OF ANY?, list2 :: LIST? OF ANY?) :: (value :: LIST? OF ANY?) ¦apoc.coll.avg(numbers :: LIST? OF NUMBER?) :: (FLOAT?) +¦apoc.coll.avgDuration(durations :: LIST? OF DURATION?) :: (DURATION?) ¦apoc.coll.combinations(coll :: LIST? OF ANY?, minSelect :: INTEGER?, maxSelect = -1 :: INTEGER?) :: (LIST? OF ANY?) ¦apoc.coll.contains(coll :: LIST? OF ANY?, value :: ANY?) :: (BOOLEAN?) ¦apoc.coll.containsAll(coll :: LIST? OF ANY?, values :: LIST? OF ANY?) :: (BOOLEAN?) diff --git a/docs/asciidoc/modules/ROOT/examples/generated-documentation/apoc.coll.avgDuration-lite.csv b/docs/asciidoc/modules/ROOT/examples/generated-documentation/apoc.coll.avgDuration-lite.csv new file mode 100644 index 0000000000..4653940f19 --- /dev/null +++ b/docs/asciidoc/modules/ROOT/examples/generated-documentation/apoc.coll.avgDuration-lite.csv @@ -0,0 +1,2 @@ +¦signature +¦apoc.coll.avgDuration(durations :: LIST? OF DURATION?) :: (DURATION?) diff --git a/docs/asciidoc/modules/ROOT/examples/generated-documentation/apoc.coll.avgDuration.adoc b/docs/asciidoc/modules/ROOT/examples/generated-documentation/apoc.coll.avgDuration.adoc new file mode 100644 index 0000000000..729058726a --- /dev/null +++ b/docs/asciidoc/modules/ROOT/examples/generated-documentation/apoc.coll.avgDuration.adoc @@ -0,0 +1,5 @@ +¦xref::overview/apoc.coll/apoc.coll.avgDuration.adoc[apoc.coll.avgDuration icon:book[]] + + +`apoc.coll.avgDuration([duration('P2DT3H'), duration('PT1H45S'), ...])` - returns the average of a list of duration values +¦label:function[] +¦label:apoc-full[] diff --git a/docs/asciidoc/modules/ROOT/examples/generated-documentation/apoc.coll.csv b/docs/asciidoc/modules/ROOT/examples/generated-documentation/apoc.coll.csv index b38e1681c4..4565d0b8a8 100644 --- a/docs/asciidoc/modules/ROOT/examples/generated-documentation/apoc.coll.csv +++ b/docs/asciidoc/modules/ROOT/examples/generated-documentation/apoc.coll.csv @@ -29,6 +29,11 @@ apoc.coll.zipToRows(list1,list2) - creates pairs like zip but emits one row per apoc.coll.avg([0.5,1,2.3]) |label:function[] |label:apoc-core[] +|xref::overview/apoc.coll/apoc.coll.adoc[apoc.coll.avgDuration icon:book[]] + +apoc.coll.avgDuration([duration('P2DT3H'), duration('PT1H45S'), ...]) - returns the average of a list of duration values +|label:function[] +|label:apoc-full[] |xref::overview/apoc.coll/apoc.coll.adoc[apoc.coll.combinations icon:book[]] apoc.coll.combinations(coll, minSelect, maxSelect:minSelect) - Returns collection of all combinations of list elements of selection size between minSelect and maxSelect (default:minSelect), inclusive diff --git a/docs/asciidoc/modules/ROOT/examples/generated-documentation/documentation.csv b/docs/asciidoc/modules/ROOT/examples/generated-documentation/documentation.csv index f583cf32b4..869300412c 100644 --- a/docs/asciidoc/modules/ROOT/examples/generated-documentation/documentation.csv +++ b/docs/asciidoc/modules/ROOT/examples/generated-documentation/documentation.csv @@ -316,6 +316,7 @@ for the provided `label` and `uuidProperty`, in case the UUID handler is already ¦function¦apoc.any.property¦apoc.any.property(thing :: ANY?, key :: STRING?) :: (ANY?)¦returns property for virtual and real, nodes, rels and maps¦true¦xref::graph-querying/node-querying.adoc ¦function¦apoc.bitwise.op¦apoc.bitwise.op(a :: INTEGER?, operator :: STRING?, b :: INTEGER?) :: (INTEGER?)¦apoc.bitwise.op(60,'|',13) bitwise operations a & b, a | b, a ^ b, ~a, a >> b, a >>> b, a << b. returns the result of the bitwise operation¦true¦ ¦function¦apoc.coll.avg¦apoc.coll.avg(numbers :: LIST? OF NUMBER?) :: (FLOAT?)¦apoc.coll.avg([0.5,1,2.3])¦true¦ +¦function¦apoc.coll.avgDuration¦apoc.coll.avgDuration(durations :: LIST? OF DURATION?) :: (DURATION?)¦apoc.coll.avgDuration([duration('P2DT3H'), duration('PT1H45S'), ...]) - returns the average of a list of duration values¦false¦ ¦function¦apoc.coll.combinations¦apoc.coll.combinations(coll :: LIST? OF ANY?, minSelect :: INTEGER?, maxSelect = -1 :: INTEGER?) :: (LIST? OF ANY?)¦apoc.coll.combinations(coll, minSelect, maxSelect:minSelect) - Returns collection of all combinations of list elements of selection size between minSelect and maxSelect (default:minSelect), inclusive¦true¦ ¦function¦apoc.coll.contains¦apoc.coll.contains(coll :: LIST? OF ANY?, value :: ANY?) :: (BOOLEAN?)¦apoc.coll.contains(coll, value) optimized contains operation (using a HashSet) (returns single row or not)¦true¦ ¦function¦apoc.coll.containsAll¦apoc.coll.containsAll(coll :: LIST? OF ANY?, values :: LIST? OF ANY?) :: (BOOLEAN?)¦apoc.coll.containsAll(coll, values) optimized contains-all operation (using a HashSet) (returns single row or not)¦true¦ diff --git a/docs/asciidoc/modules/ROOT/pages/overview/apoc.coll/apoc.coll.avgDuration.adoc b/docs/asciidoc/modules/ROOT/pages/overview/apoc.coll/apoc.coll.avgDuration.adoc new file mode 100644 index 0000000000..b3ae497e6f --- /dev/null +++ b/docs/asciidoc/modules/ROOT/pages/overview/apoc.coll/apoc.coll.avgDuration.adoc @@ -0,0 +1,30 @@ +//// +This file is generated by DocsTest, so don't change it! +//// + += apoc.coll.avgDuration +:description: This section contains reference documentation for the apoc.coll.avgDuration function. + +label:function[] label:apoc-full[] + +[.emphasis] +apoc.coll.avgDuration([duration('P2DT3H'), duration('PT1H45S'), ...]) - returns the average of a list of duration values + +== Signature + +[source] +---- +apoc.coll.avgDuration(durations :: LIST? OF DURATION?) :: (DURATION?) +---- + +== Input parameters +[.procedures, opts=header] +|=== +| Name | Type | Default +|durations|LIST? OF DURATION?|null +|=== + +[[usage-apoc.coll.avgDuration]] +== Usage Examples +include::partial$usage/apoc.coll.avgDuration.adoc[] + diff --git a/docs/asciidoc/modules/ROOT/pages/overview/apoc.coll/index.adoc b/docs/asciidoc/modules/ROOT/pages/overview/apoc.coll/index.adoc index e9ea2072b3..d973792544 100644 --- a/docs/asciidoc/modules/ROOT/pages/overview/apoc.coll/index.adoc +++ b/docs/asciidoc/modules/ROOT/pages/overview/apoc.coll/index.adoc @@ -33,6 +33,11 @@ apoc.coll.zipToRows(list1,list2) - creates pairs like zip but emits one row per apoc.coll.avg([0.5,1,2.3]) |label:function[] |label:apoc-core[] +|xref::overview/apoc.coll/apoc.coll.avgDuration.adoc[apoc.coll.avgDuration icon:book[]] + +apoc.coll.avgDuration([duration('P2DT3H'), duration('PT1H45S'), ...]) - returns the average of a list of duration values +|label:function[] +|label:apoc-full[] |xref::overview/apoc.coll/apoc.coll.combinations.adoc[apoc.coll.combinations icon:book[]] apoc.coll.combinations(coll, minSelect, maxSelect:minSelect) - Returns collection of all combinations of list elements of selection size between minSelect and maxSelect (default:minSelect), inclusive diff --git a/docs/asciidoc/modules/ROOT/partials/generated-documentation/documentation.adoc b/docs/asciidoc/modules/ROOT/partials/generated-documentation/documentation.adoc index 7d8adeed87..7c48057a73 100644 --- a/docs/asciidoc/modules/ROOT/partials/generated-documentation/documentation.adoc +++ b/docs/asciidoc/modules/ROOT/partials/generated-documentation/documentation.adoc @@ -262,6 +262,11 @@ apoc.coll.zipToRows(list1,list2) - creates pairs like zip but emits one row per apoc.coll.avg([0.5,1,2.3]) |label:function[] |label:apoc-core[] +|xref::overview/apoc.coll/apoc.coll.avgDuration.adoc[apoc.coll.avgDuration icon:book[]] + +apoc.coll.avgDuration([duration('P2DT3H'), duration('PT1H45S'), ...]) - returns the average of a list of duration values +|label:function[] +|label:apoc-full[] |xref::overview/apoc.coll/apoc.coll.combinations.adoc[apoc.coll.combinations icon:book[]] apoc.coll.combinations(coll, minSelect, maxSelect:minSelect) - Returns collection of all combinations of list elements of selection size between minSelect and maxSelect (default:minSelect), inclusive diff --git a/docs/asciidoc/modules/ROOT/partials/generated-documentation/nav.adoc b/docs/asciidoc/modules/ROOT/partials/generated-documentation/nav.adoc index 4fcbf68472..537329f590 100644 --- a/docs/asciidoc/modules/ROOT/partials/generated-documentation/nav.adoc +++ b/docs/asciidoc/modules/ROOT/partials/generated-documentation/nav.adoc @@ -50,6 +50,7 @@ This file is generated by DocsTest, so don't change it! *** xref::overview/apoc.coll/apoc.coll.split.adoc[] *** xref::overview/apoc.coll/apoc.coll.zipToRows.adoc[] *** xref::overview/apoc.coll/apoc.coll.avg.adoc[] +*** xref::overview/apoc.coll/apoc.coll.avgDuration.adoc[] *** xref::overview/apoc.coll/apoc.coll.combinations.adoc[] *** xref::overview/apoc.coll/apoc.coll.contains.adoc[] *** xref::overview/apoc.coll/apoc.coll.containsAll.adoc[] diff --git a/docs/asciidoc/modules/ROOT/partials/usage/apoc.coll.avgDuration.adoc b/docs/asciidoc/modules/ROOT/partials/usage/apoc.coll.avgDuration.adoc new file mode 100644 index 0000000000..9dd46d84d5 --- /dev/null +++ b/docs/asciidoc/modules/ROOT/partials/usage/apoc.coll.avgDuration.adoc @@ -0,0 +1,59 @@ +The `apoc.coll.avgDuration` works similar to the `avg()` function, +but it's not an aggregate function and takes a list of durations as an argument. +For example: + +[source,cypher] +---- +WITH [duration('P2DT4H1S'), duration('PT1H1S'), duration('PT1H6S'), duration('PT1H5S')] AS durations +RETURN apoc.coll.avgDuration(durations) AS value +---- + +.Results +[opts="header"] +|=== +| value +| PT13H45M3.25S +|=== + + +In case of null or empty list, a `null` result will be returned: +[source,cypher] +---- +RETURN apoc.coll.avgDuration([]) AS output; +---- + +[source,cypher] +---- +RETURN apoc.coll.avgDuration(null) AS output; +---- + +.Results +[opts="header"] +|=== +| output +| null +|=== + +In case a non-duration list is passed, a `Type mismatch` error will be thrown: +[source,cypher] +---- +RETURN apoc.coll.avgDuration([1,2,3]) AS value; +---- + +.Results +|=== +| Type mismatch: expected List but was List +|=== + + +While in case a list with all duration values is not passed, a `TypeError` will be thrown: + +[source,cypher] +---- +RETURN apoc.coll.avgDuration([duration('PT1H1S'),2,3]) AS output; +---- + +.Results +|=== +| Can't coerce `Long(2)` to Duration +|=== \ No newline at end of file diff --git a/full/src/main/java/apoc/coll/CollFull.java b/full/src/main/java/apoc/coll/CollFull.java new file mode 100644 index 0000000000..3cdea098ea --- /dev/null +++ b/full/src/main/java/apoc/coll/CollFull.java @@ -0,0 +1,38 @@ +package apoc.coll; + +import apoc.Extended; +import org.apache.commons.collections4.CollectionUtils; +import org.neo4j.procedure.Description; +import org.neo4j.procedure.Name; +import org.neo4j.procedure.UserFunction; +import org.neo4j.values.storable.DurationValue; + +import java.time.temporal.ChronoUnit; +import java.util.List; + +@Extended +public class CollFull { + + @UserFunction + @Description("apoc.coll.avgDuration([duration('P2DT3H'), duration('PT1H45S'), ...]) - returns the average of a list of duration values") + public DurationValue avgDuration(@Name("durations") List list) { + if (CollectionUtils.isEmpty(list)) return null; + + long count = 0; + + double monthsRunningAvg = 0; + double daysRunningAvg = 0; + double secondsRunningAvg = 0; + double nanosRunningAvg = 0; + for (DurationValue duration : list) { + count++; + monthsRunningAvg += (duration.get(ChronoUnit.MONTHS) - monthsRunningAvg) / count; + daysRunningAvg += (duration.get(ChronoUnit.DAYS) - daysRunningAvg) / count; + secondsRunningAvg += (duration.get(ChronoUnit.SECONDS) - secondsRunningAvg) / count; + nanosRunningAvg += (duration.get(ChronoUnit.NANOS) - nanosRunningAvg) / count; + } + + return DurationValue.approximate(monthsRunningAvg, daysRunningAvg, secondsRunningAvg, nanosRunningAvg) + .normalize(); + } +} diff --git a/full/src/main/resources/extended.txt b/full/src/main/resources/extended.txt index 9ecfd62fab..937c2dbd06 100644 --- a/full/src/main/resources/extended.txt +++ b/full/src/main/resources/extended.txt @@ -159,6 +159,7 @@ apoc.uuid.install apoc.uuid.list apoc.uuid.remove apoc.uuid.removeAll +apoc.coll.avgDuration apoc.data.email apoc.static.get apoc.static.getAll diff --git a/full/src/test/java/apoc/coll/CollFullTest.java b/full/src/test/java/apoc/coll/CollFullTest.java new file mode 100644 index 0000000000..98fa639ab4 --- /dev/null +++ b/full/src/test/java/apoc/coll/CollFullTest.java @@ -0,0 +1,74 @@ +package apoc.coll; + +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.DurationValue; + +import java.util.List; +import java.util.Map; + +import static apoc.util.TestUtil.testCall; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; + +public class CollFullTest { + + @ClassRule + public static DbmsRule db = new ImpermanentDbmsRule(); + + @BeforeClass + public static void setUp() throws Exception { + TestUtil.registerProcedure(db, CollFull.class); + } + + @Test + public void testAvgDuration() { + final List list = List.of( + DurationValue.parse("P2DT4H1S"), DurationValue.parse("PT1H1S"), DurationValue.parse("PT1H6S"), DurationValue.parse("PT1H5S")); + + // get duration from Neo4j aggregation AvgFunction + final DurationValue expected = TestUtil.singleResultFirstColumn(db, "UNWIND $list AS dur RETURN avg(dur) AS value", + Map.of("list", list)); + + // same duration values as above + testCall(db, "WITH $list AS dur RETURN apoc.coll.avgDuration(dur) AS value", + Map.of("list", list), + (row) -> assertEquals(expected, row.get("value"))); + } + + @Test + public void testAvgDurationNullOrEmpty() { + testCall(db, "WITH [] AS dur " + + "RETURN apoc.coll.avgDuration(dur) AS value", + (row) -> assertNull(row.get("value"))); + + testCall(db, "WITH null AS dur " + + "RETURN apoc.coll.avgDuration(dur) AS value", + (row) -> assertNull(row.get("value"))); + + } + + @Test + public void testAvgDurationWrongType() { + final String queryIntType = "WITH [1,2,3] AS dur " + + "RETURN apoc.coll.avgDuration(dur)"; + testWrongType(queryIntType); + + final String queryMixedType = "WITH [duration('P2DT4H1S'), duration('PT1H6S'), 1] AS dur " + + "RETURN apoc.coll.avgDuration(dur)"; + testWrongType(queryMixedType); + } + + private void testWrongType(String query) { + try { + testCall(db, query, row -> fail("should fail due to Wrong argument type")); + } catch (RuntimeException e) { + assertEquals("Wrong argument type: Can't coerce `Long(1)` to Duration", e.getMessage()); + } + } +}