简体   繁体   中英

Sort list by multiple fields and multiple criteria

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)

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 Element class;
  • 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 variable element , and inside both comparing() and thenComparingLong() its type will be inferred as Object . If we used only one of these static methods the type of the element will be correctly inferred by the compiler as Element based on the return type of the method getComparator() . But for our case, we need to provide this information explicitly.

for information on the syntax of 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]

Try this:

    final long currentTime = System.currentTimeMillis();
    list.sort((el1, el2) -> {
        if (el1.equals(el2)) {
            return 0;
        }
        boolean isEl1Future = el1.getEnd().getTime() > currentTime;
        boolean isEl2Future = el2.getEnd().getTime() > currentTime;
        if (isEl1Future != isEl2Future) {
            return isEl1Future ? -1 : 1;
        }
        if (Boolean.TRUE.equals(isEl1Future)) {
            return el1.getStart().before(el2.getStart()) ? -1 : 1;
        }
        return el1.getEnd().after(el2.getEnd()) ? -1 : 1;
    });

There are multiple things to notice. First, one is the .thenComparing(...) method, which is only taking place if the previous comparing results are equal. You can read more about its behavior in the docs .

Second, if I were you I wouldn't bother to overuse the stream if I could solve it with one simple comparator. Assuming that you are looking for a new instance of the ProgrammData list, I wrote my code in stream style, but the Comparator can be used with the List 's sort method.

private List<ProgrammData> sortedProgramms(List<ProgrammData> dataList) {
    final LocalDateTime now = LocalDateTime.now();
    return dataList.stream()
                    .sorted((e1, e2) -> {
                        if (e1.getStart().isAfter(now) && e2.getStart().isAfter(now)) {
                            return e1.getStart().compareTo(e2.getStart());
                        }
                        return e2.getEnd().compareTo(e1.getEnd());
                    })
                    .collect(Collectors.toList());
 }

The LocalDateTime().now() is using System.currentTimeMillis() inside, if there is not a more accurate given clock.

You need to split past and future events in different lists and sort them accordingly. The final step is to join both lists.

  public static void main(String[] args) {
    ProgrammData programmData1 = new ProgrammData("a", LocalDateTime.now().plusDays(1),
        LocalDateTime.now().plusDays(1));
    ProgrammData programmData2 = new ProgrammData("b", LocalDateTime.now().plusDays(2),
        LocalDateTime.now().plusDays(2));
    ProgrammData programmData3 = new ProgrammData("c", LocalDateTime.now().minusDays(1),
        LocalDateTime.now().minusDays(1));
    ProgrammData programmData4 = new ProgrammData("c", LocalDateTime.now().minusDays(2),
        LocalDateTime.now().minusDays(2));

    List<ProgrammData> programmDataList = new ArrayList<>();

    programmDataList.add(programmData1);
    programmDataList.add(programmData2);
    programmDataList.add(programmData3);
    programmDataList.add(programmData4);

    final List<ProgrammData> collect = programmDataList.stream().sorted(Comparator
        .comparing(ProgrammData::end)).toList();

    LocalDateTime localDateTime = LocalDateTime.now();
    final List<ProgrammData> pastEvents = collect.stream().filter(pd -> pd.end.isBefore(localDateTime))
        .sorted(Comparator
            .comparing(ProgrammData::end).reversed()).toList();
    final List<ProgrammData> futureEvents = collect.stream().filter(pd -> pd.end.isAfter(localDateTime)).toList();

    List<ProgrammData> sortedListAsRequired = new ArrayList<>();

    sortedListAsRequired.addAll(futureEvents);
    sortedListAsRequired.addAll(pastEvents);

    System.out.println(sortedListAsRequired);
  }

  static record ProgrammData(String name, LocalDateTime start, LocalDateTime end) {

  }

The result is something like this:

[ProgrammData[name=a, start=2022-02-23T18:08:59.564300200, end=2022-02-23T18:08:59.568806900], ProgrammData[name=b, start=2022-02-24T18:08:59.568806900, end=2022-02-24T18:08:59.568806900], ProgrammData[name=c, start=2022-02-21T18:08:59.568806900, end=2022-02-21T18:08:59.568806900], ProgrammData[name=c, start=2022-02-20T18:08:59.568806900, end=2022-02-20T18:08:59.568806900]]

Your example is confusing. As you state in the heading event2 should be handled as if it's in the future, due it's end time ( 2022-02-241800 ) is after now ( 2022-02-221200 ), so the ordered elements shoud be

event2
event3
event4
event1

If that's correct, you could try something like the following:

events.sort((e1, e2) -> {
    // -1: e1 and e2 in the past
    //  0: e1 and e2 in distinct periods
    // +1: e1 and e2 in the future
    int period = Integer.signum(
          Integer.signum(e1.getEnd().compareTo(now))
        + Integer.signum(e2.getEnd().compareTo(now))
    );
    if (period == 0) {
        return -e1.getEnd().compareTo(now);
    }
    // > 0: e1 is after e2
    // = 0: e1 is equal to e2
    // < 0: e1 is before e2
    int comparation = e1.getComparingDateTime(now).compareTo(
        e2.getComparingDateTime(now)
    );
    return period * comparation;
});

Given

class ProgramData {
    ...
    public LocalDateTime getComparingDateTime(LocalDateTime reference) {
        if (reference.isAfter(end)) {
            // Past
            return end;
        }
        // Future
        return start;
    }
    ...
}

Your example looks incorrect - event2 is in the future and should be sorted together with 3 and 4. Anyway, no matter what, you should split the list in 2, future and past events, and sort each accordingly. You could do it with a stream, or by splitting the list in two, pick whichever you like.

For setup:

public class ProgrammData {

    public static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-ddHHmm");

    private String name;
    private LocalDateTime start;
    private LocalDateTime end;

    public ProgrammData(String name, String start, String end) {
        this.name = name;
        this.start = LocalDateTime.parse(start, FORMATTER);
        this.end = LocalDateTime.parse(end, FORMATTER);
    }

    //getters and setters

    @Override
    public String toString() {
        return "ProgrammData{" +
                "name='" + name + '\'' +
                ", start=" + start +
                ", end=" + end +
                '}';
    }
}
  1. Option 1 - streams
public class ProgrammDataMain {

    public static void main(String[] args) {
        //setup
        ProgrammData event1 = new ProgrammData("event1", "2022-02-220100", "2022-02-220300");
        ProgrammData event2 = new ProgrammData("event2", "2022-02-220200", "2022-02-241800");
        ProgrammData event3 = new ProgrammData("event3", "2022-02-251200", "2022-02-281500");
        ProgrammData event4 = new ProgrammData("event4", "2022-02-261600", "2022-02-262100");
        ProgrammData event5 = new ProgrammData("event5", "2022-02-220600", "2022-02-221159");

        LocalDateTime now = LocalDateTime.parse("2022-02-221200", ProgrammData.FORMATTER);

        List<ProgrammData> list = Arrays.asList(event1, event2, event3, event4, event5);

        //sort
        Comparator<ProgrammData> futureComparator = Comparator.comparing(ProgrammData::getStart);
        Comparator<ProgrammData> pastComparator = Comparator.comparing(ProgrammData::getEnd).reversed();
        list.stream().collect(Collectors.toMap(pd -> pd.getEnd().isBefore(now),
                pd -> {
                    Comparator<ProgrammData> comparator = pd.getEnd().isBefore(now) ? pastComparator : futureComparator;
                    Set<ProgrammData> set = new TreeSet<>(comparator);
                    set.add(pd);
                    return set;
                },
                (s1, s2) -> {
                    s1.addAll(s2);
                    return s1;
                }))
                .entrySet().stream()
                .sorted(Map.Entry.comparingByKey())
                .map(Map.Entry::getValue)
                .collect(Collectors.toList())
                .forEach(set -> set.forEach(System.out::println));
    }
}

First we stream the list and collect into Map<Boolean, Set<ProgrammData>> , true key would collect past events, false - future. Then stream map entries, sort by key to make sure future events - false key, are before past, and get values(sets). Because values are in TreeSet with according comparator, they are already sorted.

  1. Option 2 - split list in 2, sort each list, collect in single list again
public class ProgrammDataMain {

    public static void main(String[] args) {
        //setup
        ProgrammData event1 = new ProgrammData("event1", "2022-02-220100", "2022-02-220300");
        ProgrammData event2 = new ProgrammData("event2", "2022-02-220200", "2022-02-241800");
        ProgrammData event3 = new ProgrammData("event3", "2022-02-251200", "2022-02-281500");
        ProgrammData event4 = new ProgrammData("event4", "2022-02-261600", "2022-02-262100");
        ProgrammData event5 = new ProgrammData("event5", "2022-02-220600", "2022-02-221159");

        LocalDateTime now = LocalDateTime.parse("2022-02-221200", ProgrammData.FORMATTER);

        List<ProgrammData> list = Arrays.asList(event1, event2, event3, event4, event5);

        //sort
        Comparator<ProgrammData> futureComparator = Comparator.comparing(ProgrammData::getStart);
        Comparator<ProgrammData> pastComparator = Comparator.comparing(ProgrammData::getEnd).reversed();
        //sort
        List<ProgrammData> futureList = new ArrayList<>();
        List<ProgrammData> pastList = new ArrayList<>();
        for (ProgrammData programmData : list) {
            if (programmData.getEnd().isBefore(now)) {
                pastList.add(programmData);
            } else {
                futureList.add(programmData);
            }
        }
        futureList.sort(futureComparator);
        pastList.sort(pastComparator);
        List<ProgrammData> resultList = new ArrayList<>(futureList);
        resultList.addAll(pastList);
        resultList.forEach(System.out::println);
    }
}

Basically, make 2 new lists, one for future, other for past, sort each, add them in a new list. Or you could save them in the initial list.

Both options result in order - events 2, 3, 4(future, sorted by start ascending), events 5, 1(past, sorted by end descending).

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM