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

J-M-L:
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)

I would say the biggest issue with the Arduino map() function is the Arduino web page documentation for map() since I don't believe the documentation on that page is clear on how the mapping is done.
With the exception of the actual code of the map() function being provided - which I'd say most arduino users won't understand or at least not the subtly of how it works -
there are only two sentences that describes how the actual mapping or mapping calculation is done and those two sentences are so vague that it isn't clear what to expect.

Consider only the two descriptive sentences without the benefit of seeing or knowing the code implementation as any good explanation of an API function should not require knowledge of the specific code implementation.

The map() function uses integer math so will not generate fractions, when the math might indicate that it should do so. Fractional remainders are truncated, and are not rounded or averaged.

The first line:
The map() function uses integer math so will not generate fractions, when the math might indicate that it should do so.
That statement is terrible in several ways.
First fractions is not a good term to use since computers actually use floating point representation not actual fractions.
I'm guessing the intent of will not generate fractions is that it only returns integers and not floating point values that can specify values that are in between integer values.
But the second part is where the real ambiguity is:
What does when the math might indicate that it should do so mean?
Any math involved is in the code implementation and how could the caller indicate using fractions (floating point)?

Is it trying to say that the all the parameters passed to map() will be converted to integers?
It simply isn't clear what that was trying say.

The second line:
Fractional remainders are truncated, and are not rounded or averaged.
This is not clear and does not help explain how the mapping is done.


Since the description of how the mapping is done as well as the main description of the map() function:

Re-maps a number from one range to another. That is, a value of fromLow would get mapped to toLow, a value of fromHigh to toHigh, values in-between to values in-between, etc.
are so vague,
the alternate map() function I presented would also conform to the current documentation and work with the example.

And THAT is the problem. The documentation is not clear how map() works.

Adding to the confusion is the example provided which is not a good example since it does not help explain how the mapping function does the mapping.
Given the minimal explanation and the example, it isn't obvious that the maximum PWM value (255) is not achieved until the maximum analogRead value of 1023 is achieved.

What probably needs to happen is an update to the map() documentation page to really explain how it works and to show two examples, that shows how to use the function for two ways of mapping.

  • truncating target values as in the "birthday" example.
  • selecting target values from evenly distributed regions which many people seem to want.

as either way of mapping can be accommodated with the current map() code by fudging the "High" range parameters.

I believe this would solve all the confusion as well as show people how to get the type of mapping they want.
This would be a very good thing.

--- bill

bperrybap:
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%)

What you are (I think) talking about is something like this (an actual example):

My 128 x 64 VFD modules from Noritake allow setting the screen brightness to any one of 8 levels (0 thru 7).

Now, if I want to have a "setBrightness()" function that takes a number from 0 to 100 (percent brightness) and convert it to 0...7 depending on the brightness value, then of course I want an equal number (or as equal as possible) number of output values for each input.

Using the original map() function in Arduino, from an input range of 0 to 100, and an output range from 0 to 7, I get this distribution:

0: 15
1: 14
2: 14
3: 15
4: 14
5: 14
6: 14
7: 1

Obviously not good for the use I described.

Doing it with floating point (then rounding the result) yields this distribution:

0 : 8
1 : 14
2 : 14
3 : 14
4 : 15
5 : 14
6 : 14
7 : 8

Taking the integer value of the floating point results yields the same distribution as the original Arduino code does.

Now, to accomplish what I mentioned above (converting 0 - 100 percent brightness to 7...0), I use this code:

// set the display brightness to one of 8 levels:
// note: zero percent turns off the VFD filament to save power
uint8_t Noritake_VFD_GUU100::setBrightness (uint8_t percent)
{
    // brightness calculation multiplies all numbers
    // by 10 so that we can do fractional math (i.e.
    // 125 is really 12.5)
    uint16_t i = 1000; // 100 * 10
    percent = (percent > 100) ? 100 : percent;
    _displayBright = 7;

    while ((i -= 125) && _displayBright) {
        if ((percent * 10) > (i + 5)) { // 0.5 rounding
            _displayBright--;
        }
    }

    // brightness 0 turns off the filament
    _writePort (FUNC_SET, 0); // function set
    _writePort (percent ? (_displayBright | SETBRITE | CATHODE) : ((_displayBright | SETBRITE) & ~CATHODE), 0);
    return _displayBright; // return calculated value (debug mostly)
}

And I get this distribution:
0 : 12
1 : 13
2 : 12
3 : 13
4 : 12
5 : 13
6 : 12
7 : 14

...which is of course what I want... the same (as far as is possible) number of steps between each value.

BUT!!!!!!! None of this has to do with what (IMHO) the Arduino map() function is for. In my opinion, the Arduino function is for doing things like converting degrees F to degrees C, converting a raw 0-1023 analog value from a sensor into a correct output that reflects the sensor's actual input (for example, a single turn potentiometer with a "swing" of 270 degrees, connected to an analog input should work like this):

p = analogRead (A0);
angle = map (p, 0, 1023, 0, 270);

And, I don't want "360 (degrees), 361, 362, 363, etc..." I want "360.25, 361,43, 362,74, etc..." the EXACT values, not clipped by using INTS.

Did I make any sense here? :slight_smile:

J-M-L:
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)

True, floating point code is slower than integer (and uses a bit more memory), but really... are a few more microseconds and a few more bytes of memory so terrible to use in order to get a correct result?

INTP:
Why is this
In: 2.00, Out: 0.50, Round: 0

Because the decimal place I specified in the demo program masked it. Here's the code:

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

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

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

    while (1);
}

(note the larger decimal place... .5 instead of .2)

...and the output result:

[b]In:    0.00000, Out:    0.00000, Round: 0 
In:    1.00000, Out:    0.24927, Round: 0 
[color=red]In:    2.00000, Out:    0.49853, Round: 0 [/color]
In:    3.00000, Out:    0.74780, Round: 1 
In:    4.00000, Out:    0.99707, Round: 1 
In:    5.00000, Out:    1.24633, Round: 1 
In:    6.00000, Out:    1.49560, Round: 1 
In:    7.00000, Out:    1.74487, Round: 2 
In:    8.00000, Out:    1.99413, Round: 2 
In:    9.00000, Out:    2.24340, Round: 2 [/b]

Notice that what before looked like "0.5" is actually 0.49853 so it did indeed "round" correctly.

the alternate map() function I presented would also conform to the current documentation and work with the example.

Sure there are many ways to achieve an integer mapping and none is right or wrong (agree though that some might be more intuitive than other)

Reading the doc (and looking at the source code to confirm) was enough for me to understand exactly what they meant and understand what they were doing. Then if this is not matching your needs, you are always free to write your own. (And look at a definition of fractions - in pure engluant sense it does not mean a quotient but really just a number with fractional part - i.e. Not a whole number)

J-M-L:
Sure there are many ways to achieve an integer mapping and none is right or wrong (agree though that some might be more intuitive than other)

I don't for one minute believe that the map() function was meant to be an INTEGER function. I think it's a mistake on the part of whoever wrote it.

No matter how you look at it, map() is doing a first order (linear) data fit. This means the data is scaled by two parameters... M (the slope) and B (the offset) in the formula [b]Y=MX+B[/b].

M and B can be, and 99.9% of the time ARE fractional numbers. Using integers makes the function both incorrect and useless.

The normal (school kid) way to figure out M and B is to take two sets of data, such as X1, Y1 and X2, Y2, then solve for M by using M = (Y1 - Y2) / (X1 - X2), then plugging M back into the formula and solve for B. Then lastly, use the same M and B on the other data set and verify that it comes out correctly.

The map() function just figures out M and B for you and gives you the result, but sadly since it uses integer math, most times it will be wrong.

map() MUST use floating point, else it's useless and wrong.

Point slope form never gets the credit it deserves. Slope intercept always hogging the spotlight.

map() MUST use floating point, else it's useless and wrong.

That might be YOUR view and I'm fine with it. Just don't use it and use what suits you.

It is not wrong because it does what it does and is being used by many (maybe sometimes unknowingly of the consequences)... So it's neither useless nor wrong in my opinion. Just need to know what you do.

M and B can be, and 99.9% of the time ARE fractional numbers. Using integers makes the function both incorrect and useless.

I disagree with that. as floating points in an arduino are not very precise, not respecting basic maths laws depending on the order in which you perfrom your math for some numbers you bring your own set of issues with float. And then it's slow which in some cases is a problem too. Remember You are not running your code on a great modern CPU with dedicated floating point computation engine. I try to stay away from float as much as I can in my code because ultimately your output or input reading are integer.

J-M-L:
Remember You are not running your code on a great modern CPU with dedicated floating point computation engine. I try to stay away from float as much as I can in my code because ultimately your output or input reading are integer.

Oh geez... a few more microseconds makes NO DIFFERENCE to 99.9% of the programs written.

Speaking of integers, I'll give you two:

int X = 4;
int Y = 3;
what is (X/Y); ?

F-L-O-A-T-I-N-G P-O-I-N-T

Yes?

INTP:
Point slope form never gets the credit it deserves. Slope intercept always hogging the spotlight.

Point slope is useless for calibrating a sensor signal (unless the intercept is zero).

By "calibrate" I don't mean adjusting the sensor, I mean adjusting the slope and intercept to convert the sensor output into usable units.

Krupski:
Oh geez... a few more microseconds makes NO DIFFERENCE to 99.9% of the programs written.

I've got to disagree with you there. It's not just a few microseconds. Floating point conversions are ridiculously slow compared to most other Arduino operations. The only exception is long int division, which is even slower.

For most of the time when you're dealing with analogRead() type values, a few errors in the low order bits are insignificant. But spending more time passing that value around is significant.

Krupski:
Oh geez... a few more microseconds makes NO DIFFERENCE to 99.9% of the programs written.

Speaking of integers, I'll give you two:

int X = 4;
int Y = 3;
what is (X/Y); ?

F-L-O-A-T-I-N-G P-O-I-N-T

Yes?

Well no - Not from the arduino platform and compiler perspective. X/Y is an integer unless you cast to float...

A few microseconds can be meaningful in many occasions especially in an very inner loop being called often. Just depends on your context and what you drive with your program.

And if you have floats (not tested with those ones but you'll get the idea) on a UNO

float a = 1.0/3.0;
float b = 77777777.0;
float c1,c2;

c1 = b * (a/b);
c2 = (a*b)/b;

Maths says c1 == c2 == a but sometimes your computer will disagree...

Read above again, I'm not saying you are wrong or that the way map works does not mislead people, I'm just saying that you need to understand the consequences of what you do. Feel free to use the code you want wherever and however you want - even the String class is totally fine for specific situations.

So it's all about understanding what you do and that at the end of the day the compiler and the architecture you run on is having an impact.

J-M-L:
I disagree with that. as floating points in an arduino are not very precise, not respecting basic maths laws depending on the order in which you perfrom your math for some numbers you bring your own set of issues with float. And then it's slow which in some cases is a problem too. Remember You are not running your code on a great modern CPU with dedicated floating point computation engine. I try to stay away from float as much as I can in my code because ultimately your output or input reading are integer.

It's not that they disrespect basic math laws, but floats have their own truncation issues. They are not magical infinite precision numbers capable of perfectly representing the value of any real number. They aren't even capable of perfectly representing all rational numbers.

The standard is IEEE-754. For a 32-bit float it sets aside a fixed number of bits (23) for the mantissa, 1 sign bit, and the rest (8) for the signed exponent. Because the mantissa is a fixed number of bits, it is only capable of perfect representing binary fractions, ie fractions where the denominator is a power of 2. Even simple fractions like 1/5 cannot be prefectly represented as a float because when converted to their binary representation the mantissa will have a repeating, non-terminating pattern, much like 1/3 or 1/7 have in their decimal representation.

Generally speaking, if the prime factors of the denominator of a rational number contain any numbers that are not prime factors of the number system's radix, that value cannot be represented in that number system in a finite number of digits. It will have an infinite repeating sequence.

The size of the mantissa also limits the depth of precision you can have, even if you do have a binary fraction. You could not represent a number like (20 + 2-40), because the mantissa is not large enough to hold that much precision. The 2-40 term will be truncated.

The limited size of the exponent field also places a constraint on the size of the numbers. 2200 or 2-200 have an exponent outside of the range the exponent field can hold, and cannot be represented in a 32-bit float.

Because of the truncation issues that happen when you do divisions by numbers that are not powers of 2, it is recommended to never compare floating-point numbers using equality (==), you are recommended to subtract them and test that the absolute value of the difference is smaller than some tolerance value that you have to come up with.

TLDR; "Throwing money at the problem" by converting all your types to float without considering their own set of issues might not magically fix all your problems. Yes, the rounding behavior is different than integers, but that does not mean it is better. The current map() function, operating on integral types is perfectly functional for all the examples that have been brought up in this thread if you are willing to use it properly and not just bang your head against a wall about how it doesn't work in exactly the way you think it should work.

Krupski:
Oh geez... a few more microseconds makes NO DIFFERENCE to 99.9% of the programs written.

Speaking of integers, I'll give you two:

int X = 4;
int Y = 3;
what is (X/Y); ?

F-L-O-A-T-I-N-G P-O-I-N-T

Yes?

This is a concrete example of what I mentioned in P2 and P3 of my previous post. 3 is not a power of 2, so the number 4/3 cannot be perfectly represented by a floating point number. The binary representation of the mantissa will have a repeating, non-terminating pattern, which will be truncated after the 24th bit.

For most purposes, the approximation will be good enough, but it can never be perfect.

disrespect

was the wrong term of course. My arduino is always very respectful of what I tell him to do :-). The problem is always the programmer :slight_smile:

J-M-L:
was the wrong term of course. My arduino is always very respectful of what I tell him to do :-). The problem is always the programmer :slight_smile:

Computers will always do exactly what they are told to do.

This does not imply that they will ever do what you want them to do. :wink: