I want to get a DTO result after implementing different kinds of filtering and grouping by with Java Stream feature.
Here is the list stored as a Map<String,Student>
Id Type Value Date
sId1 BONUS 10 1-1-2022
sId1 OUT 20 2-1-2022
sId1 IN 100 3-1-2022
sId1 OTHER "other" 4-1-2022
sId1 BONUS 10 1-2-2022
sId2 OUT 20 2-2-2022
sId3 IN 100 3-2-2022
sId2 BONUS 10 4-2-2022
sId2 OUT 20 1-3-2022
sId2 IN 100 2-3-2022
....
I want to get this result shown below after I only need the total sum except for OTHER tag and total count as Student
month - Total Value - Total Student
1 230 1 (Only sId1)
2 140 3 (sId1,sId2 and sId3)
3 120 1 (Only sId1)
Here is my Studeny Class shown below
public class Student {
private String id;
private State type;
private Object value;
private LocalDate date;
}
Here is the State as enum
public enum State {
BONUS, IN, OUT, OTHER
}
Here is my result DTO class shown below.
public class ResultDto {
private int month;
private BigDecimal totalValue;
private int totalStudents;
}
Here is the groupmetric class shown below for count and sum
public class PersonGroupMetric {
public static final PersonGroupMetric EMPTY = new PersonGroupMetric(0, 0);
private int count;
private int sum;
public PersonGroupMetric(int count, int sum) {
this.count = count;
this.sum = sum;
}
public int getCount() {
return count;
}
public void setCount(int count) {
this.count = count;
}
public int getSum() {
return sum;
}
public void setSum(int sum) {
this.sum = sum;
}
public PersonGroupMetric(Student e) {
this(1, Integer.parseInt(e.getValue().toString()));
}
public PersonGroupMetric add(PersonGroupMetric other) {
return new PersonGroupMetric(
this.count other.count,
this.sum other.sum
);
}
}
I followed this steps to handle with that.
1 ) Filter only BONUS, OUT and IN from the map.
Map<Integer, PersonGroupMetric> group = students.values().stream()
.flatMap(List::stream)
.filter(s -> s.getType() == State.BONUS || s.getType() == State.IN
|| s.getType() == OUT)
2 ) Grouping By month to calculate the results.
.collect(Collectors.groupingBy(
e -> e.getEventDate().getMonthValue(),Collectors.reducing(
PersonGroupMetric.EMPTY,
PersonGroupMetric::new,
PersonGroupMetric::add
)
));
3 ) Map Object to Dto
var fin = res.entrySet().stream()
.map(n -> new Dto(
n.getKey(),
new BigDecimal(n.getValue().getSum()),
n.getValue().getCount()
)).collect(toList());
fin.forEach(System.out::println);
Here is the result shown below after fin result.
month - Total Value - Total Student
1 230 3
2 140 3
3 120 2
There is a problem in Total student count. How can I fix it?
CodePudding user response:
In order to obtain the count of unique Students
(i.e. having distinct ids) per every month, while accumulating the data you can maintain a Set
in the Collector and offer id
of each consumed Student to the set.
To minimize the changes, you can make use of the Java 12 Collector teeing()
and provide it as a downstream collector of groupingBy()
.
teeing()
expects three arguments: two downstream Collectors, and a function that that generates the resulting value based on the partial results produced by each Collector.
As the first downstream, we can provide a combination of Collectors mapping()
toSet()
which would be responsible for maintaining a Set
of id
s. And the second downstream would be collector reducing from your code.
The merger function would generate an instance of ResultDto
based on set size and PersonGroupMetric
produced by Collector reducing()
.
That would require making one change in PersonGroupMetric
, namelly adding month
field.
public static class PersonGroupMetric {
public static final PersonGroupMetric EMPTY = new PersonGroupMetric(0, BigDecimal.ZERO, -1);
private int count;
private BigDecimal sum;
private int month;
// all-args constructor, getters
public PersonGroupMetric(Student e) {
this(1, e.getValue(), e.getEventDate().getMonthValue());
}
public PersonGroupMetric add(PersonGroupMetric other) {
return new PersonGroupMetric(
this.count other.count,
this.sum.add(other.sum),
this.month
);
}
}
And when bring the pieces together, that's how the stream might look like:
List<ResultDto> result = students.values().stream()
.flatMap(List::stream)
.filter(s -> s.getType() == State.BONUS || s.getType() == State.IN
|| s.getType() == State.OUT)
.collect(Collectors.groupingBy(
e -> e.getEventDate().getMonthValue(),
Collectors.teeing(
Collectors.mapping(Student::getId, Collectors.toSet()),
Collectors.reducing(
PersonGroupMetric.EMPTY,
PersonGroupMetric::new,
PersonGroupMetric::add
),
(set, metric) -> new ResultDto(
metric.getMonth(),
metric.getSum(),
set.size() // the number of unique ids
)
)
))
.values().stream().toList();