Search code examples
javajava-stream

How to have multiple ways of doing aggregation based on a condition when 'groupingBy' in Java streams?


Having the following class:

public class TimeInterval {
    private ZonedDateTime time;
    private Double value1;
    private Double value2;
}

where the TimeInterval.times are intervals in the day what I want to do is group by day and aggregate the values. The tricky part is that I want to apply two different types of aggregations based on a certain condition.

For example:

2018-01-01T00:00, 1.0, 2.0
2018-01-01T08:00, null, null
2018-01-01T16:00, 5.0, 6.0
2018-01-02T00:00, 1.0, 2.0
2018-01-02T08:00, 3.0, 4.0
2018-01-02T16:00, 5.0, 6.0
2018-01-03T00:00, null, null
2018-01-03T08:00, null, null
2018-01-03T16:00, null, null

Should be aggregated to:

2018-01-01, 32.0 - nulls are replaced with 0.0 in this case
2018-01-02, 44.0 - all values valid
2018-01-03, null - all intervals with null values, final value is null

The aggregation function is value1 * value2 but the point is that I want the nulls to be replaced with 0.0 in the case when only part of the intervals are with null values (2018-01-01 from the above example) but if all the intervals are with null values I want the final value for the day to be null (2018-01-03 from the above example).

How do I do that with Java streams?


Solution

  • You can use

    Map<LocalDate, Double> map = list.stream()
        .collect(groupingBy(ti -> ti.time.toLocalDate(),
            collectingAndThen(
                filtering(ti -> ti.value1 != null,
                    mapping(ti -> ti.value1 * ti.value2, reducing(Double::sum))),
            o -> o.orElse(null))
        ));
    

    Normally, you would use the toMap collector for a Reduction like this, but toMap does not allow functions to return null. Further, conditional expressions mixing numeric expressions and null are hard to understand and can easily lead to exceptions.

    So this solution filters null values first, uses the reducing collector which produces an Optional which will be empty when all values were null, so we can substitute the empty optional with the desired null result at a final step using orElse(null).

    Demo on onecompiler.com