Is there a declarative way to transform Array to Dictionary?

Earl Grey picture Earl Grey · Feb 21, 2016 · Viewed 17.7k times · Source

I want to get from this array of strings

let entries = ["x=5", "y=7", "z=10"]

to this

let keyValuePairs = ["x" : "5", "y" : "7", "z" : "10"]

I tried to use map but the problem seems to be that a key - value pair in a dictionary is not a distinct type, it's just in my mind, but not in the Dictionary type so I couldn't really provide a transform function because there is nothing to transform to. Plus map return an array so it's a no go.

Any ideas?

Answer

David Berry picture David Berry · Feb 22, 2016

Swift 4

As alluded to by fl034, this can be simplified some with Swift 4 where an error checked version looks like:

let foo = entries
    .map { $0.components(separatedBy: "=") }
    .reduce(into: [String:Int64]()) { dict, pair in
        if pair.count == 2, let value = Int64(pair[1]) {
            dict[pair[0]] = value
        }
    }

Even simpler if you don't want the values as Ints:

let foo = entries
    .map { $0.components(separatedBy: "=") }
    .reduce(into: [String:String]()) { dict, pair in
        if pair.count == 2 {
            dict[pair[0]] = pair[1]
        }
    }

Older TL;DR

Minus error checking, it looks pretty much like:

let foo = entries.map({ $0.componentsSeparatedByString("=") })
    .reduce([String:Int]()) { acc, comps in
        var ret = acc
        ret[comps[0]] = Int(comps[1])
        return ret
    }

Use map to turn the [String] into a split up [[String]] and then build the dictionary of [String:Int] from that using reduce.

Or, by adding an extension to Dictionary:

extension Dictionary {
    init(elements:[(Key, Value)]) {
        self.init()
        for (key, value) in elements {
            updateValue(value, forKey: key)
        }
    }
}

(Quite a useful extension btw, you can use it for a lot of map/filter operations on Dictionaries, really kind of a shame it doesn't exist by default)

It becomes even simpler:

let dict = Dictionary(elements: entries
    .map({ $0.componentsSeparatedByString("=") })
    .map({ ($0[0], Int($0[1])!)})
)

Of course, you can also combine the two map calls, but I prefer to break up the individual transforms.

If you want to add some error checking, flatMap can be used instead of map:

let dict2 = [String:Int](elements: entries
    .map({ $0.componentsSeparatedByString("=") })
    .flatMap({
        if $0.count == 2, let value = Int($0[1]) {
            return ($0[0], value)
        } else {
            return nil
        }})
)

Again, if you want, you can obviously merge the map into the flatMap or split them for simplicity.

let dict2 = [String:Int](elements: entries.flatMap {
    let parts = $0.componentsSeparatedByString("=")
    if parts.count == 2, let value = Int(parts[1]) {
        return (parts[0], value)
    } else {
        return nil
    }}
)