How to sum a triple nested map with Java 8 Collectors

Viewed 523

I have this map Map<LocalDate, Map<Integer, Map<EHourQuarter, Double>>>

EHourQuarter is an enum:

public enum EHourQuarter {
    FIRST(0, 14, 15),
    SECOND(15, 29, 30),
    THIRD(30, 44, 45),
    FOURTH(45, 59, 60);

    private Integer start;
    private Integer end;
    private Integer value;//this is for UI purposes
}

With values like: {2020-07-07 -> {0 -> {EHourQuarter.FIRST -> 5.5, EHourQuarter.SECOND -> 10.2, ...}, 1 -> {EHourQuarter.FIRST -> 33.2, EHourQuarter.SECOND -> 30.1, ...}, ...},

2020-07-08 -> {0 -> {EHourQuarter.FIRST -> 5.5, EHourQuarter.SECOND -> 10.2, ...}, 1 -> {EHourQuarter.FIRST -> 33.2, EHourQuarter.SECOND -> 30.1, ...}, ... }

It's a map of LocalDate of map of Integer (hour: from 0 to 23) of map of EHourQuarter of Double.

And I need to get a Map<Integer(hour), Map<EHourQuarter, Double>> containing the accumulated of every date, meaning that if dates 2020-07-07 to 2020-07-10 (4 days) contained in hour 0 each one 5 for every EHourQuarter, then the result should show in hour 0, each quarter with a value of 20.

Additionally, if by making that you could help me also with mapping that to a List of DTOs like this ones,

public class QuarterlyOccupancyDTO {
    private Integer hour;
    private Integer minute;//this is the value property of EHourQuarter
    private Double occupancy;
}

I'd much appreciate it.

At the end, the list of DTOs should contain the sum of all dates grouped by hour and minute (value property of EHourQuarter).

This is an example.

NOTE: a map can contain multiple dates and the aim is to group/sum all .

Given this map:

{
   "2020-06-26":{
      "0":{
         "FOURTH":0.0,
         "FIRST":0.0,
         "THIRD":0.0,
         "SECOND":0.0
      },
      "1":{
         "FOURTH":0.0,
         "FIRST":0.0,
         "THIRD":0.0,
         "SECOND":0.0
      },
      "2":{
         "FOURTH":0.0,
         "FIRST":0.0,
         "THIRD":0.0,
         "SECOND":0.0
      },
      "3":{
         "FOURTH":0.0,
         "FIRST":0.0,
         "THIRD":0.0,
         "SECOND":0.0
      },
      "4":{
         "FOURTH":0.0,
         "FIRST":0.0,
         "THIRD":0.0,
         "SECOND":0.0
      },
      "5":{
         "FOURTH":0.0,
         "FIRST":0.0,
         "THIRD":0.0,
         "SECOND":0.0
      },
      "6":{
         "FOURTH":0.0,
         "FIRST":0.0,
         "THIRD":0.0,
         "SECOND":0.0
      },
      "7":{
         "FOURTH":0.0,
         "FIRST":0.0,
         "THIRD":0.0,
         "SECOND":0.0
      },
      "8":{
         "FOURTH":0.0,
         "FIRST":0.0,
         "THIRD":0.0,
         "SECOND":0.0
      },
      "9":{
         "FOURTH":5.0,
         "FIRST":5.0,
         "THIRD":5.0,
         "SECOND":5.0
      },
      "10":{
         "FOURTH":5.0,
         "FIRST":5.0,
         "THIRD":5.0,
         "SECOND":5.0
      },
      "11":{
         "FOURTH":5.0,
         "FIRST":5.0,
         "THIRD":5.0,
         "SECOND":5.0
      },
      "12":{
         "FOURTH":5.0,
         "FIRST":5.0,
         "THIRD":5.0,
         "SECOND":5.0
      },
      "13":{
         "FOURTH":5.0,
         "FIRST":5.0,
         "THIRD":5.0,
         "SECOND":5.0
      },
      "14":{
         "FOURTH":5.0,
         "FIRST":5.0,
         "THIRD":5.0,
         "SECOND":5.0
      },
      "15":{
         "FOURTH":5.0,
         "FIRST":5.0,
         "THIRD":5.0,
         "SECOND":5.0
      },
      "16":{
         "FOURTH":5.0,
         "FIRST":5.0,
         "THIRD":5.0,
         "SECOND":5.0
      },
      "17":{
         "FOURTH":5.0,
         "FIRST":5.0,
         "THIRD":5.0,
         "SECOND":5.0
      },
      "18":{
         "FOURTH":0.0,
         "FIRST":5.0,
         "THIRD":0.0,
         "SECOND":0.0
      },
      "19":{
         "FOURTH":0.0,
         "FIRST":0.0,
         "THIRD":0.0,
         "SECOND":0.0
      },
      "20":{
         "FOURTH":0.0,
         "FIRST":0.0,
         "THIRD":0.0,
         "SECOND":0.0
      },
      "21":{
         "FOURTH":0.0,
         "FIRST":0.0,
         "THIRD":0.0,
         "SECOND":0.0
      },
      "22":{
         "FOURTH":0.0,
         "FIRST":0.0,
         "THIRD":0.0,
         "SECOND":0.0
      },
      "23":{
         "FOURTH":0.0,
         "FIRST":0.0,
         "THIRD":0.0,
         "SECOND":0.0
      }
   }
}

A list like this should be the answer:

[
   {
      "hour":0,
      "minute":15,
      "occupancy":0.0
   },
   {
      "hour":0,
      "minute":30,
      "occupancy":0.0
   },
   {
      "hour":0,
      "minute":45,
      "occupancy":0.0
   },
   {
      "hour":0,
      "minute":60,
      "occupancy":0.0
   },
   {
      "hour":1,
      "minute":15,
      "occupancy":0.0
   },
   {
      "hour":1,
      "minute":30,
      "occupancy":0.0
   },
   {
      "hour":1,
      "minute":45,
      "occupancy":0.0
   },
   {
      "hour":1,
      "minute":60,
      "occupancy":0.0
   },
   {
      "hour":2,
      "minute":15,
      "occupancy":0.0
   },
   {
      "hour":2,
      "minute":30,
      "occupancy":0.0
   },
   {
      "hour":2,
      "minute":45,
      "occupancy":0.0
   },
   {
      "hour":2,
      "minute":60,
      "occupancy":0.0
   },
   {
      "hour":3,
      "minute":15,
      "occupancy":0.0
   },
   {
      "hour":3,
      "minute":30,
      "occupancy":0.0
   },
   {
      "hour":3,
      "minute":45,
      "occupancy":0.0
   },
   {
      "hour":3,
      "minute":60,
      "occupancy":0.0
   },
   {
      "hour":4,
      "minute":15,
      "occupancy":0.0
   },
   {
      "hour":4,
      "minute":30,
      "occupancy":0.0
   },
   {
      "hour":4,
      "minute":45,
      "occupancy":0.0
   },
   {
      "hour":4,
      "minute":60,
      "occupancy":0.0
   },
   {
      "hour":5,
      "minute":15,
      "occupancy":0.0
   },
   {
      "hour":5,
      "minute":30,
      "occupancy":0.0
   },
   {
      "hour":5,
      "minute":45,
      "occupancy":0.0
   },
   {
      "hour":5,
      "minute":60,
      "occupancy":0.0
   },
   {
      "hour":6,
      "minute":15,
      "occupancy":0.0
   },
   {
      "hour":6,
      "minute":30,
      "occupancy":0.0
   },
   {
      "hour":6,
      "minute":45,
      "occupancy":0.0
   },
   {
      "hour":6,
      "minute":60,
      "occupancy":0.0
   },
   {
      "hour":7,
      "minute":15,
      "occupancy":0.0
   },
   {
      "hour":7,
      "minute":30,
      "occupancy":0.0
   },
   {
      "hour":7,
      "minute":45,
      "occupancy":0.0
   },
   {
      "hour":7,
      "minute":60,
      "occupancy":0.0
   },
   {
      "hour":8,
      "minute":15,
      "occupancy":0.0
   },
   {
      "hour":8,
      "minute":30,
      "occupancy":0.0
   },
   {
      "hour":8,
      "minute":45,
      "occupancy":0.0
   },
   {
      "hour":8,
      "minute":60,
      "occupancy":0.0
   },
   {
      "hour":9,
      "minute":15,
      "occupancy":5.0
   },
   {
      "hour":9,
      "minute":30,
      "occupancy":5.0
   },
   {
      "hour":9,
      "minute":45,
      "occupancy":5.0
   },
   {
      "hour":9,
      "minute":60,
      "occupancy":5.0
   },
   {
      "hour":10,
      "minute":15,
      "occupancy":5.0
   },
   {
      "hour":10,
      "minute":30,
      "occupancy":5.0
   },
   {
      "hour":10,
      "minute":45,
      "occupancy":5.0
   },
   {
      "hour":10,
      "minute":60,
      "occupancy":5.0
   },
   {
      "hour":11,
      "minute":15,
      "occupancy":5.0
   },
   {
      "hour":11,
      "minute":30,
      "occupancy":5.0
   },
   {
      "hour":11,
      "minute":45,
      "occupancy":5.0
   },
   {
      "hour":11,
      "minute":60,
      "occupancy":5.0
   },
   {
      "hour":12,
      "minute":15,
      "occupancy":5.0
   },
   {
      "hour":12,
      "minute":30,
      "occupancy":5.0
   },
   {
      "hour":12,
      "minute":45,
      "occupancy":5.0
   },
   {
      "hour":12,
      "minute":60,
      "occupancy":5.0
   },
   {
      "hour":13,
      "minute":15,
      "occupancy":5.0
   },
   {
      "hour":13,
      "minute":30,
      "occupancy":5.0
   },
   {
      "hour":13,
      "minute":45,
      "occupancy":5.0
   },
   {
      "hour":13,
      "minute":60,
      "occupancy":5.0
   },
   {
      "hour":14,
      "minute":15,
      "occupancy":5.0
   },
   {
      "hour":14,
      "minute":30,
      "occupancy":5.0
   },
   {
      "hour":14,
      "minute":45,
      "occupancy":5.0
   },
   {
      "hour":14,
      "minute":60,
      "occupancy":5.0
   },
   {
      "hour":15,
      "minute":15,
      "occupancy":5.0
   },
   {
      "hour":15,
      "minute":30,
      "occupancy":5.0
   },
   {
      "hour":15,
      "minute":45,
      "occupancy":5.0
   },
   {
      "hour":15,
      "minute":60,
      "occupancy":5.0
   },
   {
      "hour":16,
      "minute":15,
      "occupancy":5.0
   },
   {
      "hour":16,
      "minute":30,
      "occupancy":5.0
   },
   {
      "hour":16,
      "minute":45,
      "occupancy":5.0
   },
   {
      "hour":16,
      "minute":60,
      "occupancy":5.0
   },
   {
      "hour":17,
      "minute":15,
      "occupancy":5.0
   },
   {
      "hour":17,
      "minute":30,
      "occupancy":5.0
   },
   {
      "hour":17,
      "minute":45,
      "occupancy":5.0
   },
   {
      "hour":17,
      "minute":60,
      "occupancy":5.0
   },
   {
      "hour":18,
      "minute":15,
      "occupancy":5.0
   },
   {
      "hour":18,
      "minute":30,
      "occupancy":0.0
   },
   {
      "hour":18,
      "minute":45,
      "occupancy":0.0
   },
   {
      "hour":18,
      "minute":60,
      "occupancy":0.0
   },
   {
      "hour":19,
      "minute":15,
      "occupancy":0.0
   },
   {
      "hour":19,
      "minute":30,
      "occupancy":0.0
   },
   {
      "hour":19,
      "minute":45,
      "occupancy":0.0
   },
   {
      "hour":19,
      "minute":60,
      "occupancy":0.0
   },
   {
      "hour":20,
      "minute":15,
      "occupancy":0.0
   },
   {
      "hour":20,
      "minute":30,
      "occupancy":0.0
   },
   {
      "hour":20,
      "minute":45,
      "occupancy":0.0
   },
   {
      "hour":20,
      "minute":60,
      "occupancy":0.0
   },
   {
      "hour":21,
      "minute":15,
      "occupancy":0.0
   },
   {
      "hour":21,
      "minute":30,
      "occupancy":0.0
   },
   {
      "hour":21,
      "minute":45,
      "occupancy":0.0
   },
   {
      "hour":21,
      "minute":60,
      "occupancy":0.0
   },
   {
      "hour":22,
      "minute":15,
      "occupancy":0.0
   },
   {
      "hour":22,
      "minute":30,
      "occupancy":0.0
   },
   {
      "hour":22,
      "minute":45,
      "occupancy":0.0
   },
   {
      "hour":22,
      "minute":60,
      "occupancy":0.0
   },
   {
      "hour":23,
      "minute":15,
      "occupancy":0.0
   },
   {
      "hour":23,
      "minute":30,
      "occupancy":0.0
   },
   {
      "hour":23,
      "minute":45,
      "occupancy":0.0
   },
   {
      "hour":23,
      "minute":60,
      "occupancy":0.0
   }
]
2 Answers

First using flatMap create Stream<SimpleEntry<Integer, EHourQuarter>, Double> then using toMap collect as Map<SimpleEntry<Integer, EHourQuarter>, Double>. Then map into you DTO class.

List<QuarterlyOccupancyDTO> result = map.entrySet().stream()
        .flatMap(d -> d.getValue().entrySet().stream()
            .flatMap(h -> h.getValue().entrySet().stream().map(
                e -> new SimpleEntry<>(new SimpleEntry<>(h.getKey(), e.getKey()), e.getValue()))))
        .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (a, b) -> a + b))
        .entrySet()
        .stream()
        .map(m -> new QuarterlyOccupancyDTO(m.getKey().getKey(), m.getKey().getValue().getValue(), m.getValue()))
        .collect(Collectors.toList());

Note: As you don't show your code some part may not work. Full code here

First group and sum the occupancy by hour/quarter pair

(Avoid nested flatMap, as it makes code less readable)

Map<Entry<Integer, Integer>, Double> groups
    = map.entrySet()
         .stream()
         // Flatten the outer map, since you don't care about the days
         .flatMap(de -> de.getValue().entrySet().stream())
         // Flatten the map by combining hour key and quarter key into a single one
         .flatMap(he -> he.getValue()
                          .entrySet()
                          .stream()
                          .map(qe -> new SimpleEntry<>(new SimpleEntry<>(he.getKey(), qe.getKey().getValue()), qe.getValue())))
         // Sum the occupancy per each hour/quarter pair
         .collect(groupingBy(Entry::getKey, summingDouble(Entry::getValue)));

Then map the grouped entries into your DTO objects

List<QuarterlyOccupancyDTO> list =
    groups.entrySet()
          .stream()
          .map(e -> new QuarterlyOccupancyDTO(e.getKey().getKey(), e.getKey().getValue(), e.getValue()))
          .collect(toList());

Another pure functional approach:

(It's pure, but seems less readable, IMO)

Collection<QuarterlyOccupancyDTO> dtos =
    map.entrySet()
       .stream()
       // Flatten the outer map, since you don't care about the days
       .flatMap(de -> de.getValue().entrySet()
                        .stream())
       // Flatten the map by merging hour key and quarter key into a single one
       .flatMap(he -> he.getValue()
                        .entrySet()
                        .stream()
                        .map(qe -> new SimpleEntry<>(new SimpleEntry<>(he.getKey(), qe.getKey().getValue()),
                                                     qe.getValue())))
       // Map each entry into a DTO object and then reduce the occupancy per each hour/quarter pair
       .collect(
           groupingBy(Entry::getKey,
                      mapping(e -> new QuarterlyOccupancyDTO(e.getKey().getKey(), e.getKey().getValue(), e.getValue()),
                              reducing(new QuarterlyOccupancyDTO(0, 0, 0.0),
                                       (a, b) -> new QuarterlyOccupancyDTO(b.getHour(), b.getMinute(), a.getOccupancy() + b.getOccupancy())))))
       .values();
Related