From 98bf6432ef39f98aaacb904866af1278b997f442 Mon Sep 17 00:00:00 2001 From: Oleksii PELYKH Date: Tue, 25 Jun 2024 11:55:12 +0200 Subject: [PATCH] [COLLECTIONS-858] CartesianProductIterator --- .../iterators/CartesianProductIterator.java | 156 +++++++++++++ .../CartesianProductIteratorTest.java | 209 ++++++++++++++++++ 2 files changed, 365 insertions(+) create mode 100644 src/main/java/org/apache/commons/collections4/iterators/CartesianProductIterator.java create mode 100644 src/test/java/org/apache/commons/collections4/iterators/CartesianProductIteratorTest.java 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..2aea039222 --- /dev/null +++ b/src/main/java/org/apache/commons/collections4/iterators/CartesianProductIterator.java @@ -0,0 +1,156 @@ +/* + * 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.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}. + *

+ * If any of the input iterables is empty, the Cartesian product will be empty. + * If any of the input iterables is infinite, the Cartesian product will be + * infinite. + * + * @param the type of the objects being permuted + * @since 4.5.0 + */ +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; + + /** + * Constructs a new {@code CartesianProductIterator} instance with given iterables. + * + * @param iterables the iterables to create the Cartesian product from + * @throws NullPointerException if any of the iterables is null + */ + @SafeVarargs + public CartesianProductIterator(final Iterable... iterables) { + Objects.requireNonNull(iterables, "iterables"); + this.iterables = new ArrayList<>(iterables.length); + this.iterators = new ArrayList<>(iterables.length); + for (final Iterable iterable : iterables) { + Objects.requireNonNull(iterable, "iterable"); + this.iterables.add(iterable); + final Iterator iterator = iterable.iterator(); + if (!iterator.hasNext()) { + iterators.clear(); + break; + } + iterators.add(iterator); + } + } + + /** + * Returns {@code true} if the iteration has more elements. + * + * @return true if there are more tuples, otherwise false + */ + @Override + public boolean hasNext() { + return iterators.stream().anyMatch(Iterator::hasNext); + } + + /** + * 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("reached unreachable code"); + } + + @Override + public void remove() { + throw new UnsupportedOperationException("remove"); + } +} 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..b32eeb7267 --- /dev/null +++ b/src/test/java/org/apache/commons/collections4/iterators/CartesianProductIteratorTest.java @@ -0,0 +1,209 @@ +/* + * 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; + private List emptyList; + + 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('!', '?'); + emptyList = Collections.emptyList(); + } + + @Override + public boolean supportsRemove() { + return false; + } + + @Test + public void testRemoveThrows() { + final CartesianProductIterator it = makeObject(); + assertThrows(UnsupportedOperationException.class, it::remove); + } + + @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 testExhaustivity() { + 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()); + } + } + } + } + + /** + * test checking that no tuples are returned when at least one of the lists is empty + */ + @Test + public void testExhaustivityWithEmptyList() { + final List resultsList = new ArrayList<>(); + final CartesianProductIterator it = new CartesianProductIterator<>(letters, emptyList, symbols); + while (it.hasNext()) { + final List tuple = it.next(); + resultsList.add(tuple.toArray(new Character[0])); + } + assertThrows(NoSuchElementException.class, it::next); + assertEquals(0, resultsList.size()); + } + + /** + * test checking that no tuples are returned when first of the lists is empty + */ + @Test + public void testExhaustivityWithEmptyFirstList() { + final List resultsList = new ArrayList<>(); + final CartesianProductIterator it = new CartesianProductIterator<>(emptyList, numbers, symbols); + while (it.hasNext()) { + final List tuple = it.next(); + resultsList.add(tuple.toArray(new Character[0])); + } + assertThrows(NoSuchElementException.class, it::next); + assertEquals(0, resultsList.size()); + } + + /** + * test checking that no tuples are returned when last of the lists is empty + */ + @Test + public void testExhaustivityWithEmptyLastList() { + final List resultsList = new ArrayList<>(); + final CartesianProductIterator it = new CartesianProductIterator<>(letters, numbers, emptyList); + while (it.hasNext()) { + final List tuple = it.next(); + resultsList.add(tuple.toArray(new Character[0])); + } + assertThrows(NoSuchElementException.class, it::next); + assertEquals(0, resultsList.size()); + } + + /** + * test checking that no tuples are returned when all the lists are empty + */ + @Test + public void testExhaustivityWithAllEmptyLists() { + final List resultsList = new ArrayList<>(); + final CartesianProductIterator it = new CartesianProductIterator<>(emptyList, emptyList, emptyList); + while (it.hasNext()) { + final List tuple = it.next(); + resultsList.add(tuple.toArray(new Character[0])); + } + assertThrows(NoSuchElementException.class, it::next); + assertEquals(0, resultsList.size()); + } + + /** + * test checking that all tuples are returned when same list is passed multiple times + */ + @Test + public void testExhaustivityWithSameList() { + final List resultsList = new ArrayList<>(); + final CartesianProductIterator it = new CartesianProductIterator<>(letters, letters, letters); + while (it.hasNext()) { + final List tuple = it.next(); + resultsList.add(tuple.toArray(new Character[0])); + } + assertThrows(NoSuchElementException.class, it::next); + assertEquals(27, resultsList.size()); + final Iterator itResults = resultsList.iterator(); + for (final Character a : letters) { + for (final Character b : letters) { + for (final Character c : letters) { + assertArrayEquals(new Character[]{a, b, c}, itResults.next()); + } + } + } + } + + /** + * test that all tuples are provided to consumer + */ + @Test + public void testForEachRemaining() { + final List resultsList = new ArrayList<>(); + final CartesianProductIterator it = makeObject(); + it.forEachRemaining(tuple -> resultsList.add(tuple.toArray(new Character[0]))); + 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()); + } + } + } + } +}