From 90f4139bf75c94aa1ed62c5f64c3ffd78a1441d2 Mon Sep 17 00:00:00 2001 From: Thomas Neidhart Date: Thu, 1 Jan 2015 13:51:44 +0000 Subject: [PATCH] [COLLECTIONS-511] Added CollectionUtils.partition(...) methods. Thanks to Brent Worden, Nathan Blomquist. git-svn-id: https://svn.apache.org/repos/asf/commons/proper/collections/trunk@1648844 13f79535-47bb-0310-9956-ffa450edef68 --- src/changes/changes.xml | 4 + .../commons/collections4/CollectionUtils.java | 203 ++++++++++++++++++ .../collections4/CollectionUtilsTest.java | 88 ++++++++ 3 files changed, 295 insertions(+) diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 9dd06da85..608d2f055 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -22,6 +22,10 @@ + + Added new methods "CollectionUtils#partition(...)" to partition an input collection + into separate output collections based on evaluation of one or more predicates. + Harmonized signature of factory methods for functor-related classes which take a collection as input with their array counterparts. diff --git a/src/main/java/org/apache/commons/collections4/CollectionUtils.java b/src/main/java/org/apache/commons/collections4/CollectionUtils.java index 3f09ad0e7..86776d4d5 100644 --- a/src/main/java/org/apache/commons/collections4/CollectionUtils.java +++ b/src/main/java/org/apache/commons/collections4/CollectionUtils.java @@ -19,6 +19,7 @@ package org.apache.commons.collections4; import java.lang.reflect.Array; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.Comparator; import java.util.Enumeration; import java.util.HashMap; @@ -990,6 +991,208 @@ public class CollectionUtils { return outputCollection; } + /** + * Partitions all elements from inputCollection into separate output collections, + * based on the evaluation of the given predicate. + *

+ * For each predicate, the returned list will contain a collection holding + * all elements of the input collection matching the predicate. The last collection + * contained in the list will hold all elements which didn't match any predicate: + *

+     *  [C1, R] = partition(I, P1) with
+     *  I = input collection
+     *  P1 = first predicate
+     *  C1 = collection of elements matching P1
+     *  R = collection of elements rejected by all predicates
+     * 
+ *

+ * If the input collection is null, an empty list will be returned. + * If the input predicate is null, all elements of the input collection + * will be added to the rejected collection. + *

+ * Example: for an input list [1, 2, 3, 4, 5] calling partition with a predicate [x < 3] + * will result in the following output: [[1, 2], [3, 4, 5]]. + * + * @param the type of object the {@link Iterable} contains + * @param the type of the output {@link Collection} + * @param inputCollection the collection to get the input from, may be null + * @param predicate the predicate to use, may be null + * @return a list containing the output collections + * @since 4.1 + */ + public static > List partition(final Iterable inputCollection, + final Predicate predicate) { + + @SuppressWarnings("unchecked") // safe + final Class outputClass = (Class) ArrayList.class; + @SuppressWarnings("unchecked") // safe + final Predicate[] predicates = new Predicate[] { predicate }; + return partition(inputCollection, FactoryUtils.instantiateFactory(outputClass), predicates); + } + + /** + * Partitions all elements from inputCollection into an output and rejected collection, + * based on the evaluation of the given predicate. + *

+ * Elements matching the predicate are added to the outputCollection, + * all other elements are added to the rejectedCollection. + *

+ * If the input predicate is null, no elements are added to + * outputCollection or rejectedCollection. + *

+ * Note: calling the method is equivalent to the following code snippet: + *

+     *   select(inputCollection, predicate, outputCollection);
+     *   selectRejected(inputCollection, predicate, rejectedCollection);
+     * 
+ * + * @param the type of object the {@link Iterable} contains + * @param the type of the output {@link Collection} + * @param inputCollection the collection to get the input from, may be null + * @param predicate the predicate to use, may be null + * @param outputCollection the collection to output selected elements into, may not be null if the + * inputCollection and predicate are not null + * @param rejectedCollection the collection to output rejected elements into, may not be null if the + * inputCollection or predicate are not null + * @since 4.1 + */ + public static > void partition(final Iterable inputCollection, + final Predicate predicate, R outputCollection, R rejectedCollection) { + + if (inputCollection != null && predicate != null) { + for (final O element : inputCollection) { + if (predicate.evaluate(element)) { + outputCollection.add(element); + } else { + rejectedCollection.add(element); + } + } + } + } + + /** + * Partitions all elements from inputCollection into separate output collections, + * based on the evaluation of the given predicates. + *

+ * For each predicate, the returned list will contain a collection holding + * all elements of the input collection matching the predicate. The last collection + * contained in the list will hold all elements which didn't match any predicate: + *

+     *  [C1, C2, R] = partition(I, P1, P2) with
+     *  I = input collection
+     *  P1 = first predicate
+     *  P2 = second predicate
+     *  C1 = collection of elements matching P1
+     *  C2 = collection of elements matching P2
+     *  R = collection of elements rejected by all predicates
+     * 
+ *

+ * Note: elements are only added to the output collection of the first matching + * predicate, determined by the order of arguments. + *

+ * If the input collection is null, an empty list will be returned. + * If the input predicate is null, all elements of the input collection + * will be added to the rejected collection. + *

+ * Example: for an input list [1, 2, 3, 4, 5] calling partition with predicates [x < 3] + * and [x < 5] will result in the following output: [[1, 2], [3, 4], [5]]. + * + * @param the type of object the {@link Iterable} contains + * @param the type of the output {@link Collection} + * @param inputCollection the collection to get the input from, may be null + * @param predicates the predicates to use, may be null + * @return a list containing the output collections + * @since 4.1 + */ + public static > List partition(final Iterable inputCollection, + final Predicate... predicates) { + + @SuppressWarnings("unchecked") // safe + final Class outputClass = (Class) ArrayList.class; + return partition(inputCollection, FactoryUtils.instantiateFactory(outputClass), predicates); + } + + /** + * Partitions all elements from inputCollection into separate output collections, + * based on the evaluation of the given predicates. + *

+ * For each predicate, the returned list will contain a collection holding + * all elements of the input collection matching the predicate. The last collection + * contained in the list will hold all elements which didn't match any predicate: + *

+     *  [C1, C2, R] = partition(I, P1, P2) with
+     *  I = input collection
+     *  P1 = first predicate
+     *  P2 = second predicate
+     *  C1 = collection of elements matching P1
+     *  C2 = collection of elements matching P2
+     *  R = collection of elements rejected by all predicates
+     * 
+ *

+ * Note: elements are only added to the output collection of the first matching + * predicate, determined by the order of arguments. + *

+ * If the input collection is null, an empty list will be returned. + * If no predicates have been provided, all elements of the input collection + * will be added to the rejected collection. + *

+ * Example: for an input list [1, 2, 3, 4, 5] calling partition with predicates [x < 3] + * and [x < 5] will result in the following output: [[1, 2], [3, 4], [5]]. + * + * @param the type of object the {@link Iterable} contains + * @param the type of the output {@link Collection} + * @param inputCollection the collection to get the input from, may be null + * @param partitionFactory the factory used to create the output collections + * @param predicates the predicates to use, may be empty + * @return a list containing the output collections + * @since 4.1 + */ + public static > List partition(final Iterable inputCollection, + final Factory partitionFactory, final Predicate... predicates) { + + if (inputCollection == null) { + return Collections.emptyList(); + } + + if (predicates == null || predicates.length < 1) { + // return the entire input collection as a single partition + final R singlePartition = partitionFactory.create(); + select(inputCollection, PredicateUtils.truePredicate(), singlePartition); + return Collections.singletonList(singlePartition); + } + + // create the empty partitions + final int numberOfPredicates = predicates.length; + final int numberOfPartitions = numberOfPredicates + 1; + final List partitions = new ArrayList(numberOfPartitions); + for (int i = 0; i < numberOfPartitions; ++i) { + partitions.add(partitionFactory.create()); + } + + // for each element in inputCollection: + // find the first predicate that evaluates to true. + // if there is a predicate, add the element to the corresponding partition. + // if there is no predicate, add it to the last, catch-all partition. + for (final O element : inputCollection) { + boolean elementAssigned = false; + for (int i = 0; i < numberOfPredicates; ++i) { + if (predicates[i].evaluate(element)) { + partitions.get(i).add(element); + elementAssigned = true; + break; + } + } + + if (!elementAssigned) { + // no predicates evaluated to true + // add element to last partition + partitions.get(numberOfPredicates).add(element); + } + } + + return partitions; + } + /** * Returns a new Collection consisting of the elements of inputCollection * transformed by the given transformer. diff --git a/src/test/java/org/apache/commons/collections4/CollectionUtilsTest.java b/src/test/java/org/apache/commons/collections4/CollectionUtilsTest.java index a91a6d146..a9c7f741f 100644 --- a/src/test/java/org/apache/commons/collections4/CollectionUtilsTest.java +++ b/src/test/java/org/apache/commons/collections4/CollectionUtilsTest.java @@ -50,6 +50,7 @@ import org.apache.commons.collections4.collection.TransformedCollection; import org.apache.commons.collections4.collection.UnmodifiableCollection; import org.apache.commons.collections4.functors.DefaultEquator; import org.apache.commons.collections4.queue.CircularFifoQueue; +import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -1067,6 +1068,12 @@ public class CollectionUtilsTest extends MockTestCase { } }; + private static Predicate EVEN = new Predicate() { + public boolean evaluate(final Number input) { + return input.intValue() % 2 == 0; + } + }; + //Up to here @Test public void filter() { @@ -1178,6 +1185,87 @@ public class CollectionUtilsTest extends MockTestCase { assertTrue(output1.contains(4L)); } + @Test + public void partition() { + List input = new ArrayList(); + input.add(1); + input.add(2); + input.add(3); + input.add(4); + List> partitions = CollectionUtils.partition(input, EQUALS_TWO); + assertEquals(2, partitions.size()); + + // first partition contains 2 + Collection partition = partitions.get(0); + assertEquals(1, partition.size()); + assertEquals(2, CollectionUtils.extractSingleton(partition).intValue()); + + // second partition contains 1, 3, and 4 + Integer[] expected = {1, 3, 4}; + partition = partitions.get(1); + Assert.assertArrayEquals(expected, partition.toArray()); + + partitions = CollectionUtils.partition((List) null, EQUALS_TWO); + assertTrue(partitions.isEmpty()); + + partitions = CollectionUtils.partition(input); + assertEquals(1, partitions.size()); + assertEquals(input, partitions.get(0)); + } + + @Test + public void partitionWithOutputCollections() { + List input = new ArrayList(); + input.add(1); + input.add(2); + input.add(3); + input.add(4); + + List output = new ArrayList(); + List rejected = new ArrayList(); + + CollectionUtils.partition(input, EQUALS_TWO, output, rejected); + + // output contains 2 + assertEquals(1, output.size()); + assertEquals(2, CollectionUtils.extractSingleton(output).intValue()); + + // rejected contains 1, 3, and 4 + Integer[] expected = {1, 3, 4}; + Assert.assertArrayEquals(expected, rejected.toArray()); + + output.clear(); + rejected.clear(); + CollectionUtils.partition((List) null, EQUALS_TWO, output, rejected); + assertTrue(output.isEmpty()); + assertTrue(rejected.isEmpty()); + } + + @Test + public void partitionMultiplePredicates() { + List input = new ArrayList(); + input.add(1); + input.add(2); + input.add(3); + input.add(4); + List> partitions = CollectionUtils.partition(input, EQUALS_TWO, EVEN); + + // first partition contains 2 + Collection partition = partitions.get(0); + assertEquals(1, partition.size()); + assertEquals(2, partition.iterator().next().intValue()); + + // second partition contains 4 + partition = partitions.get(1); + assertEquals(1, partition.size()); + assertEquals(4, partition.iterator().next().intValue()); + + // third partition contains 1 and 3 + Integer[] expected = {1, 3}; + partition = partitions.get(2); + Assert.assertArrayEquals(expected, partition.toArray()); + } + @Test public void collect() { final Transformer transformer = TransformerUtils.constantTransformer(2L);