slashms
slashms

Reputation: 978

Combine allMatch, noneMatch and anyMatch on a single stream

I would like to have the following logic: (i know it doesn't work because it consumes the stream more than once). but i am not sure how to achieve it.

Stream<ByteBuffer> buffers = super.getBuffers().stream();
if (buffers.allMatch(b -> b.position() > 0)) {
    return OutgoingMessageStatus.FULLY_SENT;
} else if (buffers.noneMatch(b -> b.position() > 0)) {
    return OutgoingMessageStatus.WAS_NOT_SENT;
} else {
    return OutgoingMessageStatus.PARTIALLY_SENT;
}

How can I do that?

Upvotes: 6

Views: 8649

Answers (4)

jakub.sadluck
jakub.sadluck

Reputation: 11

I was just trying to solve the same question you had just with different type and predicate. Haven't found any satisfying answer so I implemented myself this way:

Enum to hold not just true and false for satisfying the predicate but also NONE (to be used for empty Collections) and ENTANGLED (to represent that some elements of collection satisfy the predicate and some do not):

public enum QuantumBoolean {
    NONE, TRUE, FALSE, ENTANGLED;

    public static QuantumBoolean fromBoolean(boolean bool) {
        if (bool) return QuantumBoolean.TRUE;
        return QuantumBoolean.FALSE;
    }
}

To leverage the Java Stream API and to iterate over the collection just once I needed to call .reduce() with specific arguments, so I packed them into new class:

public class QuantumBooleanReducer<T> {

    private final Predicate<T> predicate;
    public static final QuantumBoolean IDENTITY = QuantumBoolean.NONE;

    public QuantumBooleanReducer(Predicate<T> predicate) {
        this.predicate = predicate;
    }

    public final QuantumBoolean accumulator(QuantumBoolean state, T element) {
        QuantumBoolean newState = QuantumBoolean.fromBoolean(predicate.test(element));
        if (QuantumBoolean.NONE.equals(state))
            return newState;
        if (newState.equals(state))
            return state;
        return QuantumBoolean.ENTANGLED;
    }

    public static QuantumBoolean combiner(QuantumBoolean state1, QuantumBoolean state2) {
        if (state1.equals(state2))
            return state1;
        if (QuantumBoolean.NONE.equals(state1))
            return state2;
        if (QuantumBoolean.NONE.equals(state2))
            return state1;
        return QuantumBoolean.ENTANGLED;
    }
}

Then you could solve your problem by calling something like:

switch(buffers.stream()
              .reduce(QuantumBooleanReducer.IDENTITY,
                      new QuantumBooleanReducer<ByteBuffer>(buffer -> buffer.position() > 0)::accumulator,
                      QuantumBooleanReducer::combiner)) {
            case FALSE:
                return OutgoingMessageStatus.WAS_NOT_SENT;
            case TRUE:
                return OutgoingMessageStatus.FULLY_SENT;
            case ENTANGLED:
                return OutgoingMessageStatus.PARTIALLY_SENT;
            default:
                throw new IllegalStateException("Unexpected Quantum Boolean");
}

Upvotes: 1

Holger
Holger

Reputation: 298143

Since the result of super.getBuffers() is List<ByteBuffer>, you can iterate over it twice.

List<ByteBuffer> buffers = super.getBuffers();
if (buffers.stream().allMatch(b -> b.position() > 0)) {
    return OutgoingMessageStatus.FULLY_SENT;
} else if (buffers.stream().noneMatch(b -> b.position() > 0)) {
    return OutgoingMessageStatus.WAS_NOT_SENT;
} else {
    return OutgoingMessageStatus.PARTIALLY_SENT;
}

Note that this still doesn’t need iterating over all elements in all cases. allMatch returns as soon as it encounters a non-matching element, noneMatch returns as soon as it encounters a matching element. So in the PARTIALLY_SENT case, it is possible that it got the conclusion without looking at all elements.

An alternative is

List<ByteBuffer> buffers = super.getBuffers();
if(buffers.isEmpty()) return OutgoingMessageStatus.FULLY_SENT;
Predicate<ByteBuffer> p = b -> b.position() > 0;
boolean sent = p.test(buffers.get(0));
if(!sent) p = p.negate();
return buffers.stream().skip(1).allMatch(p)? sent?
    OutgoingMessageStatus.FULLY_SENT:
    OutgoingMessageStatus.WAS_NOT_SENT:
    OutgoingMessageStatus.PARTIALLY_SENT;
}

The status of the first element determines which condition we have to check. As soon as there is a contradicting element, allMatch returns immediately and we have a PARTIALLY_SENT situation. Otherwise, all elements match like the first which implies either “all sent” or “none sent”.

The pre-check for an empty list produces the same behavior as the original code and ensures that get(0) never breaks.


If you really have a Stream instead of a source that you can iterate multiple times, there is no simple short-cutting solutions, as that would require a stateful predicate. There are, however, simple solutions processing all elements.

Map<Boolean,Long> result=getBuffers().stream()
    .collect(Collectors.partitioningBy(b -> b.position() > 0, Collectors.counting()));
return
    result.getOrDefault(false, 0L)==0?
        OutgoingMessageStatus.FULLY_SENT:
    result.getOrDefault(true, 0L)==0?
        OutgoingMessageStatus.WAS_NOT_SENT:
        OutgoingMessageStatus.PARTIALLY_SENT;

or

return super.getBuffers().stream()
    .map(b -> b.position() > 0?
              OutgoingMessageStatus.FULLY_SENT: OutgoingMessageStatus.WAS_NOT_SENT)
    .reduce((a,b) -> a==b? a: OutgoingMessageStatus.PARTIALLY_SENT)
    .orElse(OutgoingMessageStatus.FULLY_SENT);

Upvotes: 10

Eran
Eran

Reputation: 393801

You can use filter(), and then count the number of elements that pass it :

Stream<ByteBuffer> buffers = super.getBuffers().stream();
int matches = buffers.filter(b -> b.position() > 0).count();
if (matches == super.getBuffers().size()) {
    return OutgoingMessageStatus.FULLY_SENT;
} else if (matches == 0) {
    return OutgoingMessageStatus.WAS_NOT_SENT;
} else {
    return OutgoingMessageStatus.PARTIALLY_SENT;
}

This assumes that the data source of the Stream (super.getBuffers()) has a size() method. If it doesn't, you can count the total number of ByteBuffers using an additional variable (less elegant, I know) :

int[] total = {0};
int matches = buffers.filter(b -> {total[0]++; return b.position() > 0;}).count();
if (matches == total[0]) {
    return OutgoingMessageStatus.FULLY_SENT;
} else if (matches == 0) {
    return OutgoingMessageStatus.WAS_NOT_SENT;
} else {
    return OutgoingMessageStatus.PARTIALLY_SENT;
}

The disadvantage of that approach is that it doesn't fail fast when when only some of the elements pass the filter (i.e. output should be OutgoingMessageStatus.PARTIALLY_SENT). Perhaps you can use some reduce operation that returns one of the three possible outputs and only processes as many elements as necessary.

Upvotes: 6

Peter Lawrey
Peter Lawrey

Reputation: 533492

You need a collection to reuse it.

You could use a count() instead of collect(toList()) depending on whether you need the results.

List<ByteBuffer> list = super.getBuffers();
List<ByteBuffer> buffers = list.stream().filter(b -> b.position() > 0).collect(toList());
if (buffers.size() == list.size()) {
    return OutgoingMessageStatus.FULLY_SENT;
} else if (buffers.isEmpty()) {
    return OutgoingMessageStatus.WAS_NOT_SENT;
} else {
    return OutgoingMessageStatus.PARTIALLY_SENT;
}

Upvotes: 1

Related Questions