5

I have an array of items representing a virtual carousel.

const carousel = ['a','b','c','d','e'];
let currentIndex = 0;

function move (amount) {
   const l = items.length;  // in case carousel size changes

   // need to update currentIndex

   return carousel[currentIndex];

}

What is a clean or clever way to handle moving left when currentIndex == 0 and moving right when currentIndex == length-1?

I have thought about this before and have never come with anything very clever or concise.

royhowie
  • 10,605
  • 14
  • 45
  • 66
Alexander Mills
  • 1
  • 80
  • 344
  • 642
  • 1
    _"what is the cleanest and cleverest way to handle the change when moving 'left' when `currentIndex` is 0 and moving 'right' when `currentIndex` is length - 1?"_ What do you mean by "cleanest and cleverest"? Where is `change` used? What is expected result? – guest271314 Dec 22 '16 at 02:30
  • Add the amount and mod the value by the list length. Adding 4 from 5 in a list of 8 items should return 1 — since 9 % 8 = 1. – Mr. Polywhirl Dec 22 '16 at 02:57
  • might want to look into using a library? that would be the 'cleverest' way. as for 'clean' that would change as your requirements change - async calls to update contents? items can be images with captions, webpage snippets, text with background? Your question is too broad to answer succinctly – softwarenewbie7331 Dec 22 '16 at 03:26
  • @softwarenewbie7331 OP is asking, in essence, about how to deal with a [circular array](https://en.wikipedia.org/wiki/Circular_buffer); hardly unanswerable. – royhowie Dec 22 '16 at 03:31
  • @royhowie I hope you realize that your answer makes certain assumptions as to what the OP requires as well. As for 'clean' and 'cleverest', an 'if' statement suffices to handle circular array. – softwarenewbie7331 Dec 22 '16 at 03:50
  • @softwarenewbie7331if/else will not work well when change is more extreme than 1 or -1. @royhowie's answer is generic and the best way to do this. – Alexander Mills Dec 22 '16 at 03:54
  • @softwarenewbie7331 try implementing it yourself - where an array holds 5 items and you change the carousel position by more than 1 places, for example 3 places, or 12 places – Alexander Mills Dec 22 '16 at 03:55
  • an 'if' can contain all the logic roy has included in his solution. anyway it seems that OP has received the answer he was looking for, there's no need for me to be pedantic here. – softwarenewbie7331 Dec 23 '16 at 01:57
  • Show me the money! Add an answer with an if/else solution thats as generic as the accepted answer and I will at the very least upvote it, theres not one right answer here – Alexander Mills Dec 23 '16 at 02:07

3 Answers3

14

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
royhowie
  • 10,605
  • 14
  • 45
  • 66
  • Why mod out the distance? A simple `this.index = (whateverDistance + this.index) % this.arr.length` is all you need. – Sukima Dec 22 '16 at 02:47
  • @Sukima explained inside `Carousel.move` more thoroughly, but JS keeps the sign of %, so `(0 + -13) % 5 == -3`, but `-3` is not a valid array index; adding `len` makes sure the result is within the range `{0, 1, 2, ..., len-2, len-1}`. – royhowie Dec 22 '16 at 02:49
  • your answer is good because it's generic, I changed the question to be more generic as well – Alexander Mills Dec 22 '16 at 02:51
  • hmmm you may need to make sure distance is less than array length? seems like that needs to happen, otherwise should work – Alexander Mills Dec 22 '16 at 03:01
  • @AlexanderMills that's why I added the line `distance = distance % len` – royhowie Dec 22 '16 at 03:03
  • @royhowie Huh, hmmm, i think i need to go do some more research. I know i've seen it before. Good call. – Sukima Dec 22 '16 at 03:09
  • Ok i see my mistake. @royhowie thank you for the extra clarification. I see it now. – Sukima Dec 22 '16 at 03:17
  • @Sukima I've added an explanation of modular arithmetic and the Euclidean Division Algorithm. – royhowie Dec 22 '16 at 03:18
  • @Sukima perhaps you're thinking of a [circular buffer/array](https://en.wikipedia.org/wiki/Circular_buffer)? – royhowie Dec 22 '16 at 03:30
  • Yes, though I am still trying to learn best implementation designs for a circular buffer in vanilla JS. – Sukima Dec 22 '16 at 03:37
  • Your original answer said use "modular addition" at the very top which I think is most useful – Alexander Mills Dec 22 '16 at 19:36
  • @AlexanderMills noted; I added a reference to that back (modular arithmetic is a better name than modular addition). – royhowie Dec 22 '16 at 19:55
2
currentIndex = currentIndex + change;
if (currentIndex >= l)  currentIndex = 0;
if (currentIndex < 0) currentIndex = l -  1;

This will modify the index, check if it's broken possible values and adjust to either 'side' of the carousel.

Arrisar
  • 3,762
  • 1
  • 10
  • 15
2

I had implemented an Array.prototype.rotate() a while back. It might come very handy for this job. Here is the code;

Array.prototype.rotate = function(n) {
var len = this.length;
return !(n % len) ? this.slice()
                  : this.map((e,i,a) => a[(i + (len + n % len)) % len]);
};
var a = [1,2,3,4,5,6,7,8,9],
    b = a.rotate(10);
console.log(JSON.stringify(b));
    b = a.rotate(-10);
console.log(JSON.stringify(b));
Community
  • 1
  • 1
Redu
  • 19,106
  • 4
  • 44
  • 59