9

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.*").

spongebob
  • 8,370
  • 15
  • 50
  • 83
WillShackleford
  • 6,918
  • 2
  • 17
  • 33
  • 7
    I think the answer is no. I'm reasonably confident that any solution using streams would be really convoluted and readability would suffer. – Paul Boddington Aug 03 '15 at 14:03
  • 6
    Streams are a very versatile tool but you know the saying: "when you've got a hammer, everything looks like a nail". But in reality not everything is a nail. – biziclop Aug 03 '15 at 14:06
  • This would be easy if streams supported the `partition` operation (I'm using Clojure's naming). That would allow you to reorganize it into a stream of pairs. There _is_ a way to do that at the spliterator level (I have the code for that), but it's probably not worth it for your case. – Marko Topolnik Aug 03 '15 at 14:19
  • What you are looking for is a "Transform" duplex stream that emits its own outputs from a source stream "piped" to it. I doubt Java-8 supports that much of "plumbing" with streams. – S.D. Sep 01 '15 at 14:34

8 Answers8

4

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();
    
Lukas Eder
  • 211,314
  • 129
  • 689
  • 1,509
WillShackleford
  • 6,918
  • 2
  • 17
  • 33
  • Once you assume only sequential stream processing, a lot of useful higher-order functions become just a trivial implementation step away. However, Java Streams API has a clear focus on data-parallel processing. – Marko Topolnik Aug 03 '15 at 19:38
4

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.

Marko Topolnik
  • 195,646
  • 29
  • 319
  • 436
  • 1
    I couldn't find a FixedBatchSpliterator class. – WillShackleford Aug 03 '15 at 22:03
  • That's extra support I have for better parallelization of certain tasks. It is not essential, just delete that factory method (I deleted it from the answer). Alternatively, refer to [this answer](http://stackoverflow.com/a/25507602/1103872) where I cover that class as well. – Marko Topolnik Aug 04 '15 at 05:01
1

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);
Joop Eggen
  • 107,315
  • 7
  • 83
  • 138
  • As _pbabcdefp_ lucidly anticipated, `any solution using streams would be really convoluted and readability would suffer` :) – Marko Topolnik Aug 03 '15 at 19:41
  • @MarkoTopolnik I agree; worked with Stream implementation, but in general I tend to prefer in java to still use a collection to have compact code. Some constructive things are still missing, like your splitting, your reference, maybe jOOL. – Joop Eggen Aug 04 '15 at 10:24
  • 1
    Yes, this lack has spawned a myriad of libs which add the obviously missings things, but keep in mind that in most cases these libs do not support parallelization. Personally I don't use any of them because I prefer to stay within the abilities of the JDK. As you, I prefer the old school way if something is not feasible with Streams (or, occasionally, some sort of hack). – Marko Topolnik Aug 04 '15 at 10:31
1

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);
jhhurwitz
  • 89
  • 5
0

For the case where you want the value directly after it seems that you could use som Pair or Tuple class.

String b = Stream.of(Pair.of("-a","1"),Pair.of("-b","2"),Pair.of("-c","3"))
    .filter(p -> p.first.matches("-b.*"))
    .map(p -> p.second)
    .findFirst().get();
Simon
  • 6,293
  • 2
  • 28
  • 34
0

This can be a work-around solution:

String[] args = {"-a","1","-b","2","-c","3"};

String b = IntStream.range(1, args.length)
   .mapToObj(i -> new Pair<>(args[i-1], args[i]))
   .filter(pp -> pp.getFirst().startsWith("-b"))
   .findFirst().get().getSecond();

System.out.println("b = " + b);
//=> Output: b = 2

.mapToObj is being used to make pairs of string values from args array using consecutive indices from input array args.

anubhava
  • 761,203
  • 64
  • 569
  • 643
0

Thanks to Java Streams takeWhile it is possible:

Stream<String> strings = Stream.of("a1","b2","c3");
final AtomicBoolean matchFound = new AtomicBoolean(false);
final AtomicReference<String> stringAfterMatch = new AtomicReference<>();
strings.takeWhile(string -> stringAfterMatch.get() == null).forEach(string -> {
  if (matchFound.get()) {
      stringAfterMatch.set(string);
  }
  if (string.matches("b.*")) {
    matchFound.set(true);
  }
});
String b = stringAfterMatch.get();
René Reitmann
  • 364
  • 2
  • 17
0
private static boolean containsWords(String input, String[] words) {
        
        Arrays.stream(words).allMatch(new Predicate<String>() {

            @Override
            public boolean test(String t) {
                if(input.contains(t))
                    counter+=1; //add  a global counter
                //ExtensionUtils.keyWordHash.put(t, input); // or you can put the strings to a hashmap and retrieve later
                return input.contains(t);
            }
        });
        return Arrays.stream(words).allMatch(input::contains);
    }
vrama
  • 1
  • 2