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"
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".
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:
1.0E-100
). PHP reports this notice: "sprintf()
: Requested precision of 116 digits was truncated to PHP maximum of 53 digits"$value
is 0.0
, then log10($value)
is -INF
.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"
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?
$twoDecNum = sprintf('%0.2f', round($number, 2)); The rounding correctly rounds the number and the sprintf forces it to 2 decimal places if it happens to to be only 1 decimal place after rounding. Show activity on this post. This will give you 2 number after decimal.
PHP Floats The float data type can commonly store a value up to 1.7976931348623E+308 (platform dependent), and have a maximum precision of 14 digits. PHP has the following predefined constants for floats (from PHP 7.2): PHP_FLOAT_MAX - The largest representable floating point number.
There is no difference in PHP. float , double or real are the same datatype. At the C level, everything is stored as a double . The real size is still platform-dependent.
This piece of code seems to do the job too. I don't think I managed to make it any more elegant than yours, but I spent so much time on it that I can't just throw it away :)
function formatFloat(
$value,
$noOfDigits = 14,
$separator = ',',
$decimal = '.'
) {
$exponent = floor(log10(abs($value)));
$magnitude = pow(10, $exponent);
// extract the significant digits
$mantissa = (string)abs(round(($value / pow(10, $exponent - $noOfDigits + 1))));
$formattedNum = '';
if ($exponent >= 0) { // <=> if ($value >= 1)
// just for pre-formatting
$formattedNum = number_format($value, $noOfDigits - 1, $decimal, $separator);
// then report digits from $mantissa into $formattedNum
$formattedLen = strlen($formattedNum);
$mantissaLen = strlen($mantissa);
for ($fnPos = 0, $mPos = 0; $fnPos < $formattedLen; $fnPos++, $mPos++) {
// skip non-digit
while($formattedNum[$fnPos] === $separator || $formattedNum[$fnPos] === $decimal || $formattedNum[$fnPos] === '-') {
$fnPos++;
}
$formattedNum[$fnPos] = $mPos < $mantissaLen ? $mantissa[$mPos] : '0';
}
} else { // <=> if ($value < 1)
// prepend minus sign if necessary
if ($value < 0) {
$formattedNum = '-';
}
$formattedNum .= '0' . $decimal . str_repeat('0', abs($exponent) - 1) . $mantissa;
}
// strip trailing decimal zeroes
$formattedNum = preg_replace('/\.?0*$/', '', $formattedNum);
return $formattedNum;
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With