Swift API Docs for String.index(_:offsetBy:limitedBy:) Is Still Misleading

When you look at the docs for String.index(_:offsetBy:limitedBy:), you get this description:

Returns an index that is the specified distance from the given index, unless that distance is beyond a given limiting index.

The “unless that distance is beyond a given limiting index” part got my attention. I remembered it to be a pain to use in practice. The overall API docs page looks way too innocent for that.

So what’s the example code?

let s = "Swift"
if let i = s.index(s.startIndex, offsetBy: 4, limitedBy: s.endIndex) {
    print(s[i])
}
// Prints "t"

Paste it in a Playground. You’ll see that, yes, this prints t.

But what if I change the offset to 5 to actually see how the limit at the limit works?

Here’s the changed code for you to copy and paste, with the 5 instead of 4:

//  /!\  Warning! Do not use in production!   /!\
let s = "Swift"
if let i = s.index(s.startIndex, offsetBy: 5, limitedBy: s.endIndex) {
    print(s[i])
}

What does it print?

Nothing, you hope?

Because the String is too short?

(cue sad trombone)

Surprise! It's a runtime error!

“Ok, Christian, the API sample was just for demoing a valid use. You inferred something wrong from what you saw”, you might say.

Did I?

The String.index(_:offsetBy:limitedBy:) docs go on like this:

The next example attempts to retrieve an index six positions from s.startIndex but fails, because that distance is beyond the index passed as limit.

let j = s.index(s.startIndex, offsetBy: 6, limitedBy: s.endIndex)
print(j)
// Prints "nil"

… which is true, this code returns nil and does not crash. Oh, so it’s safe to try to get to indices beyond the bounds, nice, right?

Up next is this hand-wavy explanation:

The value passed as n must not offset i beyond the bounds of the collection, unless the index passed as limit prevents offsetting beyond those bounds.

Remember: This is an API documentation, not a marketing text. So is it safe to try to access an index beyond the bounds? It’s hard to say. All of this can be utterly misleading.

What does “prevent offsetting beyong those bounds” mean?

Is it allowing indexes up to or including the limit?

The docs don’t tell. You have to force yourself to consider this to be a vague statement and test its implications. Experiences programmers will undoubtedly do that thanks to years and years of trial by fire – but that doesn’t make the docs any better. If you start developing in Swift, you might not notice the vague statement here, and the omission of a specification of what happens when index + offset == endIndex.

If you copy the sample codes from the docs to play around, and only test the resulting algorithms in your program with an offset somewhere inside the bounds of the string and and somewhere way past the end of the string, you might be tricked into believing your algorithm is working as expected. – It doesn’t, though, because these tests made you overlook what happens when the result is equal to endIndex. That’s false confidence. The code does work for the cases you tested, yes, but it utterly crashes for one case.

So the crux is this: the offset may not be set so that the resulting index is at endIndex. Because that’s not a valid limit.1

From my understanding, the API docs is conveying the wrong information through these examples. (So my initial doubt in how simple it all looked was warranted, and now I’m becoming even more paranoid, great!) The function works real nice when you know what to do, but the String.endIndex doesn’t mean what you might take away from the sample.

The docs on endIndex says:

A string’s “past the end” position—that is, the position one greater than the last valid subscript argument.

You want the limitedBy: parameter to not be past the end, you want it be at at the end. Why is the sample code using an obviously incorrect approach to show how the function works?

That means you need to figure out the index before endIndex, provided the string is long enough to actually cover that. (An empty string has its startRange equal endRange.)

The following is an actually useful, safe way to limit String index access to the String’s real length:

let s = "Swift"

if let indexAtEnd = s.index(s.endIndex, offsetBy: -1, limitedBy: s.startIndex),
    let i = s.index(s.startIndex, offsetBy: 5, limitedBy: indexAtEnd) {
    print(s[i])
}
// nothing happens, as expected!

It’s also ugly as heck.

Remember: that’s why you need to be extra cautious and thorough in your unit tests, kids.

Always be testing

  • right before,
  • at, and
  • right after the limits you want to verify.

Don’t just test somewhere before and somewhere after the limit. Imagine your tests are clamps, and your problem is being clamped so tight it cannot escape. No wiggle room, no margin for error.

—Now guess who was bitten by not testing that way in a particular place.

In other words, an update to The Archive is coming out soon.

I also filed a Feedback issue for this, finally.

  1. I know, you probably understood this point about 5 paragraphs earlier already; but no amount of editing this post seems to make the undertone of disappointment go away. I just can’t get rid of it. It … Is. Too. Strooooong.