68

Is there any built-in way to create an ordered map in Swift 2? Arrays [T] are sorted by the order that objects are appended to it, but dictionaries [K : V] aren't ordered.

For example

var myArray: [String] = []
myArray.append("val1")
myArray.append("val2")
myArray.append("val3")

//will always print "val1, val2, val3"
print(myArray)


var myDictionary: [String : String] = [:]
myDictionary["key1"] = "val1"
myDictionary["key2"] = "val2"
myDictionary["key3"] = "val3"

//Will print "[key1: val1, key3: val3, key2: val2]"
//instead of "[key1: val1, key2: val2, key3: val3]"
print(myDictionary)

Are there any built-in ways to create an ordered key : value map that is ordered in the same way that an array is, or will I have to create my own class?

I would like to avoid creating my own class if at all possible, because whatever is included by Swift would most likely be more efficient.

Jojodmo
  • 23,357
  • 13
  • 65
  • 107

13 Answers13

37

You can order them by having keys with type Int.

var myDictionary: [Int: [String: String]]?

or

var myDictionary: [Int: (String, String)]?

I recommend the first one since it is a more common format (JSON for example).

chrisamanse
  • 4,289
  • 1
  • 25
  • 32
  • 2
    I'm going to end up making a custom class, but it's pretty much going to be doing the same thing as your first example (it'll just be simpler to implement). Thank you! – Jojodmo Jun 26 '15 at 02:46
  • So, it's basically a sparse array? :-) – Nicolas Miari Dec 01 '17 at 03:44
  • 3
    FWIW, `Dictionary` ordering behavior is officially _undefined_. I doubt they would change it such that `Int` keys wouldn't be sorted they way you'd expect, nevertheless, although it might work now, there's no guarantee it will work in the future or on other systems. – GetSwifty Jun 27 '18 at 17:19
  • 2
    This is not meant to be accessed iteratively where it relies on how the dictionary internally orders its values. Instead, this solution relies on using the Int values to access its values in a known sort order, i.e. `let dictionary: [Int: String] = [0: "a", 3: "d", 1: "b", 2: "c"]`, `(0 ..< dictionary.count).map { print(dictionary[$0] }` will still print `"a" "b" "c" "d"`. However `Dictionary` internally orders its values, accessing the values by iterating `Int` values from 0 to items count will always result in the same order – chrisamanse Jun 27 '18 at 20:52
  • 1
    FYI if you're going with the tuples approach, there's no need to add the verbose `Int:`, as arrays are already indexed by integers. You could, instead, do what Mundi did, and add labels for each parameter :) – Merricat Nov 01 '18 at 00:49
  • Using a dictionary with int keys means the set of indexes might be discontinuous. You could create entries for keys -12, 0, 3, 7, 24, and 72 for example. You'd have to write code that would iterate the whole range of keys trying to fetch values at each possible key value, which would be slow. Why not just use an Array? – Duncan C Mar 11 '20 at 01:13
34

Just use an array of tuples instead. Sort by whatever you like. All "built-in".

var array = [(name: String, value: String)]()
// add elements
array.sort() { $0.name < $1.name }
// or
array.sort() { $0.0 < $1.0 }
Mundi
  • 79,884
  • 17
  • 117
  • 140
  • 72
    This is not even kind of close to an ordered map. A map provides both key uniqueness and O(1) lookup time. This provides neither. – par Apr 29 '16 at 20:21
  • @par Actually I believe it does provide O(1) lookup time, since both arrays and tuples have O(1) lookup time on their own. – Lahav May 21 '16 at 05:33
  • 9
    @Lahav Lookup time is the time to find an arbitrary element. I believe you're thinking of access time. In the case of an array the time to find an element in the worst case is O(n) assuming an unsorted array with a linear search. With a sorted array you can get better performance but still nothing close to a dictionary/map which finds any element in O(1). – par May 21 '16 at 06:22
  • @par yes I meant access time. I just assumed that by "lookup time" you meant the time to lookup the value associated with a specific key. – Lahav May 21 '16 at 06:38
  • 11
    @Lahav That is what I mean. In this implementation to find/search/lookup a value by key you have to iterate through each element of the array and compare the string at that index to the key you're looking for until you find it (so you have O(n) array performance). You can also have duplicate keys which is *definitely* not what you want. A proper dictionary/map implementation uses a hash function on the key so both search and access are O(1) *and* you're guaranteed to have only one value per key. In terms of implementing an ordered map, this answer is just completely wrong. – par May 21 '16 at 06:43
  • Accepted solution has the same limitations. You can always solve these issues (immaterial ones for OP) elsewhere. Cannot give away the cake and eat it too. – Mundi Apr 18 '17 at 19:28
  • @par Big Oh does not tell you performance, it tells you scalability O(n) may be faster than O(1), it depends on how many elements are in your list. – Justin Meiners Jan 30 '20 at 22:08
  • @JustinMeiners Big O tells you how a function tends to perform as its input grows to infinity. There is no consideration for "how many elements are in your list" -- the number of elements is infinite. With an input size of infinity, an O(1) lookup algorithm means the first element you find is the one you're searching for, always. An O(n) algorithm means in the worst case you have to look at all the elements before you find the element you want. So Big O is _specifically_ about performance. A map, by definition, provides O(1) lookup. This answer gives O(n) lookup, so it is not performant. – par Jan 31 '20 at 02:51
  • @par the kind of performance you care about for practical considerations is runtime. No program has infinitely many elements. You choose algorithms based on a reasonable assumption of how they will be used. In the case of data structures, how many items is extremely relevant. How can you say O(1) is more performant if the O(1) takes 10x longer than the O(n) on a hundred items? – Justin Meiners Jan 31 '20 at 05:03
  • @JustinMeiners Again, Big O isn't used to analyze small, fixed-size data sets. Using your example, let's say an O(1) lookup function runs in 1 second, and an O(n) function runs 10x faster on an array with 100 elements. That means the O(n) takes 1ms per element. You can see then that when the input array grows to 1,000 elements the performance of the two algorithms are equal. When we grow to 1,000,000 elements, the O(1) function still takes 1s, but the O(n) function (worst-case) now takes 1,000 seconds! [1/2] – par Jan 31 '20 at 10:36
  • Now it's really easy to see that the O(1) function is much more performant _in general_. If we know ahead of time that we'll never have more than 1,000 elements than yes, use the O(n) function. But if our data structure (such as the ordered map in this question) can have any number of elements, then you use the most efficient algorithm, and Big O tells us which one that is. [2/2] – par Jan 31 '20 at 10:38
  • @par precisely. So we are in agreement. – Justin Meiners Feb 01 '20 at 01:10
  • @JustinMeiners Glad I was able to help. – par Feb 02 '20 at 08:25
27

"If you need an ordered collection of key-value pairs and don’t need the fast key lookup that Dictionary provides, see the DictionaryLiteral type for an alternative." - https://developer.apple.com/reference/swift/dictionary

coffeecoder
  • 514
  • 7
  • 7
  • 4
    This is the answer for those who want to create `JSON` and they are pulling their hairs because Swift `dictionary` doesn't preserve the order. Needs more upvotes.... – Suhaib Apr 01 '17 at 03:31
  • 2
    @Suhaib Well, it's a literal though, so its use is limited to a small predefined set of values. It's not like you can add key-values to it after creation. – Adrian Apr 06 '17 at 00:10
  • 2
    @Suhaib, how can you use this in `JSONSerialization.data(withJSONObject: )`? – Efren Sep 07 '17 at 02:04
  • It gives this error: "'NSInvalidArgumentException', reason: '*** +[NSJSONSerialization dataWithJSONObject:options:error:]: Invalid top-level type in JSON write'" – Efren Sep 07 '17 at 02:07
  • For what it's worth, `KeyValuePairs` (formerly known as `DictionaryLiteral`) doesn't seem to be any different from an array of tuples, i.e. you don't get any of the benefits of a dictionary (no uniqueness of keys, no fast lookup). I believe most likely if one doesn't need those benefits, they'd have started from an array of tuples rather than from a dictionary. – Gobe Jan 07 '20 at 16:43
  • The API of this class is severely lacking. You can't access a `dictionary[key]` with that kind of normal subscript syntax, for example. – Andrew Koster Mar 10 '20 at 00:27
19

You can use KeyValuePairs, from documentation:

Use a KeyValuePairs instance when you need an ordered collection of key-value pairs and don’t require the fast key lookup that the Dictionary type provides.

let pairs: KeyValuePairs = ["john": 1,"ben": 2,"bob": 3,"hans": 4]
print(pairs.first!)

//prints (key: "john", value: 1)

barola_mes
  • 1,532
  • 2
  • 13
  • 16
9

if your keys confirm to Comparable, you can create a sorted dictionary from your unsorted dictionary as follows

let sortedDictionary = unsortedDictionary.sorted() { $0.key > $1.key }
Deep Parekh
  • 445
  • 5
  • 2
  • 7
    This does not return a sorted dictionary. It returns a sorted array of tuples of (key, value). – Hans Terje Bakke May 05 '18 at 20:20
  • @HansTerjeBakke If it does the job well, what's the problem with that? – Ky - Feb 28 '19 at 20:00
  • @BenLeggiero the problem is that it doesn't create a _dictionary_ which may be the main intention in the question here. A dictionary has uniqueness of keys and fast lookup, the array of tuples does not. The code provided might fit for people who don't need a dictionary, but the answer says it is a dictionary, which is wrong. – Gobe Jan 07 '20 at 16:46
  • @Gobe Uniqueness of keys might be an issue worth addressing. However, the reason a dictionary/hashmap/associative-array has a lookup time of `O(1)` is because it doesn't worry about the order of its keys; it sorts them in the way that's most efficient to look up by their hash, rather than by whatever way we humans think is reasonable. If you want human-parseable sorting, you must necessarily sacrifice machine lookup time. My expectation is that an implementer would use binary search for this, `O(logₑ(n))` – Ky - Jan 08 '20 at 15:54
  • 1
    @BenLeggiero correct, such a structure can't have all of a dictionary's benefits _and_ be sorted at the same time, but it's still wrong to say the code above returns a "dictionary". A dictionary has a certain behavior and the array of tuples has another. Here's C#'s [`SortedDictionary`](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.sorteddictionary-2?view=netframework-4.8), which is also not a Dictionary but has unique keys and performance-wise it goes as far as it can. You guessed correctly, `O(logn)` retrieval – Gobe Jan 08 '20 at 17:50
  • This answer, as written, is wrong. As others have stated, it returns an array of tuples, not a dictionary. You need to rewrite it to say "If your keys confirm to Comparable, you can convert your dictionary to a sorted array of tuples using the following code:" and then rename the result to something **other than** `sortedDictionary` – Duncan C Mar 11 '20 at 10:12
7

As Matt says, dictionaries (and sets) are unordered collections in Swift (and in Objective-C). This is by design.

If you want you can create an array of your dictionary's keys and sort that into any order you want, and then use it to fetch items from your dictionary.

NSDictionary has a method allKeys that gives you all the keys of your dictionary in an array. I seem to remember something similar for Swift Dictionary objects, but I'm not sure. I'm still learning the nuances of Swift.

EDIT:

For Swift Dictionaries it's someDictionary.keys

Community
  • 1
  • 1
Duncan C
  • 128,072
  • 22
  • 173
  • 272
3

You can use the official OrderedDictionary from the original Swift Repo

The ordered collections currently contain:

They said it is going to be merged in the Swift itself soon (in WWDC21)

Mojtaba Hosseini
  • 95,414
  • 31
  • 268
  • 278
2

Swift does not include any built-in ordered dictionary capability, and as far as I know, Swift 2 doesn't either

Then you shall create your own. You can check out these tutorials for help:

J. Chomel
  • 8,193
  • 15
  • 41
  • 69
enes
  • 329
  • 1
  • 11
2

I know i am l8 to the party but did you look into NSMutableOrderedSet ?

https://developer.apple.com/reference/foundation/nsorderedset

You can use ordered sets as an alternative to arrays when the order of elements is important and performance in testing whether an object is contained in the set is a consideration—testing for membership of an array is slower than testing for membership of a set.

AkademiksQc
  • 677
  • 1
  • 6
  • 20
1
    var orderedDictionary = [(key:String, value:String)]()
Jay Zi
  • 39
  • 1
1

As others have said, there's no built in support for this type of structure. It's possible they will add an implementation to the standard library at some point, but given it's relatively rare for it to be the best solution in most applications, so I wouldn't hold your breath.

One alternative is the OrderedDictionary project. Since it adheres to BidirectionalCollection you get most of the same APIs you're probably used to using with other Collection Types, and it appears to be (currently) reasonably well maintained.

GetSwifty
  • 7,568
  • 1
  • 29
  • 46
-3

Here's what I did, pretty straightforward:

let array = [
    ["foo": "bar"],
    ["foo": "bar"],
    ["foo": "bar"],
    ["foo": "bar"],
    ["foo": "bar"],
    ["foo": "bar"]
]

// usage
for item in array {
    let key = item.keys.first!
    let value = item.values.first!

    print(key, value)
}

Keys aren't unique as this isn't a Dictionary but an Array but you can use the array keys.

Skoua
  • 3,373
  • 3
  • 38
  • 51
  • 1
    Why not a series of tuples? Like `typealias Pair = (key: String, value: String); let array: [Pair] = [("foo", "bar"), ("foo", "bar"), ("foo", "bar")]`. Then you don't have to force-unwrap anything or instantiate a complicated dictionary. – Ky - Feb 26 '19 at 21:40
  • @BenLeggiero Yep why not! – Skoua Feb 27 '19 at 14:30
-7

use Dictionary.enumerated()

example:

let dict = [
    "foo": 1,
    "bar": 2,
    "baz": 3,
    "hoge": 4,
    "qux": 5
]


for (offset: offset, element: (key: key, value: value)) in dict.enumerated() {
    print("\(offset): '\(key)':\(value)")
}
// Prints "0: 'bar':2"
// Prints "1: 'hoge':4"
// Prints "2: 'qux':5"
// Prints "3: 'baz':3"
// Prints "4: 'foo':1"
Ky -
  • 30,724
  • 51
  • 192
  • 308
  • 1
    That's not a `Dictionary` method, that's a `Sequence` method: [`enumerated()`](https://developer.apple.com/documentation/swift/sequence/1641222-enumerated) –  Mar 25 '18 at 22:40
  • Unfortunately, @sweetswift, your solution doesn't work for the original question since it does not guarantee order. I've edited it to include an example using a `Dictionary`, and its output demonstrates that – Ky - Feb 26 '19 at 21:52