What you want to do is somewhat difficult to do using Streams, because most Streams operations occur on each individual value in the stream, independently of the others. There are stateful operations like distinct
and sort
but these are somewhat unusual and are not possible to customize.
It's might be possible to write a stateful operation of your own (similar to the stateful filter in another answer of mine). In this case it'd be a stateful flatmapper, but it's not obvious to me how one would get this to work.
Here's an alternative approach that's array-based. It assumes you have random access to the events. You say you have a list of events, so I hope that's the case. For the sake of simplicity let's set this up like so:
enum Event {
HIGH, NORMAL, LOW
}
We need a function to take two events and determine whether they match the pattern that's to be removed:
boolean match(Event e1, Event e2) {
return e1 == Event.HIGH && e2 == Event.LOW
|| e1 == Event.LOW && e2 == Event.HIGH;
}
Note that this matches the BiPredicate<Event>
functional interface.
As a final piece of setup let's introduce a helper that calls a lambda function on every index of an array within a given subrange. This is just like Arrays.setAll
except that it takes a subrange, and it operates on a boolean[].
void ArraySetRange(boolean[] array, int start, int end, IntPredicate op) {
IntStream.range(start, end).forEach(i -> array[i] = op.test(i));
}
Now the setup is done. What's the main task? Given a list of events, we want remove the sequences of events that match some pattern, and return a list of events:
List<Event> remove(List<Event> input, BiPredicate<Event,Event> matcher) {
...
The first thing we want to do is to run through the array and find all occurrences of a pair of events that match the criteria:
int n = input.size();
boolean[] flags = new boolean[n];
ArraySetRange(flags, 1, n, i -> matcher.test(input.get(i-1), input.get(i)));
This puts boolean true
values at each position in the array where this event and the event to itse left match the pattern. Note that patterns can overlap. We skip the first element of the array since there's nothing to its left.
But we want to remove the entire pattern. For every event, if the element to its right is the right end of the pattern, we want to remove this element too. That's another array operation, but this time over all array indexes except for the last:
ArraySetRange(flags, 0, n-1, i -> flags[i] || flags[i+1]);
(Note that this modifies the input array based on values in the input array. This works if processing is sequential left-to-right, but if we wanted to do this in parallel it'd be better to store the results into a separate array.)
Now we have an array flags where true
indicates presence in a pattern we want to remove. We can use a straightforward filtering operation to do this:
return IntStream.range(0, n)
.filter(i -> ! flags[i])
.mapToObj(input::get)
.collect(toList());
The complete example is here:
List<Event> remove(List<Event> input, BiPredicate<Event,Event> matcher) {
int n = input.size();
boolean[] flags = new boolean[n];
ArraySetRange(flags, 1, n, i -> matcher.test(input.get(i-1), input.get(i)));
ArraySetRange(flags, 0, n-1, i -> flags[i] || flags[i+1]);
return IntStream.range(0, n)
.filter(i -> ! flags[i])
.mapToObj(input::get)
.collect(toList());
}
and you'd call it something like:
List<Event> purgedEvents = remove(eventsToPurge, this::match);
I have a sneaking suspicion that this isn't exactly what you want. This will remove isolated pairs just fine:
NORMAL, HIGH, LOW, NORMAL → NORMAL, NORMAL
but if three "opposite" events occur in a row, they'll all be removed:
NORMAL, HIGH, LOW, HIGH, NORMAL → NORMAL, NORMAL
and if there is a sequence of events that aren't all opposites, some will get removed, but opposites may remain in the stream:
NORMAL, HIGH, HIGH, LOW, LOW, NORMAL → NORMAL, HIGH, LOW, NORMAL
It's dependent upon the exact specification of the patterns you want to remove, but I believe that you can do most things you want to do by tweaking the processing of the boolean array.