Richie Richie - 27 days ago 5
Groovy Question

Using transpose to merge lists in full outer join style

I'm trying to return a joined list. But the join has to be like a database full outer join.

For example given the following...

def x = [ [a:1, b:2], [a:1, b:3], [a:2, b:4], [a:3, b:5] ]
def y = [ [f:10, b:2, g:7], [f:100, b:3, g:8], [f:20, b:4, g:9], [f:20, b:6, g:9] ]


I'd like to return...

[[a:1, b:2, f:10, g:7], [a:1, b:3, f:100, g:8], [a:2, b:4, f:20, g:9], [a:3, b:5], [a:3, b:6, f:20, g:9]]


In a previous question someone showed my how to use transpose to join the lists.

def z = [x,y].transpose().collect { a, b -> a + b }
println z​


But the output from that misses [a:3, b:5] from the expected output. See below.

[[a:1, b:2, f:10, g:7], [a:1, b:3, f:100, g:8], [a:2, b:4, f:20, g:9], [a:3, b:6, f:20, g:9]]


Could someone assist help me with the required expression and assist me in my understanding of why the original expression was not working.

thanks

Answer Source

Firstly let's explain how GroovyCollections.transpose(List lists) works to solve this problem. For given output

def x = [ [a:1, b:2], [a:1, b:3], [a:2, b:4], [a:3, b:5] ]
def y = [ [f:10, b:2, g:7], [f:100, b:3, g:8], [f:20, b:4, g:9], [f:20, b:6, g:9] ]

expression

[x,y].transpose()

creates a list of pairs:

[
    [[a:1, b:2], [f:10, b:2, g:7]],
    [[a:1, b:3], [f:100, b:3, g:8]],
    [[a:2, b:4], [f:20, b:4, g:9]],
    [[a:3, b:5], [f:20, b:6, g:9]]
]

If we compare it with a list you expect to get

def expected = [[a:1, b:2, f:10, g:7], [a:1, b:3, f:100, g:8], [a:2, b:4, f:20, g:9], [a:3, b:5], [a:3, b:6, f:20, g:9]]

we can see that it contains not 4, but 5 elements. There is one hidden requirement that can be found after investigating desired result: if there is a pair of two maps that has at least one common key, but with two different values for each pair element, then don't merge those two pairs, but create two maps instead, where first map is a map that comes from x and is unchanged and second one is a result of merging maps from x and y.

How to check if two maps have at least one common key with different values?

We can use Collection.intersect(Collection right) for that. But we have to compare two intersections:

  1. Intersection of Map.keySet() keys
  2. Intersection of Map.Entry elements from both maps

First intersection will tell us if there are same keys in both maps, while the second intersection will tell us if they store the same value. If both expressions evaluate to true, we will merge them with a + b as it was in the example you have mentioned (we will also use the same method if both maps does not have any keys in common). But if both maps have non-empty intersection of keys while intersection of map entries is not equal to the result of first intersection we will merge these maps using [a, a+b] and we will .flatten() the result eventually. Below you can find a Groovy code that does what I have just described:

def x = [[a: 1, b: 2], [a: 1, b: 3], [a: 2, b: 4], [a: 3, b: 5]]
def y = [[f: 10, b: 2, g: 7], [f: 100, b: 3, g: 8], [f: 20, b: 4, g: 9], [f: 20, b: 6, g: 9]]
def expected = [[a: 1, b: 2, f: 10, g: 7], [a: 1, b: 3, f: 100, g: 8], [a: 2, b: 4, f: 20, g: 9], [a: 3, b: 5], [a: 3, b: 6, f: 20, g: 9]]

def shareSameKeyWithSameValue(Map<String, ?> a, Map<String, ?> b) {
    final Set<String> keysIntersectionFromEntries = (a.entrySet().intersect(b.entrySet())).key as Set
    final Set<String> keysIntersection = a.keySet().intersect(b.keySet())
    return !keysIntersectionFromEntries.isEmpty() && keysIntersectionFromEntries.containsAll(keysIntersection)
}

def result = [x, y].transpose().collect { a, b ->
    shareSameKeyWithSameValue(a, b) ? a + b : [a, a + b]
}.flatten()

assert result == expected

I hope it helps.