Reputation: 2751
I want to calculate the total sum of Salary by personalId.
My map is of type Map<String, List<Employee>>
. Where employee id
is used a Key, while value is a list of employee instances having the same id
.
Here is the list of map shown below
Id, Name , Surname, Salary
personalId1, "Name 1","Surname 1", 100
personalId2, "Name 2","Surname 2", 100
personalId3, "Name 3","Surname 3", 100
personalId1, "Name 1","Surname 1", 100
personalId2, "Name 2","Surname 2", 100
personalId2, "Name 2","Surname 2", 100
.........................
What I really want to get this result shown below.
personalId1, "Name 1","Surname 1", 200
personalId2, "Name 2","Surname 2", 300
personalId3, "Name 3","Surname 3", 100
Here is my dto class shown below
public class SalaryDto {
private String id;
private String name;
private String surname;
private BigDecimal totalSalary;
}
I tried to use java stream to get this result but I have a problem after flatMap
. To get the total by personalId, I want to use "reduce".
Here is the code snippet shown below.
List<SalaryDto> totalSalary = employees.values().stream()
.flatMap(List::stream)
.map(e-> new SalaryDto(...))
.collect(groupingBy(Personal::getID, Collectors.toList()));
How can I do that?
Upvotes: 0
Views: 939
Reputation: 29038
You can define a custom accumulation type for calculating the total salary for each id (it's a common practice to keep DTO immutable, but if you want to avoid introducing new types you can add a new behaviour into SalaryDto
but I would not advice to do so).
public class SalaryAccumulator implements Consumer<Employee> {
private String id;
private String name;
private String surname;
private BigDecimal totalSalary = BigDecimal.ZERO;
public void accept(Employee e) {
if (id == null) id = e.getId();
if (name == null) name = e.getName();
if (surname == null) surname = e.getSurname();
totalSalary = totalSalary.add(e.getSalary());
}
public SalaryAccumulator merge(SalaryAccumulator other) {
totalSalary = totalSalary.add(other.getTotalSalary());
return this;
}
public SalaryDto toSalaryDto() {
return new SalaryDto(id, name, surname, totalSalary);
}
}
And in the stream in order to turn each List
of Employee
into a SalaryDto
you can generate a stream over the list and apply collect()
passing a custom Collector as an argument.
To define a custom Collector, we need to make of the static factory method Collector.of()
. That's where SalaryAccumulator
would come into play, it would serve as a mutable container of the collector.
Map<String, List<Employee>> employees = // initializing the map
List<SalaryDto> totalSalary = employees.values().stream()
.map(list -> list.stream()
.collect(Collector.of(
SalaryAccumulator::new,
SalaryAccumulator::accept,
SalaryAccumulator::merge,
SalaryAccumulator::toSalaryDto
))
).toList();
Alternatively, in the nested stream, we can make use of the combination of Collectors collectingAndThen()
and reducing()
:
Map<String, List<Employee>> employees = // initializing the map
List<SalaryDto> totalSalary = employees.values().stream()
.map(l -> l.stream()
.map(Employee::getSalary)
.collect(Collectors.collectingAndThen(
Collectors.reducing(BigDecimal::add),
salary -> new SalaryDto(l.get(0).getId(),
l.get(0).getName(),
l.get(0).getSurname(),
salary.orElseThrow())
))
).toList();
Upvotes: 0
Reputation: 50746
They're already grouped, so all you need to do is reduce each list to a single SalaryDto
:
SalaryDto flatten(List<Employee> employees) {
BigDecimal totalSalary = employees.stream()
.map(Employee::getSalary)
.reduce(BigDecimal::add).get();
return SalaryDto.forEmployee(employees.get(0), totalSalary);
}
And now it's a simple map
call:
List<SalaryDto> salaries = employees.values()
.stream()
.map(this::flatten)
.collect(toMap(SalaryDto::getId, s -> s));
Upvotes: 1