Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using Java Streams to group together a List of objects by an attribute and reduce them to a new list of object with the average of another attribute

I have a list of SensorSample POJOs

public class SensorSample {

    private Device.SensorType sensorType; // This is an enum
    private double sample;
    private long timestamp;

    // Constructor

    // Setters

    // Getters

}

I need to group them by the timestamp, so that all the SensorSamples of the same day are together. Then I need to reduce them so that I have only one SensorSample for each day and the value of its sample is the average of the value of the sample of all the objects for that day. Is there a way to do that with Streams?

So far I got this to group them together:

Map<Long, List<SensorSample>> yearSamples = samples.stream()
                .collect(groupingBy(sample -> SECONDS_IN_A_DAY*Math.floorDiv(sample.getTimestamp(), SECONDS_IN_A_DAY)));

But I don't know how to go any further.

like image 436
Marcello Avatar asked Nov 19 '25 10:11

Marcello


2 Answers

Something like this, I think. To find the average number of a group:

Map<Long, Double> averages = samples.stream()
  .collect(groupingBy(SensorSample::getTimestamp,
   averagingDouble(SensorSample::getSample)));

I didn't expand out your formula for the day, I think it's more readable if I just called getTimestamp and leave the details out. Your code might also be more readable if you added a getDay method to SensorSample.

Also this would be easier to test if you provided an MCVE, as it's a little hard to test the code above with only one partial class to go on.

like image 60
markspace Avatar answered Nov 20 '25 22:11

markspace


it seems as if you want a List<SensorSample> as a result where each group after the groupingBy is reduced into a single SensorSample.

List<SensorSample> result = samples.stream()
                .collect(groupingBy(sample -> SECONDS_IN_A_DAY*Math.floorDiv(sample.getTimestamp(), SECONDS_IN_A_DAY))
                .entrySet()
                .stream()
                .map(e -> {
                    SensorSample sensorSample = new SensorSample();
                    sensorSample.setTimestamp(e.getKey());
                    double average = e.getValue().stream()
                            .mapToDouble(SensorSample::getSample)
                            .average().orElse(0);
                    sensorSample.setSample(average);
                    sensorSample.setSensorType(e.getValue().get(0).getSensorType());
                    return sensorSample;
                }).collect(Collectors.toList());

The map logic seems a little bit large thus I'd consider refactoring it out to a method as such:

private static SensorSample apply(Map.Entry<Long, List<SensorSample>> e) {
        SensorSample sensorSample = new SensorSample();
        sensorSample.setTimestamp(e.getKey());
        double average = e.getValue().stream()
                .mapToDouble(SensorSample::getSample)
                .average().orElse(0);
        sensorSample.setSample(average);
        sensorSample.setSensorType(e.getValue().get(0).getSensorType());
        return sensorSample;
}

Then the stream pipeline would become:

List<SensorSample> result = samples.stream()
                .collect(groupingBy(sample -> SECONDS_IN_A_DAY*Math.floorDiv(sample.getTimestamp(), SECONDS_IN_A_DAY))
                .entrySet()
                .stream()
                .map(Main::apply)
                .collect(Collectors.toList());

Where Main is the class containing the apply method.

like image 44
Ousmane D. Avatar answered Nov 20 '25 22:11

Ousmane D.



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!