From 7b996fe00f5419fdca9784ff8254a2fa8aed44ac Mon Sep 17 00:00:00 2001 From: Paul King Date: Mon, 18 May 2026 09:08:12 +1000 Subject: [PATCH] GROOVY-12016: New GDK methods: zipWithNext and groupConsecutive --- .../groovy/runtime/ArrayGroovyMethods.java | 99 +++++ .../groovy/runtime/DefaultGroovyMethods.java | 342 ++++++++++++++++++ 2 files changed, 441 insertions(+) diff --git a/src/main/java/org/codehaus/groovy/runtime/ArrayGroovyMethods.java b/src/main/java/org/codehaus/groovy/runtime/ArrayGroovyMethods.java index 222bc9b6ba3..55099e9dd41 100644 --- a/src/main/java/org/codehaus/groovy/runtime/ArrayGroovyMethods.java +++ b/src/main/java/org/codehaus/groovy/runtime/ArrayGroovyMethods.java @@ -77,6 +77,8 @@ import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; +import java.util.function.BiFunction; +import java.util.function.BiPredicate; import java.util.function.Consumer; import java.util.function.DoubleConsumer; import java.util.function.DoublePredicate; @@ -11141,6 +11143,103 @@ public static Iterator> zipping(long[] self, long[] other) { return DefaultGroovyMethods.zip(new LongArrayIterator(self), new LongArrayIterator(other)); } + //-------------------------------------------------------------------------- + // groupConsecutive + + /** + * Splits this array into a list of sublists, each a maximal run of + * adjacent elements considered equal (number-aware coercion). + *
+     * Integer[] nums = [1, 1, 2, 2, 2, 3, 1, 1]
+     * assert nums.groupConsecutive() == [[1, 1], [2, 2, 2], [3], [1, 1]]
+     * 
+ * + * @param self an array + * @return a list of the runs of adjacent equal elements + * @see DefaultGroovyMethods#groupConsecutive(Iterable) + * @since 6.0.0 + */ + public static List> groupConsecutive(T[] self) { + return DefaultGroovyMethods.groupConsecutive(new ArrayIterable<>(self)); + } + + /** + * Splits this array into runs of adjacent elements whose key, as computed + * by the given function, is equal (number-aware coercion). + *
+     * String[] fruit = ['apple', 'avocado', 'banana', 'cherry', 'citrus', 'date']
+     * assert fruit.groupConsecutive{ it[0] } == [['apple', 'avocado'], ['banana'], ['cherry', 'citrus'], ['date']]
+     * 
+ * + * @param self an array + * @param keyFn extracts the grouping key for each element + * @return a list of the runs of adjacent key-equal elements + * @see DefaultGroovyMethods#groupConsecutive(Iterable, java.util.function.Function) + * @since 6.0.0 + */ + public static List> groupConsecutive(T[] self, Function keyFn) { + return DefaultGroovyMethods.groupConsecutive(new ArrayIterable<>(self), keyFn); + } + + /** + * Splits this array into runs where the given predicate, applied to the + * current run's previous element and the next element, holds. Use this to + * opt out of the default number-aware equality. + *
+     * Number[] ns = [1, 1L, 1.0, 2, 2]
+     * assert ns.groupConsecutive{ a, b {@code ->} a.equals(b) } == [[1], [1L], [1.0], [2, 2]]
+     * 
+ * + * @param self an array + * @param sameRun tests whether the next element continues the current run + * @return a list of the runs + * @see DefaultGroovyMethods#groupConsecutive(Iterable, java.util.function.BiPredicate) + * @since 6.0.0 + */ + public static List> groupConsecutive(T[] self, BiPredicate sameRun) { + return DefaultGroovyMethods.groupConsecutive(new ArrayIterable<>(self), sameRun); + } + + //-------------------------------------------------------------------------- + // zipWithNext + + /** + * Returns a list of all the successive adjacent pairs from this array + * (a sliding window of size 2, step 1). + *
+     * Integer[] nums = [1, 2, 3, 4]
+     * assert nums.zipWithNext() == [[1, 2], [2, 3], [3, 4]]
+     * 
+ * + * @param self an array + * @return a list of the adjacent pairs + * @see DefaultGroovyMethods#zipWithNext(Iterable) + * @since 6.0.0 + */ + public static List> zipWithNext(T[] self) { + return DefaultGroovyMethods.zipWithNext(new ArrayIterable<>(self)); + } + + /** + * Applies the combiner to each successive adjacent pair from this array, + * returning the list of results. + *
+     * Integer[] nums = [1, 2, 3, 4]
+     * assert nums.zipWithNext{ a, b {@code ->} b - a } == [1, 1, 1]
+     * String[] letters = ['a', 'b', 'c', 'd']
+     * assert letters.zipWithNext{ a, b {@code ->} a + b } == ['ab', 'bc', 'cd']
+     * 
+ * + * @param self an array + * @param combiner a function applied to each adjacent pair + * @return a list of the combined adjacent pairs + * @see DefaultGroovyMethods#zipWithNext(Iterable, java.util.function.BiFunction) + * @since 6.0.0 + */ + public static List zipWithNext(T[] self, BiFunction combiner) { + return DefaultGroovyMethods.zipWithNext(new ArrayIterable<>(self), combiner); + } + //-------------------------------------------------------------------------- /** diff --git a/src/main/java/org/codehaus/groovy/runtime/DefaultGroovyMethods.java b/src/main/java/org/codehaus/groovy/runtime/DefaultGroovyMethods.java index c37dc189694..e5bf058b9e0 100644 --- a/src/main/java/org/codehaus/groovy/runtime/DefaultGroovyMethods.java +++ b/src/main/java/org/codehaus/groovy/runtime/DefaultGroovyMethods.java @@ -156,6 +156,8 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.BiFunction; +import java.util.function.BiPredicate; import java.util.function.Function; import java.util.function.Predicate; @@ -18463,6 +18465,346 @@ public void remove() { } } + //-------------------------------------------------------------------------- + // zipWithNext + + /** + * Returns a list of all the successive adjacent pairs from this Iterable + * (a sliding window of size 2, step 1). The result has one fewer element + * than the input; an empty or single-element Iterable yields an empty list. + * Each pair is a {@link Tuple2}, which is also a {@code List}. + *

+ * Example: + *

+     * assert [1, 2, 3, 4].zipWithNext() == [[1, 2], [2, 3], [3, 4]]
+     * assert [].zipWithNext() == []
+     * assert [42].zipWithNext() == []
+     * 
+ * + * @param self an Iterable + * @return a list of the adjacent pairs + * @since 6.0.0 + */ + public static List> zipWithNext(Iterable self) { + List> result = new ArrayList<>(); + addAll(result, zipWithNext(self.iterator())); + return result; + } + + /** + * Applies the combiner to each successive adjacent pair from this Iterable, + * returning the list of results. The result has one fewer element than the + * input; an empty or single-element Iterable yields an empty list. + *

+ * Example: + *

+     * assert [1, 2, 3, 4].zipWithNext{ a, b {@code ->} b - a } == [1, 1, 1]
+     * assert [3, 1, 4, 1, 5].zipWithNext{ a, b {@code ->} a {@code <=} b } == [false, true, false, true]
+     * assert 'abcd'.toList().zipWithNext{ a, b {@code ->} a + b } == ['ab', 'bc', 'cd']
+     * 
+ * + * @param self an Iterable + * @param combiner a function applied to each adjacent pair + * @return a list of the combined adjacent pairs + * @since 6.0.0 + */ + public static List zipWithNext(Iterable self, BiFunction combiner) { + List result = new ArrayList<>(); + addAll(result, zipWithNext(self.iterator(), combiner)); + return result; + } + + /** + * Returns a (lazy) iterator of all the successive adjacent pairs from this Iterator. + *

+ * Example: + *

+     * assert [1, 2, 3].iterator().zipWithNext().toList() == [[1, 2], [2, 3]]
+     * 
+ * + * @param self an Iterator + * @return an iterator of the adjacent pairs + * @since 6.0.0 + */ + public static Iterator> zipWithNext(Iterator self) { + return new ZipWithNextIterator<>(self); + } + + /** + * Returns a (lazy) iterator applying the combiner to each successive + * adjacent pair from this Iterator. + * + * @param self an Iterator + * @param combiner a function applied to each adjacent pair + * @return an iterator of the combined adjacent pairs + * @since 6.0.0 + */ + public static Iterator zipWithNext(Iterator self, BiFunction combiner) { + Iterator> pairs = new ZipWithNextIterator<>(self); + return new Iterator() { + @Override + public boolean hasNext() { + return pairs.hasNext(); + } + + @Override + public R next() { + Tuple2 pair = pairs.next(); + return combiner.apply(pair.getV1(), pair.getV2()); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + + private static final class ZipWithNextIterator implements Iterator> { + private final Iterator delegate; + private T prev; + private boolean hasPrev; + + private ZipWithNextIterator(Iterator delegate) { + this.delegate = delegate; + if (delegate.hasNext()) { + prev = delegate.next(); + hasPrev = true; + } + } + + @Override + public boolean hasNext() { + return hasPrev && delegate.hasNext(); + } + + @Override + public Tuple2 next() { + if (!hasNext()) throw new NoSuchElementException(); + T curr = delegate.next(); + Tuple2 pair = new Tuple2<>(prev, curr); + prev = curr; + return pair; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + } + + //-------------------------------------------------------------------------- + // groupConsecutive + + /** + * Splits this Iterable into a list of sublists, each a maximal run of + * adjacent elements considered equal. Element order is preserved and the + * same value may appear in more than one run (unlike {@link #groupBy(Iterable, Closure)}, + * which builds a map). Equality uses Groovy's number-aware coercion, + * consistent with {@link #unique(Collection)}. + *

+ * Example: + *

+     * assert [1, 1, 2, 2, 2, 3, 1, 1].groupConsecutive() == [[1, 1], [2, 2, 2], [3], [1, 1]]
+     * assert [1, 1L, 1.0, 2, 2].groupConsecutive() == [[1, 1L, 1.0], [2, 2]]
+     * assert [].groupConsecutive() == []
+     * assert [7].groupConsecutive() == [[7]]
+     * // run-length encoding; "dedupe consecutive" is just the run heads
+     * assert 'aaabbbcccd'.toList().groupConsecutive().collect{ run {@code ->} [run[0], run.size()] } == [['a', 3], ['b', 3], ['c', 3], ['d', 1]]
+     * assert 'aaabbbcccd'.toList().groupConsecutive()*.first().join() == 'abcd'
+     * 
+ * + * @param self an Iterable + * @return a list of the runs of adjacent equal elements + * @since 6.0.0 + */ + public static List> groupConsecutive(Iterable self) { + List> result = new ArrayList<>(); + addAll(result, groupConsecutive(self.iterator())); + return result; + } + + /** + * Returns a (lazy) iterator over the runs of adjacent equal elements + * (number-aware coercion). + * + * @param self an Iterator + * @return an iterator over the runs of adjacent equal elements + * @since 6.0.0 + */ + public static Iterator> groupConsecutive(Iterator self) { + return new GroupConsecutiveIterator<>(self, (a, b) -> coercedEquals(a, b)); + } + + /** + * Splits this Iterable into runs of adjacent elements whose key, as + * computed by the given function, is equal (number-aware coercion). + * {@code keyFn} is evaluated exactly once per element. + *

+ * Example: + *

+     * assert ['apple', 'avocado', 'banana', 'cherry', 'citrus', 'date'].groupConsecutive{ it[0] } == [['apple', 'avocado'], ['banana'], ['cherry', 'citrus'], ['date']]
+     * assert [1, 3, 5, 2, 4, 7, 9].groupConsecutive{ it % 2 } == [[1, 3, 5], [2, 4], [7, 9]]
+     * 
+ * + * @param self an Iterable + * @param keyFn extracts the grouping key for each element + * @return a list of the runs of adjacent key-equal elements + * @since 6.0.0 + */ + public static List> groupConsecutive(Iterable self, Function keyFn) { + List> result = new ArrayList<>(); + addAll(result, groupConsecutive(self.iterator(), keyFn)); + return result; + } + + /** + * Returns a (lazy) iterator over the runs of adjacent key-equal elements. + * {@code keyFn} is evaluated exactly once per element. + * + * @param self an Iterator + * @param keyFn extracts the grouping key for each element + * @return an iterator over the runs of adjacent key-equal elements + * @since 6.0.0 + */ + public static Iterator> groupConsecutive(Iterator self, Function keyFn) { + return new GroupConsecutiveByKeyIterator<>(self, keyFn); + } + + /** + * Splits this Iterable into runs where the given predicate, applied to the + * current run's previous element and the next element, holds. Use this to + * opt out of the default number-aware equality, e.g. + * {@code { a, b -> Objects.equals(a, b) }} for strict equality or + * {@code { a, b -> (a <=> b) == 0 }} for natural-order equivalence. + *

+ * Example: + *

+     * assert [1, 1L, 1.0, 2, 2].groupConsecutive{ a, b {@code ->} a.equals(b) } == [[1], [1L], [1.0], [2, 2]]
+     * assert [1.0G, 1.00G, 2.0G].groupConsecutive{ a, b {@code ->} (a {@code <=>} b) == 0 } == [[1.0, 1.00], [2.0]]
+     * 
+ * + * @param self an Iterable + * @param sameRun tests whether the next element continues the current run + * @return a list of the runs + * @since 6.0.0 + */ + public static List> groupConsecutive(Iterable self, BiPredicate sameRun) { + List> result = new ArrayList<>(); + addAll(result, groupConsecutive(self.iterator(), sameRun)); + return result; + } + + /** + * Returns a (lazy) iterator over the runs determined by the given predicate. + * + * @param self an Iterator + * @param sameRun tests whether the next element continues the current run + * @return an iterator over the runs + * @since 6.0.0 + */ + public static Iterator> groupConsecutive(Iterator self, BiPredicate sameRun) { + return new GroupConsecutiveIterator<>(self, sameRun); + } + + private static final class GroupConsecutiveIterator implements Iterator> { + private final Iterator delegate; + private final BiPredicate sameRun; + private T pending; + private boolean hasPending; + + private GroupConsecutiveIterator(Iterator delegate, BiPredicate sameRun) { + this.delegate = delegate; + this.sameRun = sameRun; + if (delegate.hasNext()) { + pending = delegate.next(); + hasPending = true; + } + } + + @Override + public boolean hasNext() { + return hasPending; + } + + @Override + public List next() { + if (!hasPending) throw new NoSuchElementException(); + List run = new ArrayList<>(); + run.add(pending); + T prev = pending; + hasPending = false; + while (delegate.hasNext()) { + T curr = delegate.next(); + if (sameRun.test(prev, curr)) { + run.add(curr); + prev = curr; + } else { + pending = curr; + hasPending = true; + break; + } + } + return run; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + } + + private static final class GroupConsecutiveByKeyIterator implements Iterator> { + private final Iterator delegate; + private final Function keyFn; + private T pending; + private K pendingKey; + private boolean hasPending; + + private GroupConsecutiveByKeyIterator(Iterator delegate, Function keyFn) { + this.delegate = delegate; + this.keyFn = keyFn; + if (delegate.hasNext()) { + pending = delegate.next(); + pendingKey = keyFn.apply(pending); // key computed once for the first element + hasPending = true; + } + } + + @Override + public boolean hasNext() { + return hasPending; + } + + @Override + public List next() { + if (!hasPending) throw new NoSuchElementException(); + List run = new ArrayList<>(); + run.add(pending); + K prevKey = pendingKey; + hasPending = false; + while (delegate.hasNext()) { + T curr = delegate.next(); + K currKey = keyFn.apply(curr); // each element's key evaluated exactly once + if (coercedEquals(prevKey, currKey)) { + run.add(curr); + prevKey = currKey; // advance, mirroring the adjacent-pair semantics + } else { + pending = curr; + pendingKey = currKey; // carried to the next run, not recomputed + hasPending = true; + break; + } + } + return run; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + } + //-------------------------------------------------------------------------- // withTraits