diff --git a/src/main/java/org/apache/commons/collections4/iterators/LexicographicPermutationIterator.java b/src/main/java/org/apache/commons/collections4/iterators/LexicographicPermutationIterator.java new file mode 100644 index 0000000000..aa4546aa44 --- /dev/null +++ b/src/main/java/org/apache/commons/collections4/iterators/LexicographicPermutationIterator.java @@ -0,0 +1,219 @@ +/* + * 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 + * + * https://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.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Objects; + +/** + * This iterator creates permutations of an input collection, using the + * lexicographical order. + *

+ * Iteration starts at the arrangement in which the elements are given, and each + * call to {@code next()} advances to the smallest arrangement greater than the + * current one. Iteration therefore ends at the largest arrangement, and the ones + * preceding the given arrangement are never returned: only a collection already + * sorted according to the ordering in use yields the complete set of + * permutations. Callers wanting the complete set must sort the collection + * beforehand, just as callers of + * {@link java.util.Collections#binarySearch(java.util.List, Object) binarySearch} + * must. Callers wanting to enumerate one part of the set, to resume from a + * previously reached arrangement or to split the work, may start anywhere. + *

+ *

+ * The starting arrangement is the iteration order of the given collection, so + * collections whose iteration order is unspecified, such as {@link java.util.HashSet}, + * make poor input: which permutations are returned is then unspecified too. + *

+ *

+ * The iterator might return fewer than n! permutations of the input collection, + * either because the collection was not sorted according to the ordering in use, + * as described above, or because duplicated permutations are skipped: equal + * elements are not distinguished from one another. + * The {@code remove()} operation is not supported, and will throw an + * {@code UnsupportedOperationException}. + *

+ *

+ * NOTE: in case an empty collection is provided, the iterator will + * return exactly one empty list as result, as 0! = 1. + *

+ *

+ * NOTE: {@link PermutationIterator} differs on both counts. It returns exactly n! + * permutations whatever the order of the input collection. The two iterators are + * therefore not interchangeable. + *

+ * + * @param the type of the objects being permuted + * @see PermutationIterator + * @since 4.7.0 + */ +public class LexicographicPermutationIterator implements Iterator> { + + /** + * The comparator used to define order of generation, + * or null if it uses the natural ordering. + */ + private final Comparator comparator; + + /** + * Next permutation to return. When a permutation is requested a copy of this + * instance is provided and the next one is computed. + */ + private List nextPermutation; + + /** + * Standard constructor for this class, using the natural ordering of the elements. + *

+ * Iteration starts at the arrangement in which the collection iterates its + * elements; sort the collection first to obtain the complete set of permutations. + *

+ * + * @param collection The collection to generate permutations for + * @throws NullPointerException if collection is null + */ + public LexicographicPermutationIterator(final Collection collection) { + this(collection, null); + } + + /** + * Constructs an instance using the given comparator to order the elements. + *

+ * Iteration starts at the arrangement in which the collection iterates its + * elements; sort the collection with the same comparator first to obtain the + * complete set of permutations. + *

+ * + * @param collection The collection to generate permutations for + * @param comparator The comparator used to define the order of generation, + * or null to use the natural ordering of the elements + * @throws NullPointerException if collection is null + */ + public LexicographicPermutationIterator(final Collection collection, final Comparator comparator) { + Objects.requireNonNull(collection, "collection"); + nextPermutation = new ArrayList<>(collection); + this.comparator = comparator; + } + + /** + * Indicates if there are more permutations available. + * + * @return true if there are more permutations, otherwise false + */ + @Override + public boolean hasNext() { + return nextPermutation != null; + } + + /** + * Returns the next permutation of the input collection. + *

+ * The returned list is a mutable copy taken before the iterator advances, never the + * iterator's own state, and it belongs to the caller. It may therefore be modified in + * any way and at any time without affecting the remaining permutations. + *

+ * + * @return A list of the permutator's elements representing a permutation + * @throws NoSuchElementException if there are no more permutations + * @throws ClassCastException if no comparator was supplied and the elements are + * not mutually {@link Comparable} + */ + @Override + public List next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + + final List result = new ArrayList<>(nextPermutation); + nextPermutation = smallestGreaterThan(nextPermutation); + return result; + } + + /** + * Always throws {@link UnsupportedOperationException}. + * + * @throws UnsupportedOperationException Always thrown. + */ + @Override + public void remove() { + throw new UnsupportedOperationException("remove() is not supported"); + } + + /** + * Compares two elements using the comparator, or their natural ordering if no + * comparator was supplied. + * + * @param e1 The first element to compare + * @param e2 The second element to compare + * @return a negative integer, zero, or a positive integer as the first element + * is less than, equal to, or greater than the second + * @throws ClassCastException if no comparator was supplied and the elements are + * not mutually {@link Comparable} + */ + @SuppressWarnings("unchecked") + private int compareElements(final E e1, final E e2) { + return comparator == null + ? ((Comparable) e1).compareTo(e2) + : comparator.compare(e1, e2); + } + + /** + * Returns the smallest arrangement of the given elements greater than the given one, + * as a new list. The permutation passed in is only read, never rearranged. + * + * @param permutation The arrangement to advance from + * @return A new list holding the next arrangement in lexicographical order, or null + * if the given one is already the largest + * @throws ClassCastException if no comparator was supplied and the elements are + * not mutually {@link Comparable} + */ + private List smallestGreaterThan(final List permutation) { + final int size = permutation.size(); + + // find the pivot: the rightmost element that is smaller than its successor. + // if there is none the given permutation is the last one in lexicographical order + int i = size - 2; + while (i >= 0 && compareElements(permutation.get(i), permutation.get(i + 1)) >= 0) { + --i; + } + + if (i < 0) { + return null; + } + + // find the rightmost element greater than the pivot; the tail is descending, + // so this is the pivot's successor in the remaining elements + int j = size - 1; + while (j >= i && compareElements(permutation.get(i), permutation.get(j)) >= 0) { + --j; + } + + // swap the pivot with its successor, then reverse the descending tail + // into ascending order to obtain the smallest larger permutation + final List nextP = new ArrayList<>(permutation); + Collections.swap(nextP, i, j); + final List subList = nextP.subList(i + 1, nextP.size()); + Collections.reverse(subList); + return nextP; + } + +} diff --git a/src/test/java/org/apache/commons/collections4/iterators/LexicographicPermutationIteratorTest.java b/src/test/java/org/apache/commons/collections4/iterators/LexicographicPermutationIteratorTest.java new file mode 100644 index 0000000000..7125b62188 --- /dev/null +++ b/src/test/java/org/apache/commons/collections4/iterators/LexicographicPermutationIteratorTest.java @@ -0,0 +1,441 @@ +/* + * 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 + * + * https://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.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Test class for LexicographicPermutationIterator. + */ +class LexicographicPermutationIteratorTest extends AbstractIteratorTest> { + + /** + * A value holder that deliberately does not implement {@link Comparable}, used to + * check that a supplied comparator is honored. + * + * @param the type of the wrapped value + */ + private static final class NonComparableObject { + + private final T value; + + NonComparableObject(final T value) { + this.value = value; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + + if (o == null || getClass() != o.getClass()) { + return false; + } + + final NonComparableObject that = (NonComparableObject) o; + return Objects.equals(value, that.value); + } + + T getValue() { + return value; + } + + @Override + public int hashCode() { + return Objects.hash(value); + } + } + + @SuppressWarnings("boxing") // OK in test code + protected Character[] testArray = { 'A', 'B', 'C' }; + + protected List testList; + + @Override + public LexicographicPermutationIterator makeEmptyIterator() { + return new LexicographicPermutationIterator<>(new ArrayList<>()); + } + + @Override + public LexicographicPermutationIterator makeObject() { + return new LexicographicPermutationIterator<>(testList); + } + + @BeforeEach + public void setUp() { + testList = new ArrayList<>(); + testList.addAll(Arrays.asList(testArray)); + } + + @Override + public boolean supportsEmptyIterator() { + return false; + } + + @Override + public boolean supportsRemove() { + return false; + } + + /** + * test checking that a collection whose elements are all equal yields a single + * permutation, equal elements not being distinguished from one another: the n! + * arrangements are all duplicates of each other and collapse into one. + */ + @Test + void testAllEqualElementsYieldSinglePermutation() { + final Iterator> permutationIterator = new LexicographicPermutationIterator<>(Arrays.asList('A', 'A', 'A')); + + assertTrue(permutationIterator.hasNext()); + assertEquals(Arrays.asList('A', 'A', 'A'), permutationIterator.next()); + + assertFalse(permutationIterator.hasNext()); + assertThrows(NoSuchElementException.class, permutationIterator::next); + } + + @Test + void testCustomComparator() { + final Iterator> permutationIterator = new LexicographicPermutationIterator<>(Arrays.asList('C', 'B', 'A'), + Comparator.reverseOrder()); + + assertTrue(permutationIterator.hasNext()); + assertEquals(Arrays.asList('C', 'B', 'A'), permutationIterator.next()); + + assertTrue(permutationIterator.hasNext()); + assertEquals(Arrays.asList('C', 'A', 'B'), permutationIterator.next()); + + assertTrue(permutationIterator.hasNext()); + assertEquals(Arrays.asList('B', 'C', 'A'), permutationIterator.next()); + + assertTrue(permutationIterator.hasNext()); + assertEquals(Arrays.asList('B', 'A', 'C'), permutationIterator.next()); + + assertTrue(permutationIterator.hasNext()); + assertEquals(Arrays.asList('A', 'C', 'B'), permutationIterator.next()); + + assertTrue(permutationIterator.hasNext()); + assertEquals(Arrays.asList('A', 'B', 'C'), permutationIterator.next()); + + assertFalse(permutationIterator.hasNext()); + } + + @Test + void testCustomComparatorWithNonComparableObjects() { + final Iterator>> permutationIterator = + new LexicographicPermutationIterator<>(Arrays.asList( + new NonComparableObject<>('A'), + new NonComparableObject<>('B')), Comparator.comparing(NonComparableObject::getValue)); + + assertTrue(permutationIterator.hasNext()); + assertEquals(Arrays.asList( + new NonComparableObject<>('A'), + new NonComparableObject<>('B')), permutationIterator.next()); + + assertTrue(permutationIterator.hasNext()); + assertEquals(Arrays.asList( + new NonComparableObject<>('B'), + new NonComparableObject<>('A')), permutationIterator.next()); + + assertFalse(permutationIterator.hasNext()); + } + + @Test + void testDuplicatedPermutationsAreSkipped() { + final Iterator> permutationIterator = new LexicographicPermutationIterator<>(Arrays.asList('A', 'A', 'B', 'B')); + + assertTrue(permutationIterator.hasNext()); + assertEquals(Arrays.asList('A', 'A', 'B', 'B'), permutationIterator.next()); + + assertTrue(permutationIterator.hasNext()); + assertEquals(Arrays.asList('A', 'B', 'A', 'B'), permutationIterator.next()); + + assertTrue(permutationIterator.hasNext()); + assertEquals(Arrays.asList('A', 'B', 'B', 'A'), permutationIterator.next()); + + assertTrue(permutationIterator.hasNext()); + assertEquals(Arrays.asList('B', 'A', 'A', 'B'), permutationIterator.next()); + + assertTrue(permutationIterator.hasNext()); + assertEquals(Arrays.asList('B', 'A', 'B', 'A'), permutationIterator.next()); + + assertTrue(permutationIterator.hasNext()); + assertEquals(Arrays.asList('B', 'B', 'A', 'A'), permutationIterator.next()); + + assertFalse(permutationIterator.hasNext()); + } + + @Test + void testEmptyCollection() { + final Iterator> permutationIterator = makeEmptyIterator(); + + // there is one permutation for an empty set: 0! = 1 + assertTrue(permutationIterator.hasNext()); + assertTrue(permutationIterator.next().isEmpty()); + + assertFalse(permutationIterator.hasNext()); + } + + /** + * test checking that forEachRemaining resumes at the current position rather than at + * the first permutation, the already returned ones being no longer remaining, and + * that it leaves the iterator exhausted. + */ + @Test + void testForEachRemainingResumesFromCurrentPosition() { + final Iterator> permutationIterator = makeObject(); + final List> permutations = new ArrayList<>(); + + assertEquals(Arrays.asList('A', 'B', 'C'), permutationIterator.next()); + assertEquals(Arrays.asList('A', 'C', 'B'), permutationIterator.next()); + + permutationIterator.forEachRemaining(permutations::add); + + assertEquals(Arrays.asList( + Arrays.asList('B', 'A', 'C'), + Arrays.asList('B', 'C', 'A'), + Arrays.asList('C', 'A', 'B'), + Arrays.asList('C', 'B', 'A')), permutations); + + assertFalse(permutationIterator.hasNext()); + } + + /** + * test checking that forEachRemaining hands the permutations to the action in the + * same lexicographical order as next() returns them. + */ + @Test + void testForEachRemainingYieldsLexicographicOrder() { + final Iterator> permutationIterator = makeObject(); + final List> permutations = new ArrayList<>(); + + permutationIterator.forEachRemaining(permutations::add); + + assertEquals(Arrays.asList( + Arrays.asList('A', 'B', 'C'), + Arrays.asList('A', 'C', 'B'), + Arrays.asList('B', 'A', 'C'), + Arrays.asList('B', 'C', 'A'), + Arrays.asList('C', 'A', 'B'), + Arrays.asList('C', 'B', 'A')), permutations); + + assertFalse(permutationIterator.hasNext()); + } + + /** + * test checking that the lists handed out by next() belong to the caller: the next + * permutation is computed and copied before the current one is returned, so mutating + * a returned list, structurally or not, leaves the remaining iteration untouched. + */ + @Test + void testMutatingReturnedListDoesNotAffectIteration() { + final Iterator> permutationIterator = makeObject(); + + final List first = permutationIterator.next(); + assertEquals(Arrays.asList('A', 'B', 'C'), first); + first.set(0, 'Z'); + first.add('Q'); + + // the damage is not merely deferred by one step, so mutate the next one too + final List second = permutationIterator.next(); + assertEquals(Arrays.asList('A', 'C', 'B'), second); + second.clear(); + + assertEquals(Arrays.asList('B', 'A', 'C'), permutationIterator.next()); + assertEquals(Arrays.asList('B', 'C', 'A'), permutationIterator.next()); + assertEquals(Arrays.asList('C', 'A', 'B'), permutationIterator.next()); + assertEquals(Arrays.asList('C', 'B', 'A'), permutationIterator.next()); + + assertFalse(permutationIterator.hasNext()); + } + + /** + * test checking the first permutation specifically, it being the one the constructor + * builds rather than next(): it must be a list of the iterator's own rather than the + * given collection, so that later changes to that collection never reach the + * iteration. The two copies live in different places, so losing one of them would + * corrupt only part of the sequence. + */ + @Test + void testMutatingSourceCollectionDoesNotAffectIteration() { + final List source = new ArrayList<>(Arrays.asList('A', 'B', 'C')); + final Iterator> permutationIterator = new LexicographicPermutationIterator<>(source); + + source.set(0, 'X'); + source.add('Y'); + + final List first = permutationIterator.next(); + assertNotSame(source, first); + assertEquals(Arrays.asList('A', 'B', 'C'), first); + + // the copy is no view of the collection either, so iteration carries on intact + assertEquals(Arrays.asList('A', 'C', 'B'), permutationIterator.next()); + } + + @Test + void testNonComparableElementsThrow() { + final Iterator>> permutationIterator = new LexicographicPermutationIterator<>( + Arrays.asList( + new NonComparableObject<>('A'), + new NonComparableObject<>('B'))); + + assertTrue(permutationIterator.hasNext()); + assertThrows(ClassCastException.class, permutationIterator::next); + } + + @Test + void testNullCollectionThrows() { + assertThrows(NullPointerException.class, () -> new LexicographicPermutationIterator<>(null)); + assertThrows(NullPointerException.class, () -> new LexicographicPermutationIterator<>(null, Comparator.reverseOrder())); + } + + @Test + void testPermutationException() { + final Iterator> permutationIterator = new LexicographicPermutationIterator<>(Arrays.asList('A', 'B')); + + assertTrue(permutationIterator.hasNext()); + assertEquals(Arrays.asList('A', 'B'), permutationIterator.next()); + + assertTrue(permutationIterator.hasNext()); + assertEquals(Arrays.asList('B', 'A'), permutationIterator.next()); + + // asking for another permutation should throw an exception + assertFalse(permutationIterator.hasNext()); + assertThrows(NoSuchElementException.class, permutationIterator::next); + } + + /** + * test checking that all the permutations are returned in lexicographical order + */ + @Test + void testPermutationExhaustivity() { + final Iterator> permutationIterator = makeObject(); + + assertTrue(permutationIterator.hasNext()); + assertEquals(Arrays.asList('A', 'B', 'C'), permutationIterator.next()); + + assertTrue(permutationIterator.hasNext()); + assertEquals(Arrays.asList('A', 'C', 'B'), permutationIterator.next()); + + assertTrue(permutationIterator.hasNext()); + assertEquals(Arrays.asList('B', 'A', 'C'), permutationIterator.next()); + + assertTrue(permutationIterator.hasNext()); + assertEquals(Arrays.asList('B', 'C', 'A'), permutationIterator.next()); + + assertTrue(permutationIterator.hasNext()); + assertEquals(Arrays.asList('C', 'A', 'B'), permutationIterator.next()); + + assertTrue(permutationIterator.hasNext()); + assertEquals(Arrays.asList('C', 'B', 'A'), permutationIterator.next()); + + assertFalse(permutationIterator.hasNext()); + } + + @Test + void testRemoveThrows() { + final Iterator> permutationIterator = makeObject(); + + assertTrue(permutationIterator.hasNext()); + assertThrows(UnsupportedOperationException.class, permutationIterator::remove); + } + + @Test + void testStreamOfPermutations() { + final Iterable> iterable = this::makeObject; + + final List> allPermutations = StreamSupport.stream(iterable.spliterator(), false) + .collect(Collectors.toList()); + + assertEquals(Arrays.asList( + Arrays.asList('A', 'B', 'C'), + Arrays.asList('A', 'C', 'B'), + Arrays.asList('B', 'A', 'C'), + Arrays.asList('B', 'C', 'A'), + Arrays.asList('C', 'A', 'B'), + Arrays.asList('C', 'B', 'A')), allPermutations); + } + + /** + * test checking that iteration starts at the given arrangement rather than at the + * smallest one, so that a collection which is not sorted yields only the + * permutations that follow it. Sorting the input is the caller's responsibility, + * as it is for binarySearch. + */ + @Test + void testUnsortedCollectionStartsAtGivenArrangement() { + final Iterator> permutationIterator = new LexicographicPermutationIterator<>(Arrays.asList('B', 'A', 'C')); + + assertTrue(permutationIterator.hasNext()); + assertEquals(Arrays.asList('B', 'A', 'C'), permutationIterator.next()); + + assertTrue(permutationIterator.hasNext()); + assertEquals(Arrays.asList('B', 'C', 'A'), permutationIterator.next()); + + assertTrue(permutationIterator.hasNext()); + assertEquals(Arrays.asList('C', 'A', 'B'), permutationIterator.next()); + + assertTrue(permutationIterator.hasNext()); + assertEquals(Arrays.asList('C', 'B', 'A'), permutationIterator.next()); + + // the two permutations starting with 'A' precede the given arrangement + // and are therefore never returned + assertFalse(permutationIterator.hasNext()); + } + + /** + * test checking that the starting arrangement is honoured for a supplied comparator + * too, the permutations preceding it under that comparator being left out. + */ + @Test + void testUnsortedCollectionStartsAtGivenArrangementWithComparator() { + final Iterator> permutationIterator = new LexicographicPermutationIterator<>(Arrays.asList('B', 'C', 'A'), + Comparator.reverseOrder()); + + assertTrue(permutationIterator.hasNext()); + assertEquals(Arrays.asList('B', 'C', 'A'), permutationIterator.next()); + + assertTrue(permutationIterator.hasNext()); + assertEquals(Arrays.asList('B', 'A', 'C'), permutationIterator.next()); + + assertTrue(permutationIterator.hasNext()); + assertEquals(Arrays.asList('A', 'C', 'B'), permutationIterator.next()); + + assertTrue(permutationIterator.hasNext()); + assertEquals(Arrays.asList('A', 'B', 'C'), permutationIterator.next()); + + assertFalse(permutationIterator.hasNext()); + } + +}