This is an automated email from the ASF dual-hosted git repository.

paulk-asert pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/groovy.git


The following commit(s) were added to refs/heads/master by this push:
     new f1f1efa842 GROOVY-12016: New GDK methods: zipWithNext and 
groupConsecutive
f1f1efa842 is described below

commit f1f1efa84271584bf47fe425763f9512d2e1daf6
Author: Paul King <[email protected]>
AuthorDate: Mon May 18 09:08:12 2026 +1000

    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 222bc9b6ba..55099e9dd4 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.Random;
 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 class ArrayGroovyMethods extends 
DefaultGroovyMethodsSupport {
         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).
+     * <pre class="language-groovy groovyTestCase">
+     * Integer[] nums = [1, 1, 2, 2, 2, 3, 1, 1]
+     * assert nums.groupConsecutive() == [[1, 1], [2, 2, 2], [3], [1, 1]]
+     * </pre>
+     *
+     * @param self an array
+     * @return a list of the runs of adjacent equal elements
+     * @see DefaultGroovyMethods#groupConsecutive(Iterable)
+     * @since 6.0.0
+     */
+    public static <T> List<List<T>> 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).
+     * <pre class="language-groovy groovyTestCase">
+     * String[] fruit = ['apple', 'avocado', 'banana', 'cherry', 'citrus', 
'date']
+     * assert fruit.groupConsecutive{ it[0] } == [['apple', 'avocado'], 
['banana'], ['cherry', 'citrus'], ['date']]
+     * </pre>
+     *
+     * @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 <T, K> List<List<T>> groupConsecutive(T[] self, Function<? 
super T, ? extends K> 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.
+     * <pre class="language-groovy groovyTestCase">
+     * Number[] ns = [1, 1L, 1.0, 2, 2]
+     * assert ns.groupConsecutive{ a, b {@code ->} a.equals(b) } == [[1], 
[1L], [1.0], [2, 2]]
+     * </pre>
+     *
+     * @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 <T> List<List<T>> groupConsecutive(T[] self, BiPredicate<? 
super T, ? super T> 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).
+     * <pre class="language-groovy groovyTestCase">
+     * Integer[] nums = [1, 2, 3, 4]
+     * assert nums.zipWithNext() == [[1, 2], [2, 3], [3, 4]]
+     * </pre>
+     *
+     * @param self an array
+     * @return a list of the adjacent pairs
+     * @see DefaultGroovyMethods#zipWithNext(Iterable)
+     * @since 6.0.0
+     */
+    public static <T> List<Tuple2<T, T>> 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.
+     * <pre class="language-groovy groovyTestCase">
+     * 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']
+     * </pre>
+     *
+     * @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 <T, R> List<R> zipWithNext(T[] self, BiFunction<? super T, ? 
super T, ? extends R> 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 c37dc18969..e5bf058b9e 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.Callable;
 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 class DefaultGroovyMethods extends 
DefaultGroovyMethodsSupport {
         }
     }
 
+    
//--------------------------------------------------------------------------
+    // 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}.
+     * <p>
+     * Example:
+     * <pre class="language-groovy groovyTestCase">
+     * assert [1, 2, 3, 4].zipWithNext() == [[1, 2], [2, 3], [3, 4]]
+     * assert [].zipWithNext() == []
+     * assert [42].zipWithNext() == []
+     * </pre>
+     *
+     * @param self an Iterable
+     * @return a list of the adjacent pairs
+     * @since 6.0.0
+     */
+    public static <T> List<Tuple2<T, T>> zipWithNext(Iterable<T> self) {
+        List<Tuple2<T, T>> 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.
+     * <p>
+     * Example:
+     * <pre class="language-groovy groovyTestCase">
+     * 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']
+     * </pre>
+     *
+     * @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 <T, R> List<R> zipWithNext(Iterable<T> self, BiFunction<? 
super T, ? super T, ? extends R> combiner) {
+        List<R> result = new ArrayList<>();
+        addAll(result, zipWithNext(self.iterator(), combiner));
+        return result;
+    }
+
+    /**
+     * Returns a (lazy) iterator of all the successive adjacent pairs from 
this Iterator.
+     * <p>
+     * Example:
+     * <pre class="language-groovy groovyTestCase">
+     * assert [1, 2, 3].iterator().zipWithNext().toList() == [[1, 2], [2, 3]]
+     * </pre>
+     *
+     * @param self an Iterator
+     * @return an iterator of the adjacent pairs
+     * @since 6.0.0
+     */
+    public static <T> Iterator<Tuple2<T, T>> zipWithNext(Iterator<T> 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 <T, R> Iterator<R> zipWithNext(Iterator<T> self, 
BiFunction<? super T, ? super T, ? extends R> combiner) {
+        Iterator<Tuple2<T, T>> pairs = new ZipWithNextIterator<>(self);
+        return new Iterator<R>() {
+            @Override
+            public boolean hasNext() {
+                return pairs.hasNext();
+            }
+
+            @Override
+            public R next() {
+                Tuple2<T, T> pair = pairs.next();
+                return combiner.apply(pair.getV1(), pair.getV2());
+            }
+
+            @Override
+            public void remove() {
+                throw new UnsupportedOperationException();
+            }
+        };
+    }
+
+    private static final class ZipWithNextIterator<T> implements 
Iterator<Tuple2<T, T>> {
+        private final Iterator<T> delegate;
+        private T prev;
+        private boolean hasPrev;
+
+        private ZipWithNextIterator(Iterator<T> delegate) {
+            this.delegate = delegate;
+            if (delegate.hasNext()) {
+                prev = delegate.next();
+                hasPrev = true;
+            }
+        }
+
+        @Override
+        public boolean hasNext() {
+            return hasPrev && delegate.hasNext();
+        }
+
+        @Override
+        public Tuple2<T, T> next() {
+            if (!hasNext()) throw new NoSuchElementException();
+            T curr = delegate.next();
+            Tuple2<T, T> 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)}.
+     * <p>
+     * Example:
+     * <pre class="language-groovy groovyTestCase">
+     * 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'
+     * </pre>
+     *
+     * @param self an Iterable
+     * @return a list of the runs of adjacent equal elements
+     * @since 6.0.0
+     */
+    public static <T> List<List<T>> groupConsecutive(Iterable<T> self) {
+        List<List<T>> 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 <T> Iterator<List<T>> groupConsecutive(Iterator<T> 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.
+     * <p>
+     * Example:
+     * <pre class="language-groovy groovyTestCase">
+     * 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]]
+     * </pre>
+     *
+     * @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 <T, K> List<List<T>> groupConsecutive(Iterable<T> self, 
Function<? super T, ? extends K> keyFn) {
+        List<List<T>> 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 <T, K> Iterator<List<T>> groupConsecutive(Iterator<T> self, 
Function<? super T, ? extends K> 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.
+     * <p>
+     * Example:
+     * <pre class="language-groovy groovyTestCase">
+     * 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]]
+     * </pre>
+     *
+     * @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 <T> List<List<T>> groupConsecutive(Iterable<T> self, 
BiPredicate<? super T, ? super T> sameRun) {
+        List<List<T>> 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 <T> Iterator<List<T>> groupConsecutive(Iterator<T> self, 
BiPredicate<? super T, ? super T> sameRun) {
+        return new GroupConsecutiveIterator<>(self, sameRun);
+    }
+
+    private static final class GroupConsecutiveIterator<T> implements 
Iterator<List<T>> {
+        private final Iterator<T> delegate;
+        private final BiPredicate<? super T, ? super T> sameRun;
+        private T pending;
+        private boolean hasPending;
+
+        private GroupConsecutiveIterator(Iterator<T> delegate, BiPredicate<? 
super T, ? super T> sameRun) {
+            this.delegate = delegate;
+            this.sameRun = sameRun;
+            if (delegate.hasNext()) {
+                pending = delegate.next();
+                hasPending = true;
+            }
+        }
+
+        @Override
+        public boolean hasNext() {
+            return hasPending;
+        }
+
+        @Override
+        public List<T> next() {
+            if (!hasPending) throw new NoSuchElementException();
+            List<T> 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<T, K> implements 
Iterator<List<T>> {
+        private final Iterator<T> delegate;
+        private final Function<? super T, ? extends K> keyFn;
+        private T pending;
+        private K pendingKey;
+        private boolean hasPending;
+
+        private GroupConsecutiveByKeyIterator(Iterator<T> delegate, Function<? 
super T, ? extends K> 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<T> next() {
+            if (!hasPending) throw new NoSuchElementException();
+            List<T> 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
 

Reply via email to