4

Here is the code:

1- It's okay when offsetBy is 4 or less

    let someString = "hello"

    if let someIndex = someString.index(someString.startIndex, 
                        offsetBy: 4, limitedBy: someString.endIndex){
       someString[someIndex]
    }
    // Prints "o"

2- It's okay when offsetBy is 6 or greater

    if let someIndex = someString.index(someString.startIndex, 
                        offsetBy: 6, limitedBy: someString.endIndex){
       someString[someIndex]
    }
    // Prints "nil"

3- But it gives error when offsetBy is 5

    if let someIndex = someString.index(someString.startIndex, 
                        offsetBy: 5, limitedBy: someString.endIndex){
       someString[someIndex]
    }
    // error

and the error is:

error: Playground execution aborted: error: Execution was interrupted, reason: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0). The process has been left at the point where it was interrupted, use "thread return -x" to return to the state before expression evaluation.

Hamish
  • 78,605
  • 19
  • 187
  • 280
Mo Hemati
  • 251
  • 1
  • 11
  • Compare [How does String.Index work in Swift 3](http://stackoverflow.com/q/39676939/2976878) – Hamish Mar 22 '17 at 16:58
  • 1
    You are using the wrong value for the limit. `endIndex` is one past the last valid index. Instead, use `limitedBy: str.index(before: str.endIndex)`. – vacawama Mar 22 '17 at 17:22
  • @Hamish thanks. I read the link. but limitedBy limits 6 and higher. why not 5? – Mo Hemati Mar 22 '17 at 17:23
  • @vacawama awesome. now it works and I know the reason. – Mo Hemati Mar 22 '17 at 17:29
  • 2
    Alternatively: `if let someIndex = someString.index(someString.startIndex, offsetBy: 5, limitedBy: someString.endIndex), someIndex != someString.endIndex { ... }` – Note that `str.index(before: str.endIndex` would fail for an empty string. – Martin R Mar 22 '17 at 17:32
  • 1
    @Arashk Because `endIndex` is still a useful index – most commonly used when forming the range `someIndex.. – Hamish Mar 22 '17 at 17:33
  • @Hamish/MartinR, right you are about the crash for an empty string. It's a shame they didn't make the API less than the limit. I notice that many people mistakenly use `endIndex` for the limit. – vacawama Mar 22 '17 at 17:38
  • @vacawama: But then `if let from = s.index(s.startIndex, offsetBy: 5, limitedBy: s.endIndex), let to = s.index(from, offsetBy: 2, limitedBy: s.endIndex)` would not work to get 2 characters at position 5. – Martin R Mar 22 '17 at 17:49
  • @MartinR/vacawama/Hamish you're right. it's gonna cash for an empty string. what a good experience you have. By the way, what's the solution for that? – Mo Hemati Mar 22 '17 at 17:50
  • Somebody should write an answer now ... or is http://stackoverflow.com/questions/39676939/how-does-string-index-work-in-swift-3 good enough as a duplicate? – Martin R Mar 22 '17 at 17:51
  • I volunteer @Hamish to write the answer since he was the first to engage the OP. :-) – vacawama Mar 22 '17 at 17:52
  • Okay, I guess I'll get typing... :) – Hamish Mar 22 '17 at 18:00
  • @MartinR, if the limit were less than, then you'd use `if let from = s.index(s.startIndex, offsetBy: 5, limitedBy: s.endIndex), let to = s.index(from, offsetBy: 1, limitedBy: s.endIndex)` and `s[from...to]` to get 2 characters at position 5. – vacawama Mar 22 '17 at 18:07
  • @vacawama: ... which I do not find intuitive, but I think we can leave it at that :) – Martin R Mar 22 '17 at 18:18
  • As it is, I find using the limit the way it is implemented now to be similar to when you used to not be able to form ranges like `0...Int.max` because Swift internally used upper limit + 1, and there is not such value for Int.max. Anyway, it is what it is as they say. And with Swift, it will probably be different in version N+1 anyway. – vacawama Mar 22 '17 at 18:25
  • hi @Arashk - are you there? You should "Tick" an answer please. – Fattie Mar 26 '17 at 13:06
  • @JoeBlow Sorry. I didn't know that. Now, I've learned it. – Mo Hemati Mar 26 '17 at 18:34
  • hi @Arashk have a great day. Yeah I believe it gives YOU more points, and, allows you to ask more questions without being moderated so much, etc. – Fattie Mar 26 '17 at 18:46

1 Answers1

5

The problem is that the limit given to index(_:offsetBy:limitedBy:) is an inclusive bound for the result. Therefore with

let someString = "hello"

the index returned by

someString.index(someString.startIndex, offsetBy: 5, limitedBy: someString.endIndex)

will be someString.endIndex, which is a past the end index, and therefore not a valid index to subscript the string with.

A simple solution therefore would be to just add a condition to your if statement to check that the index you get back isn't the endIndex:

let someString = "hello"
let offset = 5

if let someIndex = someString.index(someString.startIndex,
                                    offsetBy: offset,
                                    limitedBy: someString.endIndex
                                    ), someIndex != someString.endIndex {
    print(someString[someIndex])
}

Or a nicer option would be to use the CharacterView's indices property in order to get a collection of valid indices to subscript with (excluding endIndex), and use dropFirst(_:) and first in order to get the index at the given offset:

if let index = someString.characters.indices.dropFirst(offset).first {
    print(someString[index])
}

This takes advantage of the fact that dropFirst(_:) takes an upper bound of elements to drop, returning an empty subsequence if it's larger than the collection's count, as well as the fact that first returns nil for an empty collection.

Hamish
  • 78,605
  • 19
  • 187
  • 280
  • @Downvoter I respect your right to anonymity, but would welcome any constructive feedback. I take pride in the quality of my answers, and am always looking to improve them :) – Hamish Mar 24 '17 at 11:13