Issue
I have the following code:
list.sort(Comparator
.comparing(ProgrammData::getEnd)
.thenComparing(ProgrammData::getStart).reversed());
My problem is that I want to have my list sorted by multiple things: 1.) Group them into future events and past events (By checking if the System.currentMilliseconds() is larger than the end timestamp) 2.) Sort future events by start ascending 3.) Sort past events by end descending
Can I do this with Java 8 Lambda or do I need another way of sorting the items?
Example:
events could look like this:
name, start, end
event1, 2022-02-220100, 2022-02-220300
event2, 2022-02-220200, 2022-02-241800
event3, 2022-02-251200, 2022-02-281500
event4, 2022-02-261600, 2022-02-262100
if now() is 2022-02-221200 So the order should be:
event3 (next item in the future)
event4 (2nd next item in the future)
event2 (Ended closer to now than event1)
event1 (Longest in the past)
Solution
As I understood this problem you rely on an existing API that expects a list of events and want them to be processed altogether as a single list ordered accordingly with their start and end date-time. And that is achievable.
I assume that the time-related event data stored inside a ProgrammData object is of type String. If it's not the case, and these fields are for instance of legacy type Date then only a couple of small changes need to be done.
My idea is to encapsulate all functionality inside the utility class TwoWaySorting so that all implementation details are abstracted away. And in the client code only this line TwoWaySorting.getSorted(programmData) is needed to produce a sorted list of events.
Since the responsibility of the TwoWaySorting class isn't related to the state all its behavior is marked with static modifier and its constructor is private. It has a nested static class Element which is a wrapper on top of the ProgrammData (if my assumption that ProgrammData is part of the existing IPA is correct then it must be used as is without any changes). So Element's class concern is to represent the time-related data of the wrapped ProgrammData object in a convenient form for sorting.
That is a brief overview of what the method getSorted() does:
- it obtains an instance the Comparator, that is based on the current time;
- creates the stream over the list of events and wraps each event with an instance of the
Elementclass; - sorts the elements;
- extracts original events and collects them into a list.
To wrap an event the static method LocalDateTime.parse() which accept a CharSequence and an appropriate formatter is used to parse string-based time-related data.
The centerpiece of TwoWaySorting class is a comparator returned by the getComparator() method, let's examine it closely.
The first part of it is responsible for dividing the elements into two groups based on currentTime:
Comparator.<Element, Boolean>comparing(element -> element.getEnd().isBefore(currentTime))
As its name suggest the instance methed isBefore() of the LocalDateTime class returns true if this date-time object is before the date-time object passed as an argument.
According to the natural sorting order of boolean values false comes before true. So for an invent that ends in the future isBefore() will yield false, which means that it'll appear at the beginning of the sorted list.
The second part of the comparator is responsible for ordering of the past and future events:
.thenComparingLong(element -> element.getEnd().isAfter(currentTime) ?
element.getStart().toEpochSecond(ZoneOffset.of("+00:00")) :
element.getEnd().toEpochSecond(ZoneOffset.of("+00:00")) * -1);
Returns: a lexicographic-order comparator composed of this and then the long sort key
Method thenComparingLong() (a quote from the javadoc is shown above) returns an aggregated comparator comprised of the comparator that was obtained previously (that separates past and future events) and a comparator thas is based on the ToLongFunction provided as an argument, which compares elements accordingly with long values extracted by that function.
Method toEpochSecond() extructs from a date-time object the number of seconds from the epoch as long.
I assume that it is sufficient for this task because in the example time is described with a precision of minutes. ZoneOffset that is expected by toEpochSecond() as an argument, in this case, has no influence on the result, and offset for Greenwich can be substituted with any other valid offset.
Since future events have to be sorted in ascending order value produced by the toEpochSecond() is used as is, for past events that must be sorted in descending order it's multiplied by -1 to reverse the result of the comparison.
Note:
- in order to combine these two comparators described above together we have to provide generic type information explicitly, like that:
<Element,Boolean>comparing(). Without an explicit declaration, the compiler has not enough data to determine the type of the variableelement, and inside bothcomparing()andthenComparingLong()its type will be inferred asObject. If we used only one of these static methods the type of theelementwill be correctly inferred by the compiler asElementbased on the return type of the methodgetComparator(). But for our case, we need to provide this information explicitly.
for information about the syntax for generic methods take a look at this tutorial
TwoWaySorting class
public class TwoWaySorting {
private static final DateTimeFormatter PD_FORMATTER
= DateTimeFormatter.ofPattern("yyyy-MM-ddHHmm");
private TwoWaySorting() {} // no way and no need to instantiate this class
private static Comparator<Element> getComparator() {
LocalDateTime currentTime = LocalDateTime.now();
return Comparator.<Element, Boolean>comparing(element -> element.getEnd().isBefore(currentTime))
.thenComparingLong(element -> element.getEnd().isAfter(currentTime) ?
element.getStart().toEpochSecond(ZoneOffset.of("+00:00")) :
element.getEnd().toEpochSecond(ZoneOffset.of("+00:00")) * -1);
}
public static List<ProgrammData> getSorted(List<ProgrammData> programmData) {
Comparator<Element> twoWayComparator = getComparator();
return programmData.stream()
.map(TwoWaySorting::parseData)
.sorted(twoWayComparator)
.map(Element::getData)
.collect(Collectors.toList());
}
private static Element parseData(ProgrammData data) {
return new Element(data,
LocalDateTime.parse(data.getStart(), PD_FORMATTER),
LocalDateTime.parse(data.getEnd(), PD_FORMATTER));
}
private static class Element {
private ProgrammData data;
private LocalDateTime start;
private LocalDateTime end;
// constructor and getters
}
}
This solution is meant to be clean and reusable. So in the main() apart from the source list there's only one line that obtains a sorted list and prints it on the console.
Note: getSorted() doesn't cause mutation of the source but creates and new list.
public static void main(String[] args) {
List<ProgrammData> programmData = // a list dummy ProgrammData objects
List.of(new ProgrammData("event1", "2022-02-220100", "2022-02-220300"),
new ProgrammData("event2", "2022-02-220200", "2022-02-241800"),
new ProgrammData("event3", "2022-02-251200", "2022-02-281500"),
new ProgrammData("event4", "2022-02-261600", "2022-02-262100"));
TwoWaySorting.getSorted(programmData)
.forEach(System.out::println);
}
Output (identical with the provided example)
ProgrammData [event3, 2022-02-251200, 2022-02-281500]
ProgrammData [event4, 2022-02-261600, 2022-02-262100]
ProgrammData [event2, 2022-02-220200, 2022-02-241800]
ProgrammData [event1, 2022-02-220100, 2022-02-220300]
Answered By - Alexander Ivanchenko
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.