Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can one find an item after the match using streams?

Tags:

java

regex

lambda

With Java streams it is easy to find an element that matches a given property.
Such as:

 String b = Stream.of("a1","b2","c3")
     .filter(s -> s.matches("b.*"))
     .findFirst().get();
 System.out.println("b = " + b);

Produces:
b=b2

However often one wants a value or values right after a match, rather than the match itself. I only know how to do this with old fashion for loops.

    String args[] = {"-a","1","-b","2","-c","3"};
    String result = "";
    for (int i = 0; i < args.length-1; i++) {
        String arg = args[i];
        if(arg.matches("-b.*")) {
            result= args[i+1];
            break;
        }
    }
    System.out.println("result = " + result);

Which will produce:
result=2

Is there a clean way of doing this with Java 8 Streams? For example setting result to "2" given the array above and predicate s -> s.matches("-b.*").

If you can get the next value, it would also be useful to also be able to get a list/array of the next N values or all values until another predicate is matched such as s -> s.matches("-c.*").

like image 980
WillShackleford Avatar asked Aug 03 '15 13:08

WillShackleford


4 Answers

This is the kind of spliterator it takes to have this solved with streams:

import java.util.ArrayList;
import java.util.List;
import java.util.Spliterator;
import java.util.Spliterators.AbstractSpliterator;
import java.util.function.Consumer;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

public class PartitioningSpliterator<E> extends AbstractSpliterator<List<E>>
{
  private final Spliterator<E> spliterator;
  private final int partitionSize;

  public PartitioningSpliterator(Spliterator<E> toWrap, int partitionSize) {
    super(toWrap.estimateSize(), toWrap.characteristics());
    if (partitionSize <= 0) throw new IllegalArgumentException(
        "Partition size must be positive, but was " + partitionSize);
    this.spliterator = toWrap;
    this.partitionSize = partitionSize;
  }

  public static <E> Stream<List<E>> partition(Stream<E> in, int size) {
    return StreamSupport.stream(new PartitioningSpliterator(in.spliterator(), size), false);
  }

  @Override public boolean tryAdvance(Consumer<? super List<E>> action) {
    final HoldingConsumer<E> holder = new HoldingConsumer<>();
    if (!spliterator.tryAdvance(holder)) return false;
    final ArrayList<E> partition = new ArrayList<>(partitionSize);
    int j = 0;
    do partition.add(holder.value); while (++j < partitionSize && spliterator.tryAdvance(holder));
    action.accept(partition);
    return true;
  }

  @Override public long estimateSize() {
    final long est = spliterator.estimateSize();
    return est == Long.MAX_VALUE? est
         : est / partitionSize + (est % partitionSize > 0? 1 : 0);
  }

  static final class HoldingConsumer<T> implements Consumer<T> {
    T value;
    @Override public void accept(T value) { this.value = value; }
  }
}

Once you have this tucked away somewhere in the project, you can say

partition(Stream.of("-a","1","-b","2","-c","3"), 2)
      .filter(pair -> pair.get(0).equals("-b"))
      .findFirst()
      .map(pair -> pair.get(1))
      .orElse("");

As a side point, the presented spliterator supports parallelism by relying on the default implementation of trySplit in AbstractSpliterator.

like image 66
Marko Topolnik Avatar answered Oct 26 '22 23:10

Marko Topolnik


I found it through this blog post:
https://blog.jooq.org/when-the-java-8-streams-api-is-not-enough/

The library called jOOL has a Github link
https://github.com/jOOQ/jOOL

and Maven central Info here:
http://mvnrepository.com/artifact/org.jooq/jool/0.9.6

The code for the example became:

import org.jooq.lambda.Seq;

...

    String result = Seq.of(args)
            .skipWhile(s -> !s.matches("-b.*"))
            .skip(1)
            .findFirst()
            .get();
    
like image 38
WillShackleford Avatar answered Oct 26 '22 23:10

WillShackleford


I cannot say the following is efficient, but it follows an easily appliable pattern. (In a real functional language though, it might be efficient, when adding a filter.)

First collect [[-c, 3], [-b, 2], [-a, 1]] from the string stream.

    List<List<String>> optionLists = Stream.of("-a","1","-b","2","-c","3")
            .collect(ArrayList<List<String>>::new,
                    (lists, arg) -> {
                        if (arg.startsWith("-")) {
                            List<String> list = new LinkedList<>();
                            list.add(arg);
                            lists.add(0, list);
                        } else {
                            List<String> list = lists.get(0);
                            list.add(arg);
                        }
                    },
                    List::addAll);
    System.out.println(optionLists);

And then one might turn it in a map for all options.

    List<String> bargs = optionLists.stream()
            .collect(Collectors.toMap(lst -> lst.get(0),
                    lst -> lst.subList(1, lst.size()))).get("-b");
    System.out.println("For -b: " + bargs);
like image 24
Joop Eggen Avatar answered Oct 27 '22 01:10

Joop Eggen


As far as I can tell there doesn't seem to be an easy way to do this, and a lot of the reason for this stems from the fact that Java's Stream API lacks lacks the features of getting the index of a stream element as well as the ability to zip streams together.

We could imagine an easy solution to the problem if we could get the index of a certain element in the stream, and then simply use the .skip() function paired with the .limit(n) function to discard the elements up to and including the desired match point, and then limit the results to the next n elements.

You might want to check out Protonpack, which is a "Streams utility library for Java 8 supplying takeWhile, skipWhile, zip and unfold." With this library an easy solution for the problem might look like:

Stream<String> stringStream = Stream.of("-a","1","-b","2","-c","3");
Stream<String> nAfterMatch = 
    StreamUtils.skipWhile(stringStream, s -> !(s.matches("-b.*")))
    .limit(n);
like image 35
jhhurwitz Avatar answered Oct 27 '22 00:10

jhhurwitz