Printing floats in scientific format

Problem
According to - http://www.arduino.cc/en/Reference/Float - a float in an Arduino sketch has a range from approx 10E-38 to 10E+38 . However the standard print() (~arduino-0021\hardware\arduino\cores\arduino\print.cpp) does only print a 'valid' value if abs(f) <= MAXLONG. For many applications this is sufficient but not for all. Therefor I implemented a function to convert a float to a string representing its scientific notation.

My first implementation was into the core printFloat() method itself, but that would break backwards compatibility with other programs and I took no time to solve that. My second implemtation converts the float to a character array, that can be printed or communicated. The function also test for two specific values named NaN (Not a Number) and INF(INITY) and returns an appropiate string for these values.

Code is not optimized or tested at infinitum so remarks are welcome.

Open issues:

  • remove the static char s[16]; as multiple calls will overwrite it.
  • rounding / truncating ?
  • add aditional tests?
  • // add comments!
char * float2s(float f)
{
  return float2s(f, 2);
}

char * float2s(float f, unsigned int digits)
{
  int index = 0;
  static char s[16];                    // buffer to build string representation
  // handle sign
  if (f < 0.0)
  {
    s[index++] = '-';
    f = -f;
  } 
  // handle infinite values
  if (isinf(f))
  {
    strcpy(&s[index], "INF");
    return s;
  }
  // handle Not a Number
  if (isnan(f)) 
  {
    strcpy(&s[index], "NaN");
    return s;
  }

  // max digits
  if (digits > 6) digits = 6;
  long multiplier = pow(10, digits);     // fix int => long

  int exponent = int(log10(f));
  float g = f / pow(10, exponent);
  if ((g < 1.0) && (g != 0.0))      
  {
    g *= 10;
    exponent--;
  }
 
  long whole = long(g);                     // single digit
  long part = long((g-whole)*multiplier);   // # digits
  char format[16];
  sprintf(format, "%%ld.%%0%dld E%%+d", digits);
  sprintf(&s[index], format, whole, part, exponent);
  
  return s;
}

And a program to test various values

float f;

void setup()
{
  Serial.begin(115200);
  Serial.println("Start");  

  f = 0.0;
  Serial.println(float2s(f, 3));

  f = 3.14159265; 
  for (int i=1; i< 15; i++)
  {
    Serial.println(float2s(f, 7));
    f *=1000;
  }
  Serial.println();

  f = -3.14159265; 
  for (int i=1; i< 15; i++)
  {
    Serial.println(float2s(f, 4));
    f *=1000;
  }  
  Serial.println();

  f = 3.14159265;
  for (int i=1; i< 19; i++)
  {
    Serial.println(float2s(f, 4));
    f /= 1000;
  }
  Serial.println();

  f = -3.14159265;
  for (int i=1; i< 19; i++)
  {
    Serial.println(float2s(f, 4));
    f /= 1000;
  }
  Serial.println();

  // NAN TEST
  f = sqrt(-1);
  Serial.println(float2s(f, 3));
  f = 0.0/0.0;
  Serial.println(float2s(f, 4));
  f = INFINITY * 0;
  Serial.println(float2s(f, 5));  
  f = tan(PI/2);
  Serial.println(float2s(f, 6));   // "fails" as PI is not precise enough ...
  
  // INFINITY TEST
  f = 1.0/0;
  Serial.println(float2s(f, 1));  
  f = -1.0/0L;
  Serial.println(float2s(f, 2));
  f = 1.0/0UL;
  Serial.println(float2s(f, 3));

  Serial.println("done...");
}

void loop()
{
}

All remarks and additional tests are welcome.

remove the static char s[16]; as multiple calls will overwrite it.

Have the caller provide a buffer and include a typedef...
typedef char float2sbuf_t[16];
...to make it easy for the caller to get the buffer size correct.

Good argument for making it a typedef, however it should work with smaller char array's e.g. when people just want 2 digits "-3.1 E+12" uses only 10 chars.

Another way is to get a char * , functions like strcpy() etc makes the caller responsible for allocating.

Due to typical LCD display sizes (16 or 20 chars) I consider a shorthand scientific notation by leaving out the " E" e.g. -3.1415+12 to save precious display space.

Good idea and good start. Thanks.

Your "TODO" list:

@robtillaart:

  • remove the static char s[16]; as multiple calls will overwrite it.

Well, the issue to me is not that it is overwritten by subsequent calls. There is no recursion, and "barefoot" Arduino code doesn't have threads or anything else that requires re-entrance.

However...

Having to dedicate 16 bytes of RAM for this function might be an issue with a resource-limited processor.

On the other hand...

Having the user pass the name of an array and the length of the array (like strncpy, for example) would be easy to implement, but would make the user actually think about what is going on. And, maybe, actually to learn some fundamentals about C programming and arrays using array names in function calls. See Footnote.

  • rounding / truncating ?

I think that rounding should be done to make it consistent with standard C (and C++) output functions. I mean, we have enough trouble getting through to inexperienced people that floating point is about significant digits, not decimal places. At least we can present correctly rounded values. So, 3.1415926..., rounded to five significant decimal digits is 3.1416, not 3.1415.

  • add ad[d]ditional tests?

"Corner" tests like rounding 1.999518 E+20 to four decimal places (five significant digits) gives 1.9995 E+20.
Rounding 1.999496 E+20 do four decimal places gives the same answer.
Rounding them to three decimal places gives 2.000 E+20 and 1.999 E+20, respectively.
Stuff like that.

  • // add comments!

If I am going to use a program even one day after I write it, I put a few comments to remind my tired old brain what the heck I did (and, maybe, even why the heck I did it). Additional comments to help others understand anything is often considered lagniappe (unless it's for public release, or it's a commercial environment and the code must be supported for future debugging or enhancement). I also like to make test program outputs verbose enough to let me see where the heck the numbers came from. I may wont to (or have to) refer back to comments in the code to tell me what the numbers mean (and how to see whether there are any errors).

Here's my test program:

//
// Test of function that builds a C-style "string" for a floating point
// number in scientific notation.
//
// davekw7x
// December, 2010
//
void setup()
{
    float f;
    Serial.begin(9600);

    f = 0.0;
    // No "digits" argument means 2 digits
    Serial.print("0.0 = ");Serial.println(float2s(f));Serial.println();
    
    // Rounding with leading zeros in the fractional part
    f = 10.3456;
    Serial.print(f,4);Serial.print(" = ");Serial.println(float2s(f, 4));Serial.println();
    
    /* 
       For loops 1 and 2, note that floating point
       overflows result values of"INF"
    */
    f = PI;
    Serial.println("Loop 1");
    for (int i=1; i< 15; i++)
    {
        Serial.println(float2s(f, 7));
        f *=1000;
    }
    Serial.println();

    f = -PI;
    Serial.println("Loop 2");
    for (int i=1; i< 15; i++)
    {
        Serial.println(float2s(f, 4));
        f *=1000;
    }  
    Serial.println();

    /*
       For loops 3 and 4 note that floating point
       underflows result in values that go to zero.
    */
    f = PI;
    Serial.println("Loop 3");
    for (int i=1; i< 19; i++)
    {
        Serial.println(float2s(f, 4));
        f /= 1000;
    }
    Serial.println();

    f = -PI;
    Serial.println("Loop 4");
    for (int i=1; i< 19; i++)
    {
        Serial.println(float2s(f, 4));
        f /= 1000;
    }
    Serial.println();

    /*
       Loop 5 shows rounding as follows:
        6: 1.999518 E+20
        5: 1.99952 E+20
        4: 1.9995 E+20
        3: 2.000 E+20
        2: 2.00 E+20
        1: 2.0 E+20
        0: 2 E+20
    */

    f = 1.999518e20;
    Serial.println("Loop 5");
    for (int i = 6; i >= 0; i--) {
        Serial.print(i);Serial.print(": ");Serial.println(float2s(f, i));
    }
    Serial.println();
    
    /* 
       Loop 6 shows rounding as follows:
        6: 1.999496 E+20
        5: 1.99950 E+20
        4: 1.9995 E+20
        3: 1.999 E+20
        2: 2.00 E+20
        1: 2.0 E+20
        0: 2 E+2
    */
    
    f = 1.999496e20;
    Serial.println("Loop 6");
    for (int i = 6; i >= 0; i--) {
        Serial.print(i);Serial.print(": ");Serial.println(float2s(f, i));
    }    
    Serial.println();
    
    Serial.println("NaN tests");
    f = sqrt(-1);
    Serial.print("sqrt(-1) = ");Serial.println(float2s(f, 3));

    f = 0.0/0.0;
    Serial.print("0.0/0.0 = ");Serial.println(float2s(f, 4));

    f = INFINITY * 0;
    Serial.print("INFINITY*0 = ");Serial.println(float2s(f, 5));Serial.println();

    
    Serial.println("INFINITY tests");
    f = 1.0/0;
    Serial.print("1.0/0 = ");Serial.println(float2s(f, 1));
    //printBytes(f); 

    f = -1.0/0;
    Serial.print("1.0/-0 = ");Serial.println(float2s(f, 1)); 
    //printBytes(f); 

    f = -1.0/0L;
    Serial.print("1.0/0L = ");Serial.println(float2s(f, 2));
    
    f = 1.0/0UL;
    Serial.print("1.0/0UL = ");Serial.println(float2s(f, 3));
    
    Serial.println();
    
    // Note that tan(pi/2) may not result in INF due
    // to limited precision.
    //
    f = tan(HALF_PI);
    Serial.print("tan(");Serial.print(HALF_PI, 6);Serial.print(") = ");  
    Serial.println(float2s(f, 6));
}

void loop()
{
}

With proper implementation of rounding, the output will look something like this:


[color=#0000ff]0.0 = 0.00 E+0

10.3456 = 1.0346 E+1

Loop 1
3.141593 E+0
3.141593 E+3
3.141593 E+6
3.141593 E+9
3.141593 E+12
3.141593 E+15
3.141593 E+18
3.141593 E+21
3.141593 E+24
3.141593 E+27
3.141593 E+30
3.141593 E+33
3.141593 E+36
INF

Loop 2
-3.1416 E+0
-3.1416 E+3
-3.1416 E+6
-3.1416 E+9
-3.1416 E+12
-3.1416 E+15
-3.1416 E+18
-3.1416 E+21
-3.1416 E+24
-3.1416 E+27
-3.1416 E+30
-3.1416 E+33
-3.1416 E+36
-INF

Loop 3
3.1416 E+0
3.1416 E-3
3.1416 E-6
3.1416 E-9
3.1416 E-12
3.1416 E-15
3.1416 E-18
3.1416 E-21
3.1416 E-24
3.1416 E-27
3.1416 E-30
3.1416 E-33
3.1416 E-36
3.1416 E-39
3.1417 E-42
2.8026 E-45
0.0000 E+0
0.0000 E+0

Loop 4
-3.1416 E+0
-3.1416 E-3
-3.1416 E-6
-3.1416 E-9
-3.1416 E-12
-3.1416 E-15
-3.1416 E-18
-3.1416 E-21
-3.1416 E-24
-3.1416 E-27
-3.1416 E-30
-3.1416 E-33
-3.1416 E-36
-3.1416 E-39
-3.1417 E-42
-2.8026 E-45
0.0000 E+0
0.0000 E+0

Loop 5
6: 1.999518 E+20
5: 1.99952 E+20
4: 1.9995 E+20
3: 2.000 E+20
2: 2.00 E+20
1: 2.0 E+20
0: 2 E+20

Loop 6
6: 1.999496 E+20
5: 1.99950 E+20
4: 1.9995 E+20
3: 1.999 E+20
2: 2.00 E+20
1: 2.0 E+20
0: 2 E+20

NaN tests
sqrt(-1) = NAN
0.0/0.0 = NAN
INFINITY*0 = NAN

INFINITY tests
1.0/0 = INF
1.0/-0 = -INF
1.0/0L = -INF
1.0/0UL = INF

tan(1.570796) = -2.287733 E+7[/color]

I'll show my take on a possible implementation of the function in my next post.

Regards,

Dave

Footnote:

Here's my observation based on about six months reading this forum:
The "Arduino Way" is designed to make things "easy to use" for people who don't know much about programming. Easy to get started without having to think about or to learn many of the fundamentals of C and C++. That part of the "Arduino Way" is the thing that drives many programmers crazy when people with no background need (or think they need) to use things that go beyond the fundamentals spelled out in example sketches in the distribution or in the Arduino playground.

I mean, a function that returns something that can be used in a Serial.print (or LiquidCrystal print) statement without making the calling function declare an array and use the name of the array as an argument is, in my opinion, the "Arduino Way." Permanently giving up 15 or 16 bytes of RAM in a given sketch to do things the "Arduino Way" may be acceptable. Or not.

Making this function take the name of an array (and the length) like we do with strncpy() and snprintf() and stuff like that is (probably) the way I would do it for my own use, but...

My final words on the "Arduino Way": Chacun à son goût!

Function code used with the sketch of the previous post to give the output shown there:

//
// davekw7x
// December, 2010
//
// Build a C-style "string" for a floating point variable in a static array.
// The optional "digits" parameter tells how many decimal digits to store
// after the decimal point.  If no "digits" argument is given in the calling
// function a value of 2 is used.
//
// Utility functions to raise 10 to an unsigned int power and to print
// the hex bytes of a floating point variable are also included here.
//
char *float2s(float f, unsigned int digits=2)
{
    static char buf[16]; // Buffer to build string representation
    int index = 0;       // Position in buf to copy stuff to

    // For debugging: Uncomment the following line to see what the
    // function is working on.
    //Serial.print("In float2s: bytes of f are: ");printBytes(f);

    // Handle the sign here:
    if (f < 0.0) {
        buf[index++] = '-'; 
        f = -f;
    }
    // From here on, it's magnitude

    // Handle infinities 
    if (isinf(f)) {
        strcpy(buf+index, "INF");
        return buf;
    }
    
    // Handle NaNs
    if (isnan(f)) {
        strcpy(buf+index, "NAN");
        return buf;
    }
    
    //
    // Handle numbers.
    //
    
    // Six or seven significant decimal digits will have no more than
    // six digits after the decimal point.
    //
    if (digits > 6) {
        digits = 6;
    }
    
    // "Normalize" into integer part and fractional part
    int exponent = 0;
    if (f >= 10) {
        while (f >= 10) {
            f /= 10;
            ++exponent;
        }
    }
    else if ((f > 0) && (f < 1)) {
        while (f < 1) {
            f *= 10;
            --exponent;
        }
    }

    //
    // Now add 0.5 in to the least significant digit that will
    // be printed.

    //float rounder = 0.5/pow(10, digits);
    // Use special power-of-integer function instead of the
    // floating point library function.
    float rounder = 0.5 / ipow10(digits);
    f += rounder;

    //
    // Get the whole number part and the fractional part into integer
    // data variables.
    //
    unsigned intpart = (unsigned)f;
    unsigned long fracpart  = (unsigned long)((f - intpart) * 1.0e7);

    //
    // Divide by a power of 10 that zeros out the lower digits
    // so that the "%0.lu" format will give exactly the required number
    // of digits.
    //
    fracpart /= ipow10(6-digits+1);

    //
    // Create the format string and use it with sprintf to form
    // the print string.
    //
    char format[16];
    // If digits > 0, print
    //    int part decimal point fraction part and exponent.

    if (digits) {
      
        sprintf(format, "%%u.%%0%dlu E%%+d", digits);
        //
        // To make sure the format is what it is supposed to be, uncomment
        // the following line.
        //Serial.print("format: ");Serial.println(format);
        sprintf(buf+index, format, intpart, fracpart, exponent);
    }
    else { // digits == 0; just print the intpart and the exponent
        sprintf(format, "%%u E%%+d");
        sprintf(buf+index, format, intpart, exponent);
    }

    return buf;
} 
//
// Handy function to print hex values
// of the bytes of a float.  Sometimes
// helps you see why things don't
// get rounded to the values that you
// might think they should.
//
// You can print the actual byte values
// and compare with the floating point
// representation that is shown in a a
// reference like
//    [urlhttp://en.wikipedia.org/wiki/Floating_point[/url]
//
void printBytes(float f)
{
    unsigned char *chpt = (unsigned char *)&f;
    char buffer[5]; // Big enough to hold each printed byte 0x..\0
    //
    // It's little-endian: Get bytes from memory in reverse order
    // so that they show in "register order."
    //
    for (int i = sizeof(f)-1; i >= 0; i--) {
        sprintf(buffer, "%02x ", (unsigned char)chpt[i]);
        Serial.print(buffer);
    }
    Serial.println();
}

//
// Raise 10 to an unsigned integer power,
// It's used in this program for powers
// up to 6, so it must have a long return
// type, since in avr-gcc, an int can't hold
// 10 to the power 6.
//
// Since it's an integer function, negative
// powers wouldn't make much sense.
//
// If you want a more general function for raising
// an integer to an integer power, you could make 
// "base" a parameter.
unsigned long ipow10(unsigned power)
{
    const unsigned base = 10;
    unsigned long retval = 1;

    for (int i = 0; i < power; i++) {
        retval *= base;
    }
    return retval;
}

Note that I did not use the math library functions pow() or log(). In fact, I almost feel as if it is cheating to use floating point arithmetic. I mean, the most efficient way (probably) would be to break up the floating point representation of the number into its one-bit sign field, eight-bit biased exponent field and 23-bit reduced significand field as integer data types and work on them directly with integer arithmetic. (I started this, for my own edification, in a different project where I was comparing avr-gcc with gcc used on my Linux and Windows workstations, but didn't feel like finishing it here.) Oh, well...

If there are any problems, I would appreciate seeing test cases.

If there are any suggestions for improvements in performance or readability or anything else, I would appreciate that also.

If there are any objections to my rambling, stream-of-conscious, not-always-entirely-grammatical writing style, well times are tough and I had to lay off my proofreader and editor...

Regards,

Dave

Good work Dave, did not test your implementation (maybe tomorrow) but at least one thing tickled me:

if (f >= 10) {
        while (f >= 10) {
            f /= 10;
            ++exponent;
        }
    }

To bring the number to the range [1.0 .. 10.0> I used the pow method to minimize the number of FP ops. In your code the number 3.2 E+20 gets divided 20 times in your code while only 2 in mine + 3 from the pow() function makes 5 in total independant of the size of the float. - assuming pow(x,y) = e^(y* ln(x) -

A similar problem I got in the test loop where pi is multiplied by 1000 every time. If you change that to *10 you see the effect of rounding errors in floating point math.

I'll come back to this one soon.

tickled me

Hmmm...You must be easily amused.

minimize the number of FP ops

Well, inspired by your original post, I just kind of threw the thing together. I make no claims for optimal code size or speed. My approach to problem solving is to implement first and optimize later if it becomes important. I always felt that integer powers might be performed more efficiently with integer arithmetic, so I did throw that function into the mix, but I didn't actually test the speed or size of the result.

In other words...

I deliberately kept away from math library functions. Do you have any guesses (or measurements) as to the number of operations in the log10() function used to calculate your exponent value?

Since the avr-libc math library consists of some pretty tight assembly language code (in my opinion), maybe that's the better way to go. If I were submitting (or selling) this as a general purpose library function, I might do some benchmarking with size and speed and publish the results. In the meanwhile and in the short run, I like to concentrate on the numbers. But: See Footnote [1].

pi is multiplied by 1000 every time. If you change that to *10 you see the effect of rounding errors in floating point math.

I noticed bad outputs from the first loop in the sketch in your initial post.


[color=#0000ff]3.141592 E+0
3.141593 E+3
3.141593 E+6
3.141593 E+9
3.141594 E+12
3.141599 E+15
3.141595 E+18
3.141608 E+21
3.141596 E+24
3.141601 E+27
3.141605 E+30
3.141610 E+33
3.141598 E+36
INF
[/color]

These errors are not the result of cumulative floating point roundoff/arithmetic errors in the multiplication in the test sketch; they come from the function. That's why I did my own. (Based on, and inspired by, yours.)

With the program that I posted, the seven-significant-digit magnitudes are shown as 3.141593 for all values of exponents that don't involve floating point overflow or underflow. I did not observe any floating point arithmetic (roundoff) errors for printed values in the examples that I showed.

I mean, every floating point multiplication can result in a bit or two of roundoff error, and they can accumulate

So...

I changed the code for Loop 1 in my sketch to this:

    f = PI;
    Serial.println("Loop 1");
    for (int i = 0; i < 40; i++)
    {
        Serial.println(float2s(f, 7));
        f *= 10;
    }

Here are the results from loop1:


[color=#0000ff]Loop 1
3.141593 E+0
3.141593 E+1
3.141593 E+2
3.141593 E+3
3.141593 E+4
3.141593 E+5
3.141593 E+6
3.141593 E+7
3.141593 E+8
3.141593 E+9
3.141593 E+10
3.141593 E+11
3.141593 E+12
3.141593 E+13
3.141593 E+14
3.141593 E+15
3.141593 E+16
3.141593 E+17
3.141593 E+18
3.141593 E+19
3.141593 E+20
3.141593 E+21
3.141593 E+22
3.141593 E+23
3.141593 E+24
3.141593 E+25
3.141593 E+26
3.141593 E+27
3.141593 E+28
3.141593 E+29
3.141593 E+30
3.141593 E+31
3.141593 E+32
3.141593 E+33
3.141593 E+34
3.141593 E+35
3.141593 E+36
3.141593 E+37
3.141593 E+38
INF[/color]

You can use my printBytes() function to verify that, after repeatedly multiplying f by 10 each time through the loop for a total of 38 times, the bytes of the result are

[color=#0000ff]7f 6c 58 e1[/color]

And the result of a single multiplication of PI by 1.0E38 has the following bytes

[color=#0000ff]7f 6c 58 e0[/color]

A grand cumulative error of one (least significant) bit in the binary representation of the magnitude, and it doesn't even show up for the correctly rounded values that I printed. Not too shabby, yes? Magic!

Regards,

Dave

Footnotes:
[1] "The purpose of computing is insight, not numbers."
---Richard W. Hamming

[2] "What the heck! I like numbers. You show me yours and I'll show you mine."
---davekw7x

Hmmm...You must be easily amused.

Definitely, a day not laughed is a day not lived :slight_smile:

So I tried your version and I admit it is better, more stable.

Do you have any guesses (or measurements) as to the number of operations in the log10() function used to calculate your exponent value?

Searched math.h and searched the avr-libc-1.7.0 for the code of log10() and found the following snippet.

#define      INV_LN_10      0x3ede5bd9      /* 1.0/log(10.0)      */

ENTRY log10
      rcall      _U(log)
      ldi      rB0,  lo8(INV_LN_10)
      ldi      rB1,  hi8(INV_LN_10)
      ldi      rB2, hlo8(INV_LN_10)
      ldi      rB3, hhi8(INV_LN_10)
      rjmp      _U(__mulsf3)
ENDFUNC

So log10(x) is defined as log(x) * 1/log(10) where log(x) is the logarithmus naturalis or ln(x) and the source for that is quite complex - too time consuming to analyse - but a quick scan saw at least 5 calls to other functions, bringing the #math operations for log10(x) to at least 6.

Idea: breakup the float => get the exponent in base 2 and convert it to base 10?
Given 2^a = 10^x
==> x = a * ln2 / ln10;
==> x = a * 0,30102999566398119521373889472449; (calc.exe)
==> x = a * 0,30103; // Should do it, as exp = -38..+38

Some code optimization at the end. No need to do a (complex) sprintf to create the format string. bit faster (7 millis upon 125 ~5%).

  if (digits > 0)
  {
    char format[16] = "%u.%0Xlu E%+d";  // X will be replaced
    format[5] = '0' + digits;
    sprintf(buf + index, format, intpart, fracpart, exponent);
  } else {
    sprintf(buf + index, "%u E%+d", intpart, exponent);
  }
  return buf;