Home > Software design >  Java Stream - Not Calculating total count and total sum for Map List with filtering and grouping pro
Java Stream - Not Calculating total count and total sum for Map List with filtering and grouping pro

Time:11-22

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 ids. 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();
  • Related