This is an automated email from the ASF dual-hosted git repository.
paulk 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 662ac50b22 GROOVY-11603: Add Lazy generators for iterators (split into
two classes, add @since)
662ac50b22 is described below
commit 662ac50b2264225de916179417ddfc3020b1725f
Author: Paul King <[email protected]>
AuthorDate: Thu Apr 10 15:49:01 2025 +1000
GROOVY-11603: Add Lazy generators for iterators (split into two classes,
add @since)
---
src/main/java/groovy/util/Iterables.java | 122 +++++++++++++++++++++++++++++++
src/main/java/groovy/util/Iterators.java | 96 ++++++++----------------
2 files changed, 153 insertions(+), 65 deletions(-)
diff --git a/src/main/java/groovy/util/Iterables.java
b/src/main/java/groovy/util/Iterables.java
new file mode 100644
index 0000000000..54248fdbd7
--- /dev/null
+++ b/src/main/java/groovy/util/Iterables.java
@@ -0,0 +1,122 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 groovy.util;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.NoSuchElementException;
+
+/**
+ * Provides some iterable based generators.
+ *
+ * @since 5.0.0
+ */
+public class Iterables {
+
+ /**
+ * An iterator returning a map for each combination of elements the
iterables sources.
+ *
+ * <pre class="groovyTestCase">
+ * assert Iterables.combine(x: 1..2, y: 'a'..'c').collect().toString()
+ * == '[[x:1, y:a], [x:1, y:b], [x:1, y:c], [x:2, y:a], [x:2, y:b],
[x:2, y:c]]'
+ * assert Iterables.combine(x: 1..3, y: 'a'..'b').collect().toString()
+ * == '[[x:1, y:a], [x:1, y:b], [x:2, y:a], [x:2, y:b], [x:3, y:a],
[x:3, y:b]]'
+ * </pre>
+ *
+ * @param map the named source iterables
+ * @return the output iterator of named combinations
+ */
+ public static <K, T> Iterator<Map<K, T>> combine(Map<K, ? extends
Iterable<T>> map) {
+ return new CrossProductIterator<>(map);
+ }
+
+ private static class CrossProductIterator<K, T> implements Iterator<Map<K,
T>> {
+ private final Map<K, ? extends Iterable<T>> iterables;
+ private final Map<K, Iterator<T>> iterators = new LinkedHashMap<>();
+ private final List<K> keys = new ArrayList<>();
+ private Map<K, T> current;
+ private boolean loaded;
+ private boolean exhausted;
+
+ private CrossProductIterator(Map<K, ? extends Iterable<T>> iterables) {
+ this.iterables = iterables;
+ for (Map.Entry<K, ? extends Iterable<T>> entry :
iterables.entrySet()) {
+ K key = entry.getKey();
+ keys.add(key);
+ iterators.put(key, entry.getValue().iterator());
+ }
+ }
+
+ private void loadFirst() {
+ current = new LinkedHashMap<>();
+ for (K key : keys) {
+ Iterator<T> iterator = iterators.get(key);
+ if (!iterator.hasNext()) {
+ exhausted = true;
+ return;
+ }
+ T next = iterator.next();
+ current.put(key, next);
+ }
+ }
+
+ @Override
+ public boolean hasNext() {
+ if (!loaded) {
+ loadNext();
+ loaded = true;
+ }
+ return !exhausted;
+ }
+
+ @Override
+ public Map<K, T> next() {
+ if (!hasNext()) {
+ throw new NoSuchElementException("CrossProductIterator has
been exhausted and contains no more elements");
+ }
+ Map<K, T> ret = current;
+ loaded = false;
+ return ret;
+ }
+
+ private void loadNext() {
+ if (current == null) {
+ loadFirst();
+ } else {
+ current = new LinkedHashMap<>(current);
+ for (int i = keys.size() - 1; i >= 0; i--) {
+ K key = keys.get(i);
+ if (iterators.get(key).hasNext()) {
+ T next = iterators.get(key).next();
+ current.put(key, next);
+ break;
+ } else if (i > 0) {
+ iterators.put(key, iterables.get(key).iterator());
+ current.put(key, iterators.get(key).next());
+ } else {
+ exhausted = true;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/java/groovy/util/Iterators.java
b/src/main/java/groovy/util/Iterators.java
index 0f7974d4b0..efff0d1b72 100644
--- a/src/main/java/groovy/util/Iterators.java
+++ b/src/main/java/groovy/util/Iterators.java
@@ -37,6 +37,8 @@ import java.util.function.UnaryOperator;
/**
* Provides some iterator based generators.
+ *
+ * @since 5.0.0
*/
public class Iterators {
@@ -135,42 +137,25 @@ public class Iterators {
}
}
- /**
- * An iterator returning a map for each combination of elements the
iterables sources.
- *
- * <pre class="groovyTestCase">
- * assert Iterators.combine(x: 1..2, y: 'a'..'c').collect().toString()
- * == '[[x:1, y:a], [x:1, y:b], [x:1, y:c], [x:2, y:a], [x:2, y:b],
[x:2, y:c]]'
- * assert Iterators.combine(x: 1..3, y: 'a'..'b').collect().toString()
- * == '[[x:1, y:a], [x:1, y:b], [x:2, y:a], [x:2, y:b], [x:3, y:a],
[x:3, y:b]]'
- * </pre>
- *
- * @param map the source iterables
- * @return the output iterator
- */
- public static <K, T> Iterator<Map<K, T>> combine(Map<K, ? extends
Iterable<T>> map) {
- return new CrossProductIterator<>(map);
- }
-
/**
* An iterator returning a map for each combination of elements the
iterator sources.
* If one of the iterators is infinite, the produced elements will be
infinite
* and will need to be appropriately handled.
* By necessity, elements from the iterators are cached, so you should
- * use {@link #combine(Map)} if you have existing iterables as this will
be more efficient.
+ * use {@link Iterables#combine(Map)} if you have existing iterables as
this will be more efficient.
*
* <pre class="groovyTestCase">
- * assert Iterators.combineLazy(x: (1..2).iterator(), y:
('a'..'c').iterator()).collect().toString()
+ * assert Iterators.combine(x: (1..2).iterator(), y:
('a'..'c').iterator()).collect().toString()
* == '[[x:1, y:a], [x:1, y:b], [x:1, y:c], [x:2, y:a], [x:2, y:b],
[x:2, y:c]]'
- * assert Iterators.combineLazy(x: (1..3).iterator(), y:
('a'..'b').iterator()).collect().toString()
+ * assert Iterators.combine(x: (1..3).iterator(), y:
('a'..'b').iterator()).collect().toString()
* == '[[x:1, y:a], [x:1, y:b], [x:2, y:a], [x:2, y:b], [x:3, y:a],
[x:3, y:b]]'
* </pre>
*
- * @param map the source iterators
- * @return the output iterator
+ * @param map the named source iterators
+ * @return the output iterator of named combinations
*/
- public static <K, T> Iterator<Map<K, T>> combineLazy(Map<K, ? extends
Iterator<T>> map) {
- return combineLazy(map, false);
+ public static <K, T> Iterator<Map<K, T>> combine(Map<K, ? extends
Iterator<T>> map) {
+ return combine(map, false);
}
/**
@@ -178,10 +163,10 @@ public class Iterators {
* If one of the iterators is infinite, the produced elements will be
infinite
* and will need to be appropriately handled.
* By necessity, elements from the iterators are cached, so you should
- * use {@link #combine(Map)} if you have existing iterables as this will
be more efficient.
+ * use {@link Iterables#combine(Map)} if you have existing iterables as
this will be more efficient.
*
* <pre class="groovyTestCase">
- * assert Iterators.combineLazy(
+ * assert Iterators.combine(
* even: Iterators.iterate(0, n {@code ->} n + 2),
* odd: Iterators.iterate(1, n {@code ->} n + 2), false)
* .take(10).collect().join('\n')
@@ -198,7 +183,7 @@ public class Iterators {
* [even:0, odd:19]
* '''.trim()
*
- * assert Iterators.combineLazy(
+ * assert Iterators.combine(
* even: Iterators.iterate(0, n {@code ->} n + 2),
* odd: Iterators.iterate(1, n {@code ->} n + 2), true)
* .take(10).collect().join('\n')
@@ -216,37 +201,29 @@ public class Iterators {
* '''.trim()
* </pre>
*
- * @param map the source iterators
+ * @param map the named source iterators
* @param fairOrdering determine the ordering in which combinations are
processed, fair implies that all source iterators will be visited roughly
equally (until exhausted)
- * @return the output iterator
+ * @return the output iterator of named combinations
*/
- public static <K, T> Iterator<Map<K, T>> combineLazy(Map<K, ? extends
Iterator<T>> map, boolean fairOrdering) {
+ public static <K, T> Iterator<Map<K, T>> combine(Map<K, ? extends
Iterator<T>> map, boolean fairOrdering) {
if (fairOrdering) {
return new CrossByIndexIterator<>(map);
} else {
- return new CrossProductIterator<>(false, map);
+ return new CrossProductIterator<>(map);
}
}
private static class CrossProductIterator<K, T> implements Iterator<Map<K,
T>> {
- private final Map<K, Iterable<T>> iterables;
private final Map<K, Collection<T>> cache;
private final Map<K, Iterator<T>> iterators = new LinkedHashMap<>();
private final List<K> keys = new ArrayList<>();
private Map<K, T> current;
private boolean loaded;
private boolean exhausted;
- private boolean usingIterables;
private final Map<K, Boolean> usingCache = new LinkedHashMap<>();
- private CrossProductIterator(Map<K, Iterable<T>> iterables, Map<K,
Collection<T>> cache, int numItems) {
- this.iterables = iterables;
- this.cache = cache;
- }
-
- private CrossProductIterator(boolean usingIterables, Map<K, ? extends
Iterator<T>> iterators) {
- this(null, new LinkedHashMap<>(), iterators.size());
- this.usingIterables = usingIterables;
+ private CrossProductIterator(Map<K, ? extends Iterator<T>> iterators) {
+ cache = new LinkedHashMap<>();
for (Map.Entry<K, ? extends Iterator<T>> entry :
iterators.entrySet()) {
K key = entry.getKey();
keys.add(key);
@@ -256,26 +233,17 @@ public class Iterators {
}
}
- private CrossProductIterator(Map<K, ? extends Iterable<T>> iterables) {
- this(new LinkedHashMap<>(), null, iterables.size());
- this.usingIterables = true;
- for (Map.Entry<K, ? extends Iterable<T>> entry :
iterables.entrySet()) {
- K key = entry.getKey();
- keys.add(key);
- this.iterables.put(key, entry.getValue());
- iterators.put(key, entry.getValue().iterator());
- usingCache.put(key, false);
- }
- }
-
private void loadFirst() {
current = new LinkedHashMap<>();
for (K key : keys) {
- T next = iterators.get(key).next();
- current.put(key, next);
- if (!usingIterables) {
- cache.get(key).add(next);
+ Iterator<T> iterator = iterators.get(key);
+ if (!iterator.hasNext()) {
+ exhausted = true;
+ return;
}
+ T next = iterator.next();
+ current.put(key, next);
+ cache.get(key).add(next);
}
}
@@ -291,7 +259,7 @@ public class Iterators {
@Override
public Map<K, T> next() {
if (!hasNext()) {
- throw new NoSuchElementException("Iterator has been exhausted
and contains no more elements");
+ throw new NoSuchElementException("CrossProductIterator has
been exhausted and contains no more elements");
}
Map<K, T> ret = current;
loaded = false;
@@ -308,17 +276,13 @@ public class Iterators {
if (iterators.get(key).hasNext()) {
T next = iterators.get(key).next();
current.put(key, next);
- if (!usingIterables && !usingCache.get(key)) {
+ if (!usingCache.get(key)) {
cache.get(key).add(next);
}
break;
} else if (i > 0) {
usingCache.put(key, true);
- if (usingIterables) {
- iterators.put(key, iterables.get(key).iterator());
- } else {
- iterators.put(key, cache.get(key).iterator());
- }
+ iterators.put(key, cache.get(key).iterator());
current.put(key, iterators.get(key).next());
} else {
exhausted = true;
@@ -405,7 +369,9 @@ public class Iterators {
@Override
public Map<K, T> next() {
- if (nextItem == null) throw new NoSuchElementException();
+ if (!hasNext()) {
+ throw new NoSuchElementException("CrossByIndexIterator has
been exhausted and contains no more elements");
+ }
Map<K, T> result = nextItem;
fetchNext();
return result;