Sorting and Grouping on a list of objects

我的未来我决定 提交于 2021-01-28 05:08:12

问题


I have a List of Procedure objects as below

Procedure1  01/01/2020
Procedure2  03/01/2020
Procedure3  03/01/2020
Procedure1  04/01/2020
Procedure5  05/01/2020, 02/01/2020
Procedure2  06/01/2020

and my Procedure class is like

Class Procedure {
    List<Date> procedureDate;
    String procedureName;
}

I want to sort and group the objects based on the below conditions.

  1. All procedures should be grouped based on the procedure name.
  2. Procedures must be in descending order of procedure date. [first element in date list i.e., procedureDate.get[0]]
  3. Same Procedures grouped together should be in descending order of Date.

End result must be,

Procedure2  06/01/2020
Procedure2  03/01/2020

Procedure5  05/01/2020, 02/01/2020

Procedure1  04/01/2020
Procedure1  01/01/2020

Procedure3  03/01/2020

I was able to achieve this using Comparator and old java code. Is it possible to achieve the same using java8 streams, collectors and grouping by?


回答1:


This is a very interesting question. The solution is not as easy as it looks to be. You have to divide the solution into multiple steps:

  1. Get the max value for each grouped procedureName based on the first dates in the List<Date>.
  2. Compare the Procedure instances based on max Date value from the Map<String, Date created in the step one.
  3. If they are equal distinguish them by the name (ex. two times Procedure 2).
  4. If they are still equal, sort the Procedure instances based on their actual first date.

Here is the demo at: https://www.jdoodle.com/iembed/v0/Te.

Step 1

List<Procedure> procedures = ...

Map<String, Date> map = procedures.stream().collect(
    Collectors.collectingAndThen(
        Collectors.groupingBy(
            Procedure::getProcedureName,
            Collectors.maxBy(Comparator.comparing(s -> s.getProcedureDate().get(0)))),
    s -> s.entrySet().stream()
        .filter(e -> e.getValue().isPresent())
        .collect(Collectors.toMap(
              Map.Entry::getKey,
              e -> e.getValue().get().getProcedureDate().get(0)))));

.. explained: There is a simple way to get a Procedure with maximum first date grouped by procedureName.

Map<String, Optional<Procedure>> mapOfOptionalProcedures = procedures.stream()
    .collect(Collectors.groupingBy(
             Procedure::getProcedureName,
             Collectors.maxBy(Comparator.comparing(o -> o.getProcedureDate().get(0)))));

However, the returned structure is a bit clumsy (Map<String, Optional<Procedure>>), to make it useful and return Date directly, there is a need of additional downstream collector Collectors::collectingAndThen which uses a Function as a result mapper:

Map<String, Date> map = procedures.stream().collect(
    Collectors.collectingAndThen(
        /* grouping part */,
        s -> s.entrySet().stream()
            .filter(e -> e.getValue().isPresent())
            .collect(Collectors.toMap(
                    Map.Entry::getKey,
                    e -> e.getValue().get().getProcedureDate().get(0)))));

... which is effectively the first snippet.

Steps 2, 3 and 4

Basically, sort by the maximum date for each group. Then sort by the name and finally by the actual first date.

Collections.sort(
    procedures,
    (l, r) -> {
        int dates = map.get(r.getProcedureName()).compareTo(map.get(l.getProcedureName()));
        if (dates == 0) {
             int names =  l.getProcedureName().compareTo(r.getProcedureName());
             if (names == 0) {
                 return r.getProcedureDate().get(0).compareTo(l.getProcedureDate().get(0));
             } else return names;
        } else return dates;
    }
);

Sorted result

Using the deprecated java.util.Date according to your question, the sorted procedures will have sorted items like your expected snippet (I have overrided the Procedure::toString method)

@Override
public String toString() {
     return procedureName + " " + procedureDate;
}
Procedure2 [Mon Jan 06 00:00:00 CET 2020]
Procedure2 [Fri Jan 03 00:00:00 CET 2020]
Procedure5 [Sun Jan 05 00:00:00 CET 2020, Thu Jan 02 00:00:00 CET 2020]
Procedure1 [Sat Jan 04 00:00:00 CET 2020]
Procedure1 [Wed Jan 01 00:00:00 CET 2020]
Procedure3 [Fri Jan 03 00:00:00 CET 2020]



回答2:


My thought is coming from functional programming which is based on map-reduce. You can see groupBy/collect is actually a form of reduce anyway and this problem can be better "merge" rather than using groupBy feature of Stream. This is my implementation in pure Stream.

List<Procedure> a = List.of(
    new Procedure(...),
    ...

)


List<Procedure> b = a.stream().map((p)-> {                    // Prepare for reduce by create Map for each object
        Map<String,Procedure> mapP = new HashMap<>();
        mapP.put(p.getProcedureName(),p)
        return mapP
    }).reduce((p,q)->{                                         //Use reduce to merge
        q.entrySet().stream().forEach((qq)-> {
            if (p.containsKey(qq.getKey())) {
                p.get(qq.getKey()).setProcedureDate(
                    new ArrayList<Date>(
                        Stream.concat(
                            p.get(qq.getKey()).getProcedureDate().stream(),
                            qq.getValue().getProcedureDate().stream())
                        .collect(Collectors.toSet()))
                );
            } else {
                p.put(qq.getKey(), qq.getValue());
            }

        })

        return p;
    }).get().values().stream().map(p-> {                          //sort date inside object
            p.setProcedureDate(p.getProcedureDate().stream().sorted().collect(Collectors.toList()))
            return p;
        }
    ).sorted((x,y)->                                         //sort object by the first date

        x.procedureDate.get(0).compareTo(y.procedureDate.get(0))

    ).collect(Collectors.toList());


来源:https://stackoverflow.com/questions/63843343/sorting-and-grouping-on-a-list-of-objects

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!