2

Suppose I have a multi-map like this:

MiddleName -> null
EmailAddress -> Toni@mymail.com, Mary@hermail.com, Paul@hismail.com
FirstName -> Toni, Mary, Paul
LastName -> Barry, null ,White
Id -> null

Note how each "values" entry can be entirely null or contain the same number of values as the one with more entries (which I don't know which is) even if some are null.

I want it to "transpose" it to a list of maps like this:

MiddleName -> null
EmailAddress -> Toni@mymail.com
FirstName -> Toni
LastName -> Barry
Id -> null

MiddleName -> null
EmailAddress -> Mary@hermail
FirstName -> John
LastName -> null
Id -> null

MiddleName -> null
EmailAddress -> Paul@hismail.com
FirstName -> Paul
LastName -> White
Id -> null

I'm trying to do this with java8 streams, but can do it in the "old" style as well.

Searching stackoverflow I found a similar question [1] and a few more also related [2,3,4] that gave me some ideas but were not fully adaptable.

Now, I did implement my own solution that works as I want, but frankly this is probably the most ugly code I wrote in years...

List result = (...)
map.forEach(h -> {
    MultiValueMap<String, String> m = new LinkedMultiValueMap();
    h.entrySet().stream().forEach(e -> {
        String[] ss = e.getValue() == null ? null : e.getValue().toString().split(",");
        if (ss != null) {
            Arrays.asList(ss).stream().forEach(s -> m.add(e.getKey(), s));
        }
    });
    if (m.size() > 0) {
        int x = m.values().stream().max((o1, o2) -> o1.size() - o2.size()).get().size();
        for (int i = 0; i < x; i++) {
            Map<String, String> n = (Map) h.clone();
            Iterator<Map.Entry<String, String>> it = n.entrySet().iterator();
            while( it.hasNext()){
                Map.Entry<String, String> e = it.next();
                List<String> ss = m.get(e.getKey());
                if(ss!=null) {
                    e.setValue(ss.get(i));
                }
            }
            result.add(n);
        }
    }
});

The first pass is just to split the string into a array since originally it's a comma separated string. Then I find the max number of elements in any value, I loop all the values in the entries for that number of times and create the a map for each value with the results. To be frank I wrote this code last Friday and I can't already read it properly...

So, this was at first a simple thing to do and I ended up with this mess, is there a better way to do it?

Thanks in advance.

[1] Java8 streams : Transpose map with values as list

[2] reversing keys/values - create new instance of HashMap

[3] "Transpose" a hashmap for key->value to value->key?

[4] Java invert map

Community
  • 1
  • 1
amsmota
  • 171
  • 2
  • 11

2 Answers2

0

I would frankly try to avoid being in that situation in the first place, and start using real objects instead of maps (even your desired list of maps should really be a List<Person>), but I would do it like this:

Map<String, List<String>> multiMap = ...;
List<Map<String, String>> result = new ArrayList<>();

// find an entry with a non-null value, and get the size of the
// list
OptionalInt sizeOfLists =
    multiMap.values()
            .stream()
            .filter(Objects::nonNull)
            .mapToInt(List::size)
            .findAny();

// for each index, create a person and put each key and the
// corresponding value at that index in that map
sizeOfLists.ifPresent(size -> {
    for (int i = 0; i < size; i++) {
        int index = i;
        Map<String, String> person = new HashMap<>();
        result.add(person);
        multiMap.entrySet()
                .stream()
                .filter(entry -> entry.getValue() != null)
                .forEach(entry -> person.put(entry.getKey(), entry.getValue().get(index)));
    }
});

Note that your code is not so terrible. But it would be much more readable if you gave meaningful names to your variables instead of h, m, e, ss.

JB Nizet
  • 678,734
  • 91
  • 1,224
  • 1,255
  • Hi, thanks for your reply. I can't avoid this situation unfortunately, as this is part of a bigger stream of operations. Your code is quite readable, I can understand it just by looking at it. I'm going to try it, since my actual scenario is a little more complicated than this. And relating to my variables naming you are, of course, right... :) I'll let you know how it goes. Cheers. – amsmota Aug 15 '16 at 14:28
0

How about this (if I understood everything correctly):

    Multimap<String, String> map = ArrayListMultimap.create();
    map.put("MiddleName", null);
    map.putAll("EmailAddress", ImmutableList.of("toni@gmail.com", "mary@gmail.com", "paul@gmail.com"));

    // that's the key with the "biggest" collection within the map
    int biggest = map.asMap().entrySet().stream().collect(Collectors.reducing(0, entry -> entry.getValue().size(), Integer::max));

    Multimap<String, String> newMap = ArrayListMultimap.create();

    // "padding" the collection when required
    map.keySet().stream().forEach(key -> {
        int currentSize = map.get(key).size();
        newMap.putAll(key, map.get(key));
        if (currentSize < biggest) {
            newMap.putAll(key, Collections.nCopies(biggest - currentSize, (String) null));
        }
    });

    System.out.println(newMap); // {MiddleName=[null, null, null], EmailAddress=[toni@gmail.com, mary@gmail.com, paul@gmail.com]}
}

Mapping to some Person object is fairly easy from here.

Eugene
  • 117,005
  • 15
  • 201
  • 306