Off-By-One Errors In Swift Programming

Written by Reinder de Vries on May 15 2020 in App Development, Swift

Off-By-One Errors In Swift Programming

Off by one errors, like “Array index out of range”, happen in programming when we’ve got the boundaries of things wrong. In this article, you’ll learn what an off-by-one error is and how you can solve it with Swift programming.

Here’s what we’ll get into:

  • What an off-by-one error is and how to solve it
  • Swift’s dreaded Array index out of range error
  • Why the fencepost problem is crucial to understand
  • Approaches to find and solve the OBOE in Swift
  • Including mandatory puns about being off by one…

Ready? Let’s go.

  1. What’s An Off-By-One Error?
  2. Array Index Out of Range
  3. The Fencepost Error
  4. Solving Off-By-One Errors
  5. Further Reading

What’s An Off-By-One Error?

A popular quote from Jeff Atwood, author of Coding Horror, is this:

There are two hard things in computer science: cache invalidation, naming things, and off-by-one errors.

Off-by-one errors (OBOEs) happen when a loop iterates one time too many, or one too few. These errors can also happen when an array is zero-indexed, and you assume that it starts at 1. An off-by-one error is considered a boundary problem, that is, a problem that occurs around the boundaries of a set of data.

Let’s take a look at an example. Imagine we’re working on a bit of code, and we want to create a loop that iterates some code exactly 5 times.

for i in 0...5 {
print(i)
}

Run the above code. What happens? For some reason – accidentally or otherwise – we’ve erroneously started counting our loop at 0. Because we’ve used the closed range operator ... with 0 and 5, our loop iterates from 0 to 5, including 5. As such, the number of times the loop iterates is… 6!

It’s not likely that you make such a mistake deliberately, so the code example is a bit contrived. Consider however what happens to the loop when you change the following bits of code:

  • 0 into 1 — how many iterations do you get?
  • ... into ..< — which numbers are included in the loop, now?

The way Swift implements for loops, with for in, actually prevents us from easily making off-by-one mistakes. The default 1...5 syntax follows natural language: a loop from 1 to 5 — in my mind, that’s 1-2-3-4-5.

But, check out how the above code works for programming languages with C-style loops:

for(i = 0; i < 5; i++) {
    print(i)
}

The above code runs exactly 5 times, printing out the numbers 0 to 5 (including). However, if you change < into <=, the loop runs 6 times. When you accidentally start at 1, the code only runs 4 times. It’s easy to make a typo, right? That’s the nature of off-by-one errors!

Let’s check out another example, with a while loop in Swift:

var i = 1

while(i <= 5) {
    i += 1
    print(i)
}
// Output: 2 3 4 5 6

The above code iterates 5 times, and it has two off-by-one errors. Based on the i = 1 and i <= 5 boundaries, you’d think that this code runs from 1 to 5. It doesn’t, however. Why?

  • The condition in the while statement is executed at the start of each iteration. Because the condition is i <= 5, the loop will stop executing after i equals 6, i.e., when i <= 5 is false.
  • The placement of i += 1 is disadvantageous. Because i = 1, the first iteration will immediately execute i += 1, so i equals 2, prior to the first print() statement.

As a result, the printed values of i are 2-3-4-5-6. The loop runs 5 times, but the loop’s values are completely different from what you’d expect!

Off-by-one errors are all around us. Personally, I’m always confused about the “first floor” in an elevator. In The Netherlands, the first floor is called “1”, and the ground floor is called “BG”. Contrast this with Norway, where I’ve lived for a while. Their first floor is the actual first floor, i.e. the ground floor, and the second floor — what we call first floor — is called “2”. If I’m not mistaken, the same confusion can happen with American English and British English first floor/ground floor conventions. Lots of opportunity to end up on a floor too high or too low! “I went to a first floor convention, and ended up on the wrong floor!”

Learn how to build iOS apps

Get started with iOS 14 and Swift 5

Sign up for my iOS development course, and learn how to build great iOS 14 apps with Swift 5 and Xcode 12.

Array Index Out of Range

In the previous section, we’ve looked at how many times a loop iterates. In every example, the loop did repeat 5 times. It doesn’t matter much if it iterates 1-2-3-4-5 or 0-1-2-3-4, as long as you’ve got the number of iterations you expect (5).

But what happens when you need to use the ordinal index number of the iteration? That’s where off-by-one errors happen with arrays, and you get that dreaded Array index out of range error.

Let’s take a look at an example.

let names = ["Arthur", "Trillian", "Ford", "Zaphod", "Marvin"]

for i in 0...names.count {
    print(names[i])
}

In the above code, we want to iterate over an array of strings. The goal is to print out every name in the array by using the array index i. Smart as we are, we’ve figured out that we can get the number of items in an array with the .count property. However, there’s an off-by-one error in the above code!

When you run it, you’ll get this error message back from Swift:

Thread 1: Fatal error: Array index out of range

Here’s why:

  • The for in syntax with the m...n range syntax uses a lower bound m and an upper bound n. Loop from 1 to 5, with 1...5.
  • Arrays in Swift are zero-indexed, meaning that their first index number is 0. The index of "Arthur" in the above array is 0.
  • So far so good! We’re starting our loop with for i in 0..., so we’re starting at zero.
  • The value of names.count is equal to the number of items in the array, which is 5.
  • In your head, replace the names.count in 0...names.count with 5. What happens? We’re actually looping from 0 to 5 — so the loop repeats 6 times!

The loop repeats one too many times. As a result, you’ll get an Index out of range error. The index number 5 does not exist in the array, because the array only has indices 0-1-2-3-4. The index is “out of range”.

How can we solve this? In one of two ways:

  1. Use the half-open range operator ..<. This will make the loop iterate from m to n, but not including n. So, 0-1-2-3-4 instead of 0-1-2-3-4-5.
  2. Subtract 1 from the size of the array, with for i in 0...names.count-1. This will make the range’s upper bound equal to the last index of the array.

This off-by-one error for arrays is often called the fencepost error, which we’ll discuss next.

The Fencepost Error

Here’s an interesting question for you:

If you build a straight fence 30 meters long with posts spaced 3 meters apart, how many posts do you need?

You might be inclined to answer “10”, because 30 divided by 3 is 10. After all, the posts are spaced 3 meters apart. What you’re forgetting, however, is that last post — the right answer is “11”!

This is called the fencepost error. It’s based on the idea that sometimes we count the posts in a fence, and at other times, we count the sections of fence themselves.

Another example involves making a hotel booking. You’re checking in on Monday, and checkout happens on Friday. How many days and nights are you staying?

5 days, 4 nights. Monday to Friday equals 5 days (sections) and 4 nights (fenceposts). You’re always charged per night in a hotel. Consider that code you wrote accidentally counts the days from Monday to Friday, and you end up charging one “night” too many!

The fencepost problem illustrates that off-by-one errors always concern the boundaries of things, and where those borders are. If we go back to that array of names, we assert that, when an array starts at index 0, its last index is length - 1. When an array has indices from m to n inclusive, the size of the array is n + 1.

Solving Off-By-One Errors

How do you solve off-by-one errors? It’s important to establish a starting point or convention, first.

  • Array indices in Swift start at 0
  • “A convention we’ll use for this algorithm, is starting at 1.”
  • The first day of the year is January 1st
  • Monday corresponds to 0, so that Sunday is 6
  • Are we counting fence sections or fenceposts?

Then, you can build awareness around syntax that concerns boundaries:

  • Ranges in Swift use the m...n (inclusive) or m..<n (exclusive) syntax
  • Conditional loops can use < and >, or <= and >=, and they differ

Based on those principles, you’re equipped to tackle any off-by-one error that comes your way. Does your code accidentally skip the last item in an array? Are you trying to access a post-last array item that doesn’t exist? Is the outcome of your algorithm always off by one? Check the above syntax and principles, and chances are that you’ll find that off-by-one error.

Learn how to build iOS apps

Get started with iOS 14 and Swift 5

Sign up for my iOS development course, and learn how to build great iOS 14 apps with Swift 5 and Xcode 12.

Further Reading

In this article, we’ve discussed a common cause of bugs in programming: the off-by-one error, or OBOE. As it turns out, it’s easy to accidentally incorporate this bug into your code. Fortunately, it’s quite easy to solve, too. Awesome!

And of course, we can also just joke around, admit that we make mistakes, and remind ourselves that off-by-one errors just happen:

There are two kinds of people in the world. 1. People who understand off-by-one errors.

Want to learn more? Check out these resources:

Reinder de Vries

Hi, I'm Reinder.
I help developers play with code.

Get the Weekly

Get iOS/Swift tutorials and insights in your inbox, every Monday.
  • This field is for validation purposes and should be left unchanged.

Most Popular

Browse Topics

Swift Sandbox

Code Swift right in your browser!
Go to the Swift Sandbox

Reinder de Vries

Reinder de Vries

Reinder de Vries is a professional iOS developer. He teaches app developers how to build their own apps at LearnAppMaking.com. Since 2009 he has developed a few dozen apps for iOS, worked for global brands and lead development at several startups. When he’s not coding, he enjoys strong espresso and traveling.

×

Build great iOS apps
Learn how in my free 7-day course

  • This field is for validation purposes and should be left unchanged.

No spam, ever. Unsubscribe anytime. Privacy Policy