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..3389db68b2 --- /dev/null +++ b/src/main/java/org/apache/commons/collections4/iterators/LexicographicPermutationIterator.java @@ -0,0 +1,198 @@ +/* + * 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. + *

+ * The iterator might return fewer than n! permutations of the input collection, + * 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. + *

+ * + * @param the type of the objects being permuted + * @see PermutationIterator + * @since 4.6.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 + * 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. + * + * @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. + * + * @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 permutation 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. + * + * @return A list of the permutator's elements representing a permutation + * @throws NoSuchElementException if there are no more permutations + */ + @Override + public List next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + + final int size = nextPermutation.size(); + List nextP = null; + + // find the pivot: the rightmost element that is smaller than its successor. + // if there is none the current permutation is the last one in lexicographical order + int i = size - 2; + while (i >= 0 && compareElements(nextPermutation.get(i), nextPermutation.get(i + 1)) >= 0) { + --i; + } + + if (i >= 0) { + // 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(nextPermutation.get(i), nextPermutation.get(j)) >= 0) { + --j; + } + + // swap the pivot with its successor, then reverse the descending tail + // into ascending order to obtain the smallest larger permutation + nextP = new ArrayList<>(nextPermutation); + Collections.swap(nextP, i, j); + final List subList = nextP.subList(i + 1, nextP.size()); + Collections.reverse(subList); + } + + final List result = nextPermutation; + nextPermutation = nextP; + return result; + } + + /** + * Always throws {@link UnsupportedOperationException}. + * + * @throws UnsupportedOperationException Always thrown. + */ + @Override + public void remove() { + throw new UnsupportedOperationException("remove() is not supported"); + } + + /** + * Compares this iterator to another for equality. Two iterators are equal when + * they use equal comparators and are positioned at an equal next permutation. + * + * @param o The object to compare to this instance + * @return true if the given object is an equal iterator, otherwise false + */ + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + + if (o == null || getClass() != o.getClass()) { + return false; + } + + final LexicographicPermutationIterator that = (LexicographicPermutationIterator) o; + return Objects.equals(comparator, that.comparator) && Objects.equals(nextPermutation, that.nextPermutation); + } + + /** + * Returns a hash code consistent with {@link #equals(Object)}. Note that the + * hash code changes as the iterator advances. + * + * @return A hash code for this instance + */ + @Override + public int hashCode() { + return Objects.hash(comparator, nextPermutation); + } + + /** + * 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); + } + +} 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..c20dd8a13b --- /dev/null +++ b/src/test/java/org/apache/commons/collections4/iterators/LexicographicPermutationIteratorTest.java @@ -0,0 +1,343 @@ +/* + * 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 java.util.Collections.emptyList; +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 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 comparator that orders nothing, identified only by an id, used to check that + * equal comparators make equal iterators. + * + * @param the type of the objects compared + */ + private static final class CustomComparator implements Comparator { + + private final int id; + + CustomComparator(final int id) { + this.id = id; + } + + @Override + public int compare(final T o1, final T o2) { + return 0; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + + if (o == null || getClass() != o.getClass()) { + return false; + } + + final CustomComparator cmp = (CustomComparator) o; + return id == cmp.id; + } + + @Override + public int hashCode() { + return id; + } + } + + /** + * 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 + 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 + void testEqualsForEqualCollections() { + final Iterator> one = new LexicographicPermutationIterator<>(emptyList()); + final Iterator> another = new LexicographicPermutationIterator<>(emptyList()); + + assertEquals(one, another); + } + + @Test + void testEqualsForEqualCollectionsAndComparators() { + final Iterator> one = new LexicographicPermutationIterator<>(emptyList(), new CustomComparator<>(42)); + final Iterator> another = new LexicographicPermutationIterator<>(emptyList(), new CustomComparator<>(42)); + + assertEquals(one, another); + } + + @Test + void testHashCodeForEqualCollections() { + final Iterator> one = new LexicographicPermutationIterator<>(emptyList()); + final Iterator> another = new LexicographicPermutationIterator<>(emptyList()); + + assertEquals(one.hashCode(), another.hashCode()); + } + + @Test + void testHashCodeForEqualCollectionsAndComparators() { + final Iterator> one = new LexicographicPermutationIterator<>(emptyList(), new CustomComparator<>(42)); + final Iterator> another = new LexicographicPermutationIterator<>(emptyList(), new CustomComparator<>(42)); + + assertEquals(one.hashCode(), another.hashCode()); + } + + @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 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); + } + +}