Reputation: 9336
The title may be a bit vague, but here is what I have (in privatized code):
A class with some fields, including a BigDecimal and Date:
class MyObj{
private java.math.BigDecimal percentage;
private java.util.Date date;
// Some more irrelevant fields
// Getters and Setters
}
In another class I have a list of these objects (i.e. java.util.List<MyObj> myList
). What I want now is a Java 8 stream to check if the list is in the correct order of both dates and percentages for my validator.
For example, the following list would be truthy:
[ MyObj { percentage = 25, date = 01-01-2018 },
MyObj { percentage = 50, date = 01-02-2018 },
MyObj { percentage = 100, date = 15-04-2019 } ]
But this list would be falsey because the percentage aren't in the correct order:
[ MyObj { percentage = 25, date = 01-01-2018 },
MyObj { percentage = 20, date = 01-02-2018 },
MyObj { percentage = 100, date = 15-04-2019 } ]
And this list would also be falsey because the dates aren't in the correct order:
[ MyObj { percentage = 25, date = 10-03-2018 },
MyObj { percentage = 50, date = 01-02-2018 },
MyObj { percentage = 100, date = 15-04-2019 } ]
One possible solution might be creating Pairs
like this and then using an !
and .anyMatch
checking each individual Pair<MyObj>
. But I don't really want to create a Pair
class just for this purpose if possible.
Is there perhaps a way to use .reduce
or something to loop over pairs of MyObj
to check them? What would be the best approach here to check if all dates and percentages of the MyObj
in my list are in the correct order using Java 8 stream?
Another possibility is perhaps sorting the list by date, and then checking if they are all in order of percentage, if that's easier than checking both fields are the same time. The same issue with comparing pairs of MyObj
for the percentage still remains, though.
(PS: I will use it for a com.vaadin.server.SerializablePredicate<MyObj> validator
, and I prefer a Java 8 lambda because I've also used some for the other validators, so it would be more in line with the rest of the code. The Java 8 lambda is more a preference than requirement in my question however.)
Upvotes: 17
Views: 8272
Reputation: 21409
The upcoming Java 24 adds Gatherers.windowSliding(int windowSize)
, which will transform a stream into a stream of adjacent groups of elements. You can use this to check whether each adjacent pair matches the required ordering:
boolean isCorrectOrder = list.stream()
.gather(Gatherers.windowSliding(2))
.allMatch(w -> {
MyObj o1 = w.getFirst();
MyObj o2 = w.getLast();
return o1.getDate().compareTo(o2.getDate()) <= 0
&& o1.getPercentage().compareTo(o2.getPercentage()) <= 0;
});
Upvotes: 1
Reputation: 1284
Here is the solution by pairMap
in StreamEx
StreamEx.of(1, 2, 3, 5).pairMap((a, b) -> a <= b).allMatch(e -> e); // true
StreamEx.of(1, 2, 5, 3).pairMap((a, b) -> a <= b).allMatch(e -> e); // false
// your example:
StreamEx.of(myList)
.pairMap((a, b) -> a.getPercentage().compareTo(b.getPercentage()) <= 0 && !a.getDate().after(b.getDate()))
.allMatch(e -> e);
Upvotes: 3
Reputation: 2329
Similar to @luis g.'s answer, you can also use reduce
in combination with Optional
(empty means unsorted) and a "minimal" MyObj
as the identity:
boolean isSorted = list.stream()
.map(Optional::of)
.reduce(Optional.of(new MyObj(BigDecimal.ZERO, Date.from(Instant.EPOCH))),
(left, right) -> left.flatMap(l -> right.map(r -> l.date.compareTo(r.date)<= 0 && l.percentage.compareTo(r.percentage) <= 0 ? r : null)))
.isPresent();
Note that the accumulating function (BinaryOperator
) should be associative, which it is not in this case. Furthermore it is also not short-circuiting.
Upvotes: 0
Reputation: 758
I don't think that this is a problem that should be solved using streams. Streams apply mappings and filterings to the elements of a collection independently (probably even distributing the treatment of different elements to different CPU cores) before collecting them into a new collection again or reducing them to some sort of accumulated value. Your problem involves relations between different elements of a collection which contradicts the purpose of a stream. While there may be solutions involving streams those would be like hammering a nail into the wall with pliers. A classic loop will be perfectly fine for you: find the first occurence of an element breaking the order and return the desired result! Thus you wouldn't even need to create a pair.
Upvotes: 1
Reputation: 121088
Well if you want a short-circuiting operation, I don't think an easy solution using stream-api exists... I propose a simpler one, first define a method that in a short-circuiting way will tell you if your List is sorted or not, based on some parameter:
private static <T, R extends Comparable<? super R>> boolean isSorted(List<T> list, Function<T, R> f) {
Comparator<T> comp = Comparator.comparing(f);
for (int i = 0; i < list.size() - 1; ++i) {
T left = list.get(i);
T right = list.get(i + 1);
if (comp.compare(left, right) >= 0) {
return false;
}
}
return true;
}
And calling it via:
System.out.println(
isSorted(myList, MyObj::getPercentage) &&
isSorted(myList, MyObj::getDate));
Upvotes: 17
Reputation: 16053
Since you've mentioned that you wouldn't want to create a separate class Pairs
for this, You can use an inbuilt class for such purpose: AbstractMap.SimpleEntry
.
You can make a BiPredicate
which checks for both your comparing conditions & use that to compare all the pairs.
BiPredicate<MyObj,MyObj> isIncorrectOrder = (o1,o2) -> {
boolean wrongOrder = o1.getDate().after(o2.getDate());
return wrongOrder ? wrongOrder : o1.getPercentage().compareTo(o2.getPercentage()) > 0;
};
boolean isNotSorted = IntStream.range(1,myObjs.size())
.anyMatch(i -> isIncorrectOrder.test(myObjs.get(i-1),myObjs.get(i)));
The above solution with comparator:
Comparator<MyObj> comparator = (o1, o2) -> {
boolean wrongOrder = o1.getDate().after(o2.getDate());
return wrongOrder ? 1 : o1.getPercentage().compareTo(o2.getPercentage());
};
Predicate<AbstractMap.SimpleEntry<MyObj,MyObj>> isIncorrectOrder = pair -> comparator.compare(pair.getKey(),pair.getValue()) > 0;
boolean isNotSorted = IntStream.range(1,myObjs.size())
.mapToObj(i -> new AbstractMap.SimpleEntry<>(myObjs.get(i-1),myObjs.get(i)))
.anyMatch(isIncorrectOrder);
Upvotes: 2
Reputation: 5193
I think you are almost there by trying to use Stream.anyMatch
. You can accomplish it like this:
private static boolean isNotOrdered(List<MyObj> myList) {
return IntStream.range(1, myList.size()).anyMatch(i -> isNotOrdered(myList.get(i - 1), myList.get(i)));
}
private static boolean isNotOrdered(MyObj before, MyObj after) {
return before.getPercentage().compareTo(after.getPercentage()) > 0 ||
before.getDate().compareTo(after.getDate()) > 0;
}
We can use IntStream.range
to iterate over the elements of the list using an index. This way we can refer to any element in the list, e.g. the previous to compare it.
EDIT adding a more generic version:
private static boolean isNotOrderedAccordingTo(List<MyObj> myList, BiPredicate<MyObj, MyObj> predicate) {
return IntStream.range(1, myList.size()).anyMatch(i-> predicate.test(myList.get(i - 1), myList.get(i)));
}
This can be called as follows using the above predicate:
isNotOrderedAccordingTo(myList1, (before, after) -> isNotOrdered(before, after));
Or using method reference in a class ListNotOrdered
:
isNotOrderedAccordingTo(myList1, ListNotOrdered::isNotOrdered)
Upvotes: 6
Reputation: 7273
Yes, you can use reduce
to compare two items (though it is not the best option). You just need to create a new "empty" item as a result when you find an item out of order, like this:
boolean fullyOrdered = myList.stream()
.reduce((a, b) -> {
if ((a.percentage == null && a.date == null) || // previous item already failed
a.percentage.compareTo(b.percentage) > 0 || // pct out of order
a.date.after(b.date)) { // date out of order
return new MyObj(null, null); // return new empty MyObj
} else {
return b;
}
})
.filter(a -> a.percentage != null || a.date != null)
.isPresent();
System.out.println("fullyOrdered = " + fullyOrdered);
This will print true
only if both of your conditions are satisfied, false
otherwise.
Of course, you could make the code nicer by including some auxiliary methods in MyObj
:
class MyObj {
// ...
public MyObj() {}
public static MyObj EMPTY = new MyObj();
public boolean isEmpty() {
return percentage == null && date == null;
}
public boolean comesAfter(MyObj other) {
return this.percentage.compareTo(other.percentage) > 0 ||
this.date.after(other.date);
}
}
// ...
boolean fullyOrdered = myList.stream()
.reduce((a, b) -> (a.isEmpty() || a.comesAfter(b)) ? MyObj.EMPTY : b)
.filter(b -> !b.isEmpty())
.isPresent();
System.out.println("fullyOrdered = " + fullyOrdered);
Bear in mind that this is not short-circuiting, i.e. it will traverse the whole list even after it finds an unordered item. The only way to get out of the stream early while using reduce
would be to throw a RuntimeException
the moment you find the first unordered item, and that would be using exceptions to control program flow, which is considered bad practice. Still, I wanted to show you that you can indeed use reduce
for this purpose.
For a short-circuiting approach that will finish as soon as it finds the first out of place item, take a look at @LuCio's answer.
Upvotes: 0