Iterating in a closed loop, forwards and backwards
- javascript
Iterating over arrays is a routine task for the majority of developers who have experience with programming. However, there are some cases when it can become a little bit more interesting—but also a little bit more tricky.
In this post, I’m going to focus on “closed loop iteration,” or how to iterate over an array so that after reaching its last element, the following one will be the first element again (and so on). To make things more interesting, it has to work both forwards and backwards!
TL;DR
function clampIndex(desiredIndex, array = []) { let length = array.length; if (length === 0) return 0; return ((desiredIndex % length) + length) % length; }
The imperative way
We can write that logic imperatively, using simple conditionals. If the updated index is out of bounds, it falls back to the index of the first (if incrementing) or last (if decrementing) element:
let array = ["apple", "banana", "cherry"]; let index = 0; const incrementIndex = () => { if (index + 1 >= array.length) { index = 0; } else { index += 1; } }; const decrementIndex = () => { if (index - 1 < 0) { index = array.length - 1; } else { index -= 1; } };
This code is not terrible (and despite what some edgy content makers may say, using if/else
is okay), but there are still two aspects that I don’t particularly enjoy. It is rather verbose, and it is prone to off-by-one errors.
The declarative way
The modulo trick
I’ve got this trick from Ryan Florence’s talk about (at the time brand-new) React hooks:
// Somewhere in a React component... setCurrentIndex((currentIndex + 1) % slides.length);
What is going on here? We’re using a modulo operator (which is actually a remainder operator) to keep the index in bounds. It doesn’t matter if it’s much bigger than the actual array; it is always going to be clamped.
Example: Our array has 3 items: ["apple", "banana", "cherry"]
, but we incremented our index too many times and got to ‘4’. Since the remainder of 4 divided by 3 (the array’s length) is 1, we got the index of “banana,” which is correct (try to count if you don’t trust me).
The caveat (decrementing)
However, the example above has one significant flaw. If we try to decrease and get below 0, it won’t work correctly:
let array = ["apple", "orange", "banana"]; let testIndex = -4 % array.length; // -1!!! 🤔
As you can see, remainders of negative dividends are also negative (and there is no Math.abs()
shortcut to correct the result; if applied, the result will be a non-negative, but incorrect number).
The solution
Can we fix that? Of course we can, if we use this simple math formula:
- find the remainder of the desired index / array length,
- to the first result, add array length (no negative numbers!),
- determine the remainder of the intermediate result / array length
The resulting code is a little cryptic, but it will work for any positive and negative integer as desiredIndex
:
function clampIndex(desiredIndex, array = []) { let length = array.length; // Or throw an error, it's up to you; n % 0 === NaN!: if (length === 0) return 0; // Here comes our math formula: return ((desiredIndex % length) + length) % length; }
Alternative: Array.prototype.at()
Is there an alternative? Yes, if we are careful about browser support, we can use the brand new EcmaScript 2023’s Array.prototype.at
:
function loopOverIfIndexOutOfBounds(desiredIndex, array = []) { let length = array.length; // Or throw an error, it's up to you; n % 0 === NaN!: if (length === 0) return undefined; return array.at(desiredIndex % length); }
This code is easier to understand than the previous, remainder math-based version. However, there is a fundamental difference between those two functions! While the first one returns the index, the second returns the element at (sic!) that index. That’s why it returns undefined
instead of 0
if the array has no items.
Off-topic tip for React devs
There is a footgun hidden in the snippet I’ve cited before:
// Somewhere in a React component... setCurrentIndex((currentIndex + 1) % slides.length);
Due to the asynchronous state handling in React, this way of setting derived state is quite prone to the dreaded stale props/state error (been there, done that). I highly recommend being cautious and doing it like that:
setCurrentIndex((prevIndex) => (prevIndex + 1) % slides.length);
The anonymous function inside the state setter is always guaranteed to have access to the most recent state.
👍 Enjoy!
If you find anything in this post that should be improved (either factually or in language), feel free to edit it on Github .