Search code examples
javaspringspring-bootjava-streamjava-11

Group a list of objects by an attribute and save them on a list in the same object


I'm trying to group some records. I have this entity:

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Builder
@EqualsAndHashCode
@ToString
public class Candidate {

  private Integer personId;

  private String codeHswCandId;

  private List<String> codeHswCandIdRelated;

  private LocalDateTime updateDate;

}

And a list of them:

LocalDate today = LocalDate.of(2023,2,16);
LocalTime now = LocalTime.now();
LocalDate yesterday = LocalDate.of(2023,2,15);
LocalDate beforeYesterday = LocalDate.of(2023,2,14);

List<Candidate> candidateList = List.of(
    Candidate.builder().personId(1).codeHswCandId("1").updateDate(LocalDateTime.of(today, now)).build(),
    Candidate.builder().personId(1).codeHswCandId("2").updateDate(LocalDateTime.of(yesterday, now)).build(),
    Candidate.builder().personId(1).codeHswCandId("3").updateDate(LocalDateTime.of(beforeYesterday, now)).build(),
    Candidate.builder().personId(2).codeHswCandId("4").updateDate(LocalDateTime.of(today, now)).build(),
    Candidate.builder().personId(2).codeHswCandId("5").updateDate(LocalDateTime.of(yesterday, now)).build(),
    Candidate.builder().personId(2).codeHswCandId("6").updateDate(LocalDateTime.of(beforeYesterday, now)).build()
);

I need to group them into this structure:

List<Candidate> output = List.of(
    Candidate.builder().personId(1).codeHswCandId("1").codeHswCandIdRelated(List.of("2","3")).build(),
    Candidate.builder().personId(2).codeHswCandId("4").codeHswCandIdRelated(List.of("5","6")).build()
);

Group by the most recent personId with closest updateDate. I let his codeHswCandId and the other on codeHswCandIdRelated list.


Solution

  • Assuming you are using Java 12 or higher,

    • use the teeing collector and collect to two maps:
    • first map using personId as key and mapping to the object having the max updateDate,
    • second map grouping by personId and mapping to a list of codeHswCandId
    • and then iterate over the entries of the first map and build Candidate objects using personId from the key of the entry, codeHswCandId from the value of the entry and a filterd list of codeHswCandIdRelated strings from the value of the second map having the same key as the entry in the first map.

    code snippet:

    List<Candidate> result =
    candidateList.stream()
                 .collect(Collectors.teeing(
                         Collectors.toMap(Candidate::getPersonId,
                                          Function.identity(),
                                          BinaryOperator.maxBy(Comparator.comparing(Candidate::getUpdateDate))),
                         Collectors.groupingBy(Candidate::getPersonId,
                                               Collectors.mapping(Candidate::getCodeHswCandId,Collectors.toList())),
                         (map1, map2) -> map1.entrySet()
                                             .stream()
                                             .map(entry -> Candidate.builder()
                                                                    .personId(entry.getKey())
                                                                    .codeHswCandId(entry.getValue().getCodeHswCandId())
                                                                    .codeHswCandIdRelated(map2.get(entry.getKey())
                                                                                              .stream()
                                                                                              .filter(cd -> !cd.equals(entry.getValue().getCodeHswCandId()))
                                                                                              .collect(Collectors.toList()))
                                                                    .build())
                                             .collect(Collectors.toList())));
    

    Update

    For Java 11 you can still use a simillar approach, except you need to stream over the input twice to build the maps:

    Map<Integer, Candidate> personIdToCandidates =
            candidateList.stream()
                         .collect(Collectors.toMap(Candidate::getPersonId,
                                                   Function.identity(),
                                                   BinaryOperator.maxBy(
                                                           Comparator.comparing(Candidate::getUpdateDate))));
    
    Map<Integer, List<String>> personIdToCodeHsw =
            candidateList.stream()
                         .collect(Collectors.groupingBy(Candidate::getPersonId,
                                                        Collectors.mapping(Candidate::getCodeHswCandId,
                                                                           Collectors.toList())));
    
    List<Candidate> result =
            personIdToCandidates.entrySet()
                                .stream()
                                .map(entry -> Candidate.builder()
                                                       .personId(entry.getKey())
                                                       .codeHswCandId(entry.getValue().getCodeHswCandId())
                                                       .codeHswCandIdRelated(personIdToCodeHsw.get(entry.getKey())
                                                                                              .stream()
                                                                                              .filter(cd -> !cd.equals(entry.getValue().getCodeHswCandId()))
                                                                                              .collect(Collectors.toList()))
                                                       .build())
                                .collect(Collectors.toList());