10

I have a Map<String,List<String>> and want it to turn into Map<String,List<Long>> because each String in the list represents a Long :

Map<String,List<String>> input = ...;
Map<String,List<Long>> output= 
input.entrySet()
       .stream()
       .collect(toMap(Entry::getKey, e -> e.getValue().stream()
                                                      .map(Long::valueOf)
                                                      .collect(toList()))
               );

My main issue is each String may not represent correctly a Long; there may be some issue. Long::valueOf may raise exceptions. If this is the case, I want to return a null or empty Map<String,List<Long>>

Because I want to iterate after over this output map. But I cannot accept any error conversion; not even a single one. Any idea as to how I can return an empty output in case of incorrect String -> Long conversion?

TylerH
  • 20,799
  • 66
  • 75
  • 101
AntonBoarf
  • 1,239
  • 15
  • 31
  • I agree with Naman solution but unfortunately in the catch block, I fail to retrieve the key (Entry::getKey) for which the String -> Long conversion is incorrect – AntonBoarf Oct 02 '19 at 13:36
  • Similar discussion here: [String to int - likely bad data need to avoid exceptions](https://stackoverflow.com/questions/174502/string-to-int-in-java-likely-bad-data-need-to-avoid-exceptions/45028739#45028739) where I eventually decided to pre-check with regex (parseLong docs use the same parsing rules & you probably want to return a `LongStream` if you plan to remove `empty` results) – charles-allen Oct 02 '19 at 16:16
  • Sorry, I misunderstood. I thought you meant to return a single entry as empty/null; but now I think you mean the entire Map! – charles-allen Oct 02 '19 at 16:20
  • 1
    It's not entirely clear what the main point is - you want to return an empty map in case of an error, but still print the "key" where the error appeared to the console? I mean, information about the context where the exception appeared is usually transported up the call stack *in* the exception. Regardless of that: You specifically asked about streams, but I'd **strongly** recommend to avoid nested "collect" calls. People who have to maintain that later (and this may well be *future you*!) will wonder what the h... you did there. At least introduce some properly named helper methods. – Marco13 Oct 02 '19 at 22:53

4 Answers4

5

I personally like to provide an Optional input around number parsing:

public static Optional<Long> parseLong(String input) {
    try {
        return Optional.of(Long.parseLong(input));
    } catch (NumberFormatException ex) {
        return Optional.empty();
    }
}

Then, using your own code (and ignoring bad input):

Map<String,List<String>> input = ...;
Map<String,List<Long>> output= 
input.entrySet()
       .stream()
       .collect(toMap(Entry::getKey, e -> e.getValue().stream()
                                                      .map(MyClass::parseLong)
                                                      .filter(Optional::isPresent)
                                                      .map(Optional::get)
                                                      .collect(toList()))
               );

Additionally, consider a helper method to make this more succinct:

public static List<Long> convertList(List<String> input) {
    return input.stream()
        .map(MyClass::parseLong).filter(Optional::isPresent).map(Optional::get)
        .collect(Collectors.toList());
}

public static List<Long> convertEntry(Map.Entry<String, List<String>> entry) {
    return MyClass.convertList(entry.getValue());
}

Then you can filter the results in your stream's collector:

Map<String, List<Long>> converted = input.entrySet().stream()
    .collect(Collectors.toMap(Entry::getKey, MyClass::convertEntry));

You could also keep the empty Optional objects in your lists, and then by comparing their index in the new List<Optional<Long>> (instead of List<Long>) with the original List<String>, you can find the string which caused any erroneous inputs. You could also simply log these failures in MyClass#parseLong

However, if your desire is to not operate on any bad input at all, then surrounding the entire stream in what you're attempting to catch (per Naman's answer) is the route I would take.

Rogue
  • 11,105
  • 5
  • 45
  • 71
4

How about an explicit catch over the exception:

private Map<String, List<Long>> transformInput(Map<String, List<String>> input) {
    try {
        return input.entrySet()
                .stream()
                .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().stream()
                        .map(Long::valueOf)
                        .collect(Collectors.toList())));
    } catch (NumberFormatException nfe) {
        // log the cause
        return Collections.emptyMap();
    }
}
TylerH
  • 20,799
  • 66
  • 75
  • 101
Naman
  • 27,789
  • 26
  • 218
  • 353
  • ok sounds good...but in the catch(nfe) I would like to retrieve the specific value of key (Entry::getKey) and incorrect String for which it fails so I can log precisely where it goes wrong. Is it possible ? – AntonBoarf Oct 02 '19 at 13:30
  • @AntonBoarf If you just want to log the key, for which it failed to parse the String, use `nfe.getMessage()` – Naman Oct 02 '19 at 13:38
  • 1
    @AntonBoarf the exception’s message will contain the malformed input string. To get the responsible key, I’d do an explicit search, only when the exception happened, e.g. `input.entrySet().stream() .filter(e -> e.getValue().stream().anyMatch(s -> !new Scanner(s).hasNextLong())) .map(Map.Entry::getKey) .findAny()` – Holger Oct 02 '19 at 13:41
  • @Holder. Thanks...That seems complicated...I'm wondering if using standart for loop Java5 is not better in my case – AntonBoarf Oct 02 '19 at 13:44
  • @AntonBoarf just implement both and compare… – Holger Oct 02 '19 at 14:56
1

You can create a StringBuilder for key with exception and check if ele is numeric as below,

 public static Map<String, List<Long>> transformInput(Map<String, List<String>> input) {
    StringBuilder sb = new StringBuilder();
    try {
    return input.entrySet()
            .stream()
            .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().stream()
                    .map(ele->{
                        if (!StringUtils.isNumeric(ele)) {
                            sb.append(e.getKey()); //add exception key
                            throw new NumberFormatException();
                        }
                        return Long.valueOf(ele);
                    })
                    .collect(Collectors.toList())));
} catch (NumberFormatException nfe) {
    System.out.println("Exception key "+sb);
    return Collections.emptyMap();
}
}

Hope it helps.

Vikas
  • 6,868
  • 4
  • 27
  • 41
0

May be you can write an helper method that can check for numeric in the string and filter them out from the stream and also null values then finally collect to the Map.

// StringUtils.java
public static boolean isNumeric(String string) {
    try {
        Long.parseLong(string);
        return true;
    } catch(NumberFormatException e) {
        return false;
    }
}

This will take care everything.

And use this in your stream.

Map<String, List<Long>> newMap = map.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, entry -> mapToLongValues(entry.getValue())));

public List<Long> mapToLongValues(List<String> strs) {
    return strs.stream()
        .filter(Objects::nonNull)
        .filter(StringUtils::isNumeric)
        .map(Long::valueOf)
        .collect(Collectors.toList());
}
the_tech_maddy
  • 575
  • 2
  • 6
  • 21