I have a requirement in which I would like to combine values related to multiple keys in a HashMap
based on a pattern.
Example : I have below key value pairs in a HashMap
:
Key | Value |
---|---|
skim fac1 ccy1 | EUR |
skim fac1 ccy1rate | 0.15 |
skim fac1 ccy2 | USD |
skim fac1 ccy2rate | 0.20 |
skim fac2 ccy1 | GBP |
skim fac2 ccy1rate | 0.17 |
Now if we look at the keys in HashMap
, those are following specific pattern : skim fac1,skim fac2....skim facn which is segregation criteria for information.
I would like to segregate and combine the information within a HashMap<String,List<Map<String,String>>>
in the following manner:
Key | Value |
---|---|
skim fac1 | EUR,0.15 |
skim fac2 | USD,0.20,GBP,0.17 |
Could someone please help me in this?
Thanks.
CodePudding user response:
There is a discrepancy between the output structure you described:
HashMap<String,List<Map<String,String>>>
and the expected output in your table:
HashMap<String,List<String>>
However, in both cases the problem lies mostly in how to find the substrings to group by your values. After getting them, your problem can be solved with a single operation stream where a collect(Collectors.toMap())
is employed. The only thing that changes is how you represent your values within the List
(Map<String, String>
or simply String
).
Here is an implementation with comments detailing step by step:
public class Main {
public static void main(String[] args) {
Map<String, String> map = new HashMap<>(Map.of(
"skim fac1 ccy1", "EUR",
"skim fac1 ccy1rate", "0.15",
"skim fac1 ccy2", "USD",
"skim fac1 ccy2rate", "0.20",
"skim fac2 ccy1", "GBP",
"skim fac2 ccy1rate", "0.17"
));
//Using a set to have unique keys to group by in the resulting map
Set<String> newKeys = new HashSet<>();
//Checking for each key if its substring is equal to any other key's beginning:
// - if it does, then the substring is collected as a key to group by within the final map
//
// - if it doesn't, then another substring is generated from the previous substring until a matching value is found.
// If no value is found, then the key is collected entirely for the resulting map.
for (String key : map.keySet()) {
//This loop keeps creating substrings of the current key until:
// - the substring matches another key's beginning
// - or no more substrings can be generated
int lastIndex = key.lastIndexOf(" ");
while (lastIndex > 0) {
//Checking if the substring matches the beginning of any key except the current one
String subStr = key.substring(0, lastIndex);
if (map.keySet().stream().anyMatch(s -> !s.equals(key) && s.startsWith(subStr))) {
//If a match is found then the current substring is added to the set and the substring iteration is interrupted
newKeys.add(key.substring(0, lastIndex));
break;
}
//Creating a new substring from the previous substring if no match has been found
lastIndex = key.substring(0, lastIndex).lastIndexOf(" ");
}
//If no substrings of the current key matches the beginning of any other key, then the current key is collected
if (lastIndex < 0) {
newKeys.add(key);
}
}
//Creating the resulting map as a map of lists of maps
Map<String, List<Map<String, String>>> mapRes = map.entrySet().stream()
.collect(Collectors.toMap(
entry -> {
//Looking for the newKey which matches the beginning of the current entry's key.
return newKeys.stream().filter(s -> entry.getKey().startsWith(s)).findFirst().orElseThrow(() -> new RuntimeException("No key found"));
},
entry -> {
//Retrieving, like above, the newKey that will be used to map the current value
String newKey = newKeys.stream().filter(s -> entry.getKey().startsWith(s)).findFirst().orElseThrow(() -> new RuntimeException("No key found"));
//Returning a List with a single entry map.
//The entry's key corresponds to the rest of the substring between newKey and key (the portion of key not matched by newKey).
//while the value remains the original value.
return new ArrayList<>(List.of(Map.of(entry.getKey().substring(newKey.length()).trim(), entry.getValue())));
},
//Handling colliding cases by merging the lists together
(list1, list2) -> {
list1.addAll(list2);
return list1;
}
));
//Printing the resulting map
System.out.println("Map of lists of maps");
for (Map.Entry<String, List<Map<String, String>>> entry : mapRes.entrySet()) {
System.out.println(entry.getKey() " => " entry.getValue());
}
//Creating the resulting map as a map of lists of strings
Map<String, List<String>> mapRes2 = map.entrySet().stream()
.collect(Collectors.toMap(
entry -> {
//Looking for the newKey which matches the beginning of the current entry's key.
return newKeys.stream().filter(s -> entry.getKey().startsWith(s)).findFirst().orElseThrow(() -> new RuntimeException("No key found"));
},
entry -> {
//Returning a List with the original value.
return new ArrayList<>(List.of(entry.getValue()));
},
//Handling colliding cases by merging the lists together
(list1, list2) -> {
list1.addAll(list2);
return list1;
}
));
//Printing the resulting map
System.out.println("\nMap of lists of strings");
for (Map.Entry<String, List<String>> entry : mapRes2.entrySet()) {
System.out.println(entry.getKey() " => " entry.getValue());
}
}
}
Here is a link to test the code:
https://www.jdoodle.com/iembed/v0/s6e
Output
The output is shown for both output structures you were thinking of.
Map of lists of maps
skim fac1 => [{ccy2=USD}, {ccy1=EUR}, {ccy1rate=0.15}, {ccy2rate=0.20}]
skim fac2 => [{ccy1rate=0.17}, {ccy1=GBP}]
Map of lists of strings
skim fac1 => [USD, EUR, 0.15, 0.20]
skim fac2 => [0.17, GBP]
CodePudding user response:
As per my understanding, you want something like this Map<String, Map<String, String>>
which will store the value such as skim fac1 = {ccy2rate=0.20, ccy1=EUR, ccy2=USD, ccy1rate=0.15}
. I am giving my suggestion based on this output pattern .
Map<String, String> mainMap = new HashMap<>(Map.of(
"skim fac1 ccy1", "EUR",
"skim fac1 ccy1rate", "0.15",
"skim fac1 ccy2", "USD",
"skim fac1 ccy2rate","0.20",
"skim fac2 ccy1", "GBP",
"skim fac2 ccy1rate", "0.17"
));
String pattern = "(.*?)(\\d )(.*)";
//here considering you will only have pattern like `skim facn ...` , collect distinct `skim facn`values only.
/**
* using pattern we can eaasily identify and group the string.
* if pattern get matched then it will give out put as {skim fac}{n}{rest of the value}
* here we only interested in {skim fac}{2} hence contcatanation of 2 strings
*
*/
List<String> distinctKeys = mainMap.keySet().stream()
.map(Pattern.compile(pattern)::matcher)
.filter(Matcher::matches) //considering only valid matches
.map(m -> m.group(1) m.group(2)) //concatanation of 2 string
.distinct()//to get distinct resultset
.collect(Collectors.toList());
System.out.println(distinctKeys);
Map<String, Map<String, String>> output = new HashMap<>();
for (String key : distinctKeys) {
Map<String, String> temp = new HashMap<>();
//filtering out the required keys for each iterations
List<String> mainKeys = mainMap.keySet().stream().filter(m -> m.startsWith(key))
.collect(Collectors.toList());
for (String mainKey : mainKeys) {
//substring the last part of the key
String lastPart = mainKey.substring(key.length() 1, mainKey.length()).trim();
temp.put(lastPart, mainMap.get(mainKey));
}
output.put(key, temp);
}
for (Map.Entry<String, Map<String, String>> entry : output.entrySet()) {
System.out.println(entry.getKey() " ----> " entry.getValue());
}
Output
skim fac1 ----> {ccy2rate=0.20, ccy1=EUR, ccy2=USD, ccy1rate=0.15}
skim fac2 ----> {ccy1=GBP, ccy1rate=0.17}