From 4557a452168daff3020f477e7b395af86070b37e Mon Sep 17 00:00:00 2001 From: ClaudioConsolmagno Date: Thu, 4 Nov 2021 20:54:12 +0000 Subject: [PATCH] [COLLECTIONS-800] Adding partitionBalanced(List,int) method --- .../commons/collections4/ListUtils.java | 67 +++++++- .../commons/collections4/ListUtilsTest.java | 160 +++++++++++++++++- 2 files changed, 217 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/apache/commons/collections4/ListUtils.java b/src/main/java/org/apache/commons/collections4/ListUtils.java index dfca882384..382dad0716 100644 --- a/src/main/java/org/apache/commons/collections4/ListUtils.java +++ b/src/main/java/org/apache/commons/collections4/ListUtils.java @@ -101,10 +101,12 @@ public void visitKeepCommand(final E object) { private static class Partition extends AbstractList> { private final List list; private final int size; + private final boolean isBalanced; - private Partition(final List list, final int size) { + private Partition(final List list, final int size, final boolean isBalanced) { this.list = list; this.size = size; + this.isBalanced = isBalanced; } @Override @@ -117,9 +119,25 @@ public List get(final int index) { throw new IndexOutOfBoundsException("Index " + index + " must be less than size " + listSize); } - final int start = index * size; - final int end = Math.min(start + size, list.size()); - return list.subList(start, end); + int start; + int currentPartitionSize; + if (isBalanced) { + // evenly distribute partitions + currentPartitionSize = (int) Math.ceil((double) list.size() / (double) listSize); + start = index * currentPartitionSize; + // remainder of above is the threshold for which indices greater than will have one less element + final int threshold = (list.size() % listSize); + // when currentPartitionSize is 1 or threshold is 0 there's nothing to balance + // when index hasn't crossed the threshold we don't need balancing yet + if (currentPartitionSize > 1 && threshold > 0 && index >= threshold) { + start -= (index - threshold); // adjust start as partitions before threshold have one less element + currentPartitionSize--; // currentPartitionSize is decremented as threshold is crossed + } + } else { + start = index * size; + currentPartitionSize = Math.min(size, list.size() - start); + } + return list.subList(start, start + currentPartitionSize); } @Override @@ -486,13 +504,52 @@ public static List longestCommonSubsequence(final List listA, final Li * @throws NullPointerException if list is null * @throws IllegalArgumentException if size is not strictly positive * @since 4.0 + * @see ListUtils#partitionBalanced(List, int) */ public static List> partition(final List list, final int size) { Objects.requireNonNull(list, "list"); if (size <= 0) { throw new IllegalArgumentException("Size must be greater than 0"); } - return new Partition<>(list, size); + return new Partition<>(list, size, false); + } + + /** + * Returns consecutive {@link List#subList(int, int) sublists} of a + * list, partitioned in a way to balance entries across all sublists. For example, + * partitioning a list containing {@code [a, b, c, d, e]} with a partition + * size of 3 yields {@code [[a, b, c], [d, e]]} -- an outer list containing + * two inner lists of three and two elements, all in the original order. Partitioning + * the same input list with a partition size of 4 also yields {@code [[a, b, c], [d, e]]} + * since putting 4 elements in the first partition makes it unbalanced as the last + * partition would only have 1 element. + *

+ * The outer list is unmodifiable, but reflects the latest state of the + * source list. The inner lists are sublist views of the original list, + * produced on demand using {@link List#subList(int, int)}, and are subject + * to all the usual caveats about modification as explained in that API. + * The size of the produced list is always equals to the size of the list + * produced by using the same input with the {@link ListUtils#partition(List, int)} + * method. + *

+ * Adapted from http://code.google.com/p/guava-libraries/ + * + * @param the element type + * @param list the list to return consecutive balanced sublists of + * @param size the desired maximum size of each sublist + * @return a list of consecutive sublists balanced to have maximum size difference + * of 1 between them + * @throws NullPointerException if list is null + * @throws IllegalArgumentException if size is not strictly positive + * @since 4.5 + * @see ListUtils#partition(List, int) + */ + public static List> partitionBalanced(final List list, final int size) { + Objects.requireNonNull(list, "list"); + if (size <= 0) { + throw new IllegalArgumentException("Size must be greater than 0"); + } + return new Partition<>(list, size, true); } /** diff --git a/src/test/java/org/apache/commons/collections4/ListUtilsTest.java b/src/test/java/org/apache/commons/collections4/ListUtilsTest.java index 59d6860bed..758ad66bdc 100644 --- a/src/test/java/org/apache/commons/collections4/ListUtilsTest.java +++ b/src/test/java/org/apache/commons/collections4/ListUtilsTest.java @@ -24,9 +24,13 @@ import java.util.Collections; import java.util.HashSet; import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; import org.apache.commons.collections4.functors.EqualPredicate; import org.apache.commons.collections4.list.PredicatedList; +import org.apache.commons.lang3.tuple.Triple; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -323,19 +327,45 @@ public void testLongestCommonSubsequenceWithString() { } @Test - @SuppressWarnings("boxing") // OK in test code public void testPartition() { - final List strings = new ArrayList<>(); - for (int i = 0; i <= 6; i++) { - strings.add(i); - } + final List strings = IntStream.rangeClosed(0, 6).boxed().collect(Collectors.toList()); + // [0,1,2,3,4,5,6] -> [[0,1,2],[3,4,5],[6]] final List> partition = ListUtils.partition(strings, 3); assertNotNull(partition); assertEquals(3, partition.size()); + assertEquals(3, partition.get(0).size()); + assertEquals(3, partition.get(1).size()); assertEquals(1, partition.get(2).size()); + // [0,1,2,3,4,5,6] -> [[0,1,2,3],[4,5,6]] + final List> partition4 = ListUtils.partition(strings, 4); + assertNotNull(partition4); + assertEquals(2, partition4.size()); + assertEquals(4, partition4.get(0).size()); + assertEquals(3, partition4.get(1).size()); + + // [0,1,2,3,4,5,6] -> [[0,1,2,3,4],[5,6]] + final List> partition5 = ListUtils.partition(strings, 5); + assertNotNull(partition5); + assertEquals(2, partition5.size()); + assertEquals(5, partition5.get(0).size()); + assertEquals(2, partition5.get(1).size()); + + // [0,1,2,3,4,5,6] -> [[0,1,2,3,4,5],[6]] + final List> partition6 = ListUtils.partition(strings, 6); + assertNotNull(partition6); + assertEquals(2, partition6.size()); + assertEquals(6, partition6.get(0).size()); + assertEquals(1, partition6.get(1).size()); + + // [0,1,2,3,4,5,6] -> [[0,1,2,3,4,5,6]] + final List> partition7 = ListUtils.partition(strings, 7); + assertNotNull(partition7); + assertEquals(1, partition7.size()); + assertEquals(7, partition7.get(0).size()); + try { ListUtils.partition(null, 3); fail("failed to check for null argument"); @@ -351,10 +381,130 @@ public void testPartition() { fail("failed to check for size argument"); } catch (final IllegalArgumentException e) {} + final List> partitionMin = ListUtils.partition(strings, 1); + assertEquals(strings.size(), partitionMin.size()); + for (int i = 0; i < strings.size(); i++) { + assertEquals(1, partitionMin.get(i).size()); + assertEquals(strings.get(i), partitionMin.get(i).get(0)); + } + final List> partitionMax = ListUtils.partition(strings, Integer.MAX_VALUE); assertEquals(1, partitionMax.size()); assertEquals(strings.size(), partitionMax.get(0).size()); assertEquals(strings, partitionMax.get(0)); + + // Edge case: partitioning empty list doesn't throw exception + IntStream.rangeClosed(1, 5).forEach(it -> { + final List> partitionEmptyList = ListUtils.partition(new ArrayList<>(), it); + assertEquals(0, partitionEmptyList.size()); + assertTrue(partitionEmptyList.isEmpty()); + }); + + // Edge case: partitioning single value list doesn't throw exception + IntStream.rangeClosed(1, 5).forEach(it -> { + final List> partitionSingleElemList = ListUtils.partition(Collections.singletonList("42"), it); + assertFalse(partitionSingleElemList.isEmpty()); + assertEquals(1, partitionSingleElemList.size()); + assertEquals("42", partitionSingleElemList.get(0).get(0)); + }); + } + + @Test + public void testPartitionBalanced() { + final List evenList = IntStream.rangeClosed(0, 9).mapToObj(it -> it + "").collect(Collectors.toList()); + final List oddList = IntStream.rangeClosed(0, 12).mapToObj(it -> it + "").collect(Collectors.toList()); + Stream.of( + Triple.of(evenList, "0|1|2|3|4|5|6|7|8|9", 1), + Triple.of(evenList, "0,1|2,3|4,5|6,7|8,9", 2), + Triple.of(evenList, "0,1,2|3,4,5|6,7|8,9", 3), // balanced + Triple.of(evenList, "0,1,2,3|4,5,6|7,8,9", 4), // balanced + Triple.of(evenList, "0,1,2,3,4|5,6,7,8,9", 5), + Triple.of(evenList, "0,1,2,3,4|5,6,7,8,9", 6), // balanced + Triple.of(evenList, "0,1,2,3,4|5,6,7,8,9", 7), // balanced + Triple.of(evenList, "0,1,2,3,4|5,6,7,8,9", 8), // balanced + Triple.of(evenList, "0,1,2,3,4|5,6,7,8,9", 9), // balanced + Triple.of(evenList, "0,1,2,3,4,5,6,7,8,9", 10), + Triple.of(evenList, "0,1,2,3,4,5,6,7,8,9", 11), + Triple.of(evenList, "0,1,2,3,4,5,6,7,8,9", 12), + Triple.of(oddList, "0|1|2|3|4|5|6|7|8|9|10|11|12", 1), + Triple.of(oddList, "0,1|2,3|4,5|6,7|8,9|10,11|12", 2), + Triple.of(oddList, "0,1,2|3,4,5|6,7,8|9,10|11,12", 3), // balanced + Triple.of(oddList, "0,1,2,3|4,5,6|7,8,9|10,11,12", 4), // balanced + Triple.of(oddList, "0,1,2,3,4|5,6,7,8|9,10,11,12", 5), // balanced + Triple.of(oddList, "0,1,2,3,4|5,6,7,8|9,10,11,12", 6), // balanced + Triple.of(oddList, "0,1,2,3,4,5,6|7,8,9,10,11,12", 7), + Triple.of(oddList, "0,1,2,3,4,5,6|7,8,9,10,11,12", 8), // balanced + Triple.of(oddList, "0,1,2,3,4,5,6|7,8,9,10,11,12", 9), // balanced + Triple.of(oddList, "0,1,2,3,4,5,6|7,8,9,10,11,12", 10), // balanced + Triple.of(oddList, "0,1,2,3,4,5,6|7,8,9,10,11,12", 11), // balanced + Triple.of(oddList, "0,1,2,3,4,5,6|7,8,9,10,11,12", 12), // balanced + Triple.of(oddList, "0,1,2,3,4,5,6,7,8,9,10,11,12", 13), + Triple.of(oddList, "0,1,2,3,4,5,6,7,8,9,10,11,12", 14) + ) + .forEach(testCase -> { + final Integer partitionSize = testCase.getRight(); + final String expectedResult = testCase.getMiddle(); + final List> partition = ListUtils.partitionBalanced(testCase.getLeft(), partitionSize); + final List expectedPartitions = Arrays.asList(expectedResult.split("\\|")); + assertEquals(expectedPartitions.size(), partition.size()); + for (int i = 0; i < expectedPartitions.size(); i++) { + assertArrayEquals(expectedPartitions.get(i).split(","), partition.get(i).toArray()); + } + }); + + try { + ListUtils.partitionBalanced(null, 3); + fail("failed to check for null argument"); + } catch (final NullPointerException ignored) {} + + try { + ListUtils.partitionBalanced(oddList, 0); + fail("failed to check for size argument"); + } catch (final IllegalArgumentException ignored) {} + + try { + ListUtils.partitionBalanced(oddList, -10); + fail("failed to check for size argument"); + } catch (final IllegalArgumentException ignored) {} + + final List> lists = ListUtils.partitionBalanced(oddList, 10); + try { + lists.get(-1); + fail("failed to check for index out of bounds"); + } catch (final IndexOutOfBoundsException ignored) {} + + try { + lists.get(oddList.size()); + fail("failed to check for index out of bounds"); + } catch (final IndexOutOfBoundsException ignored) {} + + final List> partitionMin = ListUtils.partitionBalanced(oddList, 1); + assertEquals(oddList.size(), partitionMin.size()); + for (int i = 0; i < oddList.size(); i++) { + assertEquals(1, partitionMin.get(i).size()); + assertEquals(oddList.get(i), partitionMin.get(i).get(0)); + } + + final List> partitionMax = ListUtils.partitionBalanced(oddList, Integer.MAX_VALUE); + assertEquals(1, partitionMax.size()); + assertEquals(oddList.size(), partitionMax.get(0).size()); + assertEquals(oddList, partitionMax.get(0)); + + // Edge case: partitioning empty list doesn't throw exception + IntStream.rangeClosed(1, 5).forEach(it -> { + final List> partitionEmptyList = ListUtils.partitionBalanced(new ArrayList<>(), it); + assertEquals(0, partitionEmptyList.size()); + assertTrue(partitionEmptyList.isEmpty()); + }); + + // Edge case: partitioning single value list doesn't throw exception + IntStream.rangeClosed(1, 5).forEach(it -> { + final List> partitionSingleElemList = + ListUtils.partitionBalanced(Collections.singletonList("42"), it); + assertFalse(partitionSingleElemList.isEmpty()); + assertEquals(1, partitionSingleElemList.size()); + assertEquals("42", partitionSingleElemList.get(0).get(0)); + }); } @Test