I'd like to create a function formatFloat()
which takes any float and formats it as a decimal expansion string. For example:
formatFloat(1.0E+25); // "10,000,000,000,000,000,000,000,000"
formatFloat(1.0E+24); // "1,000,000,000,000,000,000,000,000"
formatFloat(1.000001); // "1.000001"
formatFloat(1.000001E-10); // "0.0000000001000001"
formatFloat(1.000001E-11); // "0.00000000001000001"
Initial ideas
Simply casting the float to a string won't work, because for floats larger than about 1.0E+14
, or smaller than about 1.0E-4
, PHP renders them in scientific notation instead of decimal expansion.
number_format()
is the obvious PHP function to try. However, this problem occurs for large floats:
number_format(1.0E+25); // "10,000,000,000,000,000,905,969,664"
number_format(1.0E+24); // "999,999,999,999,999,983,222,784"
For small floats, the difficulty is choosing how many decimal digits to ask for. One idea is to ask for a large number of decimal digits, and then rtrim()
the excess 0
s. However, this idea is flawed because the decimal expansion often doesn't end with 0
s:
number_format(1.000001, 30); // "1.000000999999999917733362053696"
number_format(1.000001E-10, 30); // "0.000000000100000099999999996746"
number_format(1.000001E-11, 30); // "0.000000000010000010000000000321"
The problem is that a floating point number has limited precision, and is usually unable to store the exact value of the literal (eg: 1.0E+25
). Instead, it stores the closest possible value which can be represented. number_format()
is revealing these "closest approximations".
Timo Frenay's solution
I discovered this comment buried deep in the sprintf()
page, surprisingly with no upvotes:
Here is how to print a floating point number with 16 significant digits regardless of magnitude:
$result = sprintf(sprintf('%%.%dF', max(15 - floor(log10($value)), 0)), $value);
The key part is the use of log10()
to determine the order of magnitude of the float, to then calculate the number of decimal digits required.
There are a few bugs which need fixing:
- The code doesn't work for negative floats.
- The code doesn't work for extremely small floats (eg:
1.0E-100
). PHP reports this notice: "sprintf()
: Requested precision of 116 digits was truncated to PHP maximum of 53 digits" - If
$value
is0.0
, thenlog10($value)
is-INF
. - Since the precision of a PHP float is "roughly 14 decimal digits", I think 14 significant digits should be displayed instead of 16.
My best attempt
This is the best solution I've come up with. It's based on Timo Frenay's solution, fixes the bugs, and uses ThiefMaster's regex for trimming excess 0
s:
function formatFloat($value)
{
if ($value == 0.0) return '0.0';
$decimalDigits = max(
13 - floor(log10(abs($value))),
0
);
$formatted = number_format($value, $decimalDigits);
// Trim excess 0's
$formatted = preg_replace('/(\.[0-9]+?)0*$/', '$1', $formatted);
return $formatted;
}
Here's an Ideone demo with 200 random floats. The code seems to work correctly for all floats smaller than about 1.0E+15
.
It's interesting to see that number_format()
works correctly for even extremely small floats:
formatFloat(1.000001E-250); // "0.0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000001"
The question
My best attempt at formatFloat()
still suffers from this problem:
formatFloat(1.0E+25); // "10,000,000,000,000,000,905,969,664"
formatFloat(1.0E+24); // "999,999,999,999,999,983,222,784"
Is there an elegant way to improve the code to solve this problem?