diff --git a/src/main/java/org/apache/commons/collections4/iterators/CartesianProductIterator.java b/src/main/java/org/apache/commons/collections4/iterators/CartesianProductIterator.java new file mode 100644 index 0000000000..3652d9caa6 --- /dev/null +++ b/src/main/java/org/apache/commons/collections4/iterators/CartesianProductIterator.java @@ -0,0 +1,152 @@ +/* + * 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 org.apache.commons.collections4.iterators; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Objects; + + +/** + * This iterator creates a Cartesian product of the input iterables, + * equivalent to nested for-loops. + *

+ * The iterables provided to the constructor are used in reverse order, each + * until exhaustion before proceeding to the next element of the prior iterable + * and repeating. Consider the following example: + *

{@code
+ * List iterable1 = Arrays.asList('A', 'B', 'C');
+ * List iterable2 = Arrays.asList('1', '2', '3');
+ * CartesianProductIterator it = new CartesianProductIterator<>(
+ *     iterable1,
+ *     iterable2);
+ * while (it.hasNext()) {
+ *     List tuple = it.next();
+ *     System.out.println(tuple.get(0) + ", " + tuple.get(1));
+ * }
+ * }
+ * The output will be: + *
+ * A, 1
+ * A, 2
+ * A, 3
+ * B, 1
+ * B, 2
+ * B, 3
+ * C, 1
+ * C, 2
+ * C, 3
+ * 
+ *

+ * The {@code remove()} operation is not supported, and will throw an + * {@code UnsupportedOperationException}. + * + * @param the type of the objects being permuted + */ +public class CartesianProductIterator implements Iterator> { + + /** + * The iterables to create the Cartesian product from. + */ + private final List> iterables; + + /** + * The iterators to generate the Cartesian product tuple from. + */ + private final List> iterators; + + /** + * The previous generated tuple of elements. + */ + private List previousTuple; + + /** + * Standard constructor for this class. + * + * @param iterables the iterables to create the Cartesian product from + * @throws NullPointerException if any of the iterables is null + */ + public CartesianProductIterator(final Iterable... iterables) { + Objects.requireNonNull(iterables, "iterables"); + this.iterables = Arrays.asList(iterables); + this.iterators = new ArrayList<>(iterables.length); + for (final Iterable iterable : iterables) { + Objects.requireNonNull(iterable, "iterable"); + final Iterator iterator = iterable.iterator(); + if (iterator.hasNext()) { + iterators.add(iterator); + } else { + iterators.clear(); + break; + } + } + } + + /** + * Indicates if there are more tuples to return. + * @return true if there are more tuples, otherwise false + */ + @Override + public boolean hasNext() { + for (final Iterator iterator : iterators) { + if (iterator.hasNext()) { + return true; + } + } + return false; + } + + /** + * Returns the next tuple of the input iterables. + * @return a list of the input iterables' elements + * @throws NoSuchElementException if there are no more tuples + */ + @Override + public List next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + + if (previousTuple == null) { + previousTuple = new ArrayList<>(iterables.size()); + for (final Iterator iterator : iterators) { + previousTuple.add(iterator.next()); + } + return new ArrayList<>(previousTuple); + } + + for (int i = iterators.size() - 1; i >= 0; i--) { + Iterator iterator = iterators.get(i); + if (iterator.hasNext()) { + previousTuple.set(i, iterator.next()); + return new ArrayList<>(previousTuple); + } + iterator = iterables.get(i).iterator(); + iterators.set(i, iterator); + previousTuple.set(i, iterator.next()); + } + throw new IllegalStateException(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException("remove() is not supported"); + } +} diff --git a/src/test/java/org/apache/commons/collections4/iterators/CartesianProductIteratorTest.java b/src/test/java/org/apache/commons/collections4/iterators/CartesianProductIteratorTest.java new file mode 100644 index 0000000000..802aa857d1 --- /dev/null +++ b/src/test/java/org/apache/commons/collections4/iterators/CartesianProductIteratorTest.java @@ -0,0 +1,100 @@ +/* + * 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 org.apache.commons.collections4.iterators; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + + +/** + * Test class for {@link CartesianProductIterator}. + */ +public class CartesianProductIteratorTest extends AbstractIteratorTest> { + + private List letters; + private List numbers; + private List symbols; + + public CartesianProductIteratorTest() { + super(CartesianProductIteratorTest.class.getSimpleName()); + } + + @Override + public CartesianProductIterator makeEmptyIterator() { + return new CartesianProductIterator<>(); + } + + @Override + public CartesianProductIterator makeObject() { + return new CartesianProductIterator<>(letters, numbers, symbols); + } + + @BeforeEach + public void setUp() { + letters = Arrays.asList('A', 'B', 'C'); + numbers = Arrays.asList('1', '2', '3'); + symbols = Arrays.asList('!', '?'); + } + + @Override + public boolean supportsRemove() { + return false; + } + + @Test + public void testEmptyCollection() { + final CartesianProductIterator it = new CartesianProductIterator<>(letters, Collections.emptyList()); + assertFalse(it.hasNext()); + assertThrows(NoSuchElementException.class, () -> it.next()); + } + + /** + * test checking that all the tuples are returned + */ + @Test + public void testCartesianProductExhaustivity() { + final List resultsList = new ArrayList<>(); + + final CartesianProductIterator it = makeObject(); + while (it.hasNext()) { + final List tuple = it.next(); + resultsList.add(tuple.toArray(new Character[0])); + } + assertThrows(NoSuchElementException.class, () -> it.next()); + assertEquals(18, resultsList.size()); + final Iterator itResults = resultsList.iterator(); + for (final Character a : letters) { + for (final Character b : numbers) { + for (final Character c : symbols) { + assertArrayEquals(new Character[]{a, b, c}, itResults.next()); + } + } + } + } +}