Forum Rule: Always post complete source code & details to reproduce any issue!

1. ## map() function improvements

Arduino's map() function has always left quite a bit to be desired. They've had a long standing bug report about the issues, and of course numerous times people have discussed using floating point.

Well, I decided it's time to put a better map() function into Teensyduino. At least I hope it's better....

For a little background, here's a simple test program:

Code:
```void setup()
{
while (!Serial) ;
Serial.println("map function test");
long in_min = 0;
long in_max = 7;
long out_min = 0;
long out_max = 3;
for (int i=in_min-5; i <= in_max+5; i++) {
Serial.print(i);
Serial.print(" --> ");
Serial.println(map(i, in_min, in_max, out_min, out_max));
}
}

void loop()
{
}```
This is supposed to map 0-7 onto 0-3. But here's what you actually get:

map function test
-5 --> -2
-4 --> -1
-3 --> -1
-2 --> 0
-1 --> 0
0 --> 0
1 --> 0
2 --> 0
3 --> 1
4 --> 1
5 --> 2
6 --> 2
7 --> 3

8 --> 3
9 --> 3
10 --> 4
11 --> 4
12 --> 5
So the first big problem is integer truncation is handled poorly.

The other big problem is not support at all for floats. If the variable you're trying to map to a new range is a float, it gets converted to an integer and then put through this poor integer math.

After much experimenting with C++ function overloading, I finally ended up with a C++ template solution using C++11 type trairs and SFINAE. The template syntax looks horribly complex.

https://github.com/PaulStoffregen/co...58d2e438233f12

With this approach we can have st42's proposed improvement to the integer algorithm, which is:

Code:
```        if ((in_max - in_min) > (out_max - out_min)) {
return (x - in_min) * (out_max - out_min+1) / (in_max - in_min+1) + out_min;
} else {
return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}```
and when the input number is a float or double, the original math gets computed as 32 or 64 bit floating point.

The integer case becomes:

map function test
-5 --> -2
-4 --> -2
-3 --> -1
-2 --> -1
-1 --> 0
0 --> 0
1 --> 0
2 --> 1
3 --> 1
4 --> 2
5 --> 2
6 --> 3
7 --> 3

8 --> 4
9 --> 4
10 --> 5
11 --> 5
12 --> 6
Now float can finally be used for map too:

map function test
-5 --> -2.14
-4 --> -1.71
-3 --> -1.29
-2 --> -0.86
-1 --> -0.43
0 --> 0.00
1 --> 0.43
2 --> 0.86
3 --> 1.29
4 --> 1.71
5 --> 2.14
6 --> 2.57
7 --> 3.00

8 --> 3.43
9 --> 3.86
10 --> 4.29
11 --> 4.71
12 --> 5.14
This change is scheduled to become part of Teensyduino 1.37.... unless you talk me out of it.

Or maybe there's some way the integer round off could be handled even better? Is st42's way really the best? It seems better than all the other alternatives, but it does round towards zero and gives more values at zero when the target range spans negative to positive.

2. Great work!

3. Originally Posted by PaulStoffregen
Arduino's map() function has always left quite a bit to be desired. They've had a long standing bug report about the issues, and of course numerous times people have discussed using floating point.

Well, I decided it's time to put a better map() function into Teensyduino. At least I hope it's better....
My 2 cents:
I would call this not a bug, as done by st42 in Arduino forum.
The method he proposed is constructed for the application where he wanted equal distribution of positive values

a counter example extends to negative input values

Code:
```long mmap0(long x, long mn1,long mx1, long mn2, long mx2)
{
return mn2+(x-mn1)*(mx2-mn2)/(mx1-mn1);
}
long mmap1(long x, long mn1,long mx1, long mn2, long mx2)
{
return mn2+(x-mn1)*(mx2-mn2+1)/(mx1-mn1+1);
}

void setup() {
// put your setup code here, to run once:

while(!Serial);
Serial.println("map function test");
long in_min = -7;
long in_max = 7;
long out_min = 0;
long out_max = 3;
for (int ii=in_min; ii <= in_max; ii++) {
Serial.print(ii);
Serial.print(" --> ");
Serial.print(mmap0(ii, in_min, in_max, out_min, out_max));
Serial.print(" --> ");
Serial.println(mmap1(ii, in_min, in_max, out_min, out_max));
}
}

void loop() {
// put your main code here, to run repeatedly:

}```
generates
Code:
```map function test
-7 --> 0 --> 0
-6 --> 0 --> 0
-5 --> 0 --> 0
-4 --> 0 --> 0
-3 --> 0 --> 1
-2 --> 1 --> 1
-1 --> 1 --> 1
0 --> 1 --> 1
1 --> 1 --> 2
2 --> 1 --> 2
3 --> 2 --> 2
4 --> 2 --> 2
5 --> 2 --> 3
6 --> 2 --> 3
7 --> 3 --> 3```
IMO the modified map generates a more un-symmetric mapping (the '1's are more on the negative side)

I'm not saying that st42's method is not solving his view of the world, but is not generally applicable.

Additionally, his argumentation in terms of steps is not correct, what is relevant is the estimation of the slope.
one can estimate the correct slope with any number of values. The problem is integer arithmetic.

You asked for a better method.
there can not be a universal solution to misbehaving integer arithmetic, it all depends on the context
For example:
I would prefer that both min and max are extreme values indicating saturation (e.g. ADC)
mapping from 16 bit to 8 bit (for displaying) I would like to maintain that min and max are extreme values that only occur once.
Both the old map and the new map will not be suitable, and I have to generate my own map function.

So context matters and should be stated.

4. Originally Posted by WMXZ
My 2 cents:
Code:
```map function test
-7 --> 0 --> 0
-6 --> 0 --> 0
-5 --> 0 --> 0
-4 --> 0 --> 0
-3 --> 0 --> 1
-2 --> 1 --> 1
-1 --> 1 --> 1
0 --> 1 --> 1
1 --> 1 --> 2
2 --> 1 --> 2
3 --> 2 --> 2
4 --> 2 --> 2
5 --> 2 --> 3
6 --> 2 --> 3
7 --> 3 --> 3```
IMO the modified map generates a more un-symmetric mapping (the '1's are more on the negative side)
Are you serious? You are mapping a 15-value range onto a 4-value range. IMO, 99% of the people out there expect a fair distribution of mapped values. The original map only maps one value to '3'. The modified version maps 3 or 4 values to each target.

I can't think of a use case where the original map function has the right behavior.

IMO, the correct behavior of a map function that is useful for most cases is to match rounded floating point math as close as is reasonably possible with integer math.

@Paul:
decltype(x) is the wrong return type to use. If the out range is larger, you get truncation (e.g. mapping uint8_t -> [0, 1000]). You should probably use 'long'.

5. Originally Posted by tni
Are you serious? You are mapping a 15-value range onto a 4-value range. IMO, 99% of the people out there expect a fair distribution of mapped values. The original map only maps one value to '3'. The modified version maps 3 or 4 values to each target.
1) Yes, I' m serious, that I'm not convinced of THAT solution
2) I'm the 1%
3) ALL my applications require the lowest and highest value to be a catch-all of values below and above. How can you differentiate between good (within boundary) and bad (outside boundary) value?
4) the example is made to be an example

6. Originally Posted by tni
IMO, the correct behavior of a map function that is useful for most cases is to match rounded floating point math as close as is reasonably possible with integer math.
I agree, approximating the float result is very likely what most people want map() to actually do, despite the comment in Arduino's documentation about integer truncation.

Originally Posted by WMXZ
1) Yes, I' m serious, that I'm not convinced of THAT solution
2) I'm the 1%
I can appreciate many differing opinions. Indeed there are many subtle usage cases.

But the topic at hand is what specifically to implement for a widely used Arduino API. My goal is to give everyone (or 99% of everyone) the most widely useful results they would intuitively expect.

Originally Posted by WMXZ
3) ALL my applications require the lowest and highest value to be a catch-all of values below and above. How can you differentiate between good (within boundary) and bad (outside boundary) value?
Arduino's documentation specifically says map() "does not constrain values to within the range" and they recommend using constrain() either before or after map() if you need to limit the range.

But again, these are generic Arduino functions meant to cater to the general population of Arduino users, most of whom are novices. The question at hand here is not "what does WMXZ need" for a particular application that's an unusual 1% case, but rather what is the best, most appropriate algorithm to use for the large population of Arduino & Teensy users who expect map to "re-map a number from one range to another" (Arduino's description).

Originally Posted by tni
decltype(x) is the wrong return type to use. If the out range is larger, you get truncation (e.g. mapping uint8_t -> [0, 1000]). You should probably use 'long'.
Like this?

https://github.com/PaulStoffregen/co...cc06b642b8962a

7. Originally Posted by WMXZ
3) ALL my applications require the lowest and highest value to be a catch-all of values below and above. How can you differentiate between good (within boundary) and bad (outside boundary) value?
In that specific case, a sigmoid or tanh approximation could be a better solution. But the map() function is (at least in my modest understanding) expected to do a simple linear mapping in the y = m*(x-c) style. You might compare the input value to the input bounds before applying the linear map() and do some separate "out-of-range" exception handling.

8. Originally Posted by PaulStoffregen
decltype(x) is the wrong return type to use. If the out range is larger, you get truncation (e.g. mapping uint8_t -> [0, 1000]). You should probably use 'long'.
Like this?

https://github.com/PaulStoffregen/co...cc06b642b8962a
Yes .

9. Originally Posted by PaulStoffregen
The question at hand here is not "what does WMXZ need" for a particular application that's an unusual 1% case, but rather what is the best, most appropriate algorithm to use for the large population of Arduino & Teensy users who expect map to "re-map a number from one range to another" (Arduino's description).
Sure, I do not need or have to like Arduino software and I'm capable of doing my own programming and others do not need/like my programs.

Is st42's way really the best?
and I simply tried to say that it depends on what you wanted to do and what you expect from the results.

I will not go into the merit of avoiding float mathematics or other methods that are known from graphics for linear interpolation and I recall similar things have been discussed in this forum some time ago, but I restate that I'm not convinced that the st42 solution is the best. It is a nice trick like a lot other heuristic tricks.

10. Paul,

Is there a possibility that your map() function improvement is not took into account when I'm using Xcode+embedXcode and Teensyduino 1.39 (latest version available for all the software) ? I have curious behaviour on map() when the var is reaching the limit (a uint8_t var) when incremented by an encoder. When I'm implementing manually your patch it does improve the global behaviour (which let me know that your map patch is not took into account), but does not solve the cycling of the value from around 250 to 0 even with the use of constraint() before or after the map().

Thanks a lot

11. I've never used Xcode+embedXcode.

I can tell you the improved map() function is definitely in the 1.39 code. The old one is long gone. So unless there's somehow an old copy laying around in embedXcode, and it somehow using that old code instead of the latest (which all seems quite unlikely), I just don't understand how you could be seeing the old map() behavior.

12. ## Trouble with results when min>max output bounds

Hi All,
When the output Min > Max, I get this:

map(0, 0, 127, 60, 30) = 60 (GOOD)
map(127, 0, 127, 60, 30) = 32 (NOT SO GOOD)

From Arduino reference:

Note that the "lower bounds" of either range may be larger or smaller than the "upper bounds" so the map() function may be used to reverse a range of numbers, for example

y = map(x, 1, 50, 50, 1);
using the current map function yields:
map(50, 1, 50, 50, 1) = 3 (NOT SO GOOD)

13. Originally Posted by SteveBar
Hi All,
When the output Min > Max, I get this:

map(0, 0, 127, 60, 30) = 60 (GOOD)
map(127, 0, 127, 60, 30) = 32 (NOT SO GOOD)

From Arduino reference:

using the current map function yields:
map(50, 1, 50, 50, 1) = 3 (NOT SO GOOD)
Yep I got that response on T4.1...
Code:
```void setup() {
while (!Serial) ;
pinMode(13, OUTPUT);
Serial.begin(115200);
delay(100);
Serial.println(map(50, 1, 50, 50, 1), DEC);
Serial.println(map(50, 50, 1, 1, 50), DEC);
}

void loop() {
delay(250);
}```
Note, I reversed it which should produce the same thing but does not.

Code:
```3

1```

14. I just wouldn’t use the map() function which obviously produces rounding/truncating errors but rather calculate once by hand the required coefficients and implement the linear conversion function y=m*x+c with the desired precision.
The map() function does theoretically the same, but it fails in many cases due to rounding and/or truncating problems during the calculation of m=(y2-y1)/(x2-x1) in integer math.

15. Code:
```        if ((in_max - in_min) > (out_max - out_min)) {
return (x - in_min) * (out_max - out_min+1) / (in_max - in_min+1) + out_min;
} else {
return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}```
Maybe the "out_min+1" & "in_min+1" should be "(out_min+1)" & "(in_min+1)" cause the "-" sign in front.

Is that the distributive property cause you need to distribute the implied "-1" ???
Steve

16. Like the wrong abs and round its just a poor implementation by the Arduino devs.
Best not to use it.

I tend to #undef abs and #undef round to get rid of this buggy crap . The similar functions provided by the c library are better.

But I see that on 8Bit, without a proper c library, 10 Years ago, they were ok.

They just missed the right point to remove it and mark it as deprecated.

17. Originally Posted by Theremingenieur
I just wouldn’t use the map() function which obviously produces rounding/truncating errors but rather calculate once by hand the required coefficients and implement the linear conversion function y=m*x+c with the desired precision.
The map() function does theoretically the same, but it fails in many cases due to rounding and/or truncating problems during the calculation of m=(y2-y1)/(x2-x1) in integer math.
I agree if it where a simple matter of "rounding/truncating errors" that skewed the distribution this way or that, but 3 is just wrong and needs to be fixed. As stated above the goal is to give results that novices would intuitively expect.

BTW I'm not trying to call out anyone's errors for fun or ego, I have learned most of what I know about Teensy programming by reading this forum!!! And I want to help by reporting bugs/sightings.
Steve

18. With (float) cast on the input var it does the math with float - low overhead add for FPU Teensy's:
Code:
```  Serial.println("map((float)0, 0, 127, 60, 30) = 60;");
Serial.println(map((float)0, 0, 127, 60, 30), DEC);
Serial.println("map((float)127, 0, 127, 60, 30) = 32;");
Serial.println(map((float)127, 0, 127, 60, 30), DEC);```
Gives:
Code:
```map((float)0, 0, 127, 60, 30) = 60;
60.0000000000
map((float)127, 0, 127, 60, 30) = 32;
30.0000000000```

19. Originally Posted by KurtE
Note, I reversed it which should produce the same thing but does not.
I've added this to my list of bugs to fix.

20. Originally Posted by defragster
Code:
```map((float)0, 0, 127, 60, 30) = 60;
60.0000000000
map((float)127, 0, 127, 60, 30) = 32;
30.0000000000```
Great suggestion, tested and implemented!
THANKS!
Steve

21. Originally Posted by SteveBar
Great suggestion, tested and implemented!
THANKS!
Steve
Awesome - Good to hear ... glad I looked at it

For T_3.5, 3.6, 4.0, 4.1: that might be a quick #ifdef workaround for Paul. Though it would lose something on bigger uint32's? Though the T_4.x's could use double.

#### Posting Permissions

• You may not post new threads
• You may not post replies
• You may not post attachments
• You may not edit your posts
•