How to get a range of items from stream using Java 8 lambda?

JavaFilterLambdaJava 8

Java Problem Overview


In a previous question [ https://stackoverflow.com/questions/22845574/how-to-dynamically-do-filtering-in-java-8 ] Stuart Marks gave a wonderful answer, and provided several useful utilities to handle selection of topN and topPercent from stream.

I'll include them here from his original answer :

@FunctionalInterface
public interface Criterion {
    Stream<Widget> apply(Stream<Widget> s);
}

Criterion topN(Comparator<Widget> cmp, long n) {
    return stream -> stream.sorted(cmp).limit(n);
}

Criterion topPercent(Comparator<Widget> cmp, double pct) {
    return stream -> {
        List<Widget> temp =
            stream.sorted(cmp).collect(toList());
        return temp.stream()
                   .limit((long)(temp.size() * pct));
    };
}

My questions here are :

[1] How to get top items from 3 to 7 from a stream with certain amount of items, so if the stream has items from A1, A2 .. A10, the call to

topNFromRange(Comparator<Widget> cmp, long from, long to) = topNFromRange(comparing(Widget::length), 3L, 7L)

will return { A3, A4, A5, A6, A7 }

The simplest way I can think of is get the top 7 [ T7 ] from original, get the top 3 [ T3 ] from original, and then get T7 - T3.

[2] How to get top items from top 10% to top 30% from a stream with certain amount of items, so if the stream has items from X1, X2 .. X100, the call to

topPercentFromRange(Comparator<Widget> cmp, double from, double to) = topNFromRange(comparing(Widget::length), 0.10, 0.30)

will return { X10, X11, X12, ..., X29, X30 }

The simplest way I can think of is get the top 30% [ TP30 ] from original, get the top 10% [ TP10 ] from original, and then get TP30 - TP10.

What are some better ways to use Java 8 Lambda to concisely express the above situations ?

Java Solutions


Solution 1 - Java

To get a range from a Stream<T>, you can use skip(long n) to first skip a set number of elements, and then you can call limit(long n) to only take a specific amount of items.

Consider a stream with 10 elements, then to get elements 3 to 7, you would normally call from a List:

list.subList(3, 7);

Now with a Stream, you need to first skip 3 items, and then take 7 - 3 = 4 items, so it becomes:

stream.skip(3).limit(4);

As a variant to @StuartMarks' solution to the second answer, I'll offer you the following solution which leaves the possibility to chain intact, it works similar to how @StuartMarks does it:

private <T> Collector<T, ?, Stream<T>> topPercentFromRangeCollector(Comparator<T> comparator, double from, double to) {
    return Collectors.collectingAndThen(
        Collectors.toList(),
        list -> list.stream()
            .sorted(comparator)
            .skip((long)(list.size() * from))
            .limit((long)(list.size() * (to - from)))
    );
}

and

IntStream.range(0, 100)
        .boxed()
        .collect(topPercentFromRangeCollector(Comparator.comparingInt(i -> i), 0.1d, 0.3d))
        .forEach(System.out::println);

This will print the elements 10 through 29.

It works by using a Collector<T, ?, Stream<T>> that takes in your elements from the stream, transforms them into a List<T>, then obtains a Stream<T>, sorts it and applies the (correct) bounds to it.

Solution 2 - Java

User skiwi already answered the first part of the question. The second part is:

> (2) How to get top items from top 10% to top 30% from a stream with certain amount of items....

To do this, you have to use a similar technique as topPercent in my answer to the other question. That is, you have to collect the elements into a list in order to be able to get a count of the elements, possibly after some upstream filtering has been done.

Once you have the count, then you compute the right values for skip and limit based on the count and the percentages you want. Something like this might work:

Criterion topPercentFromRange(Comparator<Widget> cmp, double from, double to) {
    return stream -> {
        List<Widget> temp =
            stream.sorted(cmp).collect(toList());
        return temp.stream()
                   .skip((long)(temp.size() * from))
                   .limit((long)(temp.size() * (to - from)));
    };
}

Of course you will have to do error checking on from and to. A more subtle problem is determining how many elements to emit. For example, if you have ten elements, they are at indexes [0..9], which correspond to 0%, 10%, 20%, ..., 90%. But if you were to ask for a range from 9% to 11%, the above code would emit no elements at all, not the one at 10% like you might expect. So some tinkering with the percentage computations is probably necessary to fit the semantics of what you're trying to do.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionFrankView Question on Stackoverflow
Solution 1 - JavaskiwiView Answer on Stackoverflow
Solution 2 - JavaStuart MarksView Answer on Stackoverflow