Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

PHP: format any float as a decimal expansion

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 0s. However, this idea is flawed because the decimal expansion often doesn't end with 0s:

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 is 0.0, then log10($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 0s:

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?

like image 499
TachyonVortex Avatar asked Mar 08 '14 20:03

TachyonVortex


People also ask

How can I set 2 decimal places in PHP?

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

Does PHP support float?

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.

Is float and double same PHP?

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.


1 Answers

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;

}
like image 93
RandomSeed Avatar answered Oct 05 '22 13:10

RandomSeed