1

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?

Nikolas Charalambidis
  • 40,893
  • 16
  • 117
  • 183
User_1940878
  • 321
  • 1
  • 6
  • 25
  • 2
    Please post your "old" code, so that we don't have to entirely rely on guessing, it also improves your question, by showing what you've done and are not just here for someone to write all the code for you – Lino Sep 11 '20 at 08:23
  • 1
    why is Procedure5 05/01/2020, 02/01/2020 and not Procedure5 05/01/2020 Procedure5 02/01/2020 – ΦXocę 웃 Пepeúpa ツ Sep 11 '20 at 08:29
  • @ ΦXocę 웃 Пepeúpa ツ Basically this list is derived from a document containing sentences. If the sentence is , "Some Procedure5 happened on 05/01/2020 and 02/01/2020" , there will be 2 dates associated with the Procedure5. If the sentence is "Procendure5 happened on 05/01/2020......and after some days Procedure5 happened on 02/01/2020", it will be Procendure5 - 05/01/2020 and Procedure5 -02/01/2020 (as per business requirement) – User_1940878 Sep 11 '20 at 08:36
  • 1
    You can just use group function and then sort the result,this way will be easy – TongChen Sep 11 '20 at 09:08

2 Answers2

2

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]
Nikolas Charalambidis
  • 40,893
  • 16
  • 117
  • 183
1

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());
Chayne P. S.
  • 1,558
  • 12
  • 17
  • Quite impressive. But there is no meaning of `entrySet().stream().forEach()` when `entrySet().forEach()` would do the same. – Nikolas Charalambidis Sep 11 '20 at 14:38
  • @Nikolas I know.. but Stream is asked.. so I answered – Chayne P. S. Sep 11 '20 at 14:40
  • Although the solution using Stream API was asked, it doesn't mean it should be used when it makes no sense. – Nikolas Charalambidis Sep 11 '20 at 14:42
  • 1
    @Nikolas According to User_1940878, "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?" means he/she knows how to do it without stream. So I just answer it with purely stream.. – Chayne P. S. Sep 11 '20 at 14:47