short answer
Implement a circular array via modular arithmetic. Given a distance to move, to calculate the appropriate index:
// put distance in the range {-len+1, -len+2, ..., -1, 0, 1, ..., len-2, len-1}
distance = distance % len
// add an extra len to ensure `distance+len` is non-negative
new_index = (index + distance + len) % len
long answer
Use modular arithmetic much like how you'd read a typical analog clock. The premise is to add two integers, divide by a integer, and keep the remainder. For example, 13 = 3 (mod 10)
because 13
is 1*10 + 3
and 3
is 0*10 + 3
.
But why did we choose to arrange 3
and 13
as we did? To answer that, we consider the Euclidean division algorithm (EDA). It says for two integers a
and b
there exists unique integers q
and r
such that
a = b*q + r
with 0 ≤ r < b
. This is more powerful than you'd think: it allows us to "work modulo n."
That is, we can say a = b (mod n)
iff there are unique integers q1
, r1
, q2
, and r2
such that
a = n * q1 + r1, 0 ≤ r1 < n
b = n * q2 + r2, 0 ≤ r2 < n
and r1
equals r2
. We call r1
and r2
the "remainders."
To go back to the previous example, we now know why 13 = 3 (mod 10)
. The EDA says 13 = 1*10 + 3
and that 1
and 3
are the only q
and r
satisfying the necessary constraints; by similar logic, 3 = 0*10 + 3
. Since the remainders are equal, we say that 13
and 3
are equal when "working mod 10."
Fortunately, JavaScript implements a modulo operator natively. Unfortunately, we need to watch out for a quirk, i.e., the modulo operator keeps the sign of its operands. This gives you some results like -6 % 5 == -1
and -20 % 7 == -6
. While perfectly valid mathematical statements (check why), this doesn't help us when it comes to array indices.
Lemma 1: a + n = a (mod n)
Lemma 2: -1 = n-1 (mod n)
Lemma 3: -a = n-a (mod n)
The way to overcome this is to "trick" JavaScript into using the correct sign. Suppose we have an array with length len
and current index index
; we want to move the index by a distance d
:
// put `d` within the range {-len+1, -len+2, ..., -2, -1, -0}
d = d % len
// add an extra len to ensure `d+len` is non-negative
new_index = (index + d + len) % len
We accomplish this by first putting d
within the range {-len+1, -len+2, ..., -2, -1, -0}
. Next, we add an extra len
to make sure the distance we're moving is within the range {1, 2, ..., len-1, len}
, thereby ensuring the result of the %
operation has a positive sign. We know this works because (-a+b) + a = b (mod a)
. Then we just set the new index to index + d + len (mod len)
.
More detailed implementation:
class Carousel {
// assumes `arr` is non-empty
constructor (arr, index = 0) {
this.arr = arr
this.index = index % arr.length
}
// `distance` is an integer (...-2, -1, 0, 1, 2, ...)
move (distance) {
let len = this.arr.length
distance = distance % len
let new_index = (this.index + distance + len) % len
this.index = new_index
return this.arr[this.index]
}
}
// usage:
let c = new Carousel(['a','b','c','d','e'], 1) // position pointer set at 'b'
c.move(-1) // returns 'a' as (1 + -1 + 5) % 5 == 5 % 5 == 0
c.move(-1) // returns 'e' as (0 + -1 + 5) % 5 == 4 % 5 == 4
c.move(21) // returns 'a' as (4 + 21 + 5) % 5 == 30 % 5 == 0