4

I wanted to find some objects near another object in an array. I thought I could write an extension method like this, but I get this error:

// Error: Cannot invoke 'advanceBy' with an argument list of type '(Int)'

The Int type is obviously wrong, but the indexOf method takes a Self.Distance argument and I'm not sure how to use that as a parameter type.

extension CollectionType where Generator.Element : Equatable {
    func objectNear(object: Self.Generator.Element, indexModifier: Int) -> Self.Generator.Element? {
        if let index = self.indexOf(object) {
            let newIndex = index.advancedBy(indexModifier) // this doesn't work
            //let newIndex = index.advancedBy(1) // but this this works
            if self.indices.contains(newIndex) {
                return self[newIndex]
            }
        }
        return nil
    }
}

(If there is a more Swifty approach I'd be happy to hear it, but I'd like to understand the above in any case.)

zekel
  • 9,227
  • 10
  • 65
  • 96

1 Answers1

4

CollectionType has the method

public func indexOf(element: Self.Generator.Element) -> Self.Index?

and conforms to

public protocol Indexable {
    typealias Index : ForwardIndexType
    // ...
}

Finally, ForwardIndexType has the method

public func advancedBy(n: Self.Distance) -> Self

Therefore the correct type is Index.Distance:

func objectNear(object: Self.Generator.Element, indexModifier: Index.Distance) -> Self.Generator.Element? { ... }

But note that advancing an index beyond endIndex can crash, e.g. for character collections:

let c = "abc".characters
print(c.objectNear("b", indexModifier: 1)) // Optional("c")
print(c.objectNear("b", indexModifier: 2)) // nil
print(c.objectNear("b", indexModifier: 3)) // fatal error: can not increment endIndex

A safe variant is:

func objectNear(object: Generator.Element, indexModifier: Index.Distance) -> Generator.Element? {
    if let index = indexOf(object) {
        if indexModifier > 0 && index.distanceTo(endIndex) <= indexModifier {
            return nil
        }
        if indexModifier < 0 && startIndex.distanceTo(index) < -indexModifier {
            return nil
        }
        return self[index.advancedBy(indexModifier)]
    }
    return nil
}

Alternatively, if you need the method only for collections which are indexed by an Int (such as Array) then you can define

extension CollectionType where Generator.Element : Equatable, Index == Int {

    func objectNear(object: Generator.Element, indexModifier: Int) -> Generator.Element? {
        if let index = self.indexOf(object) {
            let newIndex = index + indexModifier
            if indices.contains(newIndex) {
                return self[newIndex]
            }
        }
        return nil
    }
}
Martin R
  • 529,903
  • 94
  • 1,240
  • 1,382
  • 1
    I was just typing that =] Martin is correct. I recently encountered a similar issue with stride. – Chris Slowik Jan 16 '16 at 17:08
  • Thanks for the detailed answer. Since I am a big fan of [negative indexes](http://stackoverflow.com/questions/31007643/in-swift-whats-the-cleanest-way-to-get-the-last-two-items-in-an-array), how can I get out-of-bounds indexes to return nil [instead of the first element](https://gist.github.com/anonymous/3ebf1fee6bfbe28db196)? (Limiting by `startIndex` makes `-1` return the first element.) – zekel Jan 16 '16 at 21:29
  • 1
    @zekel: I have updated the answer with a version that should cover all combinations. There might be a more elegant one, but this is what I have so far. – Martin R Jan 16 '16 at 21:42