0

I have an input String and I want to put it into HashMap. String (this is not url params) looks like: source=google&date=2019-01-01&...

public Map<String, String> extract(String source) {
   return Arrays.stream(source.split("&"))
           .map(s -> s.trim().split("="))
           .peek(pairs -> pairs[0] = convert(pairs[0])
           .filter(this::isValid)
           .collect(Collectors.toMap(
                   key -> key[0],
                   value -> value[1],
                   (val1, val2) -> val2)
           );
}

private boolean isValid(String[] strings) {
        return strings.length == 2
                && !strings[0].isBlank()
                && !strings[1].isBlank();
    }

The result for String above is: [source=google, date=2019-01-01, ..] But if String has whitespaces like: source = google & date= 2019-01-01 the result will be [ source = google, ...]. I want to remove whitespace, so i add:

 .map(s -> s.trim().split("="))
 .peek(this::removeWhiteSpaces)


 private void removeWhiteSpace(String [] strings) {
        strings[0] = strings[0].trim();
        strings[1] = strings[1].trim();
        string[0] = convert(string[0]);
    }

It works okay, but now if String cannot be splitted to array with 2 elements, like this: source campaign&date... it will throw ArrayIndexOutOfBoundException, because string[1] doesnt exist. So i move filter above peek:

.filter(this::isValid)
.peek(pairs -> pairs[0] = convert(pairs[0])
         

Now String source campaign&date... will not pass filter, but here is another problem. Method convert can return empty String "" and i should filter them. Thats why filter was after peek operator. I can solve it with two filters, like this:

.map(s -> s.trim().split("="))
.filter(strings -> strings.length==2)
.peek(this::removeWhiteSpaces)
.filter(strings -> !strings[0].isBlank() && !strings[1].isBlank())
.collect(..)

But can i do it with one filter? Maybe my stream chain is not good at all?

UPD convert method:

private String convert(String string) {
   return conversionTable.getOrDefault(string, ""); //map with key/values
}
takotsubo
  • 666
  • 6
  • 18
  • You want to remove any space in your result or just leading spaces? – Youcef LAIDANI Nov 25 '20 at 08:16
  • `whitespace this is a string whitespace ` should be `this a string`. For key and for value at the same time (splitted by =). So leading and trailing. – takotsubo Nov 25 '20 at 08:21
  • You said *the result will be `[ source = google, ...]`* why not `[source=google,...` – Youcef LAIDANI Nov 25 '20 at 08:24
  • 1
    are you looking to `return Arrays.stream(source.split("&")) .map(s -> Arrays.stream(s.split("=")) .map(String::trim) .toArray(String[]::new) ).filter(this::isValid) .collect(Collectors.toMap(x -> x[0], x -> x[1]));` – Youcef LAIDANI Nov 25 '20 at 08:31
  • I guess you want to use map intermediate operation but instead you are using peek which is only used for debugging purposes. – Rohan Sharma Nov 25 '20 at 08:39
  • **YCF_L**, yes. **Rohan Sharma** since when peek is only used for debugging? – takotsubo Nov 25 '20 at 08:55
  • 2
    [is peek really only for debugging?](https://stackoverflow.com/q/33635717/2711488) has been asked five years ago, to answer *that* question. Besides that, why don’t you use `.split("\\s*&\\s*")` and `.split("\\s*=\\s*")` in the first place, instead of removing white-space afterwards in an extra operation? – Holger Nov 25 '20 at 11:51

1 Answers1

1

With @Holger suggestion for stripping whitespace you only need to deal with the String[] being length 0, 1 or 2 for your input data which is handled with a filter and change to the collector value -> value.length < 2 ? "" : value[1].

public Map<String, String> extract(String source) {
    return Arrays.stream(source.split("\\s*&\\s*"))
            .map(s -> s.trim().split("\\s*=\\s*")) // String[0..2] based on your input
            //.peek(a -> System.out.println(Arrays.toString(a)))
            .filter(a -> a.length > 0 && a[0].length() > 0)
            .collect(Collectors.toMap(
                    key -> key[0],
                    value -> value.length < 2 ? "" : value[1],
                    (val1, val2) -> val2)
            );

}
    System.out.println("1="+u.extract("      &source=google&date=2019-01-01&name1=value1&=&&       &  =   &flag2&arg=v"));
    System.out.println("2="+u.extract("   source = google&date=2019-01-01&flag3  & name1 = value1&flag2 &arg=v&flag4"));

The above strips blank names and prints:

1={date=2019-01-01, arg=v, flag2=, source=google, name1=value1}
2={date=2019-01-01, flag4=, arg=v, flag3=, flag2=, source=google, name1=value1}

If you are mapping keys and stripping unrecognised entries you can add this before the collect():

 .map(this::convert)
 .filter(Objects::nonNull)

Where convert is:

private String[] convert(String[] arr) {
    arr[0] = conversionTable.getOrDefault(arr[0], null);

    return arr[0] != null ? arr : null;
}
DuncG
  • 12,137
  • 2
  • 21
  • 33
  • Thanks for suggestion with another regex to split, but I also need to convert first element of array and then checks if the converted element is empty or not. – takotsubo Nov 25 '20 at 13:29
  • The filter I added in last edit should deal with the blanks such as in "&=& = &" – DuncG Nov 25 '20 at 13:35
  • My `convert` method gets first element from array, for example: `source=google` (first element after split is `source`). Method should return `ga:source`, and then checks `ga:source` for empty, because if method which converts strings cannot convert it, then return empty string. For example `sour=google`, `sourc` is not valid, then `convert` method returns `empty string here`, then filter should check this new string and skip this element. – takotsubo Nov 25 '20 at 13:43
  • So before checking is first element from array is empty or not, i should convert it. – takotsubo Nov 25 '20 at 13:47