Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Java 8 stream - merge collections of objects sharing the same Id

I have a collection of invoices :

class Invoice {
  int month;
  BigDecimal amount
}

I'd like to merge these invoices, so I get one invoice per month, and the amount is the sum of the invoices amount for this month.

For example:

invoice 1 : {month:1,amount:1000}
invoice 2 : {month:1,amount:300}
invoice 3 : {month:2,amount:2000}

Output:

invoice 1 : {month:1,amount:1300}
invoice 2 : {month:2,amount:2000}

How can I do this with java 8 streams?

EDIT : as my Invoice class was mutable and it was not a problem to modify them, I choosed Eugene's solution

Collection<Invoice>  invoices = list.collect(Collectors.toMap(Invoice::getMonth, Function.identity(), (left, right) -> {
                left.setAmount(left.getAmount().add(right.getAmount()));
                return left;
            })).values();
like image 265
jaudo Avatar asked Jun 14 '17 08:06

jaudo


2 Answers

If you could add the following copy constructor and merge method to your Invoice class:

public Invoice(Invoice another) {
    this.month = another.month;
    this.amount = another.amount;
}

public Invoice merge(Invoice another) {
    amount = amount.add(another.amount); // BigDecimal is immutable
    return this;
}

You could reduce as you want, as follows:

Collection<Invoice> result = list.stream()
    .collect(Collectors.toMap(
        Invoice::getMonth, // use month as key
        Invoice::new,      // use copy constructor => don't mutate original invoices
        Invoice::merge))   // merge invoices with same month
    .values();

I'm using Collectors.toMap to do the job, which has three arguments: a function that maps elements of the stream to keys, a function that maps elements of the stream to values and a merge function that is used to combine values when there are collisions on the keys.

like image 86
fps Avatar answered Sep 16 '22 14:09

fps


If you are OK returning a Collection it would look like this:

Collection<Invoice>  invoices = list.collect(Collectors.toMap(Invoice::getMonth, Function.identity(), (left, right) -> {
                left.setAmount(left.getAmount().add(right.getAmount()));
                return left;
            })).values();

If you really need a List:

 list.stream().collect(Collectors.collectingAndThen(Collectors.toMap(Invoice::getMonth, Function.identity(), (left, right) -> {
                left.setAmount(left.getAmount().add(right.getAmount()));
                return left;
            }), m -> new ArrayList<>(m.values())));

Both obviously assume that Invoice is mutable...

like image 25
Eugene Avatar answered Sep 17 '22 14:09

Eugene