72

I have a large array and need to access it by a key (a lookup) so I need to create Dictionary. Is there a built in function in Swift 3.0 to do so, or do I need to write it myself?

First I will need it for a class with key "String" and later on maybe I will be able to write a template version for general purpose (all types of data and key).

Cristik
  • 30,989
  • 25
  • 91
  • 127
Peter71
  • 2,180
  • 4
  • 20
  • 33
  • 1
    In array you have only values in dictionary you have key value. What keys should be ? – Oleg Gordiichuk Sep 30 '16 at 12:05
  • 2
    `So I need to create Dictionary` But what would be the keys? – Eric Aya Sep 30 '16 at 12:06
  • 1
    How did you wind up with an array in the first place? If there's a strict format with elements that can be used as keys it shouldn't be too hard. Could you perhaps show us a piece of the array? – T. Benjamin Larsen Sep 30 '16 at 12:08
  • I think I need a closure, which delivers the key, e.g. an member of the class. To make it simple my first attempt would be a string key. – Peter71 Sep 30 '16 at 12:34
  • Are the keys unique over all elements in the array? If so, it will have the same count as your array, so why do you need a dictionary? – Zonker.in.Geneva Aug 29 '19 at 07:24

13 Answers13

113

Is that it (in Swift 4)?

let dict = Dictionary(uniqueKeysWithValues: array.map{ ($0.key, $0) })

Note: As mentioned in the comment, using uniqueKeysWithValues would give a fatal error (Fatal error: Duplicate values for key: 'your_key':) if you have duplicated keys.

If you fear that may be your case, then you can use init(_:uniquingKeysWith:) e.g.

let pairsWithDuplicateKeys = [("a", 1), ("b", 2), ("a", 3), ("b", 4)] // or `let pairsWithDuplicateKeys = array.map{ ($0.key, $0) }`

let firstValues = Dictionary(pairsWithDuplicateKeys, uniquingKeysWith: { (first, _) in first })

print(firstValues)

//prints ["a": 1, "b": 2]

let lastValues = Dictionary(pairsWithDuplicateKeys, uniquingKeysWith: { (_, last) in last })

print(lastValues)

//prints ["a": 3, "b": 4]
Senseful
  • 86,719
  • 67
  • 308
  • 465
Kamil Nomtek.com
  • 1,590
  • 1
  • 12
  • 18
62

On Swift 4, you can achieve this by using Dictionary's grouping:by: initializer

For ex: You have class named A

class A {

    var name: String

    init(name: String) {
        self.name = name
    }
    // .
    // .
    // .
    // other declations and implementions
}

Next, you have an array of objects of type A

let a1 = A(name: "Joy")
let a2 = A(name: "Ben")
let a3 = A(name: "Boy")
let a4 = A(name: "Toy")
let a5 = A(name: "Tim")

let array = [a1, a2, a3, a4, a5]

Let's say you want to create a Dictionary by grouping all the names by their first letter. You use Swifts Dictionary(grouping:by:) to achieve this

let dictionary = Dictionary(grouping: array, by: { $0.name.first! })
// this will give you a dictionary
// ["J": [a1], "B": [a2, a3], "T": [a4, a5]] 

source

Note however that the resulting Dictionary "dictionary" is of type

[String : [A]]

it is not of type

[String : A]

as you may expect. (Use #uniqueKeysWithValues to achieve the latter.)

Fattie
  • 27,874
  • 70
  • 431
  • 719
Burhanuddin Sunelwala
  • 5,318
  • 3
  • 25
  • 51
  • 8
    This is the best and correct answer. All the other answers misses the point. Of course one could iterate over an array and copy values to a dictionary. The whole point is to do this using a _functioal_ construct, which has the possible added benefit of being lazy. – Steve Kuo Jan 10 '18 at 17:54
  • @SteveKuo it's better than the other answers which are rubbish, but it's wrong :) You simply use `uniqueKeysWithValues` , that's all there is to it. `grouping#by` gives you *an array of the class* as each value. You simply use `uniqueKeysWithValues` to convert arrays to dictionaries. – Fattie Aug 08 '19 at 22:33
57

I think you're looking for something like this:

extension Array {
    public func toDictionary<Key: Hashable>(with selectKey: (Element) -> Key) -> [Key:Element] {
        var dict = [Key:Element]()
        for element in self {
            dict[selectKey(element)] = element
        }
        return dict
    }
}

You can now do:

struct Person {
    var name: String
    var surname: String
    var identifier: String
}

let arr = [Person(name: "John", surname: "Doe", identifier: "JOD"),
           Person(name: "Jane", surname: "Doe", identifier: "JAD")]
let dict = arr.toDictionary { $0.identifier }

print(dict) // Result: ["JAD": Person(name: "Jane", surname: "Doe", identifier: "JAD"), "JOD": Person(name: "John", surname: "Doe", identifier: "JOD")]

If you'd like your code to be more general, you could even add this extension on Sequence instead of Array:

extension Sequence {
    public func toDictionary<Key: Hashable>(with selectKey: (Iterator.Element) -> Key) -> [Key:Iterator.Element] {
        var dict: [Key:Iterator.Element] = [:]
        for element in self {
            dict[selectKey(element)] = element
        }
        return dict
    }
}

Do note, that this causes the Sequence to be iterated over and could have side effects in some cases.

overactor
  • 1,759
  • 2
  • 15
  • 23
  • Yes, you could be right. :-) Looks excellent, but I'm a beginner so I have to work through it for understanding how this is done. Thanks a lot! – Peter71 Sep 30 '16 at 12:40
  • @Peter71 I've added a bit of a demonstration on how to use it, feel free to ask any specific questions if you get stuck on anything. – overactor Sep 30 '16 at 12:46
  • Thanks a lot. I think I got it. Now I will test how fast it is. But what are the side effects??? – Peter71 Sep 30 '16 at 12:55
  • @Peter71 some Sequences can only be iterated over once, for more information, check out [Apple's documentation of Sequence](https://developer.apple.com/reference/swift/sequence). Unless your Array is huge, performance shouldn't be an issue. And it will be hard to beat a good old fashioned for loop (and in this case I think the version with reduce isn't particularly elegant anyway). – overactor Sep 30 '16 at 12:59
  • Array is large (20.000 elements), but any other way is slow, so I will give it a try. And that .reduce is not optimal, I will believe in you. :-) I don't have any experince in this. – Peter71 Sep 30 '16 at 13:05
  • @Peter71 if the possible side effects bother you, you can implement this in an extension of `Collection` instead, that's the most general Protocol for Collections that can be iterated over non destructively and multiple times. – overactor Sep 30 '16 at 13:07
  • Fair enough but I think `rethrows` is missed in this solution. Thus it doesn't conform well to other collection methods. – Vladimir Vlasov Oct 24 '17 at 08:40
  • If variables of structures are optional it creates dictionary with keys and values containing optional("Value"), how to handle this situation – bunty kumar Jan 03 '19 at 06:15
20

As others already said, we need to understand which are the keys.

However I am trying to provide a solution to my interpretation of your question.

struct User {
    let id: String
    let firstName: String
    let lastName: String
}

Here I am assuming that 2 users with the same id cannot exist

let users: [User] = ...

let dict = users.reduce([String:User]()) { (result, user) -> [String:User] in
    var result = result
    result[user.id] = user
    return result
}

Now dict is a dictionary where the key is the user id and the value is the user value.

To access a user via its id you can now simply write

let user = dict["123"]

Update #1: General approach

Given an array of a given type Element, and a closure that determine the key of an Element, the following generic function will generate a Dictionary of type [Key:Element]

func createIndex<Key, Element>(elms:[Element], extractKey:(Element) -> Key) -> [Key:Element] where Key : Hashable {
    return elms.reduce([Key:Element]()) { (dict, elm) -> [Key:Element] in
        var dict = dict
        dict[extractKey(elm)] = elm
        return dict
    }
}

Example

let users: [User] = [
    User(id: "a0", firstName: "a1", lastName: "a2"),
    User(id: "b0", firstName: "b1", lastName: "b2"),
    User(id: "c0", firstName: "c1", lastName: "c2")
 ]

let dict = createIndex(elms: users) { $0.id }
// ["b0": {id "b0", firstName "b1", lastName "b2"}, "c0": {id "c0", firstName "c1", lastName "c2"}, "a0": {id "a0", firstName "a1", lastName "a2"}]

Update #2

As noted by Martin R the reduce will create a new dictionary for each iteration of the related closure. This could lead to huge memory consumption.

Here's another version of the createIndex function where the space requirement is O(n) where n is the length of elms.

func createIndex<Key, Element>(elms:[Element], extractKey:(Element) -> Key) -> [Key:Element] where Key : Hashable {
    var dict = [Key:Element]()
    for elm in elms {
        dict[extractKey(elm)] = elm
    }
    return dict
}
Luca Angeletti
  • 58,465
  • 13
  • 121
  • 148
  • 1
    Yeess! :-) This looks great: using reduce. I will take this. For improvement a template would be excellent using a class type and a closure to deliver the specialist key from an unknown member variable. – Peter71 Sep 30 '16 at 12:37
  • 1
    Note that this creates a new dictionary in each iteration step, so it might not be the most performant solution for large arrays. – See https://airspeedvelocity.net/2015/08/03/arrays-linked-lists-and-performance/ for some thoughts on using `reduce` for mapping operations. – Martin R Sep 30 '16 at 12:41
  • @Peter71: I added in `Update#1` the general function you requested. – Luca Angeletti Sep 30 '16 at 12:57
  • @MartinR: You are right; I added in `Update#2` a more classic approach with Space Complexity O(n). Thank you. – Luca Angeletti Sep 30 '16 at 12:57
  • Thank you too. It looks a little bit similar to overactors solution, so I think we reached the optimal code from both sides. Great! – Peter71 Sep 30 '16 at 13:08
  • @Peter71: Yes I didn't notice there was already a similar solution by _@overactors_ :) – Luca Angeletti Sep 30 '16 at 13:10
12
let pills = ["12", "34", "45", "67"]
let kk = Dictionary(uniqueKeysWithValues: pills.map{ ($0, "number") })

["12": "number", "67": "number", "34": "number", "45": "number"]

swift5 swift4

Vinoth Anandan
  • 1,157
  • 17
  • 15
9

The following converts an array to a dictionary.

let firstArray = [2,3,4,5,5] 

let dict = Dictionary(firstArray.map { ($0, 1) } , uniquingKeysWith: +)
Marcy
  • 4,611
  • 2
  • 34
  • 52
casillas
  • 16,351
  • 19
  • 115
  • 215
6

Swift 5

extension Array {

    func toDictionary() -> [Int: Element] {
        self.enumerated().reduce(into: [Int: Element]()) { $0[$1.offset] = $1.element }
    }
    
}
user_
  • 852
  • 7
  • 23
4

This extension works for all sequences (including arrays) and lets you select both key and value:

extension Sequence {
    public func toDictionary<K: Hashable, V>(_ selector: (Iterator.Element) throws -> (K, V)?) rethrows -> [K: V] {
        var dict = [K: V]()
        for element in self {
            if let (key, value) = try selector(element) {
                dict[key] = value
            }
        }

        return dict
    }
}

Example:

let nameLookup = persons.toDictionary{($0.name, $0)}
John
  • 964
  • 8
  • 21
3

Just do it simply,

let items = URLComponents(string: "https://im.qq.com?q=13&id=23")!.queryItems!

var dic = [String: Any?]()
items.foreach {
    dic[$0.name] = $0.value
}

reduce is not very suitable,

let dic: [String: Any?] = items.reduce([:]) { (result: [String: Any?], item: URLQueryItem) -> [String: Any?] in
   var r = result
   r[item.name] = item.value // will create an copy of result!!!!!!
   return r
}
DawnSong
  • 4,752
  • 2
  • 38
  • 38
2

As i understand from you're question you would like to convert to Array to Dictionary.

In my case i create extension for the Array and keys for the dictionary will be indexes of the Array.

Example:

var intArray = [2, 3, 5, 3, 2, 1]

extension Array where Element: Any {

    var toDictionary: [Int:Element] {
        var dictionary: [Int:Element] = [:]
        for (index, element) in enumerate() {
            dictionary[index] = element
        }
        return dictionary
    }

}

let dic = intArray.toDictionary
Oleg Gordiichuk
  • 15,240
  • 7
  • 60
  • 100
  • Yes, this does the job, but "manualy". Everything is handled by individual code. I'm looking for a general solution using build-in function (as seen above .reduce) – Peter71 Sep 30 '16 at 12:38
2

Compatible with Swift 5 Standard Library (Xcode 10.2+ , iOS 12.2).

Here's an example of usage of an initializer init(uniqueKeysWithValues:)

The input let array: [String] = Locale.isoRegionCodes is an array of ISO31661-2 codes represented by a string.

let countryCodeAndName: [String: String] = Dictionary(uniqueKeysWithValues: Locale.isoRegionCodes.map { ($0, Locale.current.localizedString(forRegionCode: $0) ?? "")} )

Returned dictionary, will list all regions with ISO31661-2 code as a key and a localized region name as a value.

Output:

...
"PL":"Poland"
"DE":"Germany"
"FR":"France"
"ES":"Spain"
...

Example 2:

let dictionary: [String: String] = Dictionary(uniqueKeysWithValues: [ ("key1", "value1"), ("key2", "value2")] )

Output:

["key1": "value1", "key2": "value2"]

Important:

Precondition: The sequence must not have duplicate keys.

Code below will crash an app:

let digitWords = ["one", "two", "three", "four", "five", "five"]
let wordToValue = Dictionary(uniqueKeysWithValues: zip(digitWords, 1...6))

with:

Fatal error: Duplicate values for key: 'five'

Blazej SLEBODA
  • 8,936
  • 7
  • 53
  • 93
1

If you want to follow the pattern set out by map and reduce in swift you could do something nice and functional like this:

extension Array {
    func keyBy<Key: Hashable>(_ keyFor: (Element) -> Key) -> [Key: Element] {
        var ret = [Key: Element]()
        for item in self{
            ret[keyFor(item)] = item
        }
        return ret
    }
}

Usage:

struct Dog {
    let id: Int
}

let dogs = [Dog(id: 1), Dog(id: 2), Dog(id: 3), Dog(id: 4)]
let dogsById = dogs.keyBy({ $0.id }) 
            // [4: Dog(id: 4), 1: Dog(id: 1), 3: Dog(id: 3), 2: Dog(id: 2)]
Alex Pelletier
  • 4,933
  • 6
  • 34
  • 58
1

Swift way:

extension Sequence {
    func toDictionary<Key: Hashable>(with selectKey: (Element) -> Key) -> [Key: Element] {
        reduce(into: [:]) { $0[selectKey($1)] = $1 }
    }
}

// let arr = [Person(id: 1, name: "Alan")]
// arr.toDictionary { $0.id }
// ==
// [1: Person(id: 1, name: "Alan")]
Artem Sydorenko
  • 353
  • 4
  • 8