Merge nested Maps with specified key - java

I have a Map as below,
Map<String, Map<String, String>> dateFormatsMap;
Map<String,String> m1 = Map.of("A","DD", "B", "MM", "C","YY");
Map<String,String> m2 = Map.of("A","DD-MM", "X", "MM", "C","YYYY");
Map<String,String> m3 = Map.of("X","DD", "Y", "MM", "C","YY");
dateFormatsMap = Map.of("P2",m2,"P1",m1, "p3",m3);
This map is populated from DB and has project-wise data.
What I want is to get merged Map of a specific project and one default project and in case of a duplicate, I want to keep values of a specified project.
I tried below code,
private static Map<String, String> getFormats(String project) {
Set<String> projects = Set.of("P1", project); //e.g "P1" as default project
return dateFormatsMap.entrySet().stream()
.filter(e->projects.contains(e.getKey()))
.flatMap(e->e.getValue().entrySet().stream())
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue,
(o,n)->o));
}
I call this method as,
getFormats("P2");
But this gives the varying results having values sometimes from "P1" and sometimes from "P1".
{A=DD, B=MM, C=YY, X=MM}
{A=DD-MM, B=MM, C=YYYY, X=MM}
I need to have a fixed result as,
{A=DD-MM, B=MM, C=YYYY, X=MM}
You can check ideone
I hope, I have detailed the question. I appreciate any help.

One way to deal with this without using Streams, you could also seek is using putAll -
private static Map<String, String> getFormats(String project) {
Map<String, String> baseProjFormats = dateFormatsMap.get("P1");
Map<String, String> currentProjFormats = dateFormatsMap.getOrDefault(project, Collections.emptyMap());
baseProjFormats.putAll(currentProjFormats);
return baseProjFormats;
}

I added all from the default project which are not in the specific project map and afterwards added everything from the specific project map:
private static Map<String, String> getMyFormats(String project) {
String defaultProject = "P1";
Map<String, String> specificMap = dateFormatsMap.get(project);
Map<String, String> collect = dateFormatsMap.get(defaultProject).entrySet().stream()
.filter(e -> !specificMap.containsKey(e.getKey()))
.collect(toMap(Map.Entry::getKey, Map.Entry::getValue));
collect.putAll(specificMap);
return collect;
}
or using collectingAndThen:
private static Map<String, String> getFormats(String project) {
String defaultProject = "P1";
Map<String, String> collect = dateFormatsMap.get(defaultProject).entrySet().stream()
.filter(e -> !dateFormatsMap.get(project).containsKey(e.getKey()))
.collect(collectingAndThen(
toMap(Map.Entry::getKey, Map.Entry::getValue),
map -> {
map.putAll(dateFormatsMap.get(project));
return map;
})
);
return collect;
}

I got the solution. Created two maps and then merged as below,
private static Map<String, String> getFormats(String project) {
Map<String, String> baseProjFormats = dateFormatsMap.get("P1");
Map<String, String> currentProjFormats = dateFormatsMap.get(project);
return Stream.of(baseProjFormats, currentProjFormats)
.flatMap(e -> e.entrySet().stream())
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue,
(o, n) -> n));
}
I would welcome any better solution.

I'm just going to be the guy who doesn't use streams:
public static void main(String[] args) {
Map<String, String> defaultMap = Map.of("A", "DD", "B", "MM", "C", "YY");
Map<String, String> mapToMerge = Map.of("A", "DD-MM", "X", "MM", "C", "YYYY");
// Do the magic
Map<String, String> result = new HashMap<String, String>();
result.putAll(mapToMerge);
for (Map.Entry<String, String> entry : defaultMap.entrySet())
if (!result.containsKey(entry.getKey()))
result.put(entry.getKey(), entry.getValue());
// Print results
result.entrySet().stream().forEach(System.out::println);
}

Related

Map of maps to a single map by aggregating keys

I have a structure like this one:
Map<KeyType1, Map<KeyType2, List<ValueType>>>
And also a class holding both KeyType1 and KeyType2, let's call it AggregatedKey. It can be instantiated using its constructor:
public AggregatedKey(KeyType1 keyType1, KeyType2 keyType2)
My goal is to map the structure above to something like:
Map<AggregatedKey, List<ValueType>>
So, basically, the keys should be mapped to a single aggregated key.
How can I achieve that using Java 9?
This will do the trick
Map<KeyType1, Map<KeyType2, List<String>>> m = new HashMap<>();
Map<AggregatedKey, List<String>> result = new HashMap<>();
m.entrySet().forEach(entry -> {
entry.getValue().entrySet().forEach(nestedEntry -> {
result.put(new AggregatedKey(entry.getKey(), nestedEntry.getKey()), nestedEntry.getValue());
});
});
Don't forget to implement hashcode/equals in your AggregatedKey, otherwise you'll have some trouble using the result map.
You can do it like so using streams.
first stream the entry set of the outer map
then invoke flatMap to stream the inner map's entrySet
create the AggregatedKey instance using outerEntry.getKey() and innerEntry.getKey() Note this requires that class to have a constructor accepting the keys.
then put that instance and the value from the inner map (List<ValueType>) in an AbstractMap.SimpleEntry instance to pass to the collector.
create the new map with the key and value of the SimpleEntry
Given the following source map.
Map<KeyType1, Map<KeyType2, List<ValueType>>> map =
new HashMap<>(); // contains the info to be remapped.
Here is the result
Map<AggregatedKey, List<ValueType>> result = map.entrySet()
.stream()
.flatMap(outerEntry-> outerEntry
.getValue().entrySet().stream()
.map(innerEntry -> new AbstractMap.SimpleEntry<>(
new AggregatedKey(outerEntry.getKey(),innerEntry.getKey()),
innerEntry.getValue())))
.collect(Collectors.toMap(
AbstractMap.SimpleEntry::getKey,
AbstractMap.SimpleEntry::getValue));
}
This is one of the ways:
public static void main(String[] args) {
Map<String, Map<String, String>> map = new HashMap<>();
Map<String, String> innerMap1 = new HashMap<>();
Map<String, String> innerMap2 = new HashMap<>();
innerMap1.put("k11", "v11");
innerMap1.put("k12", "v12");
innerMap1.put("k13", "v13");
innerMap2.put("k21", "v22");
innerMap2.put("k22", "v22");
map.put("k1", innerMap1);
map.put("k2", innerMap2);
Map<String, String> result = map
.entrySet()
.stream()
.flatMap(stringMapEntry ->
stringMapEntry
.getValue()
.entrySet()
.stream()
.map(stringStringEntry ->
new AbstractMap.SimpleEntry<String, String>(
buildAggregatedKey(
stringMapEntry.getKey(),
stringStringEntry.getKey()
),
stringStringEntry.getValue()
)
)
).collect(Collectors.toMap(AbstractMap.SimpleEntry::getKey, AbstractMap.SimpleEntry::getValue));
System.out.println(result);
}
private static String buildAggregatedKey(String key1, String key2){
return key1 + "_" + key2;
}
Where you change this buildAggregatedKey to meet your aggregation logic.
this is a sample test using streams where in the first step it transforms the inner element of the map and in the second it collects to a Map:
package prove.aggregatemap;
import org.junit.Assert;
import org.junit.Test;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class TestAggregator {
#Test
public void aggregate() {
Map<String, List<String>> letter_map= Map.of("first", List.of("one","two","three"),
"second", List.of("four","five","six"),
"third", List.of("seven","eight","nine"));
Map<String, List<String>> num_map= Map.of("first_num", List.of("1","2","3"), "second_num", List.of("4","5","6"), "third_num", List.of("7","8","9"));
Map<String,Map<String,List<String>>> mapOfMaps=Map.of("letter",letter_map,"num",num_map);
Map<AggregateKey, List<String>> result=mapOfMaps.entrySet().stream().flatMap(entry ->
entry.getValue().entrySet().stream().collect(Collectors.toMap(
inner_entry -> new AggregateKey(entry.getKey(), inner_entry.getKey()),
inner_entry -> inner_entry.getValue())).entrySet().stream()
).collect(Collectors.toMap(entry->entry.getKey(),entry->entry.getValue()));
Assert.assertEquals(List.of("one","two","three"),result.get(new AggregateKey("letter","first")));
Assert.assertEquals(List.of("four","five","six"),result.get(new AggregateKey("letter","second")));
Assert.assertEquals(List.of("seven","eight","nine"),result.get(new AggregateKey("letter","third")));
}
}
public static final class AggregatedKey<K1, K2> {
private final K1 one;
private final K2 two;
public AggregatedKey(K1 one, K2 two) {
this.one = one;
this.two = two;
}
}
public static <K1, K2, V> Map<AggregatedKey<K1, K2>, List<V>> convert1(Map<K1, Map<K2, List<V>>> map) {
Map<AggregatedKey<K1, K2>, List<V>> res = new HashMap<>();
for (Map.Entry<K1, Map<K2, List<V>>> one : map.entrySet())
for (Map.Entry<K2, List<V>> two : one.getValue().entrySet())
res.put(new AggregatedKey<>(one.getKey(), two.getKey()), two.getValue());
return res;
}
public static <K1, K2, V> Map<AggregatedKey<K1, K2>, List<V>> convert2(Map<K1, Map<K2, List<V>>> map) {
return map.entrySet().stream()
.flatMap(e1 -> e1.getValue().entrySet().stream()
.map(e2 -> new AggregatedKey<>(new AggregatedKey<>(e1.getKey(), e2.getKey()), e2.getValue())))
.collect(Collectors.toMap(tuple -> tuple.one, tuple -> tuple.two));
}

Efficient way to iterate and copy the values of HashMap

I want to convert:
Map<String, Map<String, List<Map<String, String>>>> inputMap
to:
Map<String, Map<String, CustomObject>> customMap
inputMap is provided in the config and is ready but I need to customMap Format. CustomObject will be derived from List<Map<String, String>> using few lines of code in a function.
I have tried a normal way of iterating input map and copying key values in customMap. Is there any efficient way of doing that using Java 8 or some other shortcut?
Map<String, Map<String, List<Map<String, String>>>> configuredMap = new HashMap<>();
Map<String, Map<String, CustomObj>> finalMap = new HashMap<>();
for (Map.Entry<String, Map<String, List<Map<String, String>>>> attributeEntry : configuredMap.entrySet()) {
Map<String, CustomObj> innerMap = new HashMap<>();
for (Map.Entry<String, List<Map<String, String>>> valueEntry : attributeEntry.getValue().entrySet()) {
innerMap.put(valueEntry.getKey(), getCustomeObj(valueEntry.getValue()));
}
finalMap.put(attributeEntry.getKey(), innerMap);
}
private CustomObj getCustomeObj(List<Map<String, String>> list) {
return new CustomObj();
}
One solution is to stream the entrySet of inputMap, and then use Collectors#toMap twice (once for the outer Map, and once for the inner Map):
Map<String, Map<String, CustomObj>> customMap = inputMap.entrySet()
.stream()
.collect(Collectors.toMap(Function.identity(), entry -> {
return entry.getValue()
.entrySet()
.stream()
.collect(Collectors.toMap(Function.identity(),
entry -> getCustomeObj(entry.getValue())));
}));
You could stream, but that ain't going to look readable; at least to me. So if you have a method:
static CustomObject fun(List<Map<String, String>> in) {
return .... // whatever processing you have here
}
you could still use the java-8 syntax, but in a different form:
Map<String, Map<String, CustomObject>> customMap = new HashMap<>();
inputMap.forEach((key, value) -> {
value.forEach((innerKey, listOfMaps) -> {
Map<String, CustomObject> innerMap = new HashMap<>();
innerMap.put(innerKey, fun(listOfMaps));
customMap.put(key, innerMap);
});
});
If you can make the inner map immutable, you could make that even shorter:
inputMap.forEach((key, value) -> {
value.forEach((innerKey, listOfMaps) -> {
customMap.put(key, Collections.singletonMap(innerKey, fun(listOfMaps)));
});
});
IMHO streaming is not so bad idea. There're no bad tools. It depends on how you're using them.
In this particular case I would extract the repeating pattern into an utility method:
public static <K, V1, V2> Map<K, V2> transformValues(Map<K, V1> map, Function<V1, V2> transformer) {
return map.entrySet()
.stream()
.collect(toMap(Entry::getKey, e -> transformer.apply(e.getValue())));
}
The method above can be implemented using any approach, though I think Stream API fits pretty well here.
Once you defined the utility method, it can be used as simple as follows:
Map<String, Map<String, CustomObj>> customMap =
transformValues(inputMap, attr -> transformValues(attr, this::getCustomObj));
The actual transformation is effectively one liner. So with proper JavaDoc for transformValues method the result code is pretty readable and maintainable.
How about Collectors.toMap for the entries both at an outer and inner level such as:
Map<String, Map<String, CustomObj>> finalMap = configuredMap.entrySet()
.stream()
.collect(Collectors.toMap(Map.Entry::getKey,
attributeEntry -> attributeEntry.getValue().entrySet()
.stream()
.collect(Collectors.toMap(Map.Entry::getKey,
valueEntry -> getCustomeObj(valueEntry.getValue())))));

Invert Map <String, List<String>> using Java 8

I need to invert map which is <String, List<String>> to Map<String,String> using java 8. with assumption that the values are unique. For example,
Input Map -
{"Fruit" -> ["apple","orange"], "Animal" -> ["Dog","Cat"]}
Output Map
{"apple" -> "Fruit", "orange" -> "Fruit", "Dog"->"Animal", "Cat" -> "Animal"}
Map <String, String> outputMap = new HashMap<>();
for (Map.Entry<String, List<String>> entry : inputMap.entrySet()) {
entry.getValue().forEach(value -> outputMap.put(value, entry.getKey()));
}
Is this right? can we achieve this using streams java 8?
You an try this way
Map <String, String> updatedMap = new HashMap<>();
oldMap.keySet()
.forEach(i -> oldMap.get(i)
.forEach(k -> updatedMap.put(k, i)));
Do like this :
public class InverterMap {
public static void main(String[] args) {
Map<String, List<String>> mp = new HashMap<String, List<String>>();
mp.put("Fruit", Arrays.asList("Apple", "Orange"));
mp.put("Animal", Arrays.asList("Dog", "Cat"));
System.out.println(mp); // It returned {Fruit=[Apple, Orange], Animal=[Dog, Cat]}
Map<String, String> invertMap = mp.entrySet().stream().collect(HashMap::new,
(m, v) -> v.getValue().forEach(k -> m.put(k, v.getKey())), Map::putAll);
System.out.println(invertMap);// It returned {Apple=Fruit, Cat=Animal, Orange=Fruit, Dog=Animal}
}
}
Read Stream.collect(Supplier supplier, BiConsumer, BiConsumer combiner) for more info.

Could you help me to merge values of several maps?

I'm trying to do the following modification:
final Map<String, List<Map<String, String>>> scopes = scopeService.fetchAndCacheScopesDetails();
final Map<String, Map<String, String>> scopesResponse = scopes.entrySet().stream().collect
(Collectors.toMap(Map.Entry::getKey, e -> e.getValue()
.stream().collect(Collectors.toMap(s -> (String) s.get(SCOPE_NM), s -> (String) s.get(SCOPE_ID))))
);
But I face "Duplicate key" error, so I'd like to change scopeResponses to Map<String, Map<String, List<String>>>
Could you tell me how to merge values s -> (String) s.get(SCOPE_ID) into a List or Set in this situation?
You need to create a Set for the value of the inner Map, and supply a merge function:
final Map<String, Map<String, Set<String>>> scopesResponse = scopes.entrySet().stream().collect
(Collectors.toMap(Map.Entry::getKey, e -> e.getValue()
.stream().collect(Collectors.toMap(s -> s.get(SCOPE_NM),
s -> {Set<String> set= new HashSet<>(); set.add(s.get(SCOPE_ID)); return set;},
(s1,s2)->{s1.addAll(s2);return s1;}))));
Or, you can construct the inner Map with groupingBy:
final Map<String, Map<String, Set<String>>> scopesResponse2 = scopes.entrySet().stream().collect
(Collectors.toMap(Map.Entry::getKey, e -> e.getValue()
.stream().collect(Collectors.groupingBy(s -> s.get(SCOPE_NM),
Collectors.mapping(s -> s.get(SCOPE_ID),Collectors.toSet())))));
You can also do it using Guava's ListMultimap (multimap is like a map of lists):
Map<String, ListMultimap<String, String>> scopesResponse = scopes.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, e -> toMultimap(e)));
where
static ImmutableListMultimap<String, String> toMultimap(
Map.Entry<String, List<Map<String, String>>> entry) {
return entry.getValue().stream().collect(ImmutableListMultimap.toImmutableListMultimap(
s -> (String) s.get(SCOPE_NM),
s -> (String) s.get(SCOPE_ID)
));
}
If the values in the lists turn out to be duplicated, and you don't want that, use SetMultimap instead.

collecting HashMap<String, List<String>> java 8

I want to be able to convert a List to a HashMap where the key is the elementName and the values is a list of something random (in this case its the Element Name). So in short I want (A->List(A), B->List(B), C-> List(C)). I tried using toMap() and passing it the keyMapper and ValueMapper but I get a compilation error. I would really appreciate if someone can help me out.
Thanks!
public static void main(String[] args) {
// TODO Auto-generated method stub
List<String> list = Arrays.asList("A","B","C","D");
Map<String, List<String>> map = list.stream().map((element)->{
Map<String, List<String>> map = new HashMap<>();
map.put(element, Arrays.asList(element));
return map;
}).collect(??);
}
Function<Map<String, String>, String> key = (map) -> {
return map.keySet().stream().findFirst().get();
};
Function<Map<String, String>, String> value = (map) -> {
return map.values().stream().findFirst().get();
};
=== This worked for me
Thanks for all the help guys! #izstas "they should operate on the elements" helped a lot :). Actually this is what I was looking for to be exact
public static void test2 (){
Function<Entry<String, List<String>>, String> key = (entry) -> {
return entry.getKey();
};
Function<Entry<String, List<String>>, List<String>> value = (entry) -> {
return new ArrayList<String>(entry.getValue());
};
BinaryOperator<List<String>> merge = (old, latest)->{
old.addAll(latest);
return old;
};
Map<String, List<String>> map1 = new HashMap<>();
map1.put("A", Arrays.asList("A1", "A2"));
map1.put("B", Arrays.asList("B1"));
map1.put("D", Arrays.asList("D1"));
Map<String, List<String>> map2 = new HashMap<>();
map2.put("C", Arrays.asList("C1","C2"));
map2.put("D", Arrays.asList("D2"));
Stream<Map<String, List<String>>> stream =Stream.of(map1, map2);
System.out.println(stream.flatMap((map)->{
return map.entrySet().stream();
}).collect(Collectors.toMap(key, value, merge)));
}
You can use the groupingBy method to manage aggregation, for example:
public static void main(String[] args) {
List<String> list = Arrays.asList("A", "B", "C", "D", "A");
Map<String, List<String>> map = list.stream().collect(Collectors.groupingBy(Function.identity()));
}
If you want more flexibility (for example to map the value and return a Set instead of a List) you can always use the groupingBy method with more parameters as specified in javadoc:
Map<City, Set<String>> namesByCity = people.stream().collect(Collectors.groupingBy(Person::getCity, mapping(Person::getLastName, toSet())));
Functions key and value you have defined in your code are not correct because they should operate on the elements of your list, and your elements are not Maps.
The following code works for me:
List<String> list = Arrays.asList("A", "B", "C", "D");
Map<String, List<String>> map = list.stream()
.collect(Collectors.toMap(Function.identity(), Arrays::asList));
First argument to Collectors.toMap defines how to make a key from the list element (leaving it as is), second argument defines how to make a value (making an ArrayList with a single element).
Thanks for all the help guys! #izstas "they should operate on the elements" helped a lot :). Actually this is what I was looking for to be exact
public static void test2 (){
Function<Entry<String, List<String>>, String> key = (entry) -> {
return entry.getKey();
};
Function<Entry<String, List<String>>, List<String>> value = (entry) -> {
return new ArrayList<String>(entry.getValue());
};
BinaryOperator<List<String>> merge = (old, latest)->{
old.addAll(latest);
return old;
};
Map<String, List<String>> map1 = new HashMap<>();
map1.put("A", Arrays.asList("A1", "A2"));
map1.put("B", Arrays.asList("B1"));
map1.put("D", Arrays.asList("D1"));
Map<String, List<String>> map2 = new HashMap<>();
map2.put("C", Arrays.asList("C1","C2"));
map2.put("D", Arrays.asList("D2"));
Stream<Map<String, List<String>>> stream =Stream.of(map1, map2);
System.out.println(stream.flatMap((map)->{
return map.entrySet().stream();
}).collect(Collectors.toMap(key, value, merge)));
}

Categories

Resources