2

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

SMAR
  • 125
  • 1
  • 11
  • You would have to come up with an algorithm that finds the desired sums from all the available combinations then group them, then filter. This is not a trivial problem – Trash Can Nov 29 '17 at 09:18
  • do you have to find all possible combinations, or do you always start with sorted list and you always add sequentially until reaching the constraint? I suggest you write the logic using for loop and if statements and later refactor to streams – hovanessyan Nov 29 '17 at 09:18
  • Do you need the combination closest to the input value? Then what you have is akin to the [Knapsack problem](https://en.wikipedia.org/wiki/Knapsack_problem). But if you just want to take items in order, until you reach the given input value - then you might be able to use `takeWhile`. – S.L. Barth is on codidact.com Nov 29 '17 at 09:20
  • The list will already be sorted so will start from the 1st and will keep on adding till i reach closest, above the given input value – SMAR Nov 29 '17 at 09:22
  • Please paste some code that you might have tried to solve this – Amar Dev Nov 29 '17 at 09:25
  • [a simple program like this can help you](https://ideone.com/1CKdlt) – Youcef LAIDANI Nov 29 '17 at 09:32
  • @ YCF_L , thank you so much for the code. I am okay doing it using the loop and checking. Was just wondering if there was a shorter method using streams. – SMAR Nov 29 '17 at 09:40
  • No, streams and filters are not suited for your task. – Ole V.V. Nov 29 '17 at 09:42
  • @Holger I think he means *first one* that overflows or is equal to 600 here – Eugene Nov 29 '17 at 10:26
  • 3
    @YCF_L: or simpler: https://ideone.com/B2WyGz – Holger Nov 29 '17 at 10:44
  • Yes @Holger this is really beautiful solution :) – Youcef LAIDANI Nov 29 '17 at 10:46
  • 1
    EIther you use a common loop, or you use a [Fenwick tree](https://en.wikipedia.org/wiki/Fenwick_tree) for this. See [this question](https://cs.stackexchange.com/questions/10538/bit-what-is-the-intuition-behind-a-binary-indexed-tree-and-how-was-it-thought-a) – fps Nov 29 '17 at 11:38
  • 1
    @FedericoPeraltaSchaffner first time I hear about this - thank u – Eugene Nov 29 '17 at 20:07
  • 1
    @Eugene Hi! It's a nice data structure, yet not widely-known. The problem I found with it is that it requires a fixed size because it's array-based. One day, when I have enough time (cough, cough), I'll create one based on an ArrayList, so the size is dynamic... – fps Nov 29 '17 at 20:14
  • Who closed this as a duplicate? Please read well, because the linked question isn't a duplicate. This question is about filtering based on an accumulated value, while the linked question is about keeping elements that match a condition over each element. – fps Nov 30 '17 at 13:49

4 Answers4

3

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.

EshtIO
  • 231
  • 4
  • 8
2

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());
Eran
  • 387,369
  • 54
  • 702
  • 768
  • I was thinking about this... but what would happen if you run it in parallel? – Eugene Nov 29 '17 at 09:51
  • @Eugene Based on the Javadoc, it would still work, but will be inefficient - `While takeWhile() is generally a cheap operation on sequential stream pipelines, it can be quite expensive on ordered parallel pipelines` – Eran Nov 29 '17 at 09:53
  • @Eugene well, you are correct, it won't work for parallel Streams unless you add some synchronization to the Predicate (which is another reason not to use this solution for parallel Streams). – Eran Nov 29 '17 at 09:55
  • I think what the OP is trying to have is either a stateful filter or a short-circuit reduce, I have tried to provide the second – Eugene Nov 29 '17 at 10:30
  • @Eran Won't the computation be simplified if you do the check as `if (sum < 600)` so that you can get rid of the nested if, else and boolean flag? – Thiyagu Jan 06 '18 at 17:09
2

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.

Holger
  • 285,553
  • 42
  • 434
  • 765
1

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()));
Eugene
  • 117,005
  • 15
  • 201
  • 306
  • 1
    I don’t know what is worse, modifying a `List` in `reduce`, depending on the processing order or abusing an exception for early return condition. Using `forEach` in this case would be simpler and cleaner (only a loop would be even simpler and cleaner). But if you keep on using an exception for that purpose, notice that you [don’t need to override `fillInStackTrace()` to disable stack traces](https://docs.oracle.com/javase/8/docs/api/java/lang/RuntimeException.html#RuntimeException-java.lang.String-java.lang.Throwable-boolean-boolean-). – Holger Nov 29 '17 at 10:33
  • @Holger what can I say - you are right, I only took it as an exercise – Eugene Nov 29 '17 at 10:47
  • 2
    Well, if you’re looking for an exercise, you can try to implement a formally correct solution using `collect`. It’s even possible to support parallel processing. The solution still will be more complicated than a loop and you should forget about short circuiting though… – Holger Nov 29 '17 at 10:50
  • Thanks to every one who helped.The solution from "Eran" helped.Used the predicate and then use list.stream().filter(pred).collect(Collectors.toList()); I have used Java 8 – SMAR Nov 30 '17 at 11:02