32

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?

Earl Grey
  • 7,426
  • 6
  • 39
  • 59
  • If the strings in the array are always in the same format, you could parse for a key value, `=`, and a value and assign said data to a dictionary. – Arc676 Feb 21 '16 at 13:03
  • @Arc676 that is an imperative approach, sure, I actually already have it....but I am looking for a declarative solution. – Earl Grey Feb 21 '16 at 13:11

8 Answers8

53

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
    }}
)
David Berry
  • 40,941
  • 12
  • 84
  • 95
9

One way to do it is in two stages with map and reduce with a tuple as an intermediate value, for instance:

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

let dict = entries.map { (str) -> (String, String) in
    let elements = str.characters.split("=").map(String.init)
    return (elements[0], elements[1])
    }.reduce([String:String]()) { (var dict, kvpair) in
        dict[kvpair.0] = kvpair.1
        return dict
}

for key in dict.keys {
    print("Value for key '\(key)' is \(dict[key]).")
}

outputs:

Value for key 'y' is Optional("7").
Value for key 'x' is Optional("5").
Value for key 'z' is Optional("10").

or with a single reduce with the same output:

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

let dict = entries.reduce([String:String]()) { (var dict, entry) in
    let elements = entry.characters.split("=").map(String.init)
    dict[elements[0]] = elements[1]
    return dict
}

for key in dict.keys {
    print("Value for key '\(key)' is \(dict[key]).")
}
Crowman
  • 25,242
  • 5
  • 48
  • 56
  • Very good. Please note that `str.characters.split{$0 == "="}` can also be written as `str.characters.split("=")` – Luca Angeletti Feb 21 '16 at 13:46
  • Also in the first map closure, you don't need to declare `var` the `str` param. It can simply be `entries.map { (str) -> (String, String) in` – Luca Angeletti Feb 21 '16 at 13:47
8

Use Swift 4's new reduce(into: Result) method:

let keyValuePairs = entries.reduce(into: [String:String]()) { (dict, entry) in
    let key = String(entry.first!)
    let value = String(entry.last!)
    dict[entry.first!] = entry.last!
}

Of course, the splitting of your String could be improved.

heyfrank
  • 5,291
  • 3
  • 32
  • 46
3

Good answers already. Here is an additional way with a collection type extension. You can convert any collection type to either a dictionary, array or set.

extension CollectionType {
    func map2Dict<K, V>(@noescape map: ((Self.Generator.Element) -> (K,    V)?))  -> [K: V] {
        var d = [K: V]()
        for e in self {
            if let kV = map(e) {
                d[kV.0] = kV.1
            }
        }

        return d
    }

    func map2Array<T>(@noescape map: ((Self.Generator.Element) -> (T)?))  -> [T] {
        var a = [T]()
        for e in self {
            if let o = map(e) {
                a.append(o)
            }
        }

        return a
    }

    func map2Set<T>(@noescape map: ((Self.Generator.Element) -> (T)?))  -> Set<T> {
        return Set(map2Array(map))
    }
 }

Here's the example usage.

let entries = ["x=5", "y=7", "z=10"]
let dict:[String: String]  = entries.map2Dict({
let components = $0.componentsSeparatedByString("=")
    return (components.first!, components.last!)
})

print("dict \(dict)")
user3255356
  • 31
  • 1
  • 1
1

I like @paulgriffiths answer but if you have an extreme aversion to runtime errors, you can take it a step further to ensure every string in the initial array actually has both required elements...

The important difference in this code compared to the others is that I check to ensure that there actually is an "=" in the string with elements on both sides. The flatMap effectively filters out any that fail.

extension Dictionary {
    func withUpdate(key: Key, value: Value) -> Dictionary<Key, Value> {
        var result = self
        result[key] = value
        return result
    }
}

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

let keyValues = entries.flatMap { str -> (String, String)? in
    let split = str.characters.split("=").map(String.init)
    return split.count > 1 ? (split[0], split[1]) : nil
}

let keyValuePairs = keyValues.reduce([String: String]()) { $0.withUpdate($1.0, value: $1.1) }
Daniel T.
  • 32,821
  • 6
  • 50
  • 72
0

And for everyone else who enjoys overloading and one-liners.

public func +<K, V>(lhs: [K:V], rhs: [K:V]) -> [K:V] {
    var lhs: [K:V] = lhs
    for (key, value) in rhs {
        lhs[key] = value
    }
    return lhs
}

let array = ["x=5", "y=7", "z=10"]
let dictionary = array.map({ $0.componentsSeparatedByString("=") }).reduce([:]) { $0 + [$1[0]: $1[1]] }

print(dictionary) // ["y": "7", "x": "5", "z": "10"]
Ian Bytchek
  • 8,804
  • 6
  • 46
  • 72
0

If you have two parallel arrays, you can use zip(), Array(), and David Berry's Dictionary extension:

let numbers = [10, 9, 8]
let strings = ["one", "two", "three", "four"]

let dictionary = Dictionary(elements: Array(zip(numbers, strings)))
// == [10: "one", 9: "two", 8: "three"]

Don't forget to add the extension:

extension Dictionary {
    init(elements:[(Key, Value)]) {
        self.init()
        for (key, value) in elements {
            updateValue(value, forKey: key)
        }
    }
}
Senseful
  • 86,719
  • 67
  • 308
  • 465
0

Simple way:

let array = ["key1", "key2", "key3"]
let dict =  array.flatMap({["\($0)" : "value"]})

Output:

[(key: "key1", value: "value"), (key: "key2", value: "value"), (key: "key3", value: "value")]


A bit more Complex way:

let array = ["key1", "key2", "key3"]
let dict =  array.enumerated().flatMap({["\($0.element)" : "value\($0.offset)"]})

Output:

[(key: "key1", value: "value0"), (key: "key2", value: "value1"), (key: "key3", value: "value2")]

Dmitrii Z
  • 145
  • 1
  • 5