This content originally appeared on DEV Community and was authored by DEV Community
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 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:
(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. DoingstartingValue - minimumValue
shifts the modulo operation to a range starting at0
. 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 useoffset % modulus
to make sure we shift by the smallest amount necessary. Since this can be negative (because the modulo operation is signed), we addmodulus
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. That makes the 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:
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:
function go (offset) {
selectedIndex = (selectedIndex + offset + options.length) % options.length
}
const goDown = () => go(1)
const goUp = () => go(-1)
-
minimumValue
here is0
because an array's index is between0
andoptions.length - 1
, so we don't need it. - We also know that
direction
is either1
or-1
so we don't need(offset % modulus)
, andoffset
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]
, sominimumValue
is1
. -
modulus
is12
because there are 12 months
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:
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π
:
function shiftAnglesPositive (startingAngle, offset) {
return (startingAngle + (offset % (2 * Math.PI)) + 2 * Math.PI) % (2 * Math.PI)
}
In action:
> 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 Joel Fulgencio on Unsplash
This content originally appeared on DEV Community and was authored by DEV Community
DEV Community | Sciencx (2022-02-27T21:27:48+00:00) Using modulo to shift a value and keep it inside a range. Retrieved from https://www.scien.cx/2022/02/27/using-modulo-to-shift-a-value-and-keep-it-inside-a-range/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.