Reputation: 135
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
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
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
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
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