Home > Software engineering >  Using Java Stream groupingBy with both key and value of map
Using Java Stream groupingBy with both key and value of map

Time:09-27

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:

  • the Category key represents all the distinct Category values across all the lineItemList elements; and
  • each PieSlice value is created such that:
    • its value is a sum of all the LineItem#amount that reference the same Category; and
    • its percentage is a ratio of the PieSlice#value (the sum of all lineListItem elements mapped to the Category) and the total amount of all LineItem#amounts combined

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:

  • only has 2 keys, Dogs and Cows, because we only have (in this example) Dogs and Cows
  • the PieSlice for Dogs would have:
    • a value of 9.00 because 5.50 3.50 is 9.00; and
    • a percentage of 0.9, because if we take the total amounts of all dogs all cows, we have a total value of 10.0; Dogs comprise 9.00 / 10.00 or 0.9 (90%) of the total animals
  • the PieSlice for Cows would have:
    • a value of 1.00; and
    • a percentage of 0.1

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?

CodePudding user response:

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}}
  • Related