My list contains sets like [1,3,5][2,6,4]
etc, all of the same size.
I tried doing this but it doesn\'t seem to work.
List&g
If you have guava
on the classpath this is a breeze:
block
.stream()
.flatMap(Set::stream)
.collect(Collectors.toCollection(TreeSet::new));
Iterable<List<Integer>> result = Iterables.partition(sorted, 3);
@Eugene's answer is sweet, because Guava is sweet. But if you happen to not have Guava in your classpath, here's another way:
List<Set<Integer>> list = block.stream()
.flatMap(Set::stream)
.sorted()
.collect(partitioning(3));
First, I'm flatmapping all the sets into one stream, then I'm sorting all the elements and finally, I'm collecting the whole sorted stream to a list of sets. For this, I'm invoking a helper method that uses a custom collector:
private static <T> Collector<T, ?, List<Set<T>>> partitioning(int size) {
class Acc {
int count = 0;
List<Set<T>> list = new ArrayList<>();
void add(T elem) {
int index = count++ / size;
if (index == list.size()) list.add(new LinkedHashSet<>());
list.get(index).add(elem);
}
Acc merge(Acc another) {
another.list.stream().flatMap(Set::stream).forEach(this::add);
return this;
}
}
return Collector.of(Acc::new, Acc::add, Acc::merge, acc -> acc.list);
}
The method receives the size of each partition and uses the Acc
local class as the mutable structure to be used by the collector. Inside the Acc
class, I'm using a List
that will contain LinkedHashSet
instances, which will hold the elements of the stream.
The Acc
class keeps the count of all the elements that have been already collected. In the add
method, I calculate the index of the list and increment this count, and if there was no set in that position of the list, I append a new empty LinkedHashSet
to it. Then, I add the element to the set.
As I'm calling sorted()
on the stream to sort its elements before collecting, I need to use data structures that preserve insertion order. This is why I'm using ArrayList
for the outer list and LinkedHashSet
for the inner sets.
The merge
method is to be used by parallel streams, to merge two previously accumulated Acc
instances. I'm just adding all the elements of the received Acc
instance to this Acc
instance, by delegating to the add
method.
Finally, I'm using Collector.of to create a collector based on the methods of the Acc
class. The last argument is a finisher function, which just returns the Acc
instance's list.
Adding another answer since this would be bigger than a comment. It's really what the accepted answer has done, but with a "smarter" combiner that does not have to stream all the time again.
private static <T> Collector<T, ?, List<Set<T>>> partitioning(int size) {
class Acc {
int count = 0;
List<List<T>> list = new ArrayList<>();
void add(T elem) {
int index = count++ / size;
if (index == list.size()) {
list.add(new ArrayList<>());
}
list.get(index).add(elem);
}
Acc merge(Acc right) {
List<T> lastLeftList = list.get(list.size() - 1);
List<T> firstRightList = right.list.get(0);
int lastLeftSize = lastLeftList.size();
int firstRightSize = firstRightList.size();
// they have both the same size, simply addAll will work
if (lastLeftSize + firstRightSize == 2 * size) {
System.out.println("Perfect!");
list.addAll(right.list);
return this;
}
// last and first from each chunk are merged "perfectly"
if (lastLeftSize + firstRightSize == size) {
System.out.println("Almost perfect");
int x = 0;
while (x < firstRightSize) {
lastLeftList.add(firstRightList.remove(x));
--firstRightSize;
}
right.list.remove(0);
list.addAll(right.list);
return this;
}
right.list.stream().flatMap(List::stream).forEach(this::add);
return this;
}
public List<Set<T>> finisher() {
return list.stream().map(LinkedHashSet::new).collect(Collectors.toList());
}
}
return Collector.of(Acc::new, Acc::add, Acc::merge, Acc::finisher);
}