7

Is it possible to override Swift's default array subscripting so that it handles negative indices like Python?

For example, a[-1] should return the last element of a, and a[-2] the element before it.

This should be possible to achieve by extending the Array type, alas, the code below won't work since it would loop infinitely:

extension Array {
   subscript (index:Int) -> [Element] {
        return (index < 0) ? self[self.count+index] : self[index]
   }
}

How bad would the idea of overriding something that fundamental be?

Andy Jazz
  • 49,178
  • 17
  • 136
  • 220
Hristo
  • 6,382
  • 4
  • 24
  • 38
  • 3
    Personally I would find that quite a bad idea. It would easily trip up an unsuspecting developer reusing/maintaining your code down the road. – onnoweb Sep 09 '15 at 21:58
  • 1
    the simplest formula is actually: `unsignedIndex = signedIndex % arrayCount`. i.e. `return self[signedIndex % count]`. But this way the index will never overflow, so be careful. – SirEnder Mar 17 '17 at 21:47

3 Answers3

18

Actually, there's a relatively good-practice, Swifty way to do this: labelled arguments.

extension CollectionType where Index : BidirectionalIndexType {
  subscript(back i: Index.Distance) -> Generator.Element {
    return self[endIndex.advancedBy(-i)]
  }
}

let ar = [1, 2, 3, 4]
ar[back: 1] // 4
ar[back: 2] // 3

You can obviously change the semantics pretty easily. This implementation, for instance, requires the index be larger than 0. Changing it so 0 returns the last element is as simple as: self[endIndex.predecessor().advancedBy(-i)], or, if you want to assume the index is negative going in: self[endIndex.advancedBy(-i)].

The advantage of the labelled argument is that it's clear, and no-one would use it by accident.

oisdk
  • 9,763
  • 4
  • 18
  • 36
7

Swift 4 version:

extension Collection where Index: Comparable {
    subscript(back i: Int) -> Iterator.Element {
        let backBy = i + 1
        return self[self.index(self.endIndex, offsetBy: -backBy)]
    }
}
johndpope
  • 5,035
  • 2
  • 41
  • 43
joel.d
  • 1,611
  • 16
  • 21
  • This had a few issues so I've corrected them. It was not actually relying on `Index` being `Comparable` so I removed the constraint. It now extends `BidirectionalCollection` which guarantees that we can iterate backwards. And it was not necessary to increment the offset by 1; this caused us to go too far (-1 should be the last element, not second-to-last). – rgov Jan 21 '19 at 17:52
1

Swift 5.7

@Hristo, Not only can we use a negative indexing in Swift subscript's functionality but also implement error handling to control whether you're "out of range" or not. So, use the following code for that.

let array: [Int] = [199, 288, 377, 455, 533, 622, 711]

enum SubscriptError: Error {
    case greaterThanZero
    case lessThanLastIndex
}

extension Collection {

    public subscript(negative i: Int) -> () throws -> Element {
        
        let backward = i - 1
        
        if i > 0 {
            return { throw SubscriptError.greaterThanZero }
        }
        if i < -1 * ((endIndex as! Int) - 1) {
            print(endIndex)
            return { throw SubscriptError.lessThanLastIndex }
        }           
        return { self[index(endIndex, offsetBy: backward)] }
    }
}

do {
    try array[negative: -6]()                      // 199
} catch {
   print("It's \(error)")
}

The results are as follows:

let array: [Int] = [199, 288, 377, 455, 533, 622, 711]

try array[negative: 2]()       //  "It's greaterThanZero"
try array[negative: 1]()       //  "It's greaterThanZero"
try array[negative: 0]()       //  711
try array[negative: -1]()      //  622
try array[negative: -2]()      //  533
try array[negative: -3]()      //  455
try array[negative: -4]()      //  377
try array[negative: -5]()      //  288
try array[negative: -6]()      //  199
try array[negative: -7]()      //  "It's lessThanLastIndex"
try array[negative: -8]()      //  "It's lessThanLastIndex"
Andy Jazz
  • 49,178
  • 17
  • 136
  • 220