Issue
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 distinctCategory
values across all thelineItemList
elements; and - each
PieSlice
value is created such that:- its
value
is a sum of all theLineItem#amount
that reference the sameCategory
; and - its
percentage
is a ratio of thePieSlice#value
(the sum of alllineListItem
elements mapped to theCategory
) and the total amount of allLineItem#amount
s combined
- its
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
andCows
, because we only have (in this example) Dogs and Cows - the
PieSlice
for Dogs would have:- a
value
of9.00
because5.50
+3.50
is9.00
; and - a
percentage
of0.9
, because if we take the total amounts of all dogs + all cows, we have a total value of10.0
; Dogs comprise9.00 / 10.00
or0.9
(90%) of the total animals
- a
- the
PieSlice
for Cows would have:- a
value
of1.00
; and - a
percentage
of0.1
- a
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?
Solution
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}}
Answered By - rgettman
Answer Checked By - Mildred Charles (JavaFixing Admin)