You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by pa...@apache.org on 2026/06/01 22:54:09 UTC
(groovy) branch master updated: GROOVY-12054: A preliminary set of "fat-free" DGM methods (cont'd)
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 334bc2f612 GROOVY-12054: A preliminary set of "fat-free" DGM methods (cont'd)
334bc2f612 is described below
commit 334bc2f612367b5ba2343f391c11bd13d194abdc
Author: Paul King <pa...@asert.com.au>
AuthorDate: Tue Jun 2 08:53:57 2026 +1000
GROOVY-12054: A preliminary set of "fat-free" DGM methods (cont'd)
---
.../groovy/runtime/DefaultGroovyMethods.java | 349 +++++++++++++++++++++
.../org/apache/groovy/util/LambdasTest.groovy | 94 ++++++
2 files changed, 443 insertions(+)
diff --git a/src/main/java/org/codehaus/groovy/runtime/DefaultGroovyMethods.java b/src/main/java/org/codehaus/groovy/runtime/DefaultGroovyMethods.java
index 78cc0e6cd4..f3225e1674 100644
--- a/src/main/java/org/codehaus/groovy/runtime/DefaultGroovyMethods.java
+++ b/src/main/java/org/codehaus/groovy/runtime/DefaultGroovyMethods.java
@@ -667,6 +667,35 @@ public class DefaultGroovyMethods extends DefaultGroovyMethodsSupport {
return false;
}
+ /**
+ * Iterates over the contents of an iterable, and checks whether the given
+ * predicate is valid for at least one element when called with that element
+ * and the supplied <code>param</code>. A "fat-free" variant of
+ * {@link #any(Iterable, Closure)} accepting a {@link BiPredicate} together with an
+ * additional argument, mirroring {@link #find(Iterable, BiPredicate, Object)}.
+ * <pre class="language-groovy groovyTestCase">
+ * import java.util.function.BiPredicate
+ * BiPredicate<Integer, Integer> divisibleBy = (n, d) -> n % d == 0
+ * assert [1, 2, 3].any(divisibleBy, 2)
+ * assert ![1, 3, 5].any(divisibleBy, 2)
+ * </pre>
+ *
+ * @param self the iterable over which we iterate
+ * @param predicate the predicate used for matching
+ * @param param the additional argument passed to the predicate
+ * @return true if any element of the iterable matches the predicate
+ * @since 6.0.0
+ */
+ @Incubating
+ public static <T, P> boolean any(Iterable<T> self, BiPredicate<? super T, ? super P> predicate, P param) {
+ for (T item : self) {
+ if (predicate.test(item, param)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
/**
* Iterates over the entries of a map, and checks whether a predicate is
* valid for at least one entry. If the
@@ -2481,6 +2510,53 @@ public class DefaultGroovyMethods extends DefaultGroovyMethodsSupport {
return new CollectIterator<>(self, transform);
}
+ /**
+ * Returns an iterator of transformed values from the source iterator using the
+ * <code>transform</code> function. A "fat-free" variant of
+ * {@link #collecting(Iterator, Closure)} accepting a {@link Function}.
+ * Since the result is lazy, it is suitable for use with potentially infinite iterators.
+ *
+ * <pre class="language-groovy groovyTestCase">
+ * import java.util.function.Function
+ * Function<Integer, Integer> next = n -> n + 1
+ * assert [1, 2, 3].repeat().collecting(next).take(6).toList() == [2, 3, 4, 2, 3, 4]
+ * </pre>
+ *
+ * @param self an Iterator
+ * @param transform the function used to transform each element
+ * @return an Iterator for the transformed values
+ * @since 6.0.0
+ */
+ public static <E, T> Iterator<T> collecting(Iterator<E> self, Function<? super E, ? extends T> transform) {
+ return new CollectingFunctionIterator<>(self, transform);
+ }
+
+ /**
+ * Returns an iterator of transformed values from the source iterator using the given
+ * two-argument <code>transform</code>, with the supplied <code>param</code> fixed as the
+ * second argument. Combines {@link #collecting(Iterator, Function)} with the right-currying
+ * performed by {@link org.apache.groovy.util.Lambdas#curryWith(BiFunction, Object)}, so that
+ * {@code iter.collecting(transform, param)} is equivalent to
+ * {@code iter.collecting(curryWith(transform, param))}.
+ * Since the result is lazy, it is suitable for use with potentially infinite iterators.
+ *
+ * <pre class="language-groovy groovyTestCase">
+ * import java.util.function.BiFunction
+ * BiFunction<Integer, Integer, Integer> add = (n, d) -> n + d
+ * assert [1, 2, 3].repeat().collecting(add, 10).take(6).toList() == [11, 12, 13, 11, 12, 13]
+ * </pre>
+ *
+ * @param self an Iterator
+ * @param transform the two-argument function used to transform each element
+ * @param param the value to fix as the function's second argument
+ * @return an Iterator for the transformed values
+ * @since 6.0.0
+ */
+ @Incubating
+ public static <E, P, T> Iterator<T> collecting(Iterator<E> self, BiFunction<? super E, ? super P, ? extends T> transform, P param) {
+ return collecting(self, item -> transform.apply(item, param));
+ }
+
private static final class CollectIterator<E, T> implements Iterator<T> {
private final Iterator<E> source;
private final Closure<T> transform;
@@ -2506,6 +2582,31 @@ public class DefaultGroovyMethods extends DefaultGroovyMethodsSupport {
}
}
+ private static final class CollectingFunctionIterator<E, T> implements Iterator<T> {
+ private final Iterator<E> source;
+ private final Function<? super E, ? extends T> transform;
+
+ private CollectingFunctionIterator(Iterator<E> source, Function<? super E, ? extends T> transform) {
+ this.source = source;
+ this.transform = transform;
+ }
+
+ @Override
+ public boolean hasNext() {
+ return source.hasNext();
+ }
+
+ @Override
+ public T next() {
+ return transform.apply(source.next());
+ }
+
+ @Override
+ public void remove() {
+ throw new UnsupportedOperationException();
+ }
+ }
+
/**
* Iterates through this Iterator transforming each item into a new value using the <code>transform</code> closure
* and adding it to the supplied <code>collector</code>.
@@ -3779,6 +3880,36 @@ public class DefaultGroovyMethods extends DefaultGroovyMethodsSupport {
return count;
}
+ /**
+ * Counts the number of occurrences which satisfy the given predicate from the
+ * items within this Iterator.
+ * A "fat-free" variant of {@link #count(Iterator, Closure)} accepting a {@link Predicate}.
+ * The iterator will become exhausted of elements after determining the count value.
+ * <p>
+ * Example usage:
+ * <pre class="language-groovy groovyTestCase">
+ * import java.util.function.Predicate
+ * Predicate<Integer> isEven = n -> n % 2 == 0
+ * assert [2, 4, 2, 1, 3, 5, 2, 4, 3].iterator().count(isEven) == 5
+ * </pre>
+ *
+ * @param self the Iterator from which we count the number of matching occurrences
+ * @param predicate a predicate condition
+ * @return the number of occurrences
+ * @since 6.0.0
+ */
+ public static <T> Number count(Iterator<T> self, Predicate<? super T> predicate) {
+ long answer = 0;
+ while (self.hasNext()) {
+ if (predicate.test(self.next())) {
+ answer++;
+ }
+ }
+ // for b/c with Java return an int if we can
+ if (answer <= Integer.MAX_VALUE) return (int) answer;
+ return answer;
+ }
+
/**
* Counts the number of occurrences of the given value inside this Iterable.
* Comparison is done using Groovy's == operator (using
@@ -3815,6 +3946,52 @@ public class DefaultGroovyMethods extends DefaultGroovyMethodsSupport {
return count(self.iterator(), closure);
}
+ /**
+ * Counts the number of occurrences which satisfy the given predicate from inside this Iterable.
+ * A "fat-free" variant of {@link #count(Iterable, Closure)} accepting a {@link Predicate}.
+ * <p>
+ * Example usage:
+ * <pre class="language-groovy groovyTestCase">
+ * import java.util.function.Predicate
+ * Predicate<Integer> isEven = n -> n % 2 == 0
+ * assert [2, 4, 2, 1, 3, 5, 2, 4, 3].count(isEven) == 5
+ * </pre>
+ *
+ * @param self the Iterable within which we count the number of occurrences
+ * @param predicate a predicate condition
+ * @return the number of occurrences
+ * @since 6.0.0
+ */
+ public static <T> Number count(Iterable<T> self, Predicate<? super T> predicate) {
+ return count(self.iterator(), predicate);
+ }
+
+ /**
+ * Counts the number of occurrences which satisfy the given two-argument condition, with the
+ * supplied <code>param</code> fixed as the second argument. Combines
+ * {@link #count(Iterable, Predicate)} with the right-currying performed by
+ * {@link org.apache.groovy.util.Lambdas#curryWith(BiPredicate, Object)}, so that
+ * {@code list.count(condition, param)} is equivalent to
+ * {@code list.count(curryWith(condition, param))}.
+ * <p>
+ * Example usage:
+ * <pre class="language-groovy groovyTestCase">
+ * import java.util.function.BiPredicate
+ * BiPredicate<Integer, Integer> divisibleBy = (n, d) -> n % d == 0
+ * assert [2, 4, 2, 1, 3, 5, 2, 4, 3].count(divisibleBy, 2) == 5
+ * </pre>
+ *
+ * @param self the Iterable within which we count the number of occurrences
+ * @param predicate a two-argument predicate condition
+ * @param param the value to fix as the predicate's second argument
+ * @return the number of occurrences
+ * @since 6.0.0
+ */
+ @Incubating
+ public static <T, P> Number count(Iterable<T> self, BiPredicate<? super T, ? super P> predicate, P param) {
+ return count(self, item -> predicate.test(item, param));
+ }
+
/**
* Counts the number of occurrences which satisfy the given closure from inside this Iterable.
* <p>
@@ -5544,6 +5721,35 @@ public class DefaultGroovyMethods extends DefaultGroovyMethodsSupport {
return true;
}
+ /**
+ * Used to determine if the given predicate is valid (i.e. returns
+ * <code>true</code> for all items in this iterable) when called with each
+ * element and the supplied <code>param</code>. A "fat-free" variant of
+ * {@link #every(Iterable, Closure)} accepting a {@link BiPredicate} together with an
+ * additional argument, mirroring {@link #findAll(Iterable, BiPredicate, Object)}.
+ * <pre class="language-groovy groovyTestCase">
+ * import java.util.function.BiPredicate
+ * BiPredicate<Integer, Integer> divisibleBy = (n, d) -> n % d == 0
+ * assert [2, 4, 6].every(divisibleBy, 2)
+ * assert ![2, 3, 4].every(divisibleBy, 2)
+ * </pre>
+ *
+ * @param self the iterable over which we iterate
+ * @param predicate the predicate used for matching
+ * @param param the additional argument passed to the predicate
+ * @return true if every element of the iterable matches the predicate
+ * @since 6.0.0
+ */
+ @Incubating
+ public static <T, P> boolean every(Iterable<T> self, BiPredicate<? super T, ? super P> predicate, P param) {
+ for (T item : self) {
+ if (!predicate.test(item, param)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
/**
* Iterates over the entries of a map, and checks whether a predicate is
* valid for all entries. If the
@@ -5714,6 +5920,58 @@ public class DefaultGroovyMethods extends DefaultGroovyMethodsSupport {
return null;
}
+ /**
+ * Finds the first value matching the given predicate.
+ * A "fat-free" variant of {@link #find(Collection, Closure)} accepting a {@link Predicate},
+ * operating on an Iterator. The iterator is consumed up to and including the first
+ * matching element.
+ * <pre class="language-groovy groovyTestCase">
+ * import java.util.function.Predicate
+ * Predicate<Integer> greaterThanOne = n -> n > 1
+ * assert [1, 2, 3].iterator().find(greaterThanOne) == 2
+ * assert [1, 2, 3].iterator().find(n -> n > 3) == null
+ * </pre>
+ *
+ * @param self an Iterator
+ * @param condition the predicate condition
+ * @return the first element matching the condition, in iteration order, or null if none matches
+ * @since 6.0.0
+ */
+ public static <T> T find(Iterator<T> self, Predicate<? super T> condition) {
+ while (self.hasNext()) {
+ T value = self.next();
+ if (condition.test(value)) {
+ return value;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Finds the first value matching the given two-argument condition, with the supplied
+ * <code>param</code> fixed as the second argument. This is the Iterator counterpart of
+ * {@link #find(Iterable, BiPredicate, Object)}, so that
+ * {@code iter.find(condition, param)} is equivalent to
+ * {@code iter.find(curryWith(condition, param))}. The iterator is consumed up to and
+ * including the first matching element.
+ * <pre class="language-groovy groovyTestCase">
+ * import java.util.function.BiPredicate
+ * BiPredicate<Integer, Integer> divisibleBy = (n, d) -> n % d == 0
+ * assert [1, 2, 3, 4, 5, 6].iterator().find(divisibleBy, 3) == 3
+ * assert [1, 2, 3, 4, 5, 6].iterator().find(divisibleBy, 7) == null
+ * </pre>
+ *
+ * @param self an Iterator
+ * @param condition a two-argument predicate condition
+ * @param param the value to fix as the predicate's second argument
+ * @return the first element matching the condition, in iteration order, or null if none matches
+ * @since 6.0.0
+ */
+ @Incubating
+ public static <T, P> T find(Iterator<T> self, BiPredicate<? super T, ? super P> condition, P param) {
+ return find(self, item -> condition.test(item, param));
+ }
+
/**
* Finds the first item matching the IDENTITY Closure (i.e. matching Groovy truth).
* <p>
@@ -6087,6 +6345,52 @@ public class DefaultGroovyMethods extends DefaultGroovyMethodsSupport {
return new FindAllIterator<>(self, transform);
}
+ /**
+ * Lazily finds all items matching the predicate condition. A "fat-free" variant of
+ * {@link #findingAll(Iterator, Closure)} accepting a {@link Predicate}.
+ * Since the result is lazy, it is suitable for use with potentially infinite iterators.
+ *
+ * <pre class="language-groovy groovyTestCase">
+ * import java.util.function.Predicate
+ * Predicate<Integer> isEven = n -> n % 2 == 0
+ * assert [1, 2, 3].repeat().findingAll(isEven).take(4).toList() == [2, 2, 2, 2]
+ * </pre>
+ *
+ * @param self a source Iterator
+ * @param condition the predicate used to select elements
+ * @return an Iterator returning the selected elements
+ * @since 6.0.0
+ */
+ public static <T> Iterator<T> findingAll(Iterator<T> self, Predicate<? super T> condition) {
+ return new FindingAllPredicateIterator<>(self, condition);
+ }
+
+ /**
+ * Lazily finds all items matching the given two-argument condition, with the supplied
+ * <code>param</code> fixed as the second argument. Combines
+ * {@link #findingAll(Iterator, Predicate)} with the right-currying performed by
+ * {@link org.apache.groovy.util.Lambdas#curryWith(BiPredicate, Object)}, so that
+ * {@code iter.findingAll(condition, param)} is equivalent to
+ * {@code iter.findingAll(curryWith(condition, param))}.
+ * Since the result is lazy, it is suitable for use with potentially infinite iterators.
+ *
+ * <pre class="language-groovy groovyTestCase">
+ * import java.util.function.BiPredicate
+ * BiPredicate<Integer, Integer> divisibleBy = (n, d) -> n % d == 0
+ * assert [1, 2, 3, 4, 5, 6].repeat().findingAll(divisibleBy, 3).take(4).toList() == [3, 6, 3, 6]
+ * </pre>
+ *
+ * @param self a source Iterator
+ * @param condition a two-argument predicate used to select elements
+ * @param param the value to fix as the predicate's second argument
+ * @return an Iterator returning the selected elements
+ * @since 6.0.0
+ */
+ @Incubating
+ public static <T, P> Iterator<T> findingAll(Iterator<T> self, BiPredicate<? super T, ? super P> condition, P param) {
+ return findingAll(self, item -> condition.test(item, param));
+ }
+
private static final class FindAllIterator<T> implements Iterator<T> {
private final Iterator<T> source;
private T current;
@@ -6132,6 +6436,51 @@ public class DefaultGroovyMethods extends DefaultGroovyMethodsSupport {
}
}
+ private static final class FindingAllPredicateIterator<T> implements Iterator<T> {
+ private final Iterator<T> source;
+ private final Predicate<? super T> condition;
+ private T current;
+ private boolean found;
+
+ private FindingAllPredicateIterator(Iterator<T> source, Predicate<? super T> condition) {
+ this.source = source;
+ this.condition = condition;
+ this.found = false;
+ advance();
+ }
+
+ @Override
+ public boolean hasNext() {
+ return found;
+ }
+
+ private void advance() {
+ while (!found && source.hasNext()) {
+ current = source.next();
+ if (condition.test(current)) {
+ found = true;
+ return;
+ }
+ }
+ }
+
+ @Override
+ public T next() {
+ if (!hasNext()) {
+ throw new NoSuchElementException("FindingAllPredicateIterator has been exhausted and contains no more elements");
+ }
+ T result = current;
+ found = false;
+ advance();
+ return result;
+ }
+
+ @Override
+ public void remove() {
+ throw new UnsupportedOperationException();
+ }
+ }
+
//--------------------------------------------------------------------------
// findIndexOf
diff --git a/src/test/groovy/org/apache/groovy/util/LambdasTest.groovy b/src/test/groovy/org/apache/groovy/util/LambdasTest.groovy
index fb82a48886..d538610fce 100644
--- a/src/test/groovy/org/apache/groovy/util/LambdasTest.groovy
+++ b/src/test/groovy/org/apache/groovy/util/LambdasTest.groovy
@@ -122,6 +122,100 @@ class LambdasTest {
['a', 'b', 'c'].collect(curryWith(repeat, 3))
}
+ @Test
+ void fatFreeLazyCollectingAndFindingAllOnInfiniteIterators() {
+ Function<Integer, Integer> next = n -> n + 1
+ // collecting(Iterator, Function) is lazy, so it terminates on an infinite source via take
+ assert [1, 2, 3].repeat().collecting(next).take(6).toList() == [2, 3, 4, 2, 3, 4]
+
+ Predicate<Integer> isEven = n -> n % 2 == 0
+ // findingAll(Iterator, Predicate) is lazy too
+ assert [1, 2, 3].repeat().findingAll(isEven).take(4).toList() == [2, 2, 2, 2]
+ }
+
+ @Test
+ void fatFreeLazyVariantsMatchClosureVariants() {
+ Function<Integer, Integer> next = n -> n + 1
+ assert [1, 2, 3].iterator().collecting(next).toList() ==
+ [1, 2, 3].iterator().collecting { it + 1 }.toList()
+
+ Predicate<Integer> isEven = n -> n % 2 == 0
+ assert [1, 2, 3, 4, 5, 6].iterator().findingAll(isEven).toList() ==
+ [1, 2, 3, 4, 5, 6].iterator().findingAll { it % 2 == 0 }.toList()
+ }
+
+ @Test
+ void fatFreeWithOverloadsForAnyAndEvery() {
+ BiPredicate<Integer, Integer> divisibleBy = (n, d) -> n % d == 0
+
+ // any(Iterable, BiPredicate, param) bakes in the right-curry
+ assert [1, 2, 3].any(divisibleBy, 2)
+ assert ![1, 3, 5].any(divisibleBy, 2)
+ assert [1, 2, 3].any(divisibleBy, 2) ==
+ [1, 2, 3].any(curryWith(divisibleBy, 2))
+
+ // every(Iterable, BiPredicate, param) bakes in the right-curry
+ assert [2, 4, 6].every(divisibleBy, 2)
+ assert ![2, 3, 4].every(divisibleBy, 2)
+ assert [2, 4, 6].every(divisibleBy, 2) ==
+ [2, 4, 6].every(curryWith(divisibleBy, 2))
+ }
+
+ @Test
+ void fatFreeCountOnIterableDirectAndWith() {
+ Predicate<Integer> isEven = n -> n % 2 == 0
+ // direct fat-free count(Iterable, Predicate)
+ assert [2, 4, 2, 1, 3, 5, 2, 4, 3].count(isEven) == 5
+
+ BiPredicate<Integer, Integer> divisibleBy = (n, d) -> n % d == 0
+ // count(Iterable, BiPredicate, param) bakes in the right-curry
+ assert [2, 4, 2, 1, 3, 5, 2, 4, 3].count(divisibleBy, 2) ==
+ [2, 4, 2, 1, 3, 5, 2, 4, 3].count(curryWith(divisibleBy, 2))
+ }
+
+ @Test
+ void fatFreeDirectFindAndCountOnIterator() {
+ Predicate<Integer> greaterThanOne = n -> n > 1
+ assert [1, 2, 3].iterator().find(greaterThanOne) == 2
+ assert [1, 2, 3].iterator().find(n -> (n as int) > 3) == null
+
+ Predicate<Integer> isEven = n -> n % 2 == 0
+ assert [2, 4, 2, 1, 3, 5, 2, 4, 3].iterator().count(isEven) == 5
+ // Iterable count(Predicate) delegates to the Iterator variant
+ assert [2, 4, 2, 1, 3, 5, 2, 4, 3].count(isEven) ==
+ [2, 4, 2, 1, 3, 5, 2, 4, 3].iterator().count(isEven)
+ }
+
+ @Test
+ void fatFreeWithVariantsForFindAndLazyIterators() {
+ BiPredicate<Integer, Integer> divisibleBy = (n, d) -> n % d == 0
+
+ // find(Iterator, BiPredicate, param) returns the first match
+ assert [1, 2, 3, 4, 5, 6].iterator().find(divisibleBy, 3) == 3
+ assert [1, 2, 3, 4, 5, 6].iterator().find(divisibleBy, 7) == null
+
+ // findingAll(Iterator, BiPredicate, param) is lazy over an infinite source
+ assert [1, 2, 3, 4, 5, 6].repeat().findingAll(divisibleBy, 3).take(4).toList() == [3, 6, 3, 6]
+
+ // collecting(Iterator, BiFunction, param) is lazy over an infinite source
+ BiFunction<Integer, Integer, Integer> add = (n, d) -> n + d
+ assert [1, 2, 3].repeat().collecting(add, 10).take(6).toList() == [11, 12, 13, 11, 12, 13]
+ }
+
+ @Test
+ void fatFreeWithVariantsMatchCurryWith() {
+ BiPredicate<Integer, Integer> divisibleBy = (n, d) -> n % d == 0
+ // no find(Iterator, Predicate) exists, so compare against Iterable find(Predicate)
+ assert [1, 2, 3, 4, 5, 6].iterator().find(divisibleBy, 3) ==
+ [1, 2, 3, 4, 5, 6].find(curryWith(divisibleBy, 3))
+ assert [1, 2, 3, 4, 5, 6].iterator().findingAll(divisibleBy, 2).toList() ==
+ [1, 2, 3, 4, 5, 6].iterator().findingAll(curryWith(divisibleBy, 2)).toList()
+
+ BiFunction<Integer, Integer, Integer> add = (n, d) -> n + d
+ assert [1, 2, 3].iterator().collecting(add, 10).toList() ==
+ [1, 2, 3].iterator().collecting(curryWith(add, 10)).toList()
+ }
+
@Test
void curryWithReturnsFreshFunctionsThatShareNoState() {
BiPredicate<Integer, Integer> divisibleBy = (n, d) -> n % d == 0