Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Multiple aggregate functions in Java 8 Stream API

I have a class defined like

public class TimePeriodCalc {
    private double occupancy;
    private double efficiency;
    private String atDate;
}

I would like to perform the following SQL statement using Java 8 Stream API.

SELECT atDate, AVG(occupancy), AVG(efficiency)
FROM TimePeriodCalc
GROUP BY atDate

I tried :

Collection<TimePeriodCalc> collector = result.stream().collect(groupingBy(p -> p.getAtDate(), ....

What can be put into the code to select multiple attributes ? I'm thinking of using multiple Collectors but really don't know how to do so.

like image 285
Kha Nguyễn Avatar asked Jul 06 '17 07:07

Kha Nguyễn


1 Answers

Here's a way with a custom collector. It only needs one pass, but it's not very easy, especially because of generics...

If you have this method:

@SuppressWarnings("unchecked")
@SafeVarargs
static <T, A, C extends Collector<T, A, Double>> Collector<T, ?, List<Double>>
averagingManyDoubles(ToDoubleFunction<? super T>... extractors) {

    List<C> collectors = Arrays.stream(extractors)
        .map(extractor -> (C) Collectors.averagingDouble(extractor))
        .collect(Collectors.toList());

    class Acc {
        List<A> averages = collectors.stream()
            .map(c -> c.supplier().get())
            .collect(Collectors.toList());

        void add(T elem) {
            IntStream.range(0, extractors.length).forEach(i ->
                collectors.get(i).accumulator().accept(averages.get(i), elem));
        }

        Acc merge(Acc another) {
            IntStream.range(0, extractors.length).forEach(i ->
                averages.set(i, collectors.get(i).combiner()
                    .apply(averages.get(i), another.averages.get(i))));
            return this;
        }

        List<Double> finish() {
            return IntStream.range(0, extractors.length)
                .mapToObj(i -> collectors.get(i).finisher().apply(averages.get(i)))
                .collect(Collectors.toList());
        }
    }
    return Collector.of(Acc::new, Acc::add, Acc::merge, Acc::finish);
}

This receives an array of functions that will extract double values from each element of the stream. These extractors are converted to Collectors.averagingDouble collectors and then the local Acc class is created with the mutable structures that are used to accumulate the averages for each collector. Then, the accumulator function forwards to each accumulator, and so with the combiner and finisher functions.

Usage is as follows:

Map<String, List<Double>> averages = list.stream()
    .collect(Collectors.groupingBy(
        TimePeriodCalc::getAtDate,
        averagingManyDoubles(
            TimePeriodCalc::getOccupancy,
            TimePeriodCalc::getEfficiency)));
like image 106
fps Avatar answered Sep 21 '22 23:09

fps