Fab
Fab

Reputation: 1215

Performing an operation between two HashMaps by using STREAM API

I have two HashMaps in the form

Map<Integer, Map<String,Double>> MAP_1
Map<Integer, Map<String,Double>> MAP_2

Let INNER be the inner map, i.e.

Map<String,Double>

For each pair of corresponding keys, I want to perform an operation (e.g. subtraction) between the two inner maps. In turn, this operation should be performed between two corresponding keys of the inner maps. The resulting map should be

Map<Integer,List<Double> RESULT

Sample maps are

MAP_1
------------------------------
|      |  2016-10-02   10.0   |
| ID1  |  2016-10-03   20.0   |
|      |  2016-10-04   30.0   |
------------------------------
|      |  2016-10-02   1.00   | 
| ID2  |  2016-10-03   2.00   |
|      |  2016-10-04   3.00   |
------------------------------

MAP_2
------------------------------
|      |  2016-10-02   2.00   |
| ID1  |  2016-10-03   3.00   |
|      |                      |
------------------------------
|      |  2016-10-02   1.00   | 
| ID3  |  2016-10-03   2.00   |
|      |  2016-10-04   3.00   |
------------------------------

RESULT
---------------
|      |  8.00 |
| ID1  |  17.0 |
|      |       |
---------------

So, regarding the outer maps, only ID1 has to be taken into account; in turn, the operation has to involve the inner (common) keys '2016-10-02' and '2016-10-03'.

Here my current code

 Set<Integer> common_keys = new LinkedHashSet<Integer>(map1.keySet());
    common_keys.retainAll(map2.keySet());
    System.out.println("Common keys: "+common_keys.toString());

    map1.forEach((k,v) -> {

        Map<String,Double> submap_1 = new LinkedHashMap<String,Double>(v);
        Map<String,Double> submap_2 = new LinkedHashMap<String,Double>(map2.get(k));

        Set<String> commons = new LinkedHashSet<String>(submap_1.keySet());
        commons.retainAll(submap_2.keySet());

        System.out.println(k+" : common days: "+commons);


        List<Double> val1 = submap_1.keySet()
                .stream()
                .filter(c -> commons.contains(c))
                .map(c -> submap_1.get(c))
                .collect(Collectors.toList());

        List<Double> val2 = submap_2.keySet()
                .stream()
                .filter(c -> commons.contains(c))
                .map(c -> submap_2.get(c))
                .collect(Collectors.toList());

        List<Double> ABS = IntStream.range(0, val1.size())
                .mapToObj(i -> val1.get(i) - val2.get(i))
                .collect(Collectors.toList());

        diff_abs.put(k, ABS);
        });

Is there a simpler and smart way to do this, by using JAVA 8' STREAM API?

Thanks in advance

Upvotes: 3

Views: 1314

Answers (3)

Holger
Holger

Reputation: 298419

You can create a re-usable method for merging two maps by retaining common keys only and applying a function to both values:

public static <K,V,R> Map<K, R> merge(Map<K,V> map1, Map<K,V> map2, BiFunction<V,V,R> f) {

    boolean hasOrder=map1.entrySet().spliterator().hasCharacteristics(Spliterator.ORDERED);
    return map1.entrySet().stream()
        .collect(hasOrder? LinkedHashMap<K,R>::new: HashMap<K,R>::new, (m,e)-> {
            V v2 = map2.get(e.getKey());
            if(v2!=null) m.put(e.getKey(), f.apply(e.getValue(), v2));
        }, Map::putAll);
}

Then, you can easily implement the merging of nested maps by using the method twice:

public static <K1, K2> Map<K1, List<Double>> merge(
    Map<K1, Map<K2, Double>> map1, Map<K1, Map<K2, Double>> map2) {

    return merge(map1, map2,
        (a,b) -> new ArrayList<>(merge(a, b, (x,y) -> x-y).values()));
}

Note that the code above is smart enough to create a LinkedHashMap if, and only if, the first map has an intrinsic order, e.g. is itself a LinkedHashMap or SortedMap, and a plain HashMap otherwise. The order of the second map is not retained anyway, which is not an issue if, as your original code seems to assume, both maps have the same order. If their order differs, there’s obviously no way to retain both orders.

Upvotes: 2

fabian
fabian

Reputation: 82461

Creating the Set is unnecessary. Replace this with a check for null in for the same key in the other map. Furthermore creating copies of the maps is unnecessary, since the maps are never modified in the method.

public static <T, U> Map<T, List<Double>> merge(Map<T, Map<U, Double>> m1, Map<T, Map<U, Double>> m2) {
    Map<T, List<Double>> result = new HashMap<>();
    m1.forEach((k, v) -> {
        Map<U, Double> v2 = m2.get(k);
        if (v2 != null) {
            // both outer maps contain the same key
            ArrayList<Double> list = new ArrayList<>();
            v.forEach((innerK, innerV) -> {
                Double d = v2.get(innerK);
                if (d != null) {
                    // matching key in both inner maps
                    list.add(innerV - d);
                }
            });
            // list.trimToSize();
            result.put(k, list);
        }
    });
    return result;
}

Upvotes: 4

kuporific
kuporific

Reputation: 10322

Just a few points on your example above. My go-to implementations for List, Set, and Map are ArrayList, HashSet, and HashMap unless I need something specifically provided by their Linked implementations, aka, consistent order (but this happens exceedingly rarely).

Here's how I would approach this:

public static <T, S> Map<T, List<Double>> method(
        Map<T, Map<S, Double>> map1,
        Map<T, Map<S, Double>> map2)
{
    Set<T> commonKeys = intersection(map1.keySet(), map2.keySet());

    return commonKeys.stream().collect(Collectors.toMap(
            Function.identity(),
            key -> {
                Map<S, Double> inner1 = map1.get(key);
                Map<S, Double> inner2 = map2.get(key);

                Set<S> innerCommonKeys = intersection(
                        inner1.keySet(),
                        inner2.keySet());

                        return innerCommonKeys.stream().map(innerKey ->
                                inner1.get(innerKey) - inner2.get(innerKey))
                                .collect(Collectors.toList());
            }));
}

private static <T> Set<T> intersection(Set<T> set1, Set<T> set2) {
    Set<T> intersection = new HashSet<T>(set1);
    intersection.retainAll(set2);
    return intersection;
}

And here is a "test" to show that it works:

public static void main(String[] args)
{
    Map<String, Map<String, Double>> map1 = new HashMap<>();
    Map<String, Double> inner11 = new HashMap<>();
    inner11.put("2016-10-02", 10.0);
    inner11.put("2016-10-03", 20.0);
    inner11.put("2016-10-04", 30.0);
    map1.put("ID1", inner11);
    Map<String, Double> inner12 = new HashMap<>();
    inner12.put("2016-10-02", 1.00);
    inner12.put("2016-10-03", 2.00);
    inner12.put("2016-10-04", 3.00);
    map1.put("ID2", inner12);

    Map<String, Map<String, Double>> map2 = new HashMap<>();
    Map<String, Double> inner21 = new HashMap<>();
    inner21.put("2016-10-02", 2.00);
    inner21.put("2016-10-03", 3.00);
    map2.put("ID1", inner21);
    Map<String, Double> inner22 = new HashMap<>();
    inner22.put("2016-10-02", 1.00);
    inner22.put("2016-10-03", 2.00);
    inner22.put("2016-10-04", 3.00);
    map2.put("ID3", inner22);

    System.out.println(method(map1, map2));
}

Output: {ID1=[8.0, 17.0]}

Upvotes: 1

Related Questions