Skip to content

Commit

Permalink
Added support for Duration to apoc.coll.avg (#2987)
Browse files Browse the repository at this point in the history
* added coll avg duration
* moved into full
* added coll avg duration
* changed implementation - added tests
* small adoc change
  • Loading branch information
vga91 committed Jul 5, 2022
1 parent a742097 commit 79243f6
Show file tree
Hide file tree
Showing 13 changed files with 227 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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?)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
¦signature
¦apoc.coll.avgDuration(durations :: LIST? OF DURATION?) :: (DURATION?)
Original file line number Diff line number Diff line change
@@ -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[]
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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¦
Expand Down
Original file line number Diff line number Diff line change
@@ -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[]

Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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[]
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Duration> but was List<Integer>
|===


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 &#96;Long(2)&#96; to Duration
|===
38 changes: 38 additions & 0 deletions full/src/main/java/apoc/coll/CollFull.java
Original file line number Diff line number Diff line change
@@ -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<DurationValue> 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();
}
}
1 change: 1 addition & 0 deletions full/src/main/resources/extended.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
74 changes: 74 additions & 0 deletions full/src/test/java/apoc/coll/CollFullTest.java
Original file line number Diff line number Diff line change
@@ -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<DurationValue> 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());
}
}
}

0 comments on commit 79243f6

Please sign in to comment.