All Downloads are FREE. Search and download functionalities are using the official Maven repository.

apoc.coll.Coll Maven / Gradle / Ivy

There is a newer version: 5.25.1
Show newest version
/*
 * 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.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.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.kernel.impl.util.ValueUtils;
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(value = "list", description = "A list to collect the standard deviation from.") List list,
            @Name(
                            value = "isBiasCorrected",
                            defaultValue = "true",
                            description =
                                    "Will perform a sample standard deviation if `isBiasCorrected`, otherwise a population standard deviation is performed.")
                    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(value = "list", description = "The list to return a running total from.") 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());
    }

    public record ZipToRowsListResult(@Description("A zipped pair.") List value) {}

    @Procedure("apoc.coll.zipToRows")
    @Description("Returns the two `LIST` values zipped together, with one row per zipped pair.")
    public Stream zipToRows(
            @Name(value = "list1", description = "The list to zip together with `list2`.") List list1,
            @Name(value = "list2", description = "The list to zip together with `list1`.") List list2) {
        if (list1.isEmpty()) return Stream.empty();
        ListIterator it = list2.listIterator();
        return list1.stream().map((e) -> new ZipToRowsListResult(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(value = "list1", description = "The list to zip together with `list2`.") List list1,
            @Name(value = "list2", description = "The list to zip together with `list1`.") 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(value = "list", description = "The list to create pairs from.") 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(value = "list", description = "The list to create pairs from.") 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(value = "coll", description = "The list of numbers to create a sum from.") 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(value = "coll", description = "The list to return the average from.") 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(value = "values", description = "The list to find the minimum in.") 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(value = "values", description = "The list to find the maximum in.") 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(value = "coll", description = "A list of values to deconstruct.") List list,
            @Name(
                            value = "limit",
                            defaultValue = "-1",
                            description = "The maximum size of elements to deconstruct from the given list.")
                    long limit,
            @Name(value = "offset", defaultValue = "0", description = "The offset to start deconstructing from.")
                    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 {
        @Description("The value of the first item.")
        public Object _1;

        @Description("The value of the second item.")
        public Object _2;

        @Description("The value of the third item.")
        public Object _3;

        @Description("The value of the fourth item.")
        public Object _4;

        @Description("The value of the fifth item.")
        public Object _5;

        @Description("The value of the sixth item.")
        public Object _6;

        @Description("The value of the seventh item.")
        public Object _7;

        @Description("The value of the eighth item.")
        public Object _8;

        @Description("The value of the ninth item.")
        public Object _9;

        @Description("The value of the tenth item.")
        public Object _10;

        @Description("The value of the first item, if it is a string value.")
        public String _1s;

        @Description("The value of the second item, if it is a string value.")
        public String _2s;

        @Description("The value of the third item, if it is a string value.")
        public String _3s;

        @Description("The value of the fourth item, if it is a string value.")
        public String _4s;

        @Description("The value of the fifth item, if it is a string value.")
        public String _5s;

        @Description("The value of the sixth item, if it is a string value.")
        public String _6s;

        @Description("The value of the seventh item, if it is a string value.")
        public String _7s;

        @Description("The value of the eighth item, if it is a string value.")
        public String _8s;

        @Description("The value of the ninth item, if it is a string value.")
        public String _9s;

        @Description("The value of the tenth item, if it is a string value.")
        public String _10s;

        @Description("The value of the first item, if it is an integer value.")
        public Long _1i;

        @Description("The value of the second item, if it is an integer value.")
        public Long _2i;

        @Description("The value of the third item, if it is an integer value.")
        public Long _3i;

        @Description("The value of the fourth item, if it is an integer value.")
        public Long _4i;

        @Description("The value of the fifth item, if it is an integer value.")
        public Long _5i;

        @Description("The value of the sixth item, if it is an integer value.")
        public Long _6i;

        @Description("The value of the seventh item, if it is an integer value.")
        public Long _7i;

        @Description("The value of the eighth item, if it is an integer value.")
        public Long _8i;

        @Description("The value of the ninth item, if it is an integer value.")
        public Long _9i;

        @Description("The value of the tenth item, if it is an integer value.")
        public Long _10i;

        @Description("The value of the first item, if it is a float value.")
        public Double _1f;

        @Description("The value of the second item, if it is a float value.")
        public Double _2f;

        @Description("The value of the third item, if it is a float value.")
        public Double _3f;

        @Description("The value of the fourth item, if it is a float value.")
        public Double _4f;

        @Description("The value of the fifth item, if it is a float value.")
        public Double _5f;

        @Description("The value of the sixth item, if it is a float value.")
        public Double _6f;

        @Description("The value of the seventh item, if it is a float value.")
        public Double _7f;

        @Description("The value of the eighth item, if it is a float value.")
        public Double _8f;

        @Description("The value of the ninth item, if it is a float value.")
        public Double _9f;

        @Description("The value of the tenth item, if it is a float value.")
        public Double _10f;

        @Description("The value of the first item, if it is a boolean value.")
        public Boolean _1b;

        @Description("The value of the second item, if it is a boolean value.")
        public Boolean _2b;

        @Description("The value of the third item, if it is a boolean value.")
        public Boolean _3b;

        @Description("The value of the fourth item, if it is a boolean value.")
        public Boolean _4b;

        @Description("The value of the fifth item, if it is a boolean value.")
        public Boolean _5b;

        @Description("The value of the sixth item, if it is a boolean value.")
        public Boolean _6b;

        @Description("The value of the seventh item, if it is a boolean value.")
        public Boolean _7b;

        @Description("The value of the eighth item, if it is a boolean value.")
        public Boolean _8b;

        @Description("The value of the ninth item, if it is a boolean value.")
        public Boolean _9b;

        @Description("The value of the tenth item, if it is a boolean value.")
        public Boolean _10b;

        @Description("The value of the first item, if it is a list value.")
        public List _1l;

        @Description("The value of the second item, if it is a list value.")
        public List _2l;

        @Description("The value of the third item, if it is a list value.")
        public List _3l;

        @Description("The value of the fourth item, if it is a list value.")
        public List _4l;

        @Description("The value of the fifth item, if it is a list value.")
        public List _5l;

        @Description("The value of the sixth item, if it is a list value.")
        public List _6l;

        @Description("The value of the seventh item, if it is a list value.")
        public List _7l;

        @Description("The value of the eighth item, if it is a list value.")
        public List _8l;

        @Description("The value of the ninth item, if it is a list value.")
        public List _9l;

        @Description("The value of the tenth item, if it is a list value.")
        public List _10l;

        @Description("The value of the first item, if it is a map value.")
        public Map _1m;

        @Description("The value of the second item, if it is a map value.")
        public Map _2m;

        @Description("The value of the third item, if it is a map value.")
        public Map _3m;

        @Description("The value of the fourth item, if it is a map value.")
        public Map _4m;

        @Description("The value of the fifth item, if it is a map value.")
        public Map _5m;

        @Description("The value of the sixth item, if it is a map value.")
        public Map _6m;

        @Description("The value of the seventh item, if it is a map value.")
        public Map _7m;

        @Description("The value of the eighth item, if it is a map value.")
        public Map _8m;

        @Description("The value of the ninth item, if it is a map value.")
        public Map _9m;

        @Description("The value of the tenth item, if it is a map value.")
        public Map _10m;

        @Description("The value of the first item, if it is a node value.")
        public Node _1n;

        @Description("The value of the second item, if it is a node value.")
        public Node _2n;

        @Description("The value of the third item, if it is a node value.")
        public Node _3n;

        @Description("The value of the fourth item, if it is a node value.")
        public Node _4n;

        @Description("The value of the fifth item, if it is a node value.")
        public Node _5n;

        @Description("The value of the sixth item, if it is a node value.")
        public Node _6n;

        @Description("The value of the seventh item, if it is a node value.")
        public Node _7n;

        @Description("The value of the eighth item, if it is a node value.")
        public Node _8n;

        @Description("The value of the ninth item, if it is a node value.")
        public Node _9n;

        @Description("The value of the tenth item, if it is a node value.")
        public Node _10n;

        @Description("The value of the first item, if it is a relationship value.")
        public Relationship _1r;

        @Description("The value of the second item, if it is a relationship value.")
        public Relationship _2r;

        @Description("The value of the third item, if it is a relationship value.")
        public Relationship _3r;

        @Description("The value of the fourth item, if it is a relationship value.")
        public Relationship _4r;

        @Description("The value of the fifth item, if it is a relationship value.")
        public Relationship _5r;

        @Description("The value of the sixth item, if it is a relationship value.")
        public Relationship _6r;

        @Description("The value of the seventh item, if it is a relationship value.")
        public Relationship _7r;

        @Description("The value of the eighth item, if it is a relationship value.")
        public Relationship _8r;

        @Description("The value of the ninth item, if it is a relationship value.")
        public Relationship _9r;

        @Description("The value of the tenth item, if it is a relationship value.")
        public Relationship _10r;

        @Description("The value of the first item, if it is a path value.")
        public Path _1p;

        @Description("The value of the second item, if it is a path value.")
        public Path _2p;

        @Description("The value of the third item, if it is a path value.")
        public Path _3p;

        @Description("The value of the fourth item, if it is a path value.")
        public Path _4p;

        @Description("The value of the fifth item, if it is a path value.")
        public Path _5p;

        @Description("The value of the sixth item, if it is a path value.")
        public Path _6p;

        @Description("The value of the seventh item, if it is a path value.")
        public Path _7p;

        @Description("The value of the eighth item, if it is a path value.")
        public Path _8p;

        @Description("The value of the ninth item, if it is a path value.")
        public Path _9p;

        @Description("The value of the tenth item, if it is a path value.")
        public Path _10p;

        @Description("The number of deconstructed elements.")
        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;
            }
        }
    }

    public record PartitionListResult(@Description("The partitioned list.") List value) {}

    @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(value = "coll", description = "The list to partition into smaller sublists.") List list,
            @Name(value = "batchSize", description = "The max size of each partitioned sublist.") long batchSize) {
        if (list == null || list.isEmpty()) return Stream.empty();
        return partitionList(list, (int) batchSize).map(PartitionListResult::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(value = "coll", description = "The list to partition into smaller sublists.") List list,
            @Name(value = "batchSize", description = "The max size of each partitioned sublist.") long batchSize) {
        if (list == null || list.isEmpty()) return new ArrayList<>();
        return partitionList(list, (int) batchSize).collect(Collectors.toList());
    }

    public record SplitListResult(@Description("The split list.") List value) {}

    @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(value = "coll", description = "The list to split into parts.") List list,
            @Name(value = "value", description = "The value to split the given list by.") 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(SplitListResult::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.")
    public boolean contains(
            @Name(value = "coll", description = "The list to search for the given value.") List coll,
            @Name(value = "value", description = "The value in the list to check for the existence of.") Object value) {
        if (coll == null || coll.isEmpty()) return false;
        return Util.containsValueEquals(coll, value);
    }

    @UserFunction("apoc.coll.set")
    @Description("Sets the element at the given index to the new value.")
    public List set(
            @Name(value = "coll", description = "The list to be updated.") List coll,
            @Name(value = "index", description = "The position of the value in the list to be updated.") long index,
            @Name(value = "value", description = "The new value to be set.") 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(value = "coll", description = "The list to insert a value into.") List coll,
            @Name(value = "index", description = "The position in the list to insert the given value.") long index,
            @Name(value = "value", description = "The value to be inserted.") 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(value = "coll", description = "The list to insert the values into.") List coll,
            @Name(value = "index", description = "The position in the list to start inserting the given values.")
                    long index,
            @Name(value = "values", description = "The values to be inserted.") 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(value = "coll", description = "The list to remove values from.") List coll,
            @Name(value = "index", description = "The starting index in the list to begin removing values from.")
                    long index,
            @Name(value = "length", defaultValue = "1", description = "The number of values to remove.") 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(value = "coll", description = "The list to find the given value in.") List coll,
            @Name(value = "value", description = "The value to find the first occurrence of in the given list.")
                    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.")
    public boolean containsAll(
            @Name(value = "coll1", description = "The list to search for the given values in.") List coll,
            @Name(value = "coll2", description = "The list of values in the given list to check for the existence of.")
                    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(value = "coll", description = "The sorted list to search for the given value.") List coll,
            @Name(value = "value", description = "The value to check for the existence of in the list.") 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(value = "coll1", description = "The sorted list to search for the given values.") List coll,
            @Name(value = "coll2", description = "The list of values to check for existence of in the given list.")
                    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.")
    public boolean isEqualCollection(
            @Name(value = "coll", description = "The list of values to compare against `list2` and check for equality.")
                    List first,
            @Name(
                            value = "values",
                            description = "The list of values to compare against `list1` and check for equality.")
                    List second) {
        if (first == null && second == null) return true;
        if (first == null || second == null || first.size() != second.size()) return false;
        return new HashSet<>(frequencies(second)).containsAll(frequencies(first));
    }

    @UserFunction("apoc.coll.toSet")
    @Description("Returns a unique `LIST` from the given `LIST`.")
    public List toSet(
            @Name(value = "coll", description = "The list of values to remove all duplicates from.")
                    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(
                            value = "coll",
                            description =
                                    "The list of numbers to create a sum from after each is cast to a java Long value.")
                    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(value = "coll", description = "The list to be sorted.") List coll) {
        if (coll == null || coll.isEmpty()) return Collections.emptyList();
        List sorted = new ArrayList<>(coll);
        Collections.sort((List) 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(value = "coll", description = "The list of nodes to be sorted.") List coll,
            @Name(value = "prop", description = "The property key on the node to be used to sort the list by.")
                    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(value = "list", description = "The list of maps to be sorted.") List> coll,
            @Name(value = "prop", description = "The property key to be used to sort the list of maps by.")
                    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(
                            value = "list1",
                            description =
                                    "The list of values to compare against `list2` and form a distinct union from.")
                    List first,
            @Name(
                            value = "list2",
                            description =
                                    "The list of values to compare against `list1` and form a distinct union from.")
                    List second) {
        if (first == null) return second;
        if (second == null) return first;

        Set set = Util.toAnyValuesSet(first);
        for (Object item : second) {
            set.add(ValueUtils.of(item));
        }
        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(value = "list1", description = "The list to remove values from.") List first,
            @Name(value = "list2", description = "The values to remove from the given list.") 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(value = "list1", description = "The list to remove values from.") List first,
            @Name(value = "list2", description = "The list of values to be removed from `list1`.")
                    List second) {
        if (first == null) return null;
        if (second == null) return first;

        var set1 = Util.toAnyValuesSet(first);
        var set2 = Util.toAnyValuesSet(second);
        set1.removeAll(set2);
        return new SetBackedList(set1);
    }

    @UserFunction("apoc.coll.intersection")
    @Description("Returns the distinct intersection of two `LIST` values.")
    public List intersection(
            @Name(
                            value = "list1",
                            description =
                                    "The list of values to compare against `list2` and form an intersection from.")
                    List first,
            @Name(
                            value = "list2",
                            description =
                                    "The list of values to compare against `list1` and form an intersection from.")
                    List second) {
        if (first == null || second == null) return Collections.emptyList();
        Set set = Util.toAnyValuesSet(first);
        set.retainAll(Util.toAnyValuesSet(second));
        return new SetBackedList(set);
    }

    @UserFunction("apoc.coll.disjunction")
    @Description("Returns the disjunct set from two `LIST` values.")
    public List disjunction(
            @Name(
                            value = "list1",
                            description = "The list of values to compare against `list2` and form a disjunction from.")
                    List first,
            @Name(
                            value = "list2",
                            description = "The list of values to compare against `list1` and form a disjunction from.")
                    List second) {
        if (first == null) return second;
        if (second == null) return first;
        Set intersection = Util.toAnyValuesSet(first);
        Set secondSet = Util.toAnyValuesSet(second);
        Set set = new HashSet<>(intersection);
        intersection.retainAll(secondSet);
        set.addAll(secondSet);
        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(value = "list1", description = "The list of values to compare against `list2` and form a union from.")
                    List first,
            @Name(value = "list2", description = "The list of values to compare against `list1` and form a union from.")
                    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(value = "coll", description = "The list to be shuffled.") 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(value = "coll", description = "The list to return a random item from.") 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(value = "coll", description = "The list to return random items from.") List coll,
            @Name(value = "itemCount", description = "The number of random items to return from the list.")
                    long itemCount,
            @Name(
                            value = "allowRepick",
                            defaultValue = "false",
                            description = "Whether elements from the original list can be selected more than once.")
                    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(value = "coll", description = "The list to check for duplicates in.") List coll) {
        if (coll == null || coll.size() <= 1) {
            return false;
        }

        var set = new HashSet<>(Math.max((int) (coll.size() / .75f) + 1, 16));
        boolean hasntAdded;
        for (Object item : coll) {
            // Use the ValueUtil.of version, as arrays and lists in Cypher may differ, but are considered the *same*.
            hasntAdded = set.add(ValueUtils.of(item));
            // If the item has already been added, then return true as it is a duplicate
            if (!hasntAdded) {
                return true;
            }
        }

        return set.size() < coll.size();
    }

    @UserFunction("apoc.coll.duplicates")
    @Description("Returns a `LIST` of duplicate items in the collection.")
    public List duplicates(
            @Name(value = "coll", description = "The list to collect duplicate values from.") List coll) {
        if (coll == null || coll.size() <= 1) {
            return Collections.emptyList();
        }

        var set = new HashSet<>(Math.max((int) (coll.size() / .75f) + 1, 16));
        Set duplicates = new LinkedHashSet<>();
        for (Object item : coll) {
            // Use the ValueUtil.of version, as arrays and lists in Cypher may differ, but are considered the *same*.
            AnyValue normalizedItem = ValueUtils.of(item);
            // If the item has already been added, then add as it is a duplicate
            if (!set.add(normalizedItem)) {
                duplicates.add(normalizedItem);
            }
        }

        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(value = "coll", description = "The list to collect duplicate values and their count from.")
                    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.computeIfAbsent(ValueUtils.of(obj), k -> new MutableInt());
            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) 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(value = "coll", description = "The list to return items and their count from.") List coll) {
        if (coll == null || coll.isEmpty()) {
            return Collections.emptyList();
        }

        // mimicking a counted bag
        Map counts = new LinkedHashMap<>(coll.size());
        List> resultList = new ArrayList<>();

        for (Object obj : coll) {
            MutableInt counter = counts.computeIfAbsent(ValueUtils.of(obj), k -> new MutableInt());
            counter.increment();
        }

        counts.forEach((o, intCounter) -> {
            int count = intCounter.intValue();
            Map entry = new LinkedHashMap<>(2);
            entry.put("item", o);
            entry.put("count", (long) 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(value = "coll", description = "The list to return items and their count from.") List coll) {
        if (coll == null) return Collections.emptyMap();
        return frequencies(coll).stream()
                .collect(Collectors.toMap(t -> Util.toPrettyPrint(t.get("item")), v -> v.get("count")));
    }

    @UserFunction("apoc.coll.occurrences")
    @Description("Returns the count of the given item in the collection.")
    public long occurrences(
            @Name(value = "coll", description = "The list to collect the count of the given value from.")
                    List coll,
            @Name(value = "item", description = "The value to count in the given list.") Object item) {
        if (coll == null || coll.isEmpty()) {
            return 0;
        }

        long occurrences = 0;

        for (Object obj : coll) {
            if (Util.valueEquals(item, 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(value = "coll", description = "The list to flatten.") List coll,
            @Name(
                            value = "recursive",
                            defaultValue = "false",
                            description = "Whether nested list items should also be flattened.")
                    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.
            To indicate that a field should be sorted according to ascending values, prefix it with a caret (^).
            It is also possible to add limits to the `LIST>` and to skip values.""")
    public List> sortMulti(
            @Name(value = "coll", description = "The list of maps to be sorted.") List> coll,
            @Name(
                            value = "orderFields",
                            defaultValue = "[]",
                            description = "The property keys to be used to sort the list of maps by.")
                    List orderFields,
            @Name(value = "limit", defaultValue = "-1", description = "The amount of results to return.") long limit,
            @Name(value = "skip", defaultValue = "0", description = "The amount to skip by.") 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(value = "coll", description = "The list to return the combinations from.") List coll,
            @Name(value = "minSelect", description = "The minimum selection size of the combination.") long minSelectIn,
            @Name(
                            value = "maxSelect",
                            defaultValue = "-1",
                            description = "The maximum selection size of the combination.")
                    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(value = "coll", description = "The list to check for duplicates.") List coll) {
        if (coll == null) return false;
        var set = new HashSet<>(Math.max((int) (coll.size() / .75f) + 1, 16));
        boolean hasntAdded;
        for (Object item : coll) {
            // Use the ValueUtil.of version, as arrays and lists in Cypher may differ, but are considered the *same*.
            hasntAdded = set.add(ValueUtils.of(item));
            // If the item has already been added, then return true as it is a duplicate
            if (!hasntAdded) {
                return false;
            }
        }
        return true;
    }

    @UserFunction("apoc.coll.dropDuplicateNeighbors")
    @Description("Removes duplicate consecutive objects in the `LIST`.")
    public List dropDuplicateNeighbors(
            @Name(value = "list", description = "The list to remove duplicate consecutive values from.")
                    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 && !Util.valueEquals(element, 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(value = "item", description = "The item to be present in the returned list.") String item,
            @Name(
                            value = "count",
                            description = "The number of times the given item should appear in the returned list.")
                    long count) {
        return Collections.nCopies((int) count, item);
    }

    @UserFunction("apoc.coll.sortText")
    @Description("Sorts the given `LIST` into ascending order.")
    public List sortText(
            @Name(value = "coll", description = "The list of strings to be sorted.") List coll,
            @Name(
                            value = "conf",
                            defaultValue = "{}",
                            description =
                                    "A map containing a single key `locale` to indicate which language to use when sorting the strings.")
                    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(value = "coll", description = "The list to create pairs from.") List values,
            @Name(value = "offset", description = "The offset to make each pair with from the given list.")
                    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;
    }

    public record PairWithOffsetListResult(@Description("The created pair.") List value) {}

    @Procedure("apoc.coll.pairWithOffset")
    @Description("Returns a `LIST` of pairs defined by the offset.")
    public Stream pairWithOffset(
            @Name(value = "coll", description = "The list to create pairs from.") List values,
            @Name(value = "offset", description = "The offset to make each pair with from the given list.")
                    long offset) {
        return pairWithOffsetFn(values, offset).stream().map(PairWithOffsetListResult::new);
    }
}