Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
apoc.coll.Coll Maven / Gradle / Ivy
/*
* Copyright (c) "Neo4j"
* Neo4j Sweden AB [http://neo4j.com]
*
* This file is part of Neo4j.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package apoc.coll;
import static apoc.convert.ConvertUtils.convertArrayToList;
import static apoc.util.Util.containsValueEquals;
import static apoc.util.Util.toAnyValues;
import static java.util.Arrays.asList;
import apoc.result.ListResult;
import apoc.util.Util;
import java.lang.reflect.Array;
import java.text.Collator;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.ListIterator;
import java.util.Locale;
import java.util.Map;
import java.util.Random;
import java.util.RandomAccess;
import java.util.Set;
import java.util.concurrent.ThreadLocalRandom;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import org.apache.commons.lang3.mutable.MutableDouble;
import org.apache.commons.lang3.mutable.MutableInt;
import org.apache.commons.lang3.tuple.Pair;
import org.neo4j.graphdb.Node;
import org.neo4j.graphdb.Path;
import org.neo4j.graphdb.Relationship;
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.NotThreadSafe;
import org.neo4j.procedure.Procedure;
import org.neo4j.procedure.UserFunction;
import org.neo4j.values.AnyValue;
public class Coll {
public static final char ASCENDING_ORDER_CHAR = '^';
@Context
public Transaction tx;
@UserFunction("apoc.coll.stdev")
@Description("Returns sample or population standard deviation with `isBiasCorrected` true or false respectively.")
public Number stdev(
@Name("list") List list,
@Name(value = "isBiasCorrected", defaultValue = "true") boolean isBiasCorrected) {
if (list == null || list.isEmpty()) return null;
final double stdev = StandardDeviation.stdDev(
list.stream().mapToDouble(Number::doubleValue).toArray(), isBiasCorrected);
if ((long) stdev == stdev) return (long) stdev;
return stdev;
}
@UserFunction("apoc.coll.runningTotal")
@Description("Returns an accumulative `LIST`.")
public List runningTotal(@Name("list") List list) {
if (list == null || list.isEmpty()) return null;
MutableDouble sum = new MutableDouble();
return list.stream()
.map(i -> {
double value = sum.addAndGet(i.doubleValue());
if (value == sum.longValue()) return sum.longValue();
return value;
})
.collect(Collectors.toList());
}
@Procedure("apoc.coll.zipToRows")
@Description("Returns the two `LIST` values zipped together, with one row per zipped pair.")
public Stream zipToRows(@Name("list1") List list1, @Name("list2") List list2) {
if (list1.isEmpty()) return Stream.empty();
ListIterator it = list2.listIterator();
return list1.stream().map((e) -> new ListResult(asList(e, it.hasNext() ? it.next() : null)));
}
@UserFunction("apoc.coll.zip")
@Description("Returns the two given `LIST` values zipped together as a `LIST>`.")
public List> zip(@Name("list1") List list1, @Name("list2") List list2) {
if (list1 == null || list2 == null) return null;
if (list1.isEmpty() || list2.isEmpty()) return Collections.emptyList();
List> result = new ArrayList<>(list1.size());
ListIterator it = list2.listIterator();
for (Object o1 : list1) {
result.add(asList(o1, it.hasNext() ? it.next() : null));
}
return result;
}
@UserFunction("apoc.coll.pairs")
@Description("Returns a `LIST` of adjacent elements in the `LIST` ([1,2],[2,3],[3,null]).")
public List> pairs(@Name("list") List list) {
if (list == null) return null;
if (list.isEmpty()) return Collections.emptyList();
return zip(list, list.subList(1, list.size()));
}
@UserFunction("apoc.coll.pairsMin")
@Description(
"Returns `LIST` values of adjacent elements in the `LIST` ([1,2],[2,3]), skipping the final element.")
public List> pairsMin(@Name("list") List list) {
if (list == null) return null;
if (list.isEmpty()) return Collections.emptyList();
return zip(list.subList(0, list.size() - 1), list.subList(1, list.size()));
}
@UserFunction("apoc.coll.sum")
@Description("Returns the sum of all the `INTEGER | FLOAT` in the `LIST`.")
public Double sum(@Name("coll") List list) {
if (list == null || list.isEmpty()) return null;
double sum = 0;
for (Number number : list) {
sum += number.doubleValue();
}
return sum;
}
@UserFunction("apoc.coll.avg")
@Description("Returns the average of the numbers in the `LIST`.")
public Double avg(@Name("coll") List list) {
if (list == null || list.isEmpty()) return null;
double avg = 0;
for (Number number : list) {
avg += number.doubleValue();
}
return (avg / (double) list.size());
}
@NotThreadSafe
@UserFunction("apoc.coll.min")
@Description("Returns the minimum of all values in the given `LIST`.")
public Object min(@Name("values") List list) {
if (list == null || list.isEmpty()) return null;
if (list.size() == 1) return list.get(0);
try (Result result = tx.execute(
"cypher runtime=slotted return reduce(res=null, x in $list | CASE WHEN res IS NULL OR x`.")
public Object max(@Name("values") List list) {
if (list == null || list.isEmpty()) return null;
if (list.size() == 1) return list.get(0);
try (Result result = tx.execute(
"cypher runtime=slotted return reduce(res=null, x in $list | CASE WHEN res IS NULL OR res` into identifiers indicating their specific type.")
public Stream elements(
@Name("coll") List list,
@Name(value = "limit", defaultValue = "-1") long limit,
@Name(value = "offset", defaultValue = "0") long offset) {
int elements = (limit < 0 ? list.size() : Math.min((int) (offset + limit), list.size())) - (int) offset;
if (elements > ElementsResult.MAX_ELEMENTS) elements = ElementsResult.MAX_ELEMENTS;
ElementsResult result = new ElementsResult();
for (int i = 0; i < elements; i++) {
result.add(list.get((int) offset + i));
}
return Stream.of(result);
}
public static class ElementsResult {
public Object _1, _2, _3, _4, _5, _6, _7, _8, _9, _10;
public String _1s, _2s, _3s, _4s, _5s, _6s, _7s, _8s, _9s, _10s;
public Long _1i, _2i, _3i, _4i, _5i, _6i, _7i, _8i, _9i, _10i;
public Double _1f, _2f, _3f, _4f, _5f, _6f, _7f, _8f, _9f, _10f;
public Boolean _1b, _2b, _3b, _4b, _5b, _6b, _7b, _8b, _9b, _10b;
public List _1l, _2l, _3l, _4l, _5l, _6l, _7l, _8l, _9l, _10l;
public Map _1m, _2m, _3m, _4m, _5m, _6m, _7m, _8m, _9m, _10m;
public Node _1n, _2n, _3n, _4n, _5n, _6n, _7n, _8n, _9n, _10n;
public Relationship _1r, _2r, _3r, _4r, _5r, _6r, _7r, _8r, _9r, _10r;
public Path _1p, _2p, _3p, _4p, _5p, _6p, _7p, _8p, _9p, _10p;
public long elements;
static final int MAX_ELEMENTS = 10;
void add(Object o) {
if (elements == MAX_ELEMENTS) return;
// Arrays are considered lists in Cypher and
// should be treated as such
if (o != null && o.getClass().isArray()) {
o = convertArrayToList(o);
}
setObject(o, (int) elements);
if (o instanceof String) {
setString((String) o, (int) elements);
}
if (o instanceof Number) {
setLong(((Number) o).longValue(), (int) elements);
setDouble(((Number) o).doubleValue(), (int) elements);
}
if (o instanceof Boolean) {
setBoolean((Boolean) o, (int) elements);
}
if (o instanceof Map) {
setMap((Map) o, (int) elements);
}
if (o instanceof List) {
setList((List) o, (int) elements);
}
if (o instanceof Node) {
setNode((Node) o, (int) elements);
}
if (o instanceof Relationship) {
setRelationship((Relationship) o, (int) elements);
}
if (o instanceof Path) {
setPath((Path) o, (int) elements);
}
elements++;
}
public void setObject(Object o, int idx) {
switch (idx) {
case 0 -> _1 = o;
case 1 -> _2 = o;
case 2 -> _3 = o;
case 3 -> _4 = o;
case 4 -> _5 = o;
case 5 -> _6 = o;
case 6 -> _7 = o;
case 7 -> _8 = o;
case 8 -> _9 = o;
case 9 -> _10 = o;
}
}
public void setString(String o, int idx) {
switch (idx) {
case 0:
_1s = o;
break;
case 1:
_2s = o;
break;
case 2:
_3s = o;
break;
case 3:
_4s = o;
break;
case 4:
_5s = o;
break;
case 5:
_6s = o;
break;
case 6:
_7s = o;
break;
case 7:
_8s = o;
break;
case 8:
_9s = o;
break;
case 9:
_10s = o;
break;
}
}
public void setLong(Long o, int idx) {
switch (idx) {
case 0 -> _1i = o;
case 1 -> _2i = o;
case 2 -> _3i = o;
case 3 -> _4i = o;
case 4 -> _5i = o;
case 5 -> _6i = o;
case 6 -> _7i = o;
case 7 -> _8i = o;
case 8 -> _9i = o;
case 9 -> _10i = o;
}
}
public void setBoolean(Boolean o, int idx) {
switch (idx) {
case 0 -> _1b = o;
case 1 -> _2b = o;
case 2 -> _3b = o;
case 3 -> _4b = o;
case 4 -> _5b = o;
case 5 -> _6b = o;
case 6 -> _7b = o;
case 7 -> _8b = o;
case 8 -> _9b = o;
case 9 -> _10b = o;
}
}
public void setDouble(Double o, int idx) {
switch (idx) {
case 0 -> _1f = o;
case 1 -> _2f = o;
case 2 -> _3f = o;
case 3 -> _4f = o;
case 4 -> _5f = o;
case 5 -> _6f = o;
case 6 -> _7f = o;
case 7 -> _8f = o;
case 8 -> _9f = o;
case 9 -> _10f = o;
}
}
public void setNode(Node o, int idx) {
switch (idx) {
case 0:
_1n = o;
break;
case 1:
_2n = o;
break;
case 2:
_3n = o;
break;
case 3:
_4n = o;
break;
case 4:
_5n = o;
break;
case 5:
_6n = o;
break;
case 6:
_7n = o;
break;
case 7:
_8n = o;
break;
case 8:
_9n = o;
break;
case 9:
_10n = o;
break;
}
}
public void setRelationship(Relationship o, int idx) {
switch (idx) {
case 0:
_1r = o;
break;
case 1:
_2r = o;
break;
case 2:
_3r = o;
break;
case 3:
_4r = o;
break;
case 4:
_5r = o;
break;
case 5:
_6r = o;
break;
case 6:
_7r = o;
break;
case 7:
_8r = o;
break;
case 8:
_9r = o;
break;
case 9:
_10r = o;
break;
}
}
public void setPath(Path o, int idx) {
switch (idx) {
case 0 -> _1p = o;
case 1 -> _2p = o;
case 2 -> _3p = o;
case 3 -> _4p = o;
case 4 -> _5p = o;
case 5 -> _6p = o;
case 6 -> _7p = o;
case 7 -> _8p = o;
case 8 -> _9p = o;
case 9 -> _10p = o;
}
}
public void setMap(Map o, int idx) {
switch (idx) {
case 0 -> _1m = o;
case 1 -> _2m = o;
case 2 -> _3m = o;
case 3 -> _4m = o;
case 4 -> _5m = o;
case 5 -> _6m = o;
case 6 -> _7m = o;
case 7 -> _8m = o;
case 8 -> _9m = o;
case 9 -> _10m = o;
}
}
public void setList(List o, int idx) {
switch (idx) {
case 0 -> _1l = o;
case 1 -> _2l = o;
case 2 -> _3l = o;
case 3 -> _4l = o;
case 4 -> _5l = o;
case 5 -> _6l = o;
case 6 -> _7l = o;
case 7 -> _8l = o;
case 8 -> _9l = o;
case 9 -> _10l = o;
}
}
}
@Procedure("apoc.coll.partition")
@Description("Partitions the original `LIST` into a new `LIST` of the given batch size.\n"
+ "The final `LIST` may be smaller than the given batch size.")
public Stream partition(@Name("coll") List list, @Name("batchSize") long batchSize) {
if (list == null || list.isEmpty()) return Stream.empty();
return partitionList(list, (int) batchSize).map(ListResult::new);
}
@UserFunction("apoc.coll.partition")
@Description("Partitions the original `LIST` into a new `LIST` of the given batch size.\n"
+ "The final `LIST` may be smaller than the given batch size.")
public List partitionFn(@Name("coll") List list, @Name("batchSize") long batchSize) {
if (list == null || list.isEmpty()) return new ArrayList<>();
return partitionList(list, (int) batchSize).collect(Collectors.toList());
}
@Procedure("apoc.coll.split")
@Description("Splits a collection by the given value.\n"
+ "The value itself will not be part of the resulting `LIST` values.")
public Stream split(@Name("coll") List list, @Name("value") Object value) {
if (list == null || list.isEmpty()) return Stream.empty();
List l = new ArrayList<>(list);
List> result = new ArrayList<>(10);
int idx = Util.indexOf(l, value);
while (idx != -1) {
List subList = l.subList(0, idx);
if (!subList.isEmpty()) result.add(subList);
l = l.subList(idx + 1, l.size());
idx = Util.indexOf(l, value);
}
if (!l.isEmpty()) result.add(l);
return result.stream().map(ListResult::new);
}
private Stream> partitionList(@Name("values") List list, @Name("batchSize") int batchSize) {
int total = list.size();
int pages = total % batchSize == 0 ? total / batchSize : total / batchSize + 1;
return IntStream.range(0, pages).parallel().boxed().map(page -> {
int from = page * batchSize;
return list.subList(from, Math.min(from + batchSize, total));
});
}
@UserFunction("apoc.coll.contains")
@Description("Returns whether or not the given value exists in the given collection (using a HashSet).")
public boolean contains(@Name("coll") List coll, @Name("value") Object value) {
if (coll == null || coll.isEmpty()) return false;
return new HashSet<>(coll).contains(value);
}
@UserFunction("apoc.coll.set")
@Description("Sets the element at the given index to the new value.")
public List set(@Name("coll") List coll, @Name("index") long index, @Name("value") Object value) {
if (coll == null) return null;
if (index < 0 || value == null || index >= coll.size()) return coll;
List list = new ArrayList<>(coll);
list.set((int) index, value);
return list;
}
@UserFunction("apoc.coll.insert")
@Description("Inserts a value into the specified index in the `LIST`.")
public List insert(
@Name("coll") List coll, @Name("index") long index, @Name("value") Object value) {
if (coll == null) return null;
if (index < 0 || value == null || index > coll.size()) return coll;
List list = new ArrayList<>(coll);
list.add((int) index, value);
return list;
}
@UserFunction("apoc.coll.insertAll")
@Description("Inserts all of the values into the `LIST`, starting at the specified index.")
public List insertAll(
@Name("coll") List coll, @Name("index") long index, @Name("values") List values) {
if (coll == null) return null;
if (index < 0 || values == null || values.isEmpty() || index > coll.size()) return coll;
List list = new ArrayList<>(coll);
list.addAll((int) index, values);
return list;
}
@UserFunction("apoc.coll.remove")
@Description(
"Removes a range of values from the `LIST`, beginning at position index for the given length of values.")
public List remove(
@Name("coll") List coll,
@Name("index") long index,
@Name(value = "length", defaultValue = "1") long length) {
if (coll == null) return null;
if (index < 0 || index >= coll.size() || length <= 0) return coll;
List list = new ArrayList<>(coll);
for (long i = index + length - 1; i >= index; i--) {
if (i < list.size()) list.remove((int) i);
}
return list;
}
@UserFunction("apoc.coll.indexOf")
@Description("Returns the index for the first occurrence of the specified value in the `LIST`.")
public long indexOf(@Name("coll") List coll, @Name("value") Object value) {
// return reduce(res=[0,-1], x in $list | CASE WHEN x=$value AND res[1]=-1 THEN [res[0], res[0]+1] ELSE
// [res[0]+1, res[1]] END)[1] as value
if (coll == null || coll.isEmpty()) return -1;
return Util.indexOf(coll, value);
}
@UserFunction("apoc.coll.containsAll")
@Description("Returns whether or not all of the given values exist in the given collection (using a HashSet).")
public boolean containsAll(@Name("coll1") List coll, @Name("coll2") List values) {
if (coll == null || coll.isEmpty() || values == null) return false;
Set objects = new HashSet<>(coll);
return values.stream().allMatch(i -> containsValueEquals(objects, i));
}
@UserFunction("apoc.coll.containsSorted")
@Description(
"Returns whether or not the given value exists in an already sorted collection (using a binary search).")
public boolean containsSorted(@Name("coll") List coll, @Name("value") Object value) {
if (coll == null || coll.isEmpty()) return false;
int batchSize = 5000 - 1; // Collections.binarySearchThreshold
List list = (coll instanceof RandomAccess || coll.size() < batchSize) ? coll : new ArrayList(coll);
return Collections.binarySearch(list, value) >= 0;
}
@UserFunction("apoc.coll.containsAllSorted")
@Description(
"Returns whether or not all of the given values in the second `LIST` exist in an already sorted collection (using a binary search).")
public boolean containsAllSorted(@Name("coll1") List coll, @Name("coll2") List values) {
if (coll == null || values == null) return false;
int batchSize = 5000 - 1; // Collections.binarySearchThreshold
List list = (coll instanceof RandomAccess || coll.size() < batchSize) ? coll : new ArrayList(coll);
for (Object value : values) {
boolean result = Collections.binarySearch(list, value) >= 0;
if (!result) return false;
}
return true;
}
@UserFunction("apoc.coll.isEqualCollection")
@Description(
"Returns true if the two collections contain the same elements with the same cardinality in any order (using a HashMap).")
public boolean isEqualCollection(@Name("coll") List first, @Name("values") List second) {
if (first == null && second == null) return true;
if (first == null || second == null || first.size() != second.size()) return false;
Map map1 =
first.stream().collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));
Map map2 =
second.stream().collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));
return map1.equals(map2);
}
@UserFunction("apoc.coll.toSet")
@Description("Returns a unique `LIST` from the given `LIST`.")
public List toSet(@Name("coll") List list) {
if (list == null) return null;
List anyValues = toAnyValues(list);
return new SetBackedList(new LinkedHashSet(anyValues));
}
@UserFunction("apoc.coll.sumLongs")
@Description("Returns the sum of all the `INTEGER | FLOAT` in the `LIST`.")
public Long sumLongs(@Name("coll") List list) {
if (list == null) return null;
long sum = 0;
for (Number number : list) {
sum += number.longValue();
}
return sum;
}
@UserFunction("apoc.coll.sort")
@Description("Sorts the given `LIST` into ascending order.")
public List sort(@Name("coll") List coll) {
if (coll == null || coll.isEmpty()) return Collections.emptyList();
List sorted = new ArrayList<>(coll);
Collections.sort((List extends Comparable>) sorted);
return sorted;
}
@UserFunction("apoc.coll.sortNodes")
@Description("Sorts the given `LIST` by the property of the nodes into descending order.")
public List sortNodes(@Name("coll") List coll, @Name("prop") String prop) {
if (coll == null || coll.isEmpty()) return Collections.emptyList();
List sorted = new ArrayList<>(coll);
int reverseOrder = reverseOrder(prop);
String cleanedProp = cleanProperty(prop);
Collections.sort(
sorted,
(x, y) -> reverseOrder * compare(x.getProperty(cleanedProp, null), y.getProperty(cleanedProp, null)));
return sorted;
}
@UserFunction("apoc.coll.sortMaps")
@Description(
"Sorts the given `LIST>` into descending order, based on the `MAP` property indicated by `prop`.")
public List> sortMaps(@Name("list") List> coll, @Name("prop") String prop) {
if (coll == null || coll.isEmpty()) return Collections.emptyList();
List> sorted = new ArrayList<>(coll);
int reverseOrder = reverseOrder(prop);
String cleanedProp = cleanProperty(prop);
Collections.sort(sorted, (x, y) -> reverseOrder * compare(x.get(cleanedProp), y.get(cleanedProp)));
return sorted;
}
public int reverseOrder(String prop) {
return prop.charAt(0) == ASCENDING_ORDER_CHAR ? 1 : -1;
}
public String cleanProperty(String prop) {
return prop.charAt(0) == ASCENDING_ORDER_CHAR ? prop.substring(1) : prop;
}
public static int compare(Object o1, Object o2) {
if (o1 == null) return o2 == null ? 0 : -1;
if (o2 == null) return 1;
if (o1.equals(o2)) return 0;
if (o1 instanceof Number && o2 instanceof Number) {
if (o1 instanceof Double || o2 instanceof Double || o1 instanceof Float || o2 instanceof Float)
return Double.compare(((Number) o1).doubleValue(), ((Number) o2).doubleValue());
return Long.compare(((Number) o1).longValue(), ((Number) o2).longValue());
}
if (o1 instanceof Boolean b1 && o2 instanceof Boolean) return b1 ? 1 : -1;
if (o1 instanceof Node n1 && o2 instanceof Node n2)
return n1.getElementId().compareTo(n2.getElementId());
if (o1 instanceof Relationship rel1 && o2 instanceof Relationship rel2)
return rel1.getElementId().compareTo(rel2.getElementId());
return o1.toString().compareTo(o2.toString());
}
@UserFunction("apoc.coll.union")
@Description("Returns the distinct union of the two given `LIST` values.")
public List union(@Name("list1") List first, @Name("list2") List second) {
if (first == null) return second;
if (second == null) return first;
Set set = new HashSet<>(first);
set.addAll(second);
return new SetBackedList(set);
}
@UserFunction("apoc.coll.removeAll")
@Description("Returns the first `LIST` with all elements also present in the second `LIST` removed.")
public List removeAll(@Name("list1") List first, @Name("list2") List second) {
if (first == null) return null;
List list = new ArrayList<>(toAnyValues(first));
if (second != null) list.removeAll(toAnyValues(second));
return list;
}
@UserFunction("apoc.coll.subtract")
@Description("Returns the first `LIST` as a set with all the elements of the second `LIST` removed.")
public List subtract(@Name("list1") List first, @Name("list2") List second) {
if (first == null) return null;
Set set = new HashSet<>(first);
if (second != null) set.removeAll(second);
return new SetBackedList(set);
}
@UserFunction("apoc.coll.intersection")
@Description("Returns the distinct intersection of two `LIST` values.")
public List intersection(@Name("list1") List first, @Name("list2") List second) {
if (first == null || second == null) return Collections.emptyList();
Set set = new HashSet<>(first);
set.retainAll(second);
return new SetBackedList(set);
}
@UserFunction("apoc.coll.disjunction")
@Description("Returns the disjunct set from two `LIST` values.")
public List disjunction(@Name("list1") List first, @Name("list2") List second) {
if (first == null) return second;
if (second == null) return first;
Set intersection = new HashSet<>(first);
intersection.retainAll(second);
Set set = new HashSet<>(first);
set.addAll(second);
set.removeAll(intersection);
return new SetBackedList(set);
}
@UserFunction("apoc.coll.unionAll")
@Description("Returns the full union of the two given `LIST` values (duplicates included).")
public List unionAll(@Name("list1") List first, @Name("list2") List second) {
if (first == null) return second;
if (second == null) return first;
List list = new ArrayList<>(first);
list.addAll(second);
return list;
}
@UserFunction("apoc.coll.shuffle")
@Description("Returns the `LIST` shuffled.")
public List shuffle(@Name("coll") List coll) {
if (coll == null || coll.isEmpty()) {
return Collections.emptyList();
} else if (coll.size() == 1) {
return coll;
}
List shuffledList = new ArrayList<>(coll);
Collections.shuffle(shuffledList);
return shuffledList;
}
@UserFunction("apoc.coll.randomItem")
@Description("Returns a random item from the `LIST`, or null on `LIST` or `LIST`.")
public Object randomItem(@Name("coll") List coll) {
if (coll == null || coll.isEmpty()) {
return null;
} else if (coll.size() == 1) {
return coll.get(0);
}
return coll.get(ThreadLocalRandom.current().nextInt(coll.size()));
}
@UserFunction("apoc.coll.randomItems")
@Description(
"Returns a `LIST` of `itemCount` random items from the original `LIST` (optionally allowing elements in the original `LIST` to be selected more than once).")
public List randomItems(
@Name("coll") List coll,
@Name("itemCount") long itemCount,
@Name(value = "allowRepick", defaultValue = "false") boolean allowRepick) {
if (coll == null || coll.isEmpty() || itemCount <= 0) {
return Collections.emptyList();
}
List pickList = new ArrayList<>(coll);
List randomItems = new ArrayList<>((int) itemCount);
Random random = ThreadLocalRandom.current();
if (!allowRepick && itemCount >= coll.size()) {
Collections.shuffle(pickList);
return pickList;
}
while (randomItems.size() < itemCount) {
Object item = allowRepick
? pickList.get(random.nextInt(pickList.size()))
: pickList.remove(random.nextInt(pickList.size()));
randomItems.add(item);
}
return randomItems;
}
@UserFunction("apoc.coll.containsDuplicates")
@Description("Returns true if a collection contains duplicate elements.")
public boolean containsDuplicates(@Name("coll") List coll) {
if (coll == null || coll.size() <= 1) {
return false;
}
Set set = new HashSet<>(coll);
return set.size() < coll.size();
}
@UserFunction("apoc.coll.duplicates")
@Description("Returns a `LIST` of duplicate items in the collection.")
public List duplicates(@Name("coll") List coll) {
if (coll == null || coll.size() <= 1) {
return Collections.emptyList();
}
Set set = new HashSet<>(coll.size());
Set duplicates = new LinkedHashSet<>();
for (Object obj : coll) {
if (!set.add(obj)) {
duplicates.add(obj);
}
}
return new ArrayList(duplicates);
}
@UserFunction("apoc.coll.duplicatesWithCount")
@Description(
"Returns a `LIST` of duplicate items in the collection and their count, keyed by `item` and `count`.")
public List> duplicatesWithCount(@Name("coll") List coll) {
if (coll == null || coll.size() <= 1) {
return Collections.emptyList();
}
// mimicking a counted bag
Map duplicates = new LinkedHashMap<>(coll.size());
List> resultList = new ArrayList<>();
for (Object obj : coll) {
MutableInt counter = duplicates.get(obj);
if (counter == null) {
counter = new MutableInt();
duplicates.put(obj, counter);
}
counter.increment();
}
duplicates.forEach((o, intCounter) -> {
int count = intCounter.intValue();
if (count > 1) {
Map entry = new LinkedHashMap<>(2);
entry.put("item", o);
entry.put("count", Long.valueOf(count));
resultList.add(entry);
}
});
return resultList;
}
@UserFunction("apoc.coll.frequencies")
@Description("Returns a `LIST` of frequencies of the items in the collection, keyed by `item` and `count`.")
public List> frequencies(@Name("coll") List coll) {
if (coll == null || coll.size() == 0) {
return Collections.emptyList();
}
// mimicking a counted bag
Map counts = new LinkedHashMap<>(coll.size());
List> resultList = new ArrayList<>();
for (Object obj : coll) {
MutableInt counter = counts.get(obj);
if (counter == null) {
counter = new MutableInt();
counts.put(obj, counter);
}
counter.increment();
}
counts.forEach((o, intCounter) -> {
int count = intCounter.intValue();
Map entry = new LinkedHashMap<>(2);
entry.put("item", o);
entry.put("count", Long.valueOf(count));
resultList.add(entry);
});
return resultList;
}
@UserFunction("apoc.coll.frequenciesAsMap")
@Description("Returns a `MAP` of frequencies of the items in the collection, keyed by `item` and `count`.")
public Map frequenciesAsMap(@Name("coll") List coll) {
if (coll == null) return Collections.emptyMap();
return frequencies(coll).stream()
.collect(Collectors.toMap(t -> t.get("item").toString(), v -> v.get("count")));
}
@UserFunction("apoc.coll.occurrences")
@Description("Returns the count of the given item in the collection.")
public long occurrences(@Name("coll") List coll, @Name("item") Object item) {
if (coll == null || coll.isEmpty()) {
return 0;
}
long occurrences = 0;
for (Object obj : coll) {
if (item.equals(obj)) {
occurrences++;
}
}
return occurrences;
}
@UserFunction("apoc.coll.flatten")
@Description("Flattens the given `LIST` (to flatten nested `LIST` values, set recursive to true).")
public List flatten(
@Name("coll") List coll, @Name(value = "recursive", defaultValue = "false") boolean recursive) {
if (coll == null) return Collections.emptyList();
if (recursive) return flattenRecursive(coll, 0); // flatten everything
return flattenRecursive(coll, 0, 2); // flatten one level of lists in the input list if not recursive
}
private static List flattenRecursive(Object aObject, int aDepth, int aStopDepth) {
List vResult = new ArrayList();
if (aDepth == aStopDepth) { // always for a future arbitrary stopping point
vResult.add(aObject);
} else {
if (aObject.getClass().isArray()) {
for (int i = 0; i < Array.getLength(aObject); i++) {
vResult.addAll(flattenRecursive(Array.get(aObject, i), aDepth + 1, aStopDepth));
}
} else if (aObject instanceof List) {
for (Object vElement : (List>) aObject) {
vResult.addAll(flattenRecursive(vElement, aDepth + 1, aStopDepth));
}
} else {
vResult.add(aObject);
}
}
return vResult;
}
private static List flattenRecursive(Object aObject, int aDepth) {
return flattenRecursive(aObject, aDepth, -1); // we only stop when all lists are flattened
}
@UserFunction("apoc.coll.sortMulti")
@Description("Sorts the given `LIST>` by the given fields.\n"
+ "To indicate that a field should be sorted according to ascending values, prefix it with a caret (^).\n"
+ "It is also possible to add limits to the `LIST>` and to skip values.")
public List> sortMulti(
@Name("coll") List> coll,
@Name(value = "orderFields", defaultValue = "[]") List orderFields,
@Name(value = "limit", defaultValue = "-1") long limit,
@Name(value = "skip", defaultValue = "0") long skip) {
List> result = new ArrayList<>(coll);
if (orderFields != null && !orderFields.isEmpty()) {
List> fields = orderFields.stream()
.map(v -> {
boolean asc = v.charAt(0) == '^';
return Pair.of(asc ? v.substring(1) : v, asc);
})
.collect(Collectors.toList());
Comparator>> compare = (o1, o2) -> {
int a = 0;
for (Pair s : fields) {
if (a != 0) break;
String name = s.getLeft();
Comparable v1 = o1.get(name);
Comparable v2 = o2.get(name);
if (v1 != v2) {
int cmp = (v1 == null) ? -1 : (v2 == null) ? 1 : v1.compareTo(v2);
a = (s.getRight()) ? cmp : -cmp;
}
}
return a;
};
Collections.sort((List>>) (List) result, compare);
}
if (skip > 0 && limit != -1L) return result.subList((int) skip, (int) (skip + limit));
if (skip > 0) return result.subList((int) skip, result.size());
if (limit != -1L) return result.subList(0, (int) limit);
return result;
}
@UserFunction("apoc.coll.combinations")
@Description(
"Returns a collection of all combinations of `LIST` elements between the selection size `minSelect` and `maxSelect` (default: `minSelect`).")
public List> combinations(
@Name("coll") List coll,
@Name(value = "minSelect") long minSelectIn,
@Name(value = "maxSelect", defaultValue = "-1") long maxSelectIn) {
int minSelect = (int) minSelectIn;
int maxSelect = (int) maxSelectIn;
maxSelect = maxSelect == -1 ? minSelect : maxSelect;
if (coll == null
|| coll.isEmpty()
|| minSelect < 1
|| minSelect > coll.size()
|| minSelect > maxSelect
|| maxSelect > coll.size()) {
return Collections.emptyList();
}
List> combinations = new ArrayList<>();
for (int i = minSelect; i <= maxSelect; i++) {
Iterator itr = new Combinations(coll.size(), i).iterator();
while (itr.hasNext()) {
List entry = new ArrayList<>(i);
int[] indexes = itr.next();
if (indexes.length > 0) {
for (int index : indexes) {
entry.add(coll.get(index));
}
combinations.add(entry);
}
}
}
return combinations;
}
@UserFunction("apoc.coll.different")
@Description("Returns true if all the values in the given `LIST` are unique.")
public boolean different(@Name("coll") List values) {
if (values == null) return false;
return new HashSet(values).size() == values.size();
}
@UserFunction("apoc.coll.dropDuplicateNeighbors")
@Description("Removes duplicate consecutive objects in the `LIST`.")
public List dropDuplicateNeighbors(@Name("list") List list) {
if (list == null) return null;
List newList = new ArrayList<>(list.size());
Object last = null;
for (Object element : list) {
if (element == null && last != null || element != null && !element.equals(last)) {
newList.add(element);
last = element;
}
}
return newList;
}
@UserFunction("apoc.coll.fill")
@Description("Returns a `LIST` with the given count of items.")
public List fill(@Name("items") String item, @Name("count") long count) {
return Collections.nCopies((int) count, item);
}
@UserFunction("apoc.coll.sortText")
@Description("Sorts the given `LIST` into ascending order.")
public List sortText(
@Name("coll") List coll, @Name(value = "conf", defaultValue = "{}") Map conf) {
if (conf == null) conf = Collections.emptyMap();
if (coll == null || coll.isEmpty()) return Collections.emptyList();
List sorted = new ArrayList<>(coll);
String localeAsStr = conf.getOrDefault("locale", "").toString();
final Locale locale = !localeAsStr.isBlank() ? Locale.forLanguageTag(localeAsStr) : null;
Collator collator = locale != null ? Collator.getInstance(locale) : Collator.getInstance();
Collections.sort(sorted, collator);
return sorted;
}
@UserFunction("apoc.coll.pairWithOffset")
@Description("Returns a `LIST` of pairs defined by the offset.")
public List> pairWithOffsetFn(@Name("coll") List values, @Name("offset") long offset) {
if (values == null) return null;
BiFunction, Long, Object> extract =
(list, index) -> index < list.size() && index >= 0 ? list.get(index.intValue()) : null;
final int length = Double.valueOf(Math.ceil((double) values.size() / Math.abs(offset)))
.intValue();
List> result = new ArrayList<>(length);
for (long i = 0; i < values.size(); i++) {
final List objects = asList(extract.apply(values, i), extract.apply(values, i + offset));
result.add(objects);
}
return result;
}
@Procedure("apoc.coll.pairWithOffset")
@Description("Returns a `LIST` of pairs defined by the offset.")
public Stream pairWithOffset(@Name("coll") List values, @Name("offset") long offset) {
return pairWithOffsetFn(values, offset).stream().map(ListResult::new);
}
}