How to flatten an array of dictionaries in Java? - java

I am working on below problem where I need to flatten array of dicts:
For example- Below is an input:
[
{'a':
{'b':
{'c':
{'d':'e'}
}
}
},
{'a':{'b':{'c':{'d':{'e':'f'}}}}},
{'a':'b'}
]
And the output will be:
[
{'a_b_c_d':'e'},
{'a_b_c_d_e':'f'},
{'a':'b'}
]
Below is what I was able to come up with. Is there any better way to solve this problem?
private static List<Map<String, String>> flatDictionary(final List<Map<String, Object>> input) {
List<Map<String, String>> listHolder = new ArrayList<>();
if(input == null || input.isEmpty()) {
return listHolder;
}
for(Map<String, Object> mapHolder : input) {
Map<String, String> m = new HashMap<>();
StringBuilder sb = new StringBuilder();
Map<String, String> output = helper(mapHolder, sb, m);
listHolder.add(output);
}
return listHolder;
}
private static Map<String, String> helper(final Map<String, Object> map, final StringBuilder sb, final Map<String, String> output) {
String mapValue = null;
for(Map.Entry<String, Object> holder : map.entrySet()) {
String key = holder.getKey();
Object value = holder.getValue();
if(value instanceof Map) {
sb.append(key).append("_");
helper((HashMap<String, Object>) value, sb, output);
} else if(value instanceof String) {
sb.append(key);
mapValue = (String) value;
}
output.put(sb.toString(), mapValue);
}
return output;
}

I would use recursion.
First define a method to flatten a single Map
public static void flatten(final String keyPrefix, final Map<String, Object> input, final Map<String, Object> output) {
for (final Map.Entry<String, Object> e : input.entrySet()) {
final var key = keyPrefix.isBlank() ? e.getKey() : keyPrefix + "_" + e.getKey();
if (e.getValue() instanceof Map) {
// if the nested Map is of the wrong type bad things may happen
flatten(key, (Map<String, Object>) e.getValue(), output);
} else {
output.put(key, e.getValue());
}
}
}
NB: This makes no attempt to deal with duplicate keys.
Usage:
public static void main(final String[] args) throws InterruptedException {
final var data = Map.of(
"A", Map.of("a", "Expect A_a"),
"B", Map.of("b1", Map.of(
"bb1", "expect B_b1_bb1",
"bb2", "expect B_b1_bb2"
)),
"C", "Expect C");
final var output = new HashMap<String, Object>();
flatten("", data, output);
output.forEach((k, v) -> System.out.printf("%s -> %s%n", k, v));
}
Output:
C -> Expect C
A_a -> Expect A_a
B_b1_bb2 -> expect B_b1_bb2
B_b1_bb1 -> expect B_b1_bb1
Now, simply define a method that loops to take your List
public static final Map<String, Object> flattenAll(final List<Map<String, Object>> input) {
final var output = new HashMap<String, Object>();
input.forEach(map -> flatten("", map, output));
return output;
}
NB: This makes no attempt to deal with duplicate keys.

Related

Java Merge Nested HashMaps at the innermost/leaf level

Say I got 2 Key Value pairs:
String k1 = "a.b.c.d";
String v1 = "123";
String k2 = "a.b.c.d";
String v2 = "456";
And the desired output is:
a {
b {
c {
d = "123",
e = "456"
}
}
}
So, I've decided the split the keys by "." and form nested HashMaps and then trying to merge them when they have duplicate keys. However, it needs to merge at the leaf or innermost level instead of the outermost level.
This is the full code:
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class TestClass {
public static void main(String []args)
{
Map<String, Object> finalMap = new HashMap<>();
Map<String, Object> outerMap1 = new HashMap<>();
Map<String, Object> outerMap2 = new HashMap<>();
String k = "a.b.c.d";
String v = "123";
outerMap1 = createNestedStructure(k, v);
k = "a.b.c.e";
v = "456";
outerMap2 = createNestedStructure(k, v);
finalMap = Stream
.concat(outerMap1.entrySet().stream(),
outerMap2.entrySet().stream())
.collect(Collectors.toMap(Entry::getKey,
Entry::getValue, (a, b) -> {
String c = a.toString() + "\n" + b.toString();
return c;
}, HashMap::new));
System.out.println(finalMap.toString());
}
public static Map<String, Object> createNestedStructure(String k, String v)
{
String[] tokens = k.split("\\.");
Map<String, String> innerMap = new HashMap<>();
v = "\"" + v + "\"";
innerMap.put(tokens[tokens.length-1], v);
Map<String, Object> middleMap = new HashMap<>();
middleMap.put(tokens[tokens.length-2], innerMap);
for(int i=tokens.length-3; i>=0; i--)
{
Map<String, Object> middleMapTmp = new HashMap<>();
middleMapTmp.put(tokens[i], middleMap);
middleMap = middleMapTmp;
}
// Map<String, Object> outerMap = new HashMap<>();
// outerMap.put(tokens[0], middleMap);
// return outerMap;
return middleMap;
}
}
I'm not sure if this is the correct approach. So suggestions on better approaches are also welcome.
Not exactly sure about your specific problem, but you could simply insert the values into the same structure instead of merging them afterward. For example, you can make a recursive insert that creates the nested maps until it inserts your value on the last key part. If the nested map already exists it uses the existing one. Something like this could do the trick:
public static void main(String[] args) {
String k1 = "a.b.c.d";
String v1 = "123";
String k2 = "a.b.c.e";
String v2 = "456";
Map<String, Object> map = new HashMap<>();
recursiveInsert(map,k1, v1);
recursiveInsert(map,k2, v2);
System.out.println(map);
}
public static void recursiveInsert(Map<String, Object> map, String key, String value) {
int index = key.indexOf('.');
if (index == -1) {
map.put(key, value);
} else {
String subKey = key.substring(0, index);
map.putIfAbsent(subKey, new HashMap<>());
recursiveInsert((Map<String, Object>) map.get(subKey), key.substring(index + 1), value);
}
}
The output of this is what you requested:
{a={b={c={d=123, e=456}}}}

How to merge 3 map of maps effectively in java

Merge below 3 maps if values exist then replace and if the value is new then add that value, for example :-
I have a Map1 as :-
{
BMW = {
SIZE=1,
SPEED=60
},
AUDI = {
SIZE=5,
SPEED=21
},
SEAT= {
SPEED=15
}
}
And a Map2 as :-
{
Suzuki = {
WHEELS_SIZE=2,
DOORS=3
},
AUDI = {
WHEELS_SIZE=5,
SIZE=4,
DOORS=5
},
SEAT= {
DOORS=4
}
}
And Map3 as :-
{
TOYOTA = {
WHEELS_SIZE=5,
DOORS=5
},
BMW= {
SIZE=10
}
}
I have a requirement to merge all 3 above so, that final map looks like
{
BMW = {
SIZE=10,
SPEED=60
},
AUDI = {
SIZE=4,
SPEED=21,
WHEELS_SIZE=5,
DOORS=5
},
SEAT= {
SPEED=15,
DOORS=4
},
Suzuki = {
WHEELS_SIZE=2,
DOORS=3
},
TOYOTA = {
WHEELS_SIZE=5,
DOORS=5
},
}
Could someone please suggest an optimized way to achieve this ? Thanks in advance!
Here is a naïve implementation:
public static void main(String[] args){
Map<String, Integer> BMW1 = new HashMap<>();
BMW1.put("SIZE", 1);
BMW1.put("SPEED", 60);
Map<String, Integer> AUDI1 = new HashMap<>();
AUDI1.put("SIZE", 5);
AUDI1.put("SPEED", 21);
Map<String, Integer> SEAT1 = new HashMap<>();
SEAT1.put("SPEED", 15);
Map<String, Map<String, Integer>> one =
Map.of("BMW", BMW1, "AUDI", AUDI1, "SEAT", SEAT1);
Map<String, Integer> SUZUKI2 = new HashMap<>();
SUZUKI2.put("WHEEL_SIZE", 2);
SUZUKI2.put("DOORS", 3);
Map<String, Integer> AUDI2 = new HashMap<>();
AUDI2.put("WHEELS_SIZE", 5);
AUDI2.put("SIZE", 4);
AUDI2.put("DOORS", 5);
Map<String, Integer> SEAT2 = new HashMap<>();
SEAT2.put("DOORS", 4);
Map<String, Map<String, Integer>> two =
Map.of("SUZUKI", SUZUKI2, "AUDI", AUDI2, "SEAT", SEAT2);
Map<String, Integer> TOYOTA3 = new HashMap<>();
TOYOTA3.put("WHEEL_SIZE", 5);
TOYOTA3.put("DOORS", 5);
Map<String, Integer> BMW3 = new HashMap<>();
BMW3.put("SIZE", 10);
Map<String, Map<String, Integer>> three =
Map.of("TOYOTA", TOYOTA3, "BMW", BMW3);
Map<String, Map<String, Integer>>[] maps = new Map[]{ one, two, three };
Map<String, Map<String, Integer>> mergedMaps = mergeMaps(maps);
printMap(mergedMaps);
}
public static Map<String, Map<String, Integer>> mergeMaps(Map<String, Map<String, Integer>>[] maps) {
Map<String, Map<String, Integer>> mergedMap = new HashMap<>();
for(Map<String, Map<String, Integer>> map : maps) {
for(Map.Entry<String, Map<String, Integer>> entry : map.entrySet()) {
if(mergedMap.containsKey(entry.getKey())){
Map<String, Integer> mergedCarStats = mergedMap.get(entry.getKey());
Map<String, Integer> carStats = entry.getValue();
mergedCarStats.putAll(carStats);
} else {
mergedMap.put(entry.getKey(), entry.getValue());
}
}
}
return mergedMap;
}
public static void printMap(Map<String, Map<String, Integer>> car) {
System.out.println("{");
for(Map.Entry<String, Map<String, Integer>> entry: car.entrySet()) {
System.out.println(String.format(" %s = {", entry.getKey()));
printStats(entry.getValue());
System.out.println(" },");
}
System.out.println("}");
}
public static void printStats(Map<String, Integer> stats) {
for(Map.Entry<String, Integer> entry : stats.entrySet()) {
System.out.println(String.format(" '%s': %d,", entry.getKey(), entry.getValue()));
}
}
If you run it you will get this output:
{
SUZUKI = {
'WHEEL_SIZE': 2,
'DOORS': 3,
},
SEAT = {
'SPEED': 15,
'DOORS': 4,
},
AUDI = {
'SPEED': 21,
'DOORS': 5,
'SIZE': 4,
'WHEELS_SIZE': 5,
},
TOYOTA = {
'WHEEL_SIZE': 5,
'DOORS': 5,
},
BMW = {
'SPEED': 60,
'SIZE': 10,
},
}
Which is what you asked for. There are a few things to consider though. Using loops this way is inefficient because the time complexity is going to be O(n^2) as there are 2 for loops. There are built in functions for merging maps since Java 8 which could be worth exploring.
Merge the three maps using java8
Map<String, Map<String, Integer>> resultMap = Stream.of(map1, map2, map3)
.flatMap(map -> map.entrySet().stream())
.collect(Collectors.toMap(
Map.Entry::getKey,
e -> new HashMap<String,Integer>(e.getValue()),
(key, value) -> {key.putAll(value); return key;}
));
Output:
{SEAT={SPEED=15, DOORS=4}, TOYOTA={WHEELS_SIZE=5, DOORS=5}, Suzuki={WHEELS_SIZE=2, DOORS=3}, AUDI={SPEED=21, WHEELS_SIZE=5, DOORS=5, SIZE=4}, BMW={SPEED=60, SIZE=10}}

Flatten nested Map containing with unknown level of nested Arrays and Maps recursively

I have a nested HashMap with String keys that contains either List, Map, or String values. I would like to flatten them like the below.
Here is the data:
import java.util.*;
import java.util.stream.*;
public class MyClass {
public static void main(String args[]) {
Map<String, Object> dates = new HashMap<String, Object>() {{
put("1999", new HashMap<String, Object>() {{
put("3", Arrays.asList("23", "24", "25"));
put("4", Arrays.asList("1", "2", "3"));
}});
put("2001", new HashMap<String, Object>() {{
put("11", new HashMap<String, Object>() {{
put("7", Arrays.asList("23", "24", "25"));
put("9", Arrays.asList("1", "2", "3"));
}});
put("12", "45");
}});
}};
System.out.println(dates);
}
}
Map looks like:
{2001={11={7=[23, 24, 25], 9=[1, 2, 3]}, 12=45},
1999={3=[23, 24, 25], 4=[1, 2, 3]}}
The flattening of map should look like this:
{2001.11.7.1=23, 2001.11.7.2=24, 2001.11.7.3=25, 2001.11.9.1=1, 2001.11.9.2=2,
2001.11.9.3=3, 2001.12=45, 1999.3.1=23, 1999.3.2=24, 1999.3.3=25,
1999.4.1=1, 1999.4.2=2, 1999.4.3=3}
Note: the level of nested arrays or maps is unknown, it may go more than 2 levels.
You can use recursion to flatten the Map. Each time you encounter a Map, recurse by flattening that Map; when you encounter a List, iterate over it and add the index to the current key. A single value can be trivially set otherwise. See the below code in action here.
public static Map<String, Object> flatten(final Map<String, Object> map) {
return flatten("", map, new HashMap<>());
//use new TreeMap<>() to order map based on key
}
#SuppressWarnings("unchecked")//recursive helper method
private static Map<String, Object> flatten(final String key, final Map<String, Object> map,
final Map<String, Object> result) {
final Set<Map.Entry<String, Object>> entries = map.entrySet();
if (!entries.isEmpty()) {
for (final Map.Entry<String, Object> entry : entries) {
//iterate over entries
final String currKey = key + (key.isEmpty() ? "" : '.') + entry.getKey();
//append current key to previous key, adding a dot if the previous key was not an empty String
final Object value = entry.getValue();
if (value instanceof Map) {//current value is a Map
flatten(currKey, (Map<String, Object>) value, result);//flatten Map
} else if (value instanceof List) {//current value is a List
final List<Object> list = (List<Object>) value;
for (int i = 0, size = list.size(); i < size; i++) {
result.put(currKey + '.' + (i + 1), list.get(i));
}
//iterate over the List and append the index to the current key when setting value
} else {
result.put(currKey, value);//set normal value
}
}
}
return result;
}
public static void main(final String[] args){
final Map<String, Object> flattened = flatten(dates);
System.out.println(flattened);
}
You can iterate over this map, and process each entry value, depending on its instance of: Map, List, or String. Since the level of nested arrays or maps is unknown, I have modified a little your code example and flat map format for clarity, also I used TreeMap instead of HashMap for entries ordering.
public static void main(String[] args) {
TreeMap<String, Object> treeMap = new TreeMap<String, Object>() {{
put("1999", new TreeMap<String, Object>() {{
put("3", Arrays.asList("23", "24", "25"));
put("4", Arrays.asList("1", "2", new TreeMap<String, Object>() {{
put("10", "42");
}}));
}});
put("2001", new TreeMap<String, Object>() {{
put("11", new TreeMap<String, Object>() {{
put("7", Arrays.asList("23", "24", "25"));
put("9", Arrays.asList("1", "2", "3"));
}});
put("12", "45");
}});
}};
TreeMap<String, String> flatMap = new TreeMap<>();
processMap("", treeMap, flatMap);
System.out.println(treeMap);
System.out.println(flatMap);
}
private static void processMap(String prefix,
Map<String, Object> map,
Map<String, String> flatMap) {
for (Map.Entry<String, Object> entry : map.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
processEntry(prefix, key, value, flatMap);
}
}
private static void processList(String prefix,
List<Object> list,
Map<String, String> flatMap) {
for (int i = 0; i < list.size(); i++) {
String key = String.valueOf(i + 1);
Object value = list.get(i);
processEntry(prefix, key, value, flatMap);
}
}
#SuppressWarnings("unchecked")
private static void processEntry(String prefix,
String key,
Object value,
Map<String, String> flatMap) {
if (value instanceof Map) {
processMap(prefix + key + ".", (Map<String, Object>) value, flatMap);
} else if (value instanceof List) {
processList(prefix + key + ":", (List<Object>) value, flatMap);
} else if (value instanceof String) {
flatMap.put(prefix + key, (String) value);
}
}
Sample map:
{1999={3=[23, 24, 25], 4=[1, 2, {10=42}]},
2001={11={7=[23, 24, 25], 9=[1, 2, 3]}, 12=45}}
Flattened map:
{1999.3:1=23, 1999.3:2=24, 1999.3:3=25, 1999.4:1=1, 1999.4:2=2, 1999.4:3.10=42,
2001.11.7:1=23, 2001.11.7:2=24, 2001.11.7:3=25,
2001.11.9:1=1, 2001.11.9:2=2, 2001.11.9:3=3, 2001.12=45}
Opposite: Restoring a value tree from its flat map representation.
private static Map<String, String> flatten(Map<String, Object> m) {
var pairs = new ArrayList<String[]>();
flatten(null, m, pairs);
return pairs.stream().collect(Collectors.toMap(x -> x[0], x -> x[1]));
}
private static void flatten(String prefix, Map<String, Object> m, List<String[]> pairs) {
for (var e : m.entrySet()) {
var k = e.getKey();
var o = m.get(k);
if (o instanceof String s) {
pairs.add(new String[]{makeKey(prefix, k), s});
} else if (o instanceof List l) {
IntStream.range(0, l.size())
.mapToObj(i -> new String[]{
makeKey(prefix, k + "." + (i +1)),
l.get(i).toString()
})
.forEach(p -> pairs.add(p));
} else if (o instanceof Map nestedMap) {
flatten(makeKey(prefix, k), nestedMap, pairs);
}
}
}
private static String makeKey(String prefix, String key) {
return String.join(".", prefix == null ? List.of(key) : List.of(prefix,key));
}

Using ComputeIfAbsent & ComputeIfPresent to put list into map

I have a blok of code that's work well for me :
for (String word : distinctWordsInOneLigne) {
Map<String, List<Integer>> map = new HashMap<>();
if (!word.isEmpty()) {
List<Integer> linePositionsOfWord = new LinkedList<>();
if (currentLine.contains(word)) {
linePositionsOfWord.add(numLine);
if (mapAllWordsPositionInFilesInFolder.containsKey(word)) {
Map<String, List<Integer>> mapList = mapAllWordsPositionInFilesInFolder.get(word);
if (mapList.containsKey(filePath)) {
List<Integer> list = mapList.get(filePath);
list.add(numLine);
} else {
mapList.put(filePath, linePositionsOfWord);
}
} else {
map.put(filePath, linePositionsOfWord);
mapAllWordsPositionInFilesInFolder.put(word, map);
}
}
}
}
NB: Map<String, Map<String, List<Integer>>> mapAllWordsPositionInFilesInFolder = new HashMap<>();
The result of i somethinglike this :
{word1={file2.txt=[7], file1.txt=[1, 2]}, word2={file2.txt=[1, 2, 9, 13], file5.txt=[2, 3, 9]}}
Now i want have some result but now by using ComputeIfAbsent & ComputeIfPresent instead of containsKey and all this if ... else.
I tried this but not work :
mapAllWordsPositionInFilesInFolder.computeIfAbsent(word,v -> new HashMap<>())
.computeIfAbsent(filePath, val -> linePositionsOfWord);
mapAllWordsPositionInFilesInFolder.computeIfPresent(word,(k,v)->{
v.computeIfPresent(filePath, (x, y) -> linePositionsOfWord.add(numLine));
return v;
});
I need help please ! thank's :)
You wouldn't use computeIfPresent() for this, but you would use computeIfAbsent() like this:
for (String word : distinctWordsInOneLigne) {
if (! word.isEmpty() && currentLine.contains(word)) {
mapAllWordsPositionInFilesInFolder.computeIfAbsent(word, k -> new HashMap<>())
.computeIfAbsent(filePath, k -> new LinkedList<>())
.add(numLine);
}
}
The original code was very badly written. Even without using computeIfPresent(), it can be cleaned up a lot, eliminating repeated code. This is how it should have been written:
for (String word : distinctWordsInOneLigne) {
if (! word.isEmpty() && currentLine.contains(word)) {
Map<String, List<Integer>> mapList = mapAllWordsPositionInFilesInFolder.get(word);
if (mapList == null) {
mapList = new HashMap<>();
mapAllWordsPositionInFilesInFolder.put(word, mapList);
}
List<Integer> linePositionsOfWord = mapList.get(filePath);
if (linePositionsOfWord == null) {
linePositionsOfWord = new LinkedList<>();
mapList.put(filePath, linePositionsOfWord);
}
linePositionsOfWord.add(numLine);
}
}
With inlining, that can be reduced to:
for (String word : distinctWordsInOneLigne) {
if (! word.isEmpty() && currentLine.contains(word)) {
Map<String, List<Integer>> mapList = mapAllWordsPositionInFilesInFolder.get(word);
if (mapList == null)
mapAllWordsPositionInFilesInFolder.put(word, mapList = new HashMap<>());
List<Integer> linePositionsOfWord = mapList.get(filePath);
if (linePositionsOfWord == null)
mapList.put(filePath, linePositionsOfWord = new LinkedList<>());
linePositionsOfWord.add(numLine);
}
}

Seeking Optimization Suggestions for Map-to-Map Conversion

I'm seeking feedback as to whether there's a more efficient approach than what I'm doing in my code shown at the bottom.
Basically, given this map:
Set<String> A_Set = new HashSet<>(Arrays.asList("1111", "2222", "5555"));
Set<String> B_Set = new HashSet<>(Arrays.asList("3333", "4444"));
Set<String> C_Set = new HashSet<>(Arrays.asList("6666"));
Set<String> D_Set = new HashSet<>(Arrays.asList("2222", "5555", "6666"));
Map<String, Set<String>> values = new HashMap<>();
values.put("A", A_Set);
values.put("B", B_Set);
values.put("C", C_Set);
values.put("D", D_Set);
which looks like this:
How do I create a Map<String, List<Boolean> map such that it looks like this:
In the most efficient way possible. My real Map has thousands of values per Set, but there are only ever 4 Sets (A, B, C, D).
Here's my current code. Can you think of a more efficient approach?
import java.util.*;
public class MapToMap {
public static void main(String[] args) {
Set<String> A_Set = new HashSet<>(Arrays.asList("1111", "2222", "5555"));
Set<String> B_Set = new HashSet<>(Arrays.asList("3333", "4444"));
Set<String> C_Set = new HashSet<>(Arrays.asList("6666"));
Set<String> D_Set = new HashSet<>(Arrays.asList("2222", "5555", "6666"));
Map<String, Set<String>> values = new HashMap<>();
values.put("A", A_Set);
values.put("B", B_Set);
values.put("C", C_Set);
values.put("D", D_Set);
Map<String, List<Boolean>> exists = new HashMap<>();
for (Map.Entry<String, Set<String>> v : values.entrySet()) {
for (String val : v.getValue()) {
if (exists.containsKey(val)) {
List<Boolean> list = exists.get(val);
list = addValue(v.getKey(), list);
exists.put(val, list);
} else {
List<Boolean> newList = new ArrayList<>(Arrays.asList(false, false, false, false));
newList = addValue(v.getKey(), newList);
exists.put(val, newList);
}
}
}
for (Map.Entry<String, List<Boolean>> s : exists.entrySet()) {
System.out.println(s);
}
}
private static List<Boolean> addValue(String key, List<Boolean> listToUse) {
List<Boolean> newList = new ArrayList<>();
if (Objects.equals("A", key)) {
newList.addAll(Arrays.asList(true, listToUse.get(1), listToUse.get(2), listToUse.get(3)));
} else if (Objects.equals("B", key)) {
newList.addAll(Arrays.asList(listToUse.get(0), true, listToUse.get(2), listToUse.get(3)));
} else if (Objects.equals("C", key)) {
newList.addAll(Arrays.asList(listToUse.get(0), listToUse.get(1), true, listToUse.get(3)));
} else if (Objects.equals("D", key)) {
newList.addAll(Arrays.asList(listToUse.get(0), listToUse.get(1), listToUse.get(2), true));
}
return newList;
}
}
Here's a solution using streams:
Map<String, List<Boolean>> exists = values.values()
.stream()
.flatMap(Set::stream)
.distinct()
.collect(Collectors.toMap(v -> v, v -> Stream.of("A", "B", "C", "D")
.map(k -> values.get(k).contains(v))
.collect(Collectors.toList())));
Ideone Demo

Categories

Resources