Using modulo to shift a value and keep it inside a range
Tags: javascript, programming
Published: 2022-02-27 21:27:48

The modulo operator is fairly simple, but often underused. In particular, I find it useful when changing a value and keeping it inside a pre-determined range.

E.g., index in an array, hours in a day, degrees on a compass.

---

First of all, a quick definition: **the modulo operator gives the remainder of a division of one number by another**. In JavaScript the modulo operator is `%`.

The number after the operator is called **modulus**.

Importantly, in JavaScript the return value is *signed*. What does this mean? `14 % 4` is `2`, and `-14 % 4` is `-2`. Some languages keep the result in `[0, modulus - 1]`. This adds _some_ complexity to the formula below.

(if you're reading this and use a different language than JavaScript, check [Wikipedia](https://en.wikipedia.org/wiki/Modulo_operation#In_programming_languages) for the details on your language of choice)

---

## Ultimate formula

The context is this: you have a starting value in a given range, you need to increase or decrease the value by a certain amount, and you need the final value to loop back and stay in that range.

This is the ultimate formula that works for all these cases:
```js
(startingValue - minimumValue + (offset % modulus) + modulus) % modulus + minimalValue
```

* `startingValue` is the value you start with. It's assumed to be in your desired range already.
* `minimumValue` is the lowest value of your desired range. Doing `startingValue - minimumValue` shifts the modulo operation to a range starting at `0`. We add it back at the end to shift the value back to the desired range. NB: `minimumValue` can be negative too!
* `offset` is the amount you want to shift your starting value by. It can be negative, positive, and as small or large as you want.
We use `offset % modulus` to make sure we shift by the smallest amount necessary. Since this can be negative (because the modulo operation is signed), we add `modulus` to that to make sure the result stays in range. (see below)
* `modulus` is the length of your desired range.

Adding the modulus doesn't affect the result, and guarantees that adding `offset % modulus` will keep the number positive in the case where `offset` is negative.

E.g., if you're looking at 24 hours and your offset is `-50`, `offset % modulus` is `-2`. Removing two hours is equivalent to adding `-2 + 24` hours which is `22`. In other words, this ensures that we're always _adding_ to the value. When we subtract, we sometimes can get a negative value, which leads us to the same problem and solution.

Let's put this in practice with concrete use cases!


---


## Cycling through an array

It's very common to need to cycle through an array and loop back on the other end. E.g., you change the selected item of a dropdown and need to go back at the top once you reach the bottom.

I have seen code like this to achieve this:

```js
const options = ['alpha', 'beta', 'gamma', 'delta']
let selectedIndex = 0

function goDown () {
  selectedIndex = selectedIndex + 1
  if (selectedIndex === options.length) {
    selectedIndex = 0
  }
}
function goUp () {
  selectedIndex = selectedIndex - 1
  if (selectedIndex === -1) {
    selectedIndex = options.length - 1
  }
}
```

It works! However, using the formula above, you can combine the two functions:
```js
function go (offset) {
  selectedIndex = (selectedIndex + offset + options.length) % options.length
}
const goDown = () => go(1)
const goUp = () => go(-1)
```

* `minimumValue` here is `0` because an array's index is between `0` and `options.length - 1`, so we don't need it.
* We also know that `direction` is either `1` or `-1` so we don't need `(offset % modulus)`, and `offset` is enough.


---


## Time-related modulo

Most time units loop back: there are 12 months in a year, 24 hours in a day, 60 minutes in an hour, etc.

Because time is finicky, you may want to use dedicated time functions for this. Sometimes you can just put a modulo and be on your way!

One use-case is starting from a month index, adding or subtracting a certain number of months, and wanting to know which month you end up on.

* Your desired range is `[1, 12]`, so `minimumValue` is `1`.
* `modulus` is `12` because there are 12 months

```js
function shiftMonth (startingMonth, offset) {
  return (startingMonth - 1 + (offset % 12) + 12) % 12 + 1
}
```

Once again, the `- 1` sets your initial value back into `[0, 11]`, then you can do your regular operation, and you add `1` again at the end to shift back the range to `[1, 12]`.


---

## Angles and non-integer values

And this works with non-integer values!

For example, say you have to keep track of a direction in radians, but want to keep the value between `-π` and `π`.

* `minimumValue` is `-Math.PI`
* `modulus` is the length of the range: `2 * Math.PI`


You can then have the following function:

```js
function shiftAngles (startingAngle, offset) {
  return (startingAngle + Math.PI + (offset % (2 * Math.PI)) + 2 * Math.PI) % (2 * Math.PI) - Math.PI
}
```

For contrast, this function keeps the angle between `0` and `2π`:
```js
function shiftAnglesPositive (startingAngle, offset) {
  return (startingAngle + (offset % (2 * Math.PI)) + 2 * Math.PI) % (2 * Math.PI)
}
```

In action:
```js
> shiftAngles(Math.PI / 3, -5 * Math.PI) / Math.PI
-0.6666666666666667
> shiftAnglesPositive(Math.PI / 3, -5 * Math.PI) / Math.PI
1.3333333333333333
```

---

I'll be honest, it's a bit of a mouthful of a formula, and it can look too clever for its own good. But it has the benefit of just working without missing edge cases, especially when the offset is unknown. If you don't use it, you end up with a bunch of `if`s and it's quite easy to slip up.


---

Photo by <a href="https://unsplash.com/@joelft?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Joel Fulgencio</a> on <a href="https://unsplash.com/photos/VSrHD079L78">Unsplash</a>