Java 8 stream - concatenation of collections of objects having the same id
I have a collection of accounts:
class Invoice {
int month;
BigDecimal amount
}
I would like to combine these invoices, so I get one invoice per month and the amount is the sum of the sum of the invoices for that 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 using java 8 streams?
EDIT: Since my Invoice class was changed and it was not a problem to change them, I chose 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();
source to share
If you return ok Collection
, it looks 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 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 it Invoice
is mutable ...
source to share
If you can add the following constructor and merge method to your class Invoice
:
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 can reduce it however you like like this:
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 use Collectors.toMap
to do a job that has three arguments: a function that maps stream items to keys, a function that maps stream items to values, and a merge function that is used to merge values ββwhen keys collide.
source to share
You can do something like
Map<Integer, Invoice> invoiceMap = invoices.stream()
.collect(Collectors.groupingBy( // group invoices by month
invoice -> invoice.month
))
.entrySet().stream() // once you have them grouped stream then again so...
.collect(Collectors.toMap(
entry -> entry.getKey(), // we can mantain the key (month)
entry -> entry.getValue().stream() // and streaming all month invoices
.reduce((invoice, invoice2) -> // add all the ammounts
new Invoice(invoice.month, invoice.amount.add(invoice2.amount)))
.orElse(new Invoice(entry.getKey(), new BigDecimal(0))) // In case we don't have any invoice (unlikeable)
));
source to share
Here is my library solution: AbacusUtil
Stream.of(invoices)
.groupBy2(Invoice::getMonth, Invoice::getAmount, BigDecimal::add)
.map(e -> new Invoice(e.getKey(), e.getValue())) // Probably we should not modify original invoices. create new instances.
.toList();
source to share
I think if your app does not support lambda then this might be a suitable answer like (Android minSdkVersion = 16 does not support lambda)
public static List<Invoice> mergeAmount(List<Invoice> invoiceList) {
List<Invoice> newInvoiceList = new ArrayList<>();
for(Invoice inv: invoiceList) {
boolean isThere = false;
for (Invoice inv1: newInvoiceList) {
if (inv1.getAmount() == inv.getAmount()) {
inv1.setAmount(inv1.getAmoount()+inv.getAmount());
isThere = true;
break;
}
}
if (!isThere) {
newInvoiceList.add(inv);
}
}
return newInvoiceList;
}
source to share