javaeventsjava-stream

Stream group incoming sort events


I have a series of events, each event contains a timestamp, device, serialnumber, and measurement.

class Event {
    private String device;
    private String description;
    private String serialnumber;
    private Measurement measurement;
}

class Measurement {
    private LocalDateTime timestamp;
    private int value;
}

I have a stream of these events, and I would like to aggregate them into a simpler structure, dropping the serial number, then group them by device, and then sorting the measurements by timestamp and value.

{device: "device_1", description: "first device", serialnumber: "1", measurement: { timestamp: 2022-04-23T18:20:22Z, value: 180}}
{device: "device_2", description: "second device", serialnumber: "2", measurement: { timestamp: 2022-04-23T18:20:28Z, value: 120}}
{device: "device_2", description: "second device", serialnumber: "2", measurement: { timestamp: 2022-04-23T18:20:20Z, value: 160}}
{device: "device_1", description: "first device", serialnumber: "1", measurement: { timestamp: 2022-04-23T18:20:22Z, value: 170}}
[
    {
        device: "device_1",
        description: "first device",
        measurements: [
            { timestamp: 2022-04-23T18:20:22Z, value: 170},
            { timestamp: 2022-04-23T18:20:22Z, value: 180}
        ]
    },
    {
        device: "device_2",
        description: "second device",
        measurements: [
            { timestamp: 2022-04-23T18:20:20Z, value: 160},
            { timestamp: 2022-04-23T18:20:28Z, value: 120}
        ]
    }
]

I've managed to get the required format by creating a "builder" class in which you can insert events that get then processed and added to the data members in the correct format/order. However, I think it would be nicer to somehow achieve this on the fly without another extra class, but using the groupingBy and toMap (and other?) stream methods.


Solution

  • You could group your Event objects by their device, and then collect the Measurement in a TreeSet to sort them by their timestamp and value.

    Map<String, TreeSet<Measurement>> map = dataSource.stream()
        .collect(Collectors.groupingBy(
                        Event::getDevice,
                        Collectors.mapping(Event::getMeasurement,
                                Collectors.toCollection(TreeSet::new)
                        )
                )
        );
    

    In this scenario, you need to either implement the Comparable interface on the Measurement class and provide a definition for compareTo, or pass a Comparator to the TreeSet with the desired sorting logic. In both cases, remember to provide a proper definition so that this is consistent with equals. According to the Comparable interface:

    It is strongly recommended (though not required) that natural orderings be consistent with equals. This is so because sorted sets (and sorted maps) without explicit comparators behave "strangely" when they are used with elements (or keys) whose natural ordering is inconsistent with equals. In particular, such a sorted set (or sorted map) violates the general contract for set (or map), which is defined in terms of the equals method.

    @Data
    @AllArgsConstructor
    class Event {
        private String device;
        private String description;
        private String serialnumber;
        private Measurement measurement;
    }
    
    @Data
    @AllArgsConstructor
    class Measurement implements Comparable<Measurement> {
        private LocalDateTime timestamp;
        private int value;
    
        //Redefinition of Measurement's natural ordering by its timestamp and value
        @Override
        public int compareTo(Measurement m) {
            Comparator<Measurement> cmp = Comparator.comparing(Measurement::getTimestamp)
                .thenComparing(Measurement::getValue);
            
            return cmp.compare(this, m);
        }
    }
    

    Here is a demo at OneCompiler