Reputation: 635
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:
Category
key represents all the distinct Category
values across all the lineItemList
elements; andPieSlice
value is created such that:
value
is a sum of all the LineItem#amount
that reference the same Category
; andpercentage
is a ratio of the PieSlice#value
(the sum of all lineListItem
elements mapped to the Category
) and the total amount of all LineItem#amount
s combinedFor 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:
Dogs
and Cows
, because we only have (in this example) Dogs and CowsPieSlice
for Dogs would have:
value
of 9.00
because 5.50
+ 3.50
is 9.00
; andpercentage
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 animalsPieSlice
for Cows would have:
value
of 1.00
; andpercentage
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?
Upvotes: 3
Views: 225
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 PieSlice
s. 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