130

Math was never my strong suit in school :(

int input_start = 0;    // The lowest number of the range input.
int input_end = 254;    // The largest number of the range input.
int output_start = 500; // The lowest number of the range output.
int output_end = 5500;  // The largest number of the range output.

int input = 127; // Input value.
int output = 0;

How can I convert the input value to the corresponding output value of that range?

For example, an input value of "0" would equal an output value of "500", an input value of "254" would equal an output value of "5500". I can't figure out how to calculate an output value if an input value is say 50 or 101.

I'm sure it's simple, I can't think right now :)

Edit: I just need whole numbers, no fractions or anything.

PsylentKnight
  • 149
  • 14
Joe
  • 2,223
  • 4
  • 18
  • 13

6 Answers6

270

Let's forget the math and try to solve this intuitively.

First, if we want to map input numbers in the range [0, x] to output range [0, y], we just need to scale by an appropriate amount. 0 goes to 0, x goes to y, and a number t will go to (y/x)*t.

So, let's reduce your problem to the above simpler problem.

An input range of [input_start, input_end] has input_end - input_start + 1 numbers. So it's equivalent to a range of [0, r], where r = input_end - input_start.

Similarly, the output range is equivalent to [0, R], where R = output_end - output_start.

An input of input is equivalent to x = input - input_start. This, from the first paragraph will translate to y = (R/r)*x. Then, we can translate the y value back to the original output range by adding output_start: output = output_start + y.

This gives us:

output = output_start + ((output_end - output_start) / (input_end - input_start)) * (input - input_start)

Or, another way:

/* Note, "slope" below is a constant for given numbers, so if you are calculating
   a lot of output values, it makes sense to calculate it once.  It also makes
   understanding the code easier */
slope = (output_end - output_start) / (input_end - input_start)
output = output_start + slope * (input - input_start)

Now, this being C, and division in C truncates, you should try to get a more accurate answer by calculating things in floating-point:

double slope = 1.0 * (output_end - output_start) / (input_end - input_start)
output = output_start + slope * (input - input_start)

If wanted to be even more correct, you would do a rounding instead of truncation in the final step. You can do this by writing a simple round function:

#include <math.h>
double round(double d)
{
    return floor(d + 0.5);
}

Then:

output = output_start + round(slope * (input - input_start))
Alok Singhal
  • 93,253
  • 21
  • 125
  • 158
  • 19
    I've written this function at least a few times over the decades, and yet this explanation is better than I could give myself. +1 for pulling out the divide - was useful for 6.5 million calculations per run. – delrocco Oct 24 '18 at 19:30
  • 3
    This is one of the best answers I've seen on this site. Thank you! – foobarbaz Feb 16 '19 at 19:53
  • What a beautiful way of approaching the problem! Excellently answered! – AdeleGoldberg Oct 07 '19 at 12:43
  • I've found the order of operations to be incorrect here. It should be `output_start + ((output_end - output_start) * (input - input_start)) / (input_end - input_start) `. See [this equation](https://rosettacode.org/wiki/Map_range) – stacksonstacks Aug 27 '20 at 23:56
  • @stacksonstacks sorry I don't see the difference. You mean that I should multiply first and then divide? `(a * b) / c` is the same as `(a / c) * b` -- mathematically at least. For floating-point accuracy, and making sure you don't have overflows, you will need to be very careful about the order of operations and might even have different order of operations depending on the input values. – Alok Singhal Aug 28 '20 at 17:34
  • good point, I'm using fixed width and mapping from a larger range to a smaller range and this seemed to make a major difference (which it shouldn't mathematically) – stacksonstacks Aug 30 '20 at 21:41
  • Thank you so much for such a clear, intuitive and therefore beautiful way of explaining this! – Manuel Martinez Oct 01 '20 at 02:38
  • 7
    This assumes a linear mapping. If you want sinusoidal, parabolic, exponential, logarithmic, inverse parabolic, elliptical, or circular I've developed a [Desmos graph with those formulas](https://www.desmos.com/calculator/ewnq4hyrbz). – rgm Jun 08 '21 at 03:11
  • Hi, thank you so much for this answer! I wish to ask, how do I map the input scale to start from 0 if the original range is [input_start, input_end] , with input_end –  Mar 27 '22 at 06:48
  • @newtocodingyikes so let's say you want to map [10, 5] to [0, 1]. The formula above will still work. In this case, `slope = (1 - 0) / (5 - 10) = -0.2`, and then you can convert a particular `input` value to `[0, 1]` range. Since the input / output ranges are "flipped", increasing input will result in a decreasing output. – Alok Singhal Mar 27 '22 at 16:51
44

Arduino has this built-in as map.

Example:

/* Map an analog value to 8 bits (0 to 255) */
void setup() {}

void loop()
{
  int val = analogRead(0);
  val = map(val, 0, 1023, 0, 255);
  analogWrite(9, val);
}

It also has the implementation on that page:

long map(long x, long in_min, long in_max, long out_min, long out_max)
{
  return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}
Dustin
  • 89,080
  • 21
  • 111
  • 133
  • 12
    FYI, their `map` function is broken. It was converted from a floating point function by simply changing all the arguments to int, and it has a broken distribution. – Mud Jan 22 '14 at 20:20
  • @Mud Still, it's useful if we change everything back to floats. – Kotauskas Apr 27 '20 at 11:33
28

the formula is

f(x) = (x - input_start) / (input_end - input_start) * (output_end - output_start) + output_start

I'll hook up this post here: https://betterexplained.com/articles/rethinking-arithmetic-a-visual-guide/ as it helped me a lot when trying to come up with this intuitively. Once you understand what the post is saying, it's trivial to come up with these formulas on your own. Note that I used to struggle with such questions as well. (I have no affiliations - just found it very useful)

say you have range [input_start..input_end], let's start by normalising it such that 0 is input_start, and 1 is input_end. this is simple technique to make the problem easier.

how do we do that? we'll, we'd have to shift everything left by input_start amount, such that if input x happens to be input_start, it should give zero.

so, let's say f(x) is the function that does the conversion.

f(x) = x - input_start

let's try it:

f(input_start) = input_start - input_start = 0

works for input_start.

at this point, it does not work for input_end yet, as we have not scaled it.

let's just scale it down by the length of the range, then we'll have the biggest value (input_end) mapped to one.

f(x) = (x - input_start) / (input_end - input_start)

ok, let's give it a try with input_end.

f(input_end) = (input_end - input_start) / (input_end - input_start) = 1

awesome, seems to work.

okay, next step, we'll actually scale it to output range. It is as trivial as just multiplying with the actual length of the output range, as such:

f(x) = (x - input_start) / (input_end - input_start) * (output_end - output_start)

now, actually, we're almost done, we just have to shift it to right so that 0 starts from output_start.

f(x) = (x - input_start) / (input_end - input_start) * (output_end - output_start) + output_start

let's give it a quick try.

f(input_start) = (input_start - input_start) / (input_end - input_start) * (output_end - output_start) + output_start

you see that the first part of equation is pretty much multiplied by zero, thus cancelling everything out, giving you

f(input_start) = output_start

let's try input_end as well.

f(input_end) = (input_end - input_start) / (input_end - input_start) * (output_end - output_start) + output_start

which in turn will end up as:

f(input_end) = output_end - output_start + output_start = output_end

as you can see, it now seems to be mapped correctly.

Erti-Chris Eelmaa
  • 25,338
  • 6
  • 61
  • 78
17

The crucial point here is to do the integer division (which includes rounding) at the right place. None of the answers so far got the parentheses right. Here is the right way:

int input_range = input_end - input_start;
int output_range = output_end - output_start;

output = (input - input_start)*output_range / input_range + output_start;
Sven Marnach
  • 574,206
  • 118
  • 941
  • 841
9

This is GUARANTEED to map ANY range to ANY range

I wrote this method, which follows precisely the algebraic formula for mapping a number from one range to another. The calculations are done with doubles to maintain precision, then at the end, it will return a Double with however many decimal places you specify in the method arguments.

It is not necessary to name the low and high ends of a range as low or high because it makes no difference if one end is lower or higher than the other end of either range, the method will still map the number correctly.

For example, if you stated any range as [-100 to 300] or [300 to -100] it would not make any difference. The remap will still come out accurately.

Here is how you would use the method in your code:

mapOneRangeToAnother(myNumber, fromRangeA, fromRangeB, toRangeA, toRangeB, decimalPrecision)

Here is an example of how to use the method:

Source range: -400 to 800
Destination range: 10000 to 3500
Number to re-map: 250

double sourceA = -400;
double sourceB = 800;
double destA = 10000;
double destB = 3500;
double myNum = 250;
        
double newNum = mapOneRangeToAnother(myNum,sourceA,sourceB,destA,destB,2);
    
Result: 6479.17

And if you need an integer back, just pass in 0 decimal places for precision and cast the result to int like this:

int myResult = (int) mapOneRangeToAnother(myNumber, 500, 200, -350, -125, 0);

Or you could declare the method to return an int and remove the decimalPrecision argument, then change the last two lines to:

int calcScale = (int) Math.pow(10, 0);
return (int) Math.round(finalNumber * calcScale) / calcScale;

In the OPs question, they would use the function like this:

int myResult = (int) mapOneRangeToAnother(input, input_start, input_end, output_start, output_end, 0);

and here is the method:

Edit: As was pointed out by @CoryGross, if the same number is passed into the method for both end points of the from range, there will be a divide by zero. And since this method is supposed to calculate a new number based on two RANGEs of numbers, if either range has the same value for its end points, the calculated result will be meaningless, so we need to return null in that case.

This example was written in Java

public static Double mapOneRangeToAnother(double sourceNumber, double fromA, double fromB, double toA, double toB, int decimalPrecision ) {
    double deltaA = fromB - fromA;
    double deltaB = toB - toA;
    if(deltaA == 0 || deltaB == 0) {  //One set of end-points is not a range, therefore, cannot calculate a meaningful number.
        return null;
    }
    double scale  = deltaB / deltaA;
    double negA   = -1 * fromA;
    double offset = (negA * scale) + toA;
    double finalNumber = (sourceNumber * scale) + offset;
    int calcScale = (int) Math.pow(10, decimalPrecision);
    return (double) Math.round(finalNumber * calcScale) / calcScale;
}

In my use case, I needed to fade out the opacity of a JavaFX Control, but since opacity is a number from 0 to 1, I simply used the method to remap the range 1 to 100 (based on a for loop that incremented an int from 0 to 100) to the range 0 to 1 and it worked perfectly.

Though I know now that I could have created my loop by changing the increment from 1 to something like .01 like this:

for(double x=0; x<=1; x+=.01 {
    //Code to change controls opacity
}

I just pointed this out for anyone that might be doing something similar to what I was doing. The method works perfectly as described.

:-)

Michael Sims
  • 2,360
  • 1
  • 16
  • 29
  • Great function. To this I added quick bounding logic for sourceNumbers that may fall outside the desired 'from' range and works great. – Jordan Grant Feb 07 '22 at 09:53
  • @JordanGrant - Makes sense to add that check especially if the input number will be unknown during runtime (ie a number input by the user). Glad you found this useful. :) – Michael Sims Feb 08 '22 at 20:42
  • Careful, will throw if you ever end up passing in values such that `fromA == fromB` due to division by zero. – Cory Gross Nov 27 '22 at 16:15
  • @CoryGross Can you show me an example of calling the method as you described? – Michael Sims Dec 14 '22 at 16:52
  • @MichaelSims For example, I believe that calling `mapOneRangeToAnother(1, 1, 1, 2, 2, 0)` would throw an error due to division by zero. – Cory Gross Dec 28 '22 at 04:12
  • @CoryGross I tested your example, and the result comes back as 0.0 without any errors being thrown. – Michael Sims May 19 '23 at 09:25
  • @MichaelSims Ah, I was assuming you're code was written in C as that is what the question was tagged with. Looks like it's Java based on the Math class being used? Apparently in Java, when there is a division by zero, it only throws if it is an integer zero, since you are dividing by a floating point zero Java for some reason does not throw but instead will return NaN (Not a Number). So in your code `scale` gets set to `NaN`. So the behavior will not be as expected in the division by zero case, but will not throw since it's dividing by float zero. Sorry for the confusion. – Cory Gross Jun 02 '23 at 01:20
  • @MichaelSims You can see what I mean here: https://www.mycompiler.io/view/B0feBtvTYPV – Cory Gross Jun 02 '23 at 01:31
  • @CoryGross - I see what you mean ... and my first thought is that if the same number is used for the end points in the first range, then a range is not even being specified, so in that case, I think perhaps the method should return null because the use of a non-range has no context in the scope of the method, which is intended to map a number from one range over to a different range. I will update the method accordingly. – Michael Sims Sep 03 '23 at 03:18
5
output = ((input - input_start)/(input_end - input_start)) * (output_end - output_start) + output_start

What that does is find out proportionally "how far into" the input range the input is. It then applies that proportion to the size of the output range to find out in absolute terms how far into the output range the output should be. It then adds the start of the output range to get the actual output number.

QuantumMechanic
  • 13,795
  • 4
  • 45
  • 66
  • This will always give `500`, except for the input `254`, which will yield `5500`. – Sven Marnach Apr 20 '11 at 14:39
  • 1
    It should be `output = ((out_end - out_start)/(input_end - input_start)) * input + output_start;` – jackw11111 May 20 '21 at 01:50
  • 1
    @jackw11111 Nice one. Here improved version `input * ((output_end - output_start) / (input_end - input_start)) + output_start` – rofrol Jul 05 '21 at 11:28
  • @rofrol My mistake, it actually should be `output = (output_end - output_start)/(input_end - input_start) *( input - input_start) + output_start;` by using the formula `y - y1 = m(x-x1)`. – jackw11111 Jul 14 '21 at 07:51
  • I ended using the same algorithm from here https://stackoverflow.com/questions/5731863/mapping-a-numeric-range-onto-another/5732390#5732390 with fix for precision issues from ES6 version https://rosettacode.org/wiki/Map_range#JavaScript – rofrol Jul 14 '21 at 10:12