Tuesday, November 4, 2008

PHP and outputting floats

I was writing a script in PHP to compare two data sources, and in the report I was outputting using code like this:
echo "A={$vA}, B={$vB}\n";

What was strange is I was getting entries like
A=3.4E+6, B=340000
A=1.4E+6, B=1500000
A=2146666.66667, B=2.3E+6

My first guess was some data was int, so I added asserts for that, but, no, everything is float.

As often happens, the answer was to be found in the user contributed notes in the PHP online manual: http://jp.php.net/manual/en/language.types.float.php#83577

I just have to repeat his final sentence: I have to be honest: this is one of the strangest things I have seen in any language in over 20 years of coding, and it is a colossal pain to work around.

Also in the notes are complaints that output of floating point numbers uses the locale. Using number_format is one solution for that, or using %F in sprintf is also locale-independent apparently. So my code becomes:
echo sprintf("A=%F, B=%F\n",$vA,$vB);

Unfortunately that now gives me entries like:
A=481.000000,B=1150000.000000

I have hit this zero-noise issue using sprintf in C++ too.

So here is my solution (be aware that this adds CPU cycles):

/**
* Format a float, only showing significant decimal places
*
* @param float $v
* @return String
*/
function fmt_float($v){
$s=(string)$v;
if(strpos($s,'E+')===false)return $s;
$s=sprintf("%.9F",$v); //Write with all decimal places
$s=rtrim($s,'0'); //Chop off trailing zeroes
$s=rtrim($s,'.'); //Chop off decimal point if it is left at the end.
return $s;
}
...
echo "A=".fmt_float($vA).", B=".fmt_float($vB)."\n";

I'll throw that into the next fclib release. Can anyone make a better version?

Note: The first two lines mean use PHP's built-in float to string conversion by default and just use our own when it ended up in scientific notation. I found this gave better results (because using %.9F sometimes writes 120.0000000001 instead of simply 120.000000000, whereas the PHP built-in conversion is fine (giving "120") ). However if you also needed a locale-independent solution, then uncomment those first two lines (as PHP's built-in conversion uses locale).