Search code examples
collectionsgroovymetaclassmultimap

Groovy: Transforming a String into a Multimap


So I have a String which looks a little something like this:

text = "foo/bar;baz/qux"

My end goal is to split this String into a Multimap like this:

["level1" : ["foo", "baz"], "level2" : ["bar", "qux"]]

I also added Multimap-support to LinkedHashMap's metaClass:

LinkedHashMap.metaClass.multiPut << { key, value ->
    delegate[key] = delegate[key] ?: []; delegate[key] += value
}

The String needs to be split at semi-colon and then again at forwardslash. Currently I'm populating my Multimap within a nested for-loop but obviously there's a Groovier way of doing this. Thus I was wondering what my options are?

I'm thinking something along the lines of:

def final myMap = text.split(';')
        .collectEntries { it.split('/')
        .eachWithIndex { entry, index -> ["level${index + 1}" : entry] }}

Solution

  • You can use withDefault on your returned Map to get rid of the ternary:

    def text = "foo/bar;baz/qux;foo/bar/woo"
    
    def result = text.split(';')*.split('/').inject([:].withDefault {[]}) { map, value ->
        value.eachWithIndex { element, idx ->
            map["level${idx+1}"] << element
        }
        map
    }
    
    assert result == [level1:['foo', 'baz', 'foo'], level2:['bar', 'qux', 'bar'], level3:['woo']]
    

    If you don't want duplicates in your results, then you can use a Set in your withDefault (then convert back to a List afterwards):

    def text = "foo/bar;baz/qux;foo/bar/woo"
    
    def result = text.split(';')*.split('/').inject([:].withDefault {[] as Set}) { map, value ->
        value.eachWithIndex { element, idx ->
            map["level${idx+1}"] << element
        }
        map
    }.collectEntries { key, value -> [key, value as List] }
    
    assert result == [level1:['foo', 'baz'], level2:['bar', 'qux'], level3:['woo']]