Java Stream group by 02 fields and aggregate by sum on 2 BigDecimal fields

Viewed 358

Need help for a case on Stream with groupingBy I would like to be able to group by 2 different fields and have the sum of other BigDecimal fields, according to the different groupings. Here is my entity :

    public class Customer {
        private String name;
        private String type;
        private BigDecimal total;
        private BigDecimal balance;

// Setter, getter

}

Let's suppose I have as input this list:

Customer custa = new Customer("A", "STANDARD", new BigDecimal("1000"), new BigDecimal("1500"));
    Customer custa1 = new Customer("A", "VIP", new BigDecimal("2000"), new BigDecimal("2500"));
    Customer custb = new Customer("B", "STANDARD", new BigDecimal("3000"), new BigDecimal("3500"));
    Customer custc = new Customer("C", "STANDARD", new BigDecimal("4000"), new BigDecimal("4500"));
    Customer custa2 = new Customer("A", "VIP", new BigDecimal("1500"), new BigDecimal("2500"));
    List<Customer> listCust = new ArrayList<>();
    listCust.add(custa);
    listCust.add(custa1);
    listCust.add(custb);
    listCust.add(custc);
    listCust.add(custa2);

The result should be

[
    {"A", "STANDARD", new BigDecimal("1000"), new BigDecimal("1500")},
    {"A", "VIP", new BigDecimal("3500"), new BigDecimal("5000")},
    {"B", "STANDARD", new BigDecimal("3000"), new BigDecimal("3500")},
    {"C", "STANDARD", new BigDecimal("4000"), new BigDecimal("4500")}
]

I have a beginning of solution, below, but I block at the moment of adding a second aggregation to sum up the balance:

listCust.stream()
                .collect(Collectors.groupingBy(Customer::getName
                    , Collectors.groupingBy(Customer::getType
                        , Collectors.reducing(BigDecimal.ZERO,Customer::getTotal,BigDecimal::add)))
                )
                .entrySet()
3 Answers
Collection<Customer> customers = listCust.stream()
                                         .collect(Collectors.toMap(
                                                 customer -> customer.getName() + '-' + customer.getType(),
                                                 Function.identity(), (one, two) -> {
                                                     String name = one.getName();
                                                     String type = one.getType();
                                                     BigDecimal total = one.getTotal().add(two.getTotal());
                                                     BigDecimal balance = one.getBalance().add(two.getBalance());
                                                     return new Customer(name, type, total, balance);
                                                 })).values();

An alternative to using string conncatenation for the intermediate hashmap that provides more flexibility could be to use an Entry. This would allow you to change your grouping types and handles the case of null values if you wrap the key or value in Optional.ofNullable. It does have the drawback of limiting you to only two elements to group by.

Customer custa = new Customer("A", "STANDARD", new BigDecimal("1000"), new BigDecimal("1500"));
Customer custa1 = new Customer("A", "VIP", new BigDecimal("2000"), new BigDecimal("2500"));
Customer custb = new Customer("B", "STANDARD", new BigDecimal("3000"), new BigDecimal("3500"));
Customer custc = new Customer("C", "STANDARD", new BigDecimal("4000"), new BigDecimal("4500"));
Customer custa2 = new Customer("A", "VIP", new BigDecimal("1500"), new BigDecimal("2500"));
Customer custa3 = new Customer(null, "VIP", new BigDecimal("1500"), new BigDecimal("2500"));
List<Customer> listCust = new ArrayList<>();
listCust.add(custa);
listCust.add(custa1);
listCust.add(custb);
listCust.add(custc);
listCust.add(custa2);
listCust.add(custa3);
Collection<Customer> result =  listCust.stream()
        .collect(Collectors.toMap(
                c -> Map.entry(Optional.ofNullable(c.getName()), c.getType()),
                Function.identity(),
                (c1, c2) -> new Customer(c1.getName(),
                        c1.getType(),
                        c1.getTotal().add(c2.getTotal()),
                        c1.getBalance().add(c2.getBalance()))))
        .values();
System.out.println(result);

outupt (inserted newlines for readability)

[
    Customer{name='null', type='VIP', total=1500, balance=2500},
    Customer{name='B', type='STANDARD', total=3000, balance=3500},
    Customer{name='C', type='STANDARD', total=4000, balance=4500},
    Customer{name='A', type='VIP', total=3500, balance=5000},
    Customer{name='A', type='STANDARD', total=1000, balance=1500}
]

You can do it using toMap using the merge function, stream the customer list and collect them into Map using name-type as key, and use the merge function, to sum up the total and balance for customers having same key

Collection<Customer> result = listCust.stream()
            .collect(Collectors.toMap(cu -> cu.getName() + "-" + cu.getType(),
                    Function.identity(), (c1, c2) -> {
                        c1.setTotal(c1.getTotal().add(c2.getTotal()));
                        c1.setBalance(c1.getBalance().add(c2.getBalance()));
                        return c1;
                    })).values();
Related