String manipulation in Java 8 Streams - java

I have a stream of Strings like-
Token1:Token2:Token3
Here ':' is delimiter character. Here Token3 String may contain delimiter character in it or may be absent.
We have to convert this stream into map with Token1 as key and value is array of two strings- array[0] = Token2 and array[1] = Token3 if Token3 is present, else null.
I have tried something like-
return Arrays.stream(inputArray)
.map( elem -> elem.split(":"))
.filter( elem -> elem.length==2 )
.collect(Collectors.toMap( e-> e[0], e -> {e[1],e[2]}));
But It didn't work. Beside that it do not handle the case if Token3 is absent or contain delimiter character in it.
How can I accomplish it in Java8 lambda expressions?

You can map every input string to the regex Matcher, then leave only those which actually match and collect via toMap collector using Matcher.group() method:
Map<String, String[]> map = Arrays.stream(inputArray)
.map(Pattern.compile("([^:]++):([^:]++):?(.+)?")::matcher)
.filter(Matcher::matches)
.collect(Collectors.toMap(m -> m.group(1), m -> new String[] {m.group(2), m.group(3)}));
Full test:
String[] inputArray = {"Token1:Token2:Token3:other",
"foo:bar:baz:qux", "test:test"};
Map<String, String[]> map = Arrays.stream(inputArray)
.map(Pattern.compile("([^:]++):([^:]++):?(.+)?")::matcher)
.filter(Matcher::matches)
.collect(Collectors.toMap(m -> m.group(1), m -> new String[] {m.group(2), m.group(3)}));
map.forEach((k, v) -> {
System.out.println(k+" => "+Arrays.toString(v));
});
Output:
test => [test, null]
foo => [bar, baz:qux]
Token1 => [Token2, Token3:other]
The same problem could be solved with String.split as well. You just need to use two-arg split version and specify how many parts at most do you want to have:
Map<String, String[]> map = Arrays.stream(inputArray)
.map(elem -> elem.split(":", 3)) // 3 means that no more than 3 parts are necessary
.filter(elem -> elem.length >= 2)
.collect(Collectors.toMap(m -> m[0],
m -> new String[] {m[1], m.length > 2 ? m[2] : null}));
The result is the same.

You can achieve what you want with the following:
return Arrays.stream(inputArray)
.map(elem -> elem.split(":", 3)) // split into at most 3 parts
.filter(arr -> arr.length >= 2) // discard invalid input (?)
.collect(Collectors.toMap(arr -> arr[0], arr -> Arrays.copyOfRange(arr, 1, 3))); // will add null as the second element if the array length is 2

Related

Split String into Map using Java Streams

I want to split the following String and store it into a Map.
String = "key_a:<value_a1>\r\n\r\nkey_b:<value_b1>\r\n\r\nkey_c:<value_c1, value_c2, value_c3>"
The string can have line breaks in between the pairs. A key can have multiple values that are separated by a , and begin with a < and end with a >.
Now this String needs to be converted to a Map<String, List<String>>.
The structure of the map should look like this:
key_a={value_a1},
key_b={value_b1},
key_c={value_c1, value_c2, value_c3}
I currently only have the logic for splitting apart the different key-value-pairs from each other, but I don't know how to implement the logic that splits the values apart from each other, removes the brackets and maps the attributes.
String strBody = "key_a:<value_a1>\r\n\r\nkey_b:<value_b1>\r\n\r\nkey_c:<value_c1, value_c2, value_c3>"
Map<String, List<String>> map = Pattern.compile("\\r?\\n")
.splitAsStream(strBody)
.map(s -> s.split(":"))
//...logic for splitting values apart from each other, removing <> brackets and storing it in the map
)
You can filter the arrays having two values and then use Collectors.groupingBy to group the elements into Map, You can find more examples here about groupingBy and `mapping
Map<String, List<String>> map = Pattern.compile("\\r?\\n")
.splitAsStream(strBody)
.map(s -> s.split(":"))
.filter(arr -> arr.length == 2)
.collect(Collectors.groupingBy(arr -> arr[0],
Collectors.mapping(arr -> arr[1].replaceAll("[<>]", ""),
Collectors.toList())));
An additional approach which also splits the list of values:
Map<String,List<String>> result =
Pattern.compile("[\\r\\n]+")
.splitAsStream(strBody)
.map(s -> s.split(":"))
.map(arr -> new AbstractMap.SimpleEntry<>(
arr[0],
Arrays.asList(arr[1].replaceAll("[<>]", "").split("\\s*,\\s"))))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
Your input has two \r\n to separate the entries, you need to split it by it as well, otherwise you will get empty entries, which you then need to filter out.
I'd remove the angle brackets from the string before processing it in the stream.
And then only the step of collection remains.
Map<String, String> map = Pattern.compile("\\r?\\n\\r?\\n")
.splitAsStream(strBody.replaceAll("[<>]",""))
.map(s -> s.split(":"))
.collect(Collectors.toMap(e -> e[0], e-> e[1]));
Try this.
String strBody = "key_a:<value_a1>\r\n\r\nkey_b:<value_b1>\r\n\r\nkey_c:<value_c1, value_c2, value_c3>";
Map<String, List<String>> result = Arrays.stream(strBody.split("\\R\\R"))
.map(e -> e.split(":", 2))
.collect(Collectors.toMap(a -> a[0],
a -> List.of(a[1].replaceAll("^<|>$", "").split("\\s,\\s*"))));
System.out.println(result);
output
{key_c=[value_c1, value_c2, value_c3], key_b=[value_b1], key_a=[value_a1]}

List<String> to Map<String,String> with stream, with String splitting

I have List with Strings:
List<String> cookiesUpdate = Arrays.asList("A=2" , "B=3");
I want to convert it to Map:
{
"A": "2",
"B": "3"
}
Code:
Map<String, String> cookies = cookiesUpdate.stream()
.collect(Collectors.toMap(String::toString, String::toString));
How to write those splitters above? If compiler thinks key String is Object.
.split("=")[0];
.split("=")[1];
Split should be done by "=" (or "\\s*=\\s" to exclude whitespaces around =)
Update Also it is better to provide limit argument to String::split to split at the first occurrence of "=", thanks #AndrewF for suggestion!
Fix toMap collector to use the first element of the array as key and the last as the value; a merge function may be needed if several namesake cookies are possible
Map<String, String> map = cookies.stream()
.map(ck -> ck.split("\\s*=\\s*", 2)) // Stream<String[]>
.filter(arr -> arr.length > 1) // ignore invalid cookies
.collect(Collectors.toMap(arr -> arr[0], arr -> arr[1], (v1, v2) -> v1));
If there are multiple cookies with the same name, it may be worth to collect them into Set<String> thus keeping all unique values. For this, Collectors.groupingBy and Collectors.mapping:
Map<String, Set<String>> map2 = cookies.stream()
.map(ck -> ck.split("\\s*=\\s*", 2)) // Stream<String[]>
.filter(arr -> arr.length > 1) // ignore invalid cookies
.collect(Collectors.groupingBy(
arr -> arr[0],
Collectors.mapping(arr -> arr[1], Collectors.toSet())
));

Get all values if Keys contain same substring in Java HashMap

I am trying to get all Values from Keys that contain the same substring. For example:
If a Key's string is "AAABBB'
and another Key's string is 'XXXBBB'
I want to get the Value from both of those Keys. (Since BBB matches)
The relationship of the substring match should be 3 characters in length. The prefix is from index 0-3 and the suffix index is from 3-6.
For example: AAABBB
(AAA is the suffix and BBB is the prefix.)
(The relationship AABBBA is ignored because AAB and BBA do not match.)
I'm trying to avoid using nested for loops because my algorithm will run very slow at O(N^2). I'm working on finding a solution with 1 HashMap and 1 for loop.
HashMap a = new HashMap();
map.put("AAABBB", 1);
map.put("CCCPPP", 2);
map.put("XXXBBB", 3);
map.put("AAAOOO",4);
for (Entry<String, String> entry : a.entrySet()) {
String prefix = entry.getKey().substring(0,3);
String suffix = entry.getKey().substring(3,6);
if(map.contains("ANY_SUBSTRING" + suffix){
System.out.println(map.get("ANY_SUBSTRING" + suffix);
}
}
Output: (1,3)
AAABBB => 1
XXXBBB => 3
I have following approach with streams.
Define a function to extract the suffix or prefix of each key of your map
Stream your maps entryset and group by prefix/suffix
filter those out which have no prefix/suffix incommon
Using your example map and assuming each key length is = 6
Map<String,Integer> map = new HashMap<>();
map.put("AAABBB", 1);
map.put("CCCPPP", 2);
map.put("XXXBBB", 3);
map.put("AAAOOO",4);
Function<Entry<String, Integer>,String> prefix = e -> e.getKey().substring(0,3);
Function<Entry<String, Integer>,String> suffix = e -> e.getKey().substring(3);
Map<String,List<Integer>> resultBySuffix =
map.entrySet().stream()
.collect(Collectors.groupingBy( suffix ,
Collectors.mapping(Entry::getValue, Collectors.toList())
)).entrySet().stream()
.filter(e -> e.getValue().size() > 1)
.collect(Collectors.toMap(Entry::getKey, Entry::getValue));
System.out.println(resultBySuffix);
Map<String,List<Integer>> resultByPrefix =
map.entrySet().stream()
.collect(Collectors.groupingBy( prefix ,
Collectors.mapping(Entry::getValue, Collectors.toList())
)).entrySet().stream()
.filter(e -> e.getValue().size() > 1)
.collect(Collectors.toMap(Entry::getKey, Entry::getValue));
System.out.println(resultByPrefix);
No idea what the time complexity of the above example is like. But I think you can see what is going on (in terms of readability)

Proper usage of Streams in Java

I've a use-case where I need to parse key-value pairs (separated by =) and put these key-value pairs in a LinkedHashMap.
I want to ignore the following type of Strings
key is empty or contains only spaces
value is empty or contains only spaces
those Strings which don't contain a =.
Now, I have solved it using imperative style and by using streams also.
The following are the 2 variants:
Solution by iterative style - for loop and lots of if
public static Map<String, String> getMap1(String[] array) {
Map<String, String> map = new LinkedHashMap<>();
for (int i = 0; i < array.length; i++) {
String currentString = array[i];
int index = currentString.indexOf('=');
// ignoring strings that don't contain '='
if (index == -1) continue;
String key = currentString.substring(0, index).trim();
String value = currentString.substring(index + 1).trim();
// ignoring strings with empty key or value
if (key.length() == 0 || value.length() == 0) continue;
map.put(key, value);
}
return map;
}
Solution that uses Streams - pretty clean code
public static Map<String, String> getMap(String[] array) {
return Arrays.stream(array)
.filter(s -> s.indexOf('=') != -1) // ignore strings that don't contain '='
.filter(s -> s.substring(0, s.indexOf('=')).trim().length() != 0) // key should be present
.filter(s -> s.substring(s.indexOf('=') + 1).trim().length() != 0) // value should be present
.collect(Collectors.toMap(
s -> s.substring(0, s.indexOf('=')).trim(),
s -> s.substring(s.indexOf('=') + 1).trim(),
(first, second) -> second,
LinkedHashMap::new));
}
I'm worried here because while using Streams, I'm calling the indexOf method multiple times. (And for big strings, I can end-up recalculating the same thing again and again).
Is there a way I can avoid re-computation done by indexOf method in such a way that the code is still clean. (I know talking about clean-code is very subjective, but I want don't want to open multiple streams, of loop through the original string-array and subsequently pre-computing the indices of = and re-using that).
Clubbing multiple filters into a single filter again seem to be an option but that would make my predicate pretty ugly.
(This is a result of my idle musing where I wish to learn/improve).
What about this:
String[] array = {"aaa2=asdas","aaa=asdasd"};
LinkedHashMap<String, String> aaa = Arrays.stream(array)
.map(s -> s.split("=", 2))
.filter(s -> s.length == 2) // ignore strings that don't contain '='
.peek(s -> { s[0] = s[0].trim(); })
.peek(s -> { s[1] = s[1].trim(); })
.filter(s -> s[0].length() != 0) // key should be present
.filter(s -> s[1].length() != 0) // value should be present
.collect(Collectors.toMap(
s -> s[0],
s -> s[1],
(first, second) -> second,
LinkedHashMap::new));
I'd use split instead of indexOf and StringUtils to check that your keys and values are not empty.
public static Map<String, String> getMap(String[] array) {
return Arrays.stream(array)
.filter(s -> s.contains("="))
.map(s -> s.split("="))
.filter(s -> s.length == 2 && isNotBlank(s[0]) && isNotBlank(s[1]))
.collect(Collectors.toMap(
s -> s[0].trim(),
s -> s[1].trim()));
}

split string and store it into HashMap java 8

I want to split below string and store it into HashMap.
String responseString = "name~peter-add~mumbai-md~v-refNo~";
first I split the string using delimeter hyphen (-) and storing it into ArrayList as below:
public static List<String> getTokenizeString(String delimitedString, char separator) {
final Splitter splitter = Splitter.on(separator).trimResults();
final Iterable<String> tokens = splitter.split(delimitedString);
final List<String> tokenList = new ArrayList<String>();
for(String token: tokens){
tokenList.add(token);
}
return tokenList;
}
List<String> list = MyClass.getTokenizeString(responseString, "-");
and then using the below code to convert it to HashMap using stream.
HashMap<String, String> = list.stream()
.collect(Collectors.toMap(k ->k.split("~")[0], v -> v.split("~")[1]));
The stream collector doesnt work as there is no value against refNo.
It works correctly if I have even number of elements in ArrayList.
Is there any way to handle this? Also suggest how I can use stream to do these two tasks (I dont want to use getTokenizeString() method) using stream java 8.
Unless Splitter is doing any magic, the getTokenizeString method is obsolete here. You can perform the entire processing as a single operation:
Map<String,String> map = Pattern.compile("\\s*-\\s*")
.splitAsStream(responseString.trim())
.map(s -> s.split("~", 2))
.collect(Collectors.toMap(a -> a[0], a -> a.length>1? a[1]: ""));
By using the regular expression \s*-\s* as separator, you are considering white-space as part of the separator, hence implicitly trimming the entries. There’s only one initial trim operation before processing the entries, to ensure that there is no white-space before the first or after the last entry.
Then, simply split the entries in a map step before collecting into a Map.
First of all, you don't have to split the same String twice.
Second of all, check the length of the array to determine if a value is present for a given key.
HashMap<String, String> map=
list.stream()
.map(s -> s.split("~"))
.collect(Collectors.toMap(a -> a[0], a -> a.length > 1 ? a[1] : ""));
This is assuming you want to put the key with a null value if a key has no corresponding value.
Or you can skip the list variable :
HashMap<String, String> map1 =
MyClass.getTokenizeString(responseString, "-")
.stream()
.map(s -> s.split("~"))
.collect(Collectors.toMap(a -> a[0], a -> a.length > 1 ? a[1] : ""));
private final String dataSheet = "103343262,6478342944, 103426540,84528784843, 103278808,263716791426, 103426733,27736529279,
103426000,27718159078, 103218982,19855201547, 103427376,27717278645,
103243034,81667273413";
final int chunk = 2;
AtomicInteger counter = new AtomicInteger();
Map<String, String> pairs = Arrays.stream(dataSheet.split(","))
.map(String::trim)
.collect(Collectors.groupingBy(i -> counter.getAndIncrement() / chunk))
.values()
.stream()
.collect(toMap(k -> k.get(0), v -> v.get(1)));
result:
pairs =
"103218982" -> "19855201547"
"103278808" -> "263716791426"
"103243034" -> "81667273413"
"103426733" -> "27736529279"
"103426540" -> "84528784843"
"103427376" -> "27717278645"
"103426000" -> "27718159078"
"103343262" -> "6478342944"
We need to group each 2 elements into key, value pairs, so will partion the list into chunks of 2, (counter.getAndIncrement() / 2) will result same number each 2 hits ex:
IntStream.range(0,6).forEach((i)->System.out.println(counter.getAndIncrement()/2));
prints:
0
0
1
1
2
2
You may use the same idea to partition list into chunks.
Another short way to do :
String responseString = "name~peter-add~mumbai-md~v-refNo~";
Map<String, String> collect = Arrays.stream(responseString.split("-"))
.map(s -> s.split("~", 2))
.collect(Collectors.toMap(a -> a[0], a -> a.length > 1 ? a[1] : ""));
System.out.println(collect);
First you split the String on basis of - , then you map like map(s -> s.split("~", 2))it to create Stream<String[]> like [name, peter][add, mumbai][md, v][refNo, ] and at last you collect it to toMap as a[0] goes to key and a[1] goes to value.

Categories