degath
degath

Reputation: 1621

Extend output of stream with groupingBy & summingInt in Java8

I have an Topic:

@Entity
public class Topic {
    @Id
    private int id;
    private LocalDate date;
    private String name;
    private int points;
    @JoinColumn(name = "user_id", nullable = false)
    private User user;
}

I'm getting list of topics in given date by spring data jpa method:

List topics = topicRepository.findByDateBetween(begin, end); which on output has e.g:

Topic(id=1, date="2018-01-01", name="Java examples", User(...), 12)
Topic(id=2, date="2018-02-02", name="Java examples", User(...), 34)
Topic(id=3, date="2018-02-02", name="Java examples", User(...), 56)
Topic(id=4, date="2018-03-03", name="Java examples", User(...), 78)
Topic(id=5, date="2018-03-03", name="Java examples", User(...), 90)

What I try to achive is to filter my result output as (if date && User is the same add points)

Topic(id=1, date="2018-01-01", name="Java examples", User(...), 12)
Topic(id=2, date="2018-02-02", name="Java examples", User(...), 90)
Topic(id=4, date="2018-03-03", name="Java examples", User(...), 168)

My actual solution returns map with date as key and summed points as value, but I need to have more data given in output like User or name.

return topics.stream()
        .collect(Collectors.groupingBy(Topic::getDate,
                Collectors.summingInt(Topic::getPoints)));

Maybe there is another way to instead of map return created dto for that case? e.g.

@Data
public class ResultDto {
    private LocalDate date;
    private String name;
    private int points;
    private User user;
}

Upvotes: 3

Views: 1087

Answers (2)

Misha
Misha

Reputation: 28133

A simple way to group by a subset of fields is to use a TreeMap with a custom Comparator. Suppose you were to define

Comparator<Topic> byDateAndUser = Comparator.comparing(Topic::getDate)
            .thenComparing(t -> t.getUser().getUserId());

Map<Topic,...> map = new TreeMap<>(byDateAndUser);

The resulting map would use the supplied comparator instead of equals to determine equality and will thus treat all topics with the same date and user as the same.

This feature of TreeMap allows you to compute a Map of topic to the total points and it will only contain one entry for each combination of date/user:

import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.summingInt;

Comparator<Topic> byDateAndUser = Comparator.comparing(Topic::getDate)
            .thenComparing(t -> t.getUser().getUserId());

Map<Topic, Integer> pointTotals = topics.stream()
        .collect(groupingBy(
                topic -> topic, 
                () -> new TreeMap<>(byDateAndUser), 
                summingInt(Topic::getPoints)
        ));

Upvotes: 2

fps
fps

Reputation: 34460

Assuming User implements hashCode and equals consistently, you could group to a map of date/user composite key and ResultDto values. To do this, you need two operations: one to map to and the other one to aggregate points for each group. (You can place these operations either in ResultDto or in a utility class, etc, here I'm assuming the first one is in a Mapper utility class and the 2nd one in ResultDto):

public final class Mapper {

    private Mapper() { }

    public static ResultDto fromTopic(Topic topic) {
        ResultDto result = new ResultDto();
        result.setDate(topic.getDate());
        result.setName(topic.getName());
        result.setPoints(topic.getPoints());
        result.setUser(topic.getUser());
        return result;
    }
}

In ResultDto:

public ResultDto merge(ResultDto another) {
    this.points += another.points;
    return this;
}

Note that I'm assigning the first Topic.name found in the Mapper.fromTopic mapping operation. I'm assuming this is an inconsistency in your example, please take it into account if you use this approach in a real world scenario.

Now we can stream the topics and group by date/user:

Map<List<Object>, ResultDto> groups = topics.stream()
    .collect(Collectors.toMap(
        topic -> Arrays.asList(topic.getDate, topic.getUser()), // Java 9: List.of
        Mapper::fromTopic,
        ResultDto::merge));

This collects to a map whose key is a List with its first element being the date and the second, the user. These 2-element lists aren't very useful for later usage, here we're only using them to create a composite key and group topics by this key. The values of the map is a ResultDto instance that is initially created from the topic and then merged with other topics that belong to the same group (same date and user). In this ResultDto.merge operation the points are being summed.

The results you need are the values of the map:

Collection<ResultDto> results = groups.values(); // or new ArrayList<>(groups.values())

EDIT: Here's a slightly more succinct variant without streams:

Map<List<Object>, ResultDto> groups = new HashMap<>();

topics.forEach(topic -> groups.merge(
    List.of(topic.getDate(), topic.getUser()), // Java 8: Arrays.asList
    Mapper.fromTopic(topic),
    ResultDto::merge));

Collection<ResultDto> results = groups.values();

Upvotes: 1

Related Questions