2

I want to iterate over an arbitrary Swift collection and get both the element and its index.

Basically an alternative to:

for (idx, el) in collection.enumerate() {
    print("element at \(idx) is \(el)")
}

but that gives me real generic indexes, not just sequential integers starting at 0.

Of course, the solution is going to be a part of a generic function that accepts any kind of collection, otherwise the difference wouldn't be very important.

Is there a better way than a naïve loop like below?

var idx = collection.startIndex, endIdx = collection.endIndex
while idx < endIdx {
    let el = collection[idx]
    print("element at \(idx) is \(el)")
    idx = idx.successor()
}

Writing that seems fairly error-prone. I know I can turn that code into a snippet, but I'd like to find a more concise and more idiomatic solution if possible.

Andrey Tarantsov
  • 8,965
  • 7
  • 54
  • 58
  • from my tests idx.dynamicType is Int but you say generic index am i missing something? – Ali Kıran Jul 09 '16 at 12:31
  • @AliKıran: It depends on the collection. Arrays have integer indices, but strings for example have a special String.CharacterView.Index type. Also the indices of a collection need not start at zero (e.g. array slices). – Martin R Jul 09 '16 at 12:43
  • I hope that the linked-to "duplicate" solves your problem. Otherwise let me know and I'll reopen the question. – Martin R Jul 09 '16 at 13:16
  • @MartinR thank you i get it i just misunderstood the question – Ali Kıran Jul 09 '16 at 13:30
  • @MartinR Thanks, the other question is more narrowly formulated but the answer is exactly correct. I think the answer to this one was better and more to the point, though. I'm not sure what the SO policy is for such cases; if it were left to my discretion, I'd leave both. Thank you! – Andrey Tarantsov Jul 09 '16 at 16:01
  • @AndreyTarantsov: I have re-activated the answer and reopened the question. – Martin R Jul 10 '16 at 21:54
  • @MartinR: Awesome, thank you. – Andrey Tarantsov Jul 11 '16 at 16:31

1 Answers1

1

For any collection the indices property returns a range of the valid indices. To iterate over the indices and the corresponding elements in parallel you can use zip():

for (idx, el) in zip(collection.indices, collection) {
    print(idx, el)
}

Example for an array slice:

let a = ["a", "b", "c", "d", "e", "f"]
let slice = a[2 ..< 5]

for (idx, el) in zip(slice.indices, slice) {
    print("element at \(idx) is \(el)")
}

Output:

element at 2 is c
element at 3 is d
element at 4 is e

You can define a custom extension method for that purpose (taken from How to enumerate a slice using the original indices?):

// Swift 2:
extension CollectionType {
    func indexEnumerate() -> AnySequence<(index: Index, element: Generator.Element)> {
        return AnySequence(zip(indices, self))
    }
}

// Swift 3:
extension Collection {
    func indexEnumerate() -> AnySequence<(Indices.Iterator.Element, Iterator.Element)> {
        return AnySequence(zip(indices, self))
    }
}

Example for a character view:

let chars = "az".characters
for (idx, el) in chars.indexEnumerate() {
    print("element at \(idx) is \(el)")
}

Output:

element at 0 is a
element at 1 is 
element at 3 is 
element at 7 is z
Community
  • 1
  • 1
Martin R
  • 529,903
  • 94
  • 1,240
  • 1,382
  • Why are the `chars` example's indices 0, 1, 3, 7 instead of 0, 1, 2, 3? – dantiston Jul 09 '16 at 16:23
  • 1
    @dantiston: Because the string indices internally refer to UTF-16 code points. But you should not care about these numbers. If you use the proper methods (like advancedBy, distanceTo) to operate on indices, everything works as expected. – Martin R Jul 09 '16 at 16:26