javalambdajava-8java-stream

How to create a map of maps from a list of maps with Java 8 streaming api


Background

I have a list of maps that looks something like this:

[
  {
    "name": "A",
    "old": 0.25,
    "new": 0.3
  },
  {
    "name": "B",
    "old": 0.3,
    "new": 0.35
  },
  {
    "name": "A",
    "old": 0.75,
    "new": 0.7
  },
  {
    "name": "B",
    "old": 0.7,
    "new": 0.6
  }
]

and I want the output to look like this:

{
  "A": {
    "old": 1,
    "new": 1
  },
  "B": {
    "old": 1,
    "new": 0.95
  }
}

...where the values of old and new are summed for each related entry.

The data type of the list of maps is List<Map<String, Object>>, so the output should be a Map<String, Map<String, Double>>.

What I've Tried

With some diagram drawing, documentation reading, and trial and error, I was able to come up with this:

data.stream()
    .collect(
        Collectors.groupingBy(entry -> entry.get("name"),
            Collectors.summingDouble(entry ->
                Double.parseDouble(entry.get("old").toString())))
    );

to produce an object of type Map<String, Double>, where the output is

{
  "A": 1,
  "B": 1
}

for the summations of the old values. However, I can't quite transform it into a map of maps. Something like this:

data.stream()
    .collect(
        Collectors.groupingBy(entry -> entry.get("name"),
            Collectors.mapping(
                Collectors.groupingBy(entry -> entry.get("old"),
                    Collectors.summingDouble(entry ->
                        Double.parseDouble(entry.get("old").toString())
                    )
                ),
                Collectors.groupingBy(entry -> entry.get("new"),
                    Collectors.summingDouble(entry ->
                        Double.parseDouble(entry.get("new").toString())
                    )
                )
            )
        )
    );

doesn't work, because Collectors.mapping() only takes one mapping function and a downstream collector, but I'm not sure how to map two values at once.

Is there another function I need to create mappings of two different values? Any suggestions on better ways of doing this is greatly appreciated as well.


Solution

  • You can use streams, but you can also use Map's computeIfAbsent and merge methods:

    Map<String, Map<String, Double>> result = new LinkedHashMap<>();
    data.forEach(entry -> {
        String name = (String) entry.get("name");
        Map<String, Double> map = result.computeIfAbsent(name, k -> new HashMap<>());
        map.merge("old", (Double) entry.get("old"), Double::sum);
        map.merge("new", (Double) entry.get("new"), Double::sum);
    });