The Stream.flatMap()
operation transforms a stream of
a, b, c
into a stream that contains zero or more elements for each input el
IntStream.range(0, 10)
.mapToObj(n -> IntStream.of(n, n / 2, n / 3))
.reduce(IntStream.empty(), IntStream::concat)
.forEach(System.out::println);
As you see elements are mapped to Streams too, and then concatenated into one large stream.
This is what I came up with:
interface OptionalBinaryOperator<T> extends BiFunction<T, T, Optional<T>> {
static <T> OptionalBinaryOperator<T> of(BinaryOperator<T> binaryOperator,
BiPredicate<T, T> biPredicate) {
return (t1, t2) -> biPredicate.test(t1, t2)
? Optional.of(binaryOperator.apply(t1, t2))
: Optional.empty();
}
}
class StreamUtils {
public static <T> Stream<T> reducePartially(Stream<T> stream,
OptionalBinaryOperator<T> conditionalAccumulator) {
Stream.Builder<T> builder = Stream.builder();
stream.reduce((t1, t2) -> conditionalAccumulator.apply(t1, t2).orElseGet(() -> {
builder.add(t1);
return t2;
})).ifPresent(builder::add);
return builder.build();
}
}
Unfortunately, I did not have the time to make it lazy, but it can be done by writing a custom Spliterator
delegating to stream.spliterator()
that would follow the logic above (instead of utilizing stream.reduce()
, which is a terminal operation).
PS. I just realized you wanted <T,U>
conversion, and I wrote about <T,T>
conversion. If you can first map from T
to U
, and then use the function above, then that's it (even if it's suboptimal).
If it's something more complex, the kind of condition for reducing/merging would need to be defined before proposing an API (e.g. Predicate<T>
, BiPredicate<T,T>
, BiPredicate<U,T>
, or maybe even Predicate<List<T>>
).
Take a look at collapse
in StreamEx
StreamEx.of("a1", "a2", "c1", "c2", "c3").collapse((a, b) -> a.charAt(0) == b.charAt(0))
.map(e -> e.substring(0, 1)).forEach(System.out::println);
Or my fork with more function: groupBy
, split
, sliding
...
StreamEx.of("a1", "a2", "c1", "c2", "c3").collapse((a, b) -> a.charAt(0) == b.charAt(0))
.map(e -> e.substring(0, 1)).forEach(System.out::println);
// a
// c
StreamEx.of("a1", "a2", "c1", "c2", "c3").splitToList(2).forEach(System.out::println);
// [a1, a2]
// [c1, c2]
// [c3]
StreamEx.of("a1", "a2", "c1", "c2", "c3").groupBy(e -> e.charAt(0))
.forEach(System.out::println);
// a=[a1, a2]
// c=[c1, c2, c3]
Finally I figured out that flatMap
is its own "inverse" so to say. I oversaw that flatMap
not necessarily increases the number of elements. It may also decrease the number of elements by emitting an empty stream for some of the elements. To implement a group-by operation, the function called by flatMap
needs minimal internal state, namely the most recent element. It either returns an empty stream or, at the end of a group, it returns the reduced-to group representative.
Here is a quick implementation where groupBorder
must return true
if the two elements passed in do not belong to the same group, i.e. between them is the group border. The combiner
is the group function that combines, for example (1,a), (1,a), (1,a) into (3,a), given that your group elements are, tuples (int, string).
public class GroupBy<X> implements Function<X, Stream<X>>{
private final BiPredicate<X, X> groupBorder;
private final BinaryOperator<X> combiner;
private X latest = null;
public GroupBy(BiPredicate <X, X> groupBorder,
BinaryOperator<X> combiner) {
this.groupBorder = groupBorder;
this.combiner = combiner;
}
@Override
public Stream<X> apply(X elem) {
// TODO: add test on end marker as additonal parameter for constructor
if (elem==null) {
return latest==null ? Stream.empty() : Stream.of(latest);
}
if (latest==null) {
latest = elem;
return Stream.empty();
}
if (groupBorder.test(latest, elem)) {
Stream<X> result = Stream.of(latest);
latest = elem;
return result;
}
latest = combiner.apply(latest, elem);
return Stream.empty();
}
}
There is one caveat though: to ship the last group of the whole stream, an end marker must be stuck as the last element into the stream. The above code assumes it is null
, but an additional end-marker-tester could be added.
I could not come up with a solution that does not rely on the end marker.
Further I did not also convert between incoming and outgoing elements. For a unique-operation, this would just work. For a count-operation, a previous step would have to map individual elements to a counting object.
You can hack your way around. See the following example:
Stream<List<String>> stream = Stream.of("Cat", "Dog", "Whale", "Mouse")
.collect(Collectors.collectingAndThen(
Collectors.partitioningBy(a -> a.length() > 3),
map -> Stream.of(map.get(true), map.get(false))
));