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).


2 comments:

darren said...

So, what I originally had (instead of the two rtrim() lines) was:
$s=sprintf("%F",$v);
$s=preg_replace('|([.0]*)$|','',$s);

Can you spot the bug?
Here's a hint. What does 120.0000 become?

A bug fix gives us:
$s=preg_replace('|(0*)$|','',$s); //Remove trailing zeroes
* if($s[strlen($s)-1]=='.')$s=substr($s,0,strlen($s)-1); //Chop off decimal point if it is final character

But then I remembered rtrim() takes an optional 2nd parameter, and is both clearer and almost certainly quicker.

darren said...

I just got bitten by a similar problem:
if($n!=$expected_n){
echo "Expected $expected_n but got $n\n";
}

was complaining with this message:
Expected 8.66666666667, but got 8.66666666667.

Spooky isn't it! The explanation is that $n is a float, while $expected_n is a string. So for the comparison the string is cast to a float! And we already know the problems that can cause.

The solution is to change the comparison to:
if(abs($n-$expected_n)>1.0e-8){...}