SMAR
SMAR

Reputation: 135

Java 8 stream filter

I hope someone can help me I am trying to find a way with which i can filter a list based on a condition

public class Prices {
    private String item;
    private double price;
    //......    
}

For example i have a list of above object List has the following data

item, price

a     100,
b     200,
c     250,
d     350,
e     450

is there a way to use streams and filter on List so that at the end of it we are left with only objects that have a sum of prices less that a given input value

Say if the input value is 600, so the resultant list would only have a,b,c,d as these are the objects whose price, when added to each other, the sum takes it closer to 600. So e would not be included in the final filtered list. If the input/given value is 300 then the filtered list will only have a and b.

The list is already sorted and will start from the top and keep on adding till the given value is reached

Thanks Regards

Upvotes: 2

Views: 1282

Answers (4)

Holger
Holger

Reputation: 298113

The simplest solution for this kind of task is still a loop, e.g.

double priceExpected = 600;
int i = 0;
for(double sumCheck = 0; sumCheck < priceExpected && i < list.size(); i++)
    sumCheck += list.get(i).getPrice();
List<Prices> resultList = list.subList(0, i);

A Stream solution fulfilling all formal criteria for correctness, is much more elaborated:

double priceThreshold = 600;
List<Prices> resultList = list.stream().collect(
    () -> new Object() {
        List<Prices> current = new ArrayList<>();
        double accumulatedPrice;
    },
    (o, p) -> {
        if(o.accumulatedPrice < priceThreshold) {
            o.current.add(p);
            o.accumulatedPrice += p.getPrice();
        }
    },
    (a,b) -> {
        if(a.accumulatedPrice+b.accumulatedPrice <= priceThreshold) {
            a.current.addAll(b.current);
            a.accumulatedPrice += b.accumulatedPrice;
        }
        else for(int i=0; a.accumulatedPrice<priceThreshold && i<b.current.size(); i++) {
            a.current.add(b.current.get(i));
            a.accumulatedPrice += b.current.get(i).getPrice();
        }
    }).current;

This would even work in parallel by just replacing stream() with parallelStream(), but it would not only require a sufficiently large source list to gain a benefit, since the loop can stop at the first element exceeding the threshold, the result list must be significantly larger than ¹/n of the source list (where n is the number of cores) before the parallel processing can have an advantage at all.

Also the loop solution shown above is non-copying.

Upvotes: 2

EshtIO
EshtIO

Reputation: 241

You can write this static method, that create suitable predicate:

public static Predicate<Prices> byLimitedSum(int limit) {
    return new Predicate<Prices>() {
        private int sum = 0;
        @Override
        public boolean test(Prices prices) {
            if (sum < limit) {
                sum += prices.price;
                return true;
            }
            return false;
        }
    };
}

And use it:

List<Prices> result = prices.stream()
        .filter(byLimitedSum(600))
        .collect(Collectors.toList());

But it is bad solution for parallelStream.

Anyway i think in this case stream and filter using is not so good decision, cause readability is not so good. Better way, i think, is write util static method like this:

public static List<Prices> filterByLimitedSum(List<Prices> prices, int limit) {
    List<Prices> result = new ArrayList<>();
    int sum = 0;
    for (Prices price : prices) {
        if (sum < limit) {
            result.add(price);
            sum += price.price;
        } else {
            break;
        }
    }
    return result;
}

Or you can write wrapper for List<Prices> and add public method into new class. Use streams wisely.

Upvotes: 3

Eugene
Eugene

Reputation: 120848

Using a simple for loop would be much much simpler, and this is abusive indeed as Holger mentions, I took it only as an exercise.

Seems like you need a stateful filter or a short-circuit reduce. I can think of this:

static class MyException extends RuntimeException {

    private final List<Prices> prices;

    public MyException(List<Prices> prices) {
        this.prices = prices;
    }

    public List<Prices> getPrices() {
        return prices;
    }

    // make it a cheap "stack-trace-less" exception
    @Override
    public Throwable fillInStackTrace() {
        return this;
    }
}

This is needed to break from the reduce when we are done. From here the usage is probably trivial:

 List<Prices> result;

    try {
        result = List.of(
                new Prices("a", 100),
                new Prices("b", 200),
                new Prices("c", 250),
                new Prices("d", 350),
                new Prices("e", 450))
                .stream()
                .reduce(new ArrayList<>(),
                        (list, e) -> {
                            double total = list.stream().mapToDouble(Prices::getPrice).sum();
                            ArrayList<Prices> newL = new ArrayList<>(list);
                            if (total < 600) {
                                newL.add(e);
                                return newL;
                            }

                            throw new MyException(newL);
                        },
                        (left, right) -> {
                            throw new RuntimeException("Not for parallel");
                        });
    } catch (MyException e) {
        e.printStackTrace();
        result = e.getPrices();
    }

    result.forEach(x -> System.out.println(x.getItem()));

Upvotes: 1

Eran
Eran

Reputation: 393771

Given you requirements, you can use Java 9's takeWhile.

You'll need to define a Predicate having a state:

Predicate<Prices> pred = new Predicate<Prices>() {
    double sum = 0.0;
    boolean reached = false;
    public boolean test (Prices p) {
        sum += p.getPrice();
        if (sum >= 600.0) { // reached the sum
            if (reached) { // already reached the some before, reject element
                return false;
            } else { // first time we reach the sum, so current element is still accepted
                reached = true;
                return true;
            }
        } else { // haven't reached the sum yet, accept current element
            return true;
        }
    }
};

List<Prices> sublist = 
    input.stream()
         .takeWhile(pred)
         .collect(Collectors.toList());

Upvotes: 2

Related Questions