Proposed update for the printFloat code of print.cpp

In the Arduino Core library the printing of floats is done with the use of unsigned long numbers.
The current 1.0.4 implementation prints "ovf" when a float is larger that maxlong (approx).
This is not correct.

To solve this I added code to printFloat so it will print print large ( > 1E+9) and small ( < 1E-3) numbers automatically in scientific notation: x.xxxxE+nn or x.xxxxE-nn

There is also the engineering E-notation that only allows multiples of 3 as exponent. Could be implemented in a similar way. However that would require an interface change of print() for floats as one should be able to select the representation. The interface would change to something like print(float number, uint8_t digits, uint8_t repr = 0); For repr there could be defines or an enum { NORMAL, ENGINEERING, SCIENTIFIC, .... } or just 0,1,2

please review and test.

printFloat() can be found in print.cpp - one of the core libs.
update: do not use this code, it has some bugs ; => use zip file from reply #10 instead

size_t Print::printFloat(double number, uint8_t digits) 
{ 
  size_t n = 0;
  int exponent = 0;
  
  if (isnan(number)) return print("nan");
  if (isinf(number)) return print("inf");
 
  if ( abs(number) > 1000000000.0)
  { 
    while (abs(number) > 10.0)
    {
      number /= 10.0;
      exponent++;
    }
  } 
  else if (abs(number) < 0.001) 
  { 
    while (abs(number) < 1.0)
    {
      number *= 10.0;
      exponent--;
    }
  }

  // Handle negative numbers
  if (number < 0.0)
  {
     n += print('-');
     number = -number;
  }

  // Round correctly so that print(1.999, 2) prints as "2.00"
  double rounding = 0.5;
  for (uint8_t i=0; i<digits; ++i)
    rounding /= 10.0;
  
  number += rounding;

  // Extract the integer part of the number and print it
  unsigned long int_part = (unsigned long)number;
  double remainder = number - (double)int_part;
  n += print(int_part);

  // Print the decimal point, but only if there are digits beyond
  if (digits > 0) {
    n += print("."); 
  }

  // Extract digits from the remainder one at a time
  while (digits-- > 0)
  {
    remainder *= 10.0;
    int toPrint = int(remainder);
    n += print(toPrint);
    remainder -= toPrint; 
  } 
  
if (exponent != 0)
  {
    n += print('E');
    if (exponent > 0)
    {
       n += print('+');
    }
    n += print(exponent);
  }

  return n;
}

Simple test sketch to show it works at least for some values

//
//    FILE: testEnotation.ino
//  AUTHOR: Rob Tillaart
//    DATE: 2013-05-12
//
// PURPOSE: test scientific notation above 1E9
//
void setup()
{
  Serial.begin(115200);

  float f = 355.0/113.0;
  for (int i = 0; i < 45; i++)
  {
    Serial.println(f, 6);
    f = f * 10;
  }

  f = 355.0/113.0;
  for (int i = 0; i < 45; i++)
  {
    Serial.println(f, 6);
    f = f / 10;
  }  
}

void loop()
{
}

as always comments are welcome

also posted as a issue on github - Proposed update for the printFloat code of print.cpp · Issue #247 · arduino/ArduinoCore-avr · GitHub -

I am tinkering with changing the interface for print for floats as mentioned above

print(float number, uint8_t digits = 2, uint8_t notation = 0);
println(float number, uint8_t digits = 2, uint8_t notation = 0);

by using the 0 as default parameter for notation, it will be compatible with existing sketches.

For the notation parameter I would like to use terms familiar from calculators.

#define DEF 0   // my calculator shows nothing so DEF from DEFAULT
#define SCI 1
#define ENG 2
#define FIX 3    // not implemented

is this enough?
are there modes missing?
does one need/like long symbolic names: DEFAULT, SCIENTIFIC, ENGINEERING, ?
other remarks wrt the interface?

Another question: what is prefered: E or e or nothing, and why? (in my order of preference)

3.141593E+25
3.141593+25 // uses 1 char less footprint on e.g. LCD ==> separate notation mode? e.g. println(355.0/113.0, 6, SCI_SHORT);
3.141593e+25 // less readable

small update to support negative infinity

  if (isnan(number)) return print("nan");
  if (isinf(number))
  { 
    if (number < 0) print("-");
    return print("inf");
  }

Question: with normal printing the print "looses" digits in the output, like this

-3.141593
-0.314159
-0.031416
-0.003142
-3.141593-4
-3.141593-5
-3.141593-6

Question: should the scientific notation be used for all numbers < 1.0 or < 0.001 or < 0.1 or 0.5 ? or other value? Why?

So like this

-3.141593  
-3.141593-1
-3.141593-2
-3.141593-3
-3.141593-4
-3.141593-5
-3.141593-6

Beta of new print.cpp and print.h, to be installed in -- C:\Program Files (x86)\arduino-1.0.4\hardware\arduino\cores\arduino

disclaimers:

  • not tested thoroughly
  • not tested with earlier versions,
  • use at your own risk...
    update: depreciated, use zip from reply #10

print.zip (3.06 KB)

When testing the beta print.cpp/.h (from the zip above) one can get a warning about redefine of DEFAULT.

solution: remove the line "#define DEFAULT 0" from print.h file to remove the warning (one can use DEF instead or just use no 3rd param).

Another question: what is prefered: E or e or nothing, and why? (in my order of preference)

3.141593E+25
3.141593+25 // uses 1 char less footprint on e.g. LCD ==> separate notation mode? e.g. println(355.0/113.0, 6, SCI_SHORT);
3.141593e+25 // less readable

Excel gave the answer, when writing to a csv file for Excel it did not recognize the 3.141593+25 formatted value as a valid number.
I'm going for the big E notation as this is more readable on all screens than lower case.

Question: should the scientific notation be used for all numbers < 1.0 or < 0.001 or < 0.1 or 0.5 ? or other value? Why?

The default for printing floats shows 2 decimal places. That means that values < 0.01 will be printed as 0.00 even when they are not zero.

Because the E notation would use more chars on screen it might screw up the layout on an LCD display
e.g. 1/1000 => 1.00E-3 so that is 3 chars more than 0.00

So in conclusion:

  • for the default mode small numbers will not be printed in E notation, only large numbers will.
    This keeps it backwards compatible in terms of the max #places of the number.
  • for the SCI & ENG mode numbers smaller than 1.0 will be printed in E notation.

does one need/like long symbolic names: DEFAULT, SCIENTIFIC, ENGINEERING, ?

No, => DEFAULT is already defined and keeping the SCI and ENG (same as on calculator) is sufficient.

note: did testing, fixed bugs, optimized code ==> Attached new version of print.zip, imho the SCI and ENG work good enough for now

update: test sketch used to test new printFloat routine

//
//    FILE: testEnotation.ino
//  AUTHOR: Rob Tillaart
//    DATE: 2013-05-12
//
// PURPOSE: test scientific notation
//
void setup()
{
  Serial.begin(115200);


  Serial.println("\tout of range values\n");
  Serial.println(1.0/0.0, 6);
  Serial.println(-1.0/0.0, 6);
  Serial.println(sin(0)/0, 6);
  Serial.println("---------\n");

  Serial.println("\tprintln(f, 6)\n");
  float f = -PI;
  for (int i = 0; i < 45; i++)
  {
    Serial.println(f, 6);
    f = f * 10;
  }
  Serial.println("---------\n");


  Serial.println("\tprintln(f, 6)\n");
  f = PI;
  for (int i = 0; i < 45; i++)
  {
    Serial.println(f, 6);
    f = f / 10;
  }  
  Serial.println("---------\n");


  Serial.println("\tprintln(f, 6, SCI)\n");
  f = PI;
  for (int i = 0; i < 45; i++)
  {
    Serial.println(f, 6, SCI);
    f = f * 10;
  }
  Serial.println("---------\n");


  Serial.println("\tprintln(f, 6, ENG)\n");
  f = PI;
  for (int i = 0; i < 45; i++)
  {
    Serial.println(f, 6, ENG);
    f = f * 10;
  }
  Serial.println("---------\n");


  Serial.println("\tprintln(f, 6, -1) // not supported -> default\n");
  f = -PI;
  for (int i = 0; i < 50; i++)
  {
    Serial.println(f, 6, -1);  // illegal param
    f = f / 10;
  }  
  Serial.println("---------\n");


  Serial.println("\tnumber without decimal part\n");
  f = 12345.0;
  Serial.println(f); // default 2
  Serial.println(0);  // int converted to float
  Serial.println((float)12345, 2);  // int converted to float
  Serial.println((float)-12345, 2);  
  Serial.println("---------\n");

  Serial.println(-0, 6, SCI);
  Serial.println(12345, 6, SCI);
  Serial.println(-12345, 6, SCI);
  Serial.println("---------\n");

  Serial.println(0, 6, ENG);
  Serial.println(12345, 6, ENG);
  Serial.println(-12345, 6, ENG);
  Serial.println("---------\n");

  Serial.println(0, 6, -1);
  Serial.println(12345, 6, -1);
  Serial.println(-12345, 6, -1);
  Serial.println("---------\n");

  Serial.println("\tspecial number\n");
  f = 10.0;
  Serial.println(f); // default 2
  Serial.println(f, 2);
  Serial.println(f, 2, SCI);  
  Serial.println(f, 2, ENG);
  Serial.println("---------\n");

  Serial.println("\ndone\n");
}

void loop() {}

Print.zip (3.25 KB)

Cool!
It seems there is an issue with when single precision exp < -38 and ENG notation

3.1416E-36
314.1593E-39
31.4159E-39
3.1416E-39
314.1600E-42
31.4157E-42
3.1417E-42
313.8908E-45
30.8286E-45

The same with SCI:

3.1416E-35
3.1416E-36
3.1416E-37
3.1416E-38
3.1416E-39
3.1416E-40
3.1416E-41
3.1417E-42
3.1389E-43
3.0829E-44

It seems there is an issue with when single precision exp < -38 and ENG notation

It is reaching the end of the 32-bit float universe. From - Single-precision floating-point format - Wikipedia -
The minimum positive (subnormal) value ? 1.4 × 10^?45.
The minimum positive normal value ? 1.18 × 10^?38.
The maximum representable value ? 3.4 × 10^38.

With ENG notation you have constant a variation of the (significant) digits where SCI has the same every time.
If you round the ENG numbers you should get the SCI ones - check it -

Personally I go for SCI as it has a constant length (except for sign*) and will always fit on a fixed area on a LCD.

(*)maybe add an explicit leading sign would enforce this for positive and negative numbers? like a multimeter?

With ENG notation you have constant a variation of the (significant) digits where SCI has the same every time.

What about "constant significant digits ENG"? For example:

3.141E-9
314.1E-12
31.41E-12
3.141E-12

In engineering practice you do care more about the total number of "valid" digits in ENG, rather than number of digits after the decimal point..

What about "constant significant digits ENG"? For example:

makes sense, would mean that the parameter "digits" get another meaning when using the ENG option.
It is not complex to code, so when time permits I'll give it a try.

Hi,

how do I use this library?

if I just import it, I can't instantiate any Print object because it's virutal...

Any suggestion?

You don't directly instantiate it. This is a replacement for the Print.cpp file in the hardware/arduino/avr/cores/arduino directory. You overwrite the files in that directory with the new ones. This re-defines the print class which propagates to Serial, HardwareSerial and so on.

If you do this, then you must do it again everytime you update the Arduino IDE installation because that will re-install a default Print.cpp file.

Included updated code for print.cpp and print.h work with IDE 1.8.1
This is not the latest version IDE and I will check 1.8.5 compatibility a.s.a.p.

This code also prints 64 bit integers, signed and unsigned.
(so you can get that prime sieve beyond 32 bit)

Unfortunatelly I did not have time to test all integers and floats :wink:
so remarks and comments are welcome.

Print_20171009.zip (4.31 KB)

Checked compatibility with IDE 1.8.5 ==> OK

aweatherguy.... I follow your instruccions and replace both files print.h and print.cpp into my directory

C:\Program Files (x86)\Arduino\hardware\arduino\avr\cores\arduino... but it is still not working.

My example code is:

double Wo = 500;
double L5 ;
double resultado;

void setup()
{
Serial.begin(9600);
}

void loop()

{
L5 = pow(Wo,5);
Serial.println(L5);
resultado = L5 - (L5-2);//31249999999999;
Serial.println(resultado);
while(1);
}

And the serial monitor is showing

ovf
2.00

I even try removing the files (print.h, print.cpp) from the directory, expecting to see a error message. But de IDE still showing

ovf
2.00