hotmeatballsoup
hotmeatballsoup

Reputation: 635

Using Java Stream groupingBy with both key and value of map

Java 11 here. I have the following POJOs:

public enum Category {
    Dogs,
    Cats,
    Pigs,
    Cows;
}

@Data // using Lombok to generate getters, setters, ctors, etc.
public class LineItem {
    private String description;
    private Category category;
    private BigDecimal amount;
}

@Data
public class PieSlice {
    private BigDecimal value;
    private BigDecimal percentage;
}

I will have a lineItemList : List<LineItem> and want to convert them into a Map<Category,PieSlice> whereby:

For example:

List<LineItem> lineItemList = new ArrayList<>();
LineItem dog1 = new LineItem();
LineItem dog2 = new LineItem();
LineItem cow1 = new LineItem();

dog1.setCategory(Category.Dogs);
dog2.setCategory(Category.Dogs);
cow1.setCategory(Category.Cows);

dog1.setAmount(BigDecimal.valueOf(5.50);
dog2.setAmount(BigDecimal.valueOf(3.50);
cow1.setAmount(BigDecimal.valueOf(1.00);

Given the above setup I would want a Map<Category,PieSlice> that looks like:

My best attempt only yields a Map<Category,List<LineItem>> which is not what I want:

List<LineItem> allLineItems = getSomehow();
Map<Category,List<LineItem>> notWhatIWant = allLineItems.stream()
    .collect(Collectors.groupingBy(LineItem::getCategory());

Can anyone spot how I can use the Streams API to accomplish what I need here?

Upvotes: 3

Views: 225

Answers (1)

rgettman
rgettman

Reputation: 178333

To collect to what you want, you need 2 steps, one to calculate the sum of the LineItem values (in your case 10.0), and the other to collect into the map that you need.

First, get the overall sum, which we'll use later to divide the values. The reduce method sums up the values. The first argument is the identity value, in this case, 0. The second argument adds in each item as it comes, and the third argument combines two intermediate results. Here, they're both add.

BigDecimal sum = lineItemList.stream()
                .map(LineItem::getAmount)
                .reduce(BigDecimal.ZERO, BigDecimal::add, BigDecimal::add);

Then, create the PieSlices. This uses the specific overload of Collectors.toMap that allows you to merge entries with the same key, so you can sum them up.

The first argument is the key extractor function, the second argument is the value extractor function, the third argument is the merging function, and the fourth (optional) is the map supplier function, in case you want a specific implementation of Map.

Map<Category, PieSlice> result = lineItemList.stream()
    .collect(Collectors.toMap(LineItem::getCategory,
        li -> new PieSlice(li.getAmount(), li.getAmount().divide(sum, 2, RoundingMode.HALF_EVEN)),
        (a, b) -> new PieSlice(a.getValue().add(b.getValue()), a.getPercentage().add(b.getPercentage())),
        HashMap::new));

This assumes that the indicated constructor for PieSlice is available, and if I add a suitable toString method in PieSlice, I get this map:

{Dogs=PieSlice{9.0, 0.90}, Cows=PieSlice{1.0, 0.10}}

Upvotes: 3

Related Questions