How come map() function is proper and correct? What is the idea behind it?

gawroon7 is correct that there is an issue in the map() function and I have pointed out the problem is that is that map() is incorrectly calculating the number of points in each of the ranges when doing the percentage calculation to calculate the offset into the target range.

map() defines each range as a from,to pair and the range is inclusive of from and to.
See here: map() - Arduino Reference
So the number of points in a range pair is (from-to +1)
map() incorrectly calculates the number of points as (from - to)

Because of this, the miscalculation of the mapped value that map() returns is affected more as a range size gets smaller since the "off by one" miscalculation of the number of points in the range is a larger percentage of the actual number of points in the range.
i.e. smaller ranges will see a larger calculation error and large ranges will see a much smaller error in mapped values.

gawroon7 offered this correction to map()

y = (x - in_min) * (out_max - out_min +1) / (in_max - in_min) + out_min

However, both range sizes need to be incremented by one not just the output range as gawroon7 showed in the first post since both ranges are inclusive of the "to" point.

So it really needs to be:

y = (x - in_min) * (out_max - out_min +1) / (in_max - in_min+1) + out_min

--- bill


More discussion with respect to AOL's comments below.

AWOL:
Argue and assume all you like, but map (1020, 0, 255, 0, 1023) for example, does give the same value as 1020 / 4.

(map (1020, 0, 256, 0, 1024) does)

I don't make assumptions. I based my comments on math and the math in map() is incorrect.

map(3,1,10,1,5) should be 2 not 1

Why are you bringing up an entirely different set of range mappings than what was being discussed?
The discussion was mapping a larger range to a smaller range.
And in the case of post #6, post #9 and my posts, specifically mapping a value from a range 0 to 1023 to a range of 0 to 255
Post #6 was saying:

val/4 != map(val, 0,1023, 0, 255)

to which in post #9 you said:

. . . you fail the interview.
The two are not equivalent.

And my comments and examples related to post #6 and post #9 but also shows what the OP (gawroon7) was seeing.
But the main point I was making was that if the map() calculation were correct, then val/4 would be equal to map(val,0,1023,0,255) for all values 0 to 1023.

The ranges in your recent post #19 is not what INTP (post#3), your response to #6 (post#9), and my response to your #6 (post #18) were talking about. Go back and read the posts closer (particularly #6 and my #18) and in particular, pay close attention to the documentation on map() on the page:

The map() documentation is very clear and very explicitly states that the ranges are
fromLow to fromHigh and toLow to toHigh (inclusive)
It does not state that the 2nd paramter is must be one higher than the actual highest value in the range.
In fact it states that if you send it fromHigh you will get toHigh.
So if you have a range of values that can be any value from 0 to 1023 (inclusive) you should be using 0,1023 not 0,1024 when using map()

So the proper range as shown in my examples and in the example in post #6 were using the
ranges: map(x, 0, 1023, 0, 255)

And that is why I showed examples and provided and example sketch that used the proper parameters.
I showed a simpler example that dropped the ranges down for demonstration to be able to show the full mapping table.
And it shows how the map function is incorrect by showing a simple example that shows that map(3,1,10,1,5) returns 1 instead of 2

But if you fix the portion of the calculation that calculates the ranges correctly to calculate it correctly, then the final mapped value will also be correct.

i.e. when the range size is correct
map(3,1,10,1,5) will properly return 2 instead of incorrectly return 1 as it does now.

--- bill

And to answer the question in your title, the idea behind map is Linear interpolation. When you map 1-10 to 1-5, you are basically drawing a point at (1,1) a second point at (10,5), and connecting them with a line. The value you supply to map is the X coordinate, the the value map returns is the Y coordinate for that point on the line. It's high school (middle school if you were in the advanced classes) algebra.

bperrybap:
pay close attention to the documentation on map() on the page:
map() - Arduino Reference

I agree @bperrybap. You do need to pay close attention to the documentation...

Fractional remainders are truncated, and are not rounded or averaged.

That is precisely what map does.

OK, for all those people SLAMMING me....

This is not a rounding or truncation issue.

The map() documentation and the examples on the map() documentation page are VERY clear that the
map() range values are inclusive. i.e. a range of 0,1023 includes 0 and 1023 so it is a total of 1024 points.
So if mapping something simple such as 0 to 1023 analog read values to 0 to 255 digitalWrite() values

you do not use 0,1024 and 0,256 when mapping the 0-1023 values to 0-255 values as as using 1024 and 256 are specifying High values that are outside the to and from range set.

You would use:

wval = map(rval, 0,1023, 0,255);

which exactly matches what is in the example on the map() documentation page.

However because of a miscalculation in the map() function not all values will be mapped correctly.
Also the miscalculation of the mapped values is aggravated when the ranges are small, and especially when one range set is much larger than the other small range set.
Again, this is not a rounding or truncation issue.

And this isn't a case of being able to "fix" it by modifying the map() documentation to indicate a number of points in the range rather than an ending value for each range or something goofy like having to specify a High value that is one higher than the actual High value.


To keep things really simple lets look at few really simple examples.

From all you guys that are saying map() is working correctly, and as intended,
I"m curious as to your reasoning/explanation on a few really simple specific mapping examples:

  1. output from map(val, 0,99, 0,1) across range of values 0 to 99

What would you expect the results to be?

The current map() function will return:

  • 0 for all values 0 to 98
  • 1 for the value 99
    Not what I'd expect and probably not what most people would expect either.

The modified map() function I provided will return:

  • 0 for values 0 to 49
  • 1 for values 50 to 99
    This seems to be more in line with expectations and probably what was intended.
  1. output from map(val, 0,99, 1,3) across range of values 0 to 99
    What would you expect the results to be?

The current map() function will return:

  • 1 for all values 0 to 49
  • 2 for all values 50 to 98
  • 3 for the value 99

Again I don't think this was the intended or desired behavior.

The modified map() function I provided will return:

  • 1 for all values 0 to 33
  • 2 for all values 34 to 66
  • 3 for all values 67 to 99

As far as I'm concerned, the current map() is broken and simply does not behave the way most people would expect.

The OP was tripped on this issue because he was mapping a large number set to a small number set which is where the issue shows up.

I'd love to hear some kind of explanation from you guys as to why you believe that the current way map() is working is correct and is working as intended.

Anybody???

--- bill

Bill

Again I don't think this was the intended or desired behavior.

My turn to disagree...

It is intended and described as such in the documentation - they say they truncate by doing integer math. So when you map 0 - 99 to 0 - 1, for the 99 first values (0-98) you will get a floating point approximation of 0,xxxx which is rounded to 0 and then for 99 you get 99/99 which is a pure 1 so gives you 1.

The way it works is that the low start value maps always to the target low value, the high start value maps to the high target value and everything in between is TRUNCATED.

So to me it's intended, documented as such and not a surprise once you understand it. If you don't like it and want closest integer approximation (rounding up or down to the closest int) then you indeed want something different than what is offered.

I agree that this is confusing at first, probably not what most of us have in mind with mapping - but I would not call that wrong. I would just say if it's not what you need, then code the one you need. Doing float or double math comes at a high execution price that sometimes you might not want.

OK, for all those people SLAMMING me....

This attitude is what gets you that "slamming". And nobody's just slamming you. I've probably been the meanest person so far, and my post still has facts and information.

This is not a rounding or truncation issue.

You're actually correct about this technically, but for a much more subtle reason than you understand.

Please...

The theory behind map() is, as I linked previously, linear interpolation. The 4 values you put in form a pair of points (in_min, out_min) and (in_max, out_max), and these points define a line. Just like in algebra class, you can take those points and convert it to a standard line equation in a form such as y = mx + b or Ax + By + C = 0. When you pass an x value into map(), it is equivalent to solving for y in those line equations.

It is true that the ranges are converted inclusively. None of us have said anything differently. map(x, 0, 1023, 0, 255) and map(x, 0, 1024, 0, 256) will both convert the extremes of the range correctly (0 -> 0, 1023 -> 255). Where you're having trouble is that the middle is not behaving as you are expecting. That is because the slope of the line defined by the map points is slightly different. When you use (0,0) and (1024,256) as the points, this gives a slope of exactly 0.25, which is what you want. Using (0,0) and (1023,255) gives a slope of about 0.24927, which is very slightly smaller and is what is producing the undesirable behavior that led you to make this thread.

The effect becomes terrible when there is a large difference in the range sizes, as you've discovered.

  1. output from map(val, 0,99, 0,1) across range of values 0 to 99
map(val, 0, 100, 0, 2);

If you want the mapped value to change halfway through, that means you desire a slope of 0.02 (1/50), which is what my values give. The values you tried give a slope of 0010101 (1/99), half as much as the one you want.

  1. output from map(val, 0,99, 1,3) across range of values 0 to 99
map( val, 0, 100, 1, 4 );

You desire a slope of 0.03 (3/100), but the values you put in have a slope of about 0.02020 (2/99).

Abandon the notion that your arguments have to be the endpoints of the ranges you are converting between. The reference page even says Does not constrain values to within the range, because out-of-range values are sometimes intended and useful. Instead, the values are points that define a line equation. Once you get that in your head, you can take advantage of the fact that there is not only a single unique pair of points that defines a line, there are literally infinite possibilities. For example:

map( x, 0, 1024, 0, 256 );
map( x, 0, 4, 0, 1);
map( x, 0, 256, 0, 64);
map( x, -12, 4096, -3, 1024);

All 4 sets of parameters used in these map functions define the same line1, so they will all provide equivalent results for all inputs2.

which exactly matches what is in the example on the map() documentation page.

However because of a miscalculation in the map() function not all values will be mapped correctly.

map() is not broken, you simply are not using it properly.

...and honestly, it's hard to blame you for that when you point out that even the official reference page does it wrong.

1 If I have not made a mistake in my arithmetic. I spot checked it with a few randomly chosen x values though and it all works out.
2 All inputs that do not cause the intermediate calculations to overflow a long int variable.

Well, put, jiggy.

I, too, made a 'wtf map' type of thread when I was very new to the Arduino. Staring at the formula on the reference page made it make sense.
After understanding that only the 1st max would equal 2nd max, just bumping up the 2nd max by 1 integer gave me the behavior I wanted. And I wanted a pretty drastic map- analog input into only 3 discrete numbers.

I went whole hog on the point thing. I got sick of trying to remember what order the stupid 4 parameters are supposed to be in, so I made a template point struct with X and Y members and overloaded map to take it a pair of them.

template<class Coord>
long map(long value, mrd::Point<Coord> p1, mrd::Point<Coord> p2)
{
	return map(value, p1.X, p2.X, p1.Y, p2.Y);
}

Best part is, p1 and p2 are interchangeable, since even if you swap the two points they still define the same line. You don't need to care which goes to which argument.

OK guys, so one more time.
Look I fully understand math. I had a double major of engineering and math with more than 6 years of Calculus and Differential Equations and I have been doing embedded s/w for 40 years. So don't keep trying to lecture me on noobie mistakes like fence-post errors, or assume I don't understand something like linear interpolation.
And please stop saying that the way map() is working is "normal" and should be expected since integers are used in the calculations and that the results are simply do to rounding and truncation issues that are not solvable.
It only makes you look silly.

So lets get back to the code and the issues.

The issue in the Arduino map() function is that the calculation being used has issues.

Because of the calculation being used, it returns not only unexpected results, but results that are not properly mapping the values between the ranges.

Like when mapping from a larger set to significantly smaller set, map() should still return a smooth linear mapping but it doesn't.
(I've provided examples earlier)

Also there is the issue of boundary regions when selecting the mapped value from the target range set.

For example, if there are 3 numbers in the target range set then the middle number in the target range should be selected when the value in the primary set is 1/3 of the way through NOT 1/2 in order to provide a proper linear mapping.

i.e. The region of a 3 number range set should be mapped like this based on the % into the source range:
Assuming a target range of A to C

A-------------------------B-------------------------C
AAAAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCC
|                |                 |                |
0               33%      50%       67%             100%

The current map() function maps it like this:

A-------------------------B-------------------------C
|AAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBBBBBBBBBBBC
|                         |                         |
0                        50%                       100%

The current mapping method seems particularly silly when using a 2 number target set since the 2nd number will only be used when the highest value in the source rangeset is used.
I just can't imagine anyone that really wants and expects it to work this way.

I've shown simple examples of these problems.
And there are even tutorials attempting to show users how to work around the miscalculations in map()
like this one:

Even if the current calculation were switched to using floating point and did rounding it still wouldn't correctly handle the target boundary regions shown above.

All this can be fixed by changing the calculation.

And just to show you that it can be fixed here is a new and improved map() function that maps things as expected and does not depend on any floating point.

This is nearly identical to the calculation proposed by gawroon7 in the very fist post.
(see next post for updated code)

long map(long x, long in_min, long in_max, long out_min, long out_max)
{
        if( x == in_max)
                return out_max;
        else
                 return (x - in_min) * (out_max - out_min+1) / (in_max - in_min) + out_min;
}

I've tested it on several different types of rangesets and it seems to be working correctly.
small to big, big to small, negative ranges.
There is one Issue that I've seen and that is
using a normal range source and a reverse range target using negative numbers is not working correctly, but I think I can make an adjustment to fix that.

However, for all the examples above,
this will provide proper linear mapping even when the source rangeset is much larger than the target and also uses proper boundary regions when selecting the target values.

i.e. it fixes all the issues I've been talking about and solves the OP mapping issues as well.

UPDATE:
Seen next post for updated map() function that solved the reverse target range issue.

--- bill

update that fixes reverse target ranges:

long map(long x, long in_min, long in_max, long out_min, long out_max)
{
	if( x == in_max)
		return out_max;
	else if(out_min < out_max)
		return (x - in_min) * (out_max - out_min+1) / (in_max - in_min) + out_min;
	else
		return (x - in_min) * (out_max - out_min-1) / (in_max - in_min) + out_min;
}

--- bill

bperrybap:
OK guys, so one more time.
Look I fully understand math. I had a double major of engineering and math with more than 6 years of Calculus and Differential Equations and I have been doing embedded s/w for 40 years. So don't keep trying to lecture me on noobie mistakes like fence-post errors, or assume I don't understand something like linear interpolation.
And please stop saying that the way map() is working is "normal" and should be expected since integers are used in the calculations and that the results are simply do to rounding and truncation issues that are not solvable.
It only makes you look silly

Ive as much or math and CS background as you do and and still kinda disagree with you. See my post #25.

Here is a little challenge for you and your math background: please offer an integer result map function that behaves like you would want it to work, that guarantees that extreme values are mapped to each other (floating point might get you in trouble depending how you write this), that is not too much slower than the current one, and works with large range mapping and short range mapping as you explain.

I think performance was the decision driver in the way that was implemented, offering that the first range value maps to the fist target value and last one to the last target value and something happens in between that is a step looking function, not a line.

and something happens in between that is a step looking function, not a line.

Exactly, and the truncation error can be somewhat reduced by examining the remainder of the final division.

Integer math, truncation, it's just a piecewise function like when you have to answer your age in whole years when you're given how many days old you are.
E.g., 0-364 days old, you're still not 1 year old, you need to hit 365 to make 1 year old,
map(x, 0, 365, 0, 1) behaves that way.
And you still return/respond "1 yr old" from 365 days up to and including 729, and so on.

J-M-L:
Here is a little challenge for you and your math background: please offer an integer result map function that behaves like you would want it to work, that guarantees that extreme values are mapped to each other (floating point might get you in trouble depending how you write this), that is not too much slower than the current one, and works with large range mapping and short range mapping as you explain.

Huh?
I already wrote that integer based map() function. See post #30
http://forum.arduino.cc/index.php?topic=417690.msg2877460#msg2877460

Not sure what you mean by "extreme values".

Run that map function in post #30 against the standard map() function with your favorite test cases.
And to make it even easier for you, here is a sketch to try it on actual Arduino h/w.
It will show you the full range of mapping and the differences between the original map() function and this new one.

All you have to do is modify the in and out range values and run it.

long newmap(long x, long in_min, long in_max, long out_min, long out_max)
{
	if( x == in_max)
		return out_max;
	else if(out_min < out_max)
		return (x - in_min) * (out_max - out_min+1) / (in_max - in_min) + out_min;
	else
		return (x - in_min) * (out_max - out_min-1) / (in_max - in_min) + out_min;
}

void setup(void)
{
long int val, valend, mval, mval2, cval, imin,imax,omin,omax;
char buf[80];
int incr;

	Serial.begin(9600);
	while(!Serial){}

	// fill in in & out ranges 
	imin = 1; imax = 100;
	omin = 1; omax = 3;

	if(imax > imin)
		incr = 1;
	else
		incr =-1;

	val = imin;
	valend = imax + incr;
	while(val != valend)
	{
		sprintf(buf,"map(%4ld,%ld,%ld,%ld,%ld) ",val,imin,imax,omin,omax);
		Serial.print(buf);
		mval = map(val, imin, imax, omin, omax);
		mval2 = newmap(val, imin, imax, omin, omax);
		sprintf(buf, "val: %4ld, map: %4ld, newmap: %4ld", val, mval, mval2);
		Serial.print(buf);

		if((mval != mval2))
			Serial.print(" <---map Diff");
		Serial.print("\n");

		val += incr;
	}
}
void loop(void){}

Now go try your favorite ranges.
Here are a few of my favorite interesting test cases:

1,100,1,3

0,1023,1,5

1,100,1,10

1,10,10,1

1,10,1,5

1,5,0,255

0,1023,0,1

You will notice a much smoother (better/correct) mapping and I would say it generates the mappings that people would expect.

--- bill

INTP:
Integer math, truncation, it's just a piecewise function like when you have to answer your age in whole years when you're given how many days old you are.
E.g., 0-364 days old, you're still not 1 year old, you need to hit 365 to make 1 year old,
map(x, 0, 365, 0, 1) behaves that way.
And you still return/respond "1 yr old" from 365 days up to and including 729, and so on.

Ah, now we are getting some useful comments about mapping range sets that can be addressed.

This brings up the point I've mentioned a few times and that is how to actually do the mapping within the target range set.
There are 2 different ways it could be done.
Should the mapping into the target rage be based on distributing the the target points evenly within the range or should they be based on selecting from truncation points?

The examples above and the current map() function assumes truncation points at the individual values in the target range which is why many people see seemingly strange or unexpected results.

The map() function I provided evenly distributes the all the target points within the target range during selection which is what many people seem to be expecting or wanting.

You can see the difference in the figures I showed in post #29
http://forum.arduino.cc/index.php?topic=417690.msg2877435#msg2877435

And you can see that with the current map() function when using a 3 point target range of A,B,C you don't start selecting the B value until you get to 50% of of the source range set and won't get C until you hit 100%.

When evenly distributing the target values within the range you start selecting B at 33% and C at 67%.

These are two different ways of doing mappings.

Given the number of people that talk about having issues with map() and using work arounds like fudging the high/max values of a range to get the desired results,
I'm assuming that many people expected the values in the target range to be selected by evenly distributing them within the range during selection.

It is the way I would expect a map() routine to work.

And that is what my alternate mapping routine does.

--- bill

The theory behind map() is, as I linked previously, linear interpolation.

IMHO, the map() function is supposed to scale an input by a certain offset and slope. I never once assumed that it was supposed to produce a "piecewise linear approximation".

If map() is re-written to use floating point, it does everything anyone could want:

int main (void)
{
    init ();
    Serial.begin (115200);

    double in, out;
    int round;
    char buffer[64];

    for (in = 0; in < 1024; in++) {
        out = map (in, 0, 1023, 0, 255);
        round = (out + 0.5);
        sprintf (buffer, "In: %7.2f, Out: %7.2f, Round: %d\n", in, out, round);
        Serial.print (buffer);
    }

    while (1);
}

Yields......

** **In:    0.00, Out:    0.00, Round: 0 In:    1.00, Out:    0.25, Round: 0 In:    2.00, Out:    0.50, Round: 0 In:    3.00, Out:    0.75, Round: 1 In:    4.00, Out:    1.00, Round: 1 In:    5.00, Out:    1.25, Round: 1 In:    6.00, Out:    1.50, Round: 1 In:    7.00, Out:    1.74, Round: 2 In:    8.00, Out:    1.99, Round: 2 In:    9.00, Out:    2.24, Round: 2 In:   10.00, Out:    2.49, Round: 2 ///////////// snip //////////// In:  510.00, Out:  127.13, Round: 127 In:  511.00, Out:  127.38, Round: 127 In:  512.00, Out:  127.62, Round: 128 In:  513.00, Out:  127.87, Round: 128 In:  514.00, Out:  128.12, Round: 128 In:  515.00, Out:  128.37, Round: 128 In:  516.00, Out:  128.62, Round: 129 In:  517.00, Out:  128.87, Round: 129 In:  518.00, Out:  129.12, Round: 129 In:  519.00, Out:  129.37, Round: 129 In:  520.00, Out:  129.62, Round: 130 ///////////// snip //////////// In: 1010.00, Out:  251.76, Round: 252 In: 1011.00, Out:  252.01, Round: 252 In: 1012.00, Out:  252.26, Round: 252 In: 1013.00, Out:  252.51, Round: 253 In: 1014.00, Out:  252.76, Round: 253 In: 1015.00, Out:  253.01, Round: 253 In: 1016.00, Out:  253.26, Round: 253 In: 1017.00, Out:  253.50, Round: 254 In: 1018.00, Out:  253.75, Round: 254 In: 1019.00, Out:  254.00, Round: 254 In: 1020.00, Out:  254.25, Round: 254 In: 1021.00, Out:  254.50, Round: 255 In: 1022.00, Out:  254.75, Round: 255 In: 1023.00, Out:  255.00, Round: 255** **
All you need is this:

double map (double x, double x1, double x2, double y1, double y2)
{
    return (x - x1) * (y2 - y1) / (x2 - x1) + y1;
}

Krupski:
IMHO, the map() function is supposed to scale an input by a certain offset and slope. I never once assumed that it was supposed to produce a "piecewise linear approximation".

If map() is re-written to use floating point, it does everything anyone could want:

I'm not sure what the goal of that mapping is.

That calculation can not correctly map points into the target rangeset if all points in the targetset are considered be inside equal sized regions within the target range - even if using rounding.

i.e. in that example, not all the target values are spread across 4 source digits as is the case with the integer version I presented earlier.

Since it is using the same calculation as the original map() function it suffers from the same issue for this type of mapping, just a bit less pronounced since you use floats and you round the output.

Have a look at this rangeset for more clarification
1,100,1,3

The original map function:
1 to 50 maps to 1 (first 50%)
51 to 99 maps to 2 (2nd 49%)
100 maps to 3 (remaining 1%)

Output of yours:
1 to 25 maps to 1 (first 25%)
26 to 75 maps to 2 (2nd 50%)
76 to 100 maps to 3 (remaining 25%)

The one I provided: (without having to use any floats or rounding)
1 to 33 maps to 1 (first 34%)
34 to 66 maps to 2 (2nd 33%)
67 to 100 maps to 3 (remaining 33%)

All source points are mapped into regions that are sized as even as you can get given the ranges.

If you are wanting/trying to map a value from a source set to a value in a target rangeset based on target values being inside equal sized regions within the target rangeset, then just using floating point with the same calculation as the original map() calculation won't get you there.

--- bill

In: 2.00, Out: 0.50, Round: 0

Why is this

bperrybap:
Huh?
I already wrote that integer based map() function. See post #30

You did sorry got confused with Krupski code based on doubles which is slower than integer mapping.

Your function works as most people would expect map to work - agree with that but still view that the doc by mentioning truncating rather than rounding is clear to me (hence why I don't use it :slight_smile: and do the math manually)

At the risk of exposing my ignorance (I skimmed this thread and only partially understood it), what leaps to mind is Arduino's random() function--in which the lower bound is inclusive and the upper is exclusive. Doesn't this function use map for scaling?

Maybe there's an issue with documentation?

--Michael