2

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 me 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 help me with the required expression and assist me in my understanding of why the original expression was not working?

Szymon Stepniak
  • 40,216
  • 10
  • 104
  • 131
Richie
  • 4,989
  • 24
  • 90
  • 177
  • Possible duplicate of [how to join list of maps in groovy](https://stackoverflow.com/questions/45791945/how-to-join-list-of-maps-in-groovy) – Rao Aug 27 '17 at 14:12
  • 1
    Have you tried the solution by me in your [previous question](https://stackoverflow.com/questions/45791945/how-to-join-list-of-maps-in-groovy) which gives the desired result? – Rao Aug 27 '17 at 14:13
  • By the way, there is no `a:3, b:6` combination. Isn't it? – Rao Aug 27 '17 at 14:20
  • The result you wish to get in that constellation is virtually impossible to get with those inputs and transpose. Transpose will only ever give you N items, where N is the length of its shortest input. So if you want 5 items out of it and you get 2 times 4 items as input, you would have to add some "padding" (e.g. empty maps) on the places, where they don't have a matching partner. And at that point you are better off not using transpose at all. – cfrick Aug 28 '17 at 08:47

1 Answers1

2

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.

Szymon Stepniak
  • 40,216
  • 10
  • 104
  • 131