PHP: formatting any float as a decimal extension

I would like to create a formatFloat() function that accepts any float and formats it as a decimal extension 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

Just casting a float for a string will not work, because for a float more than about 1.0E+14 , or less about 1.0E-4 , PHP maps them to scientific notation instead of decimal extension .

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 the number of decimal digits for the query. One idea is to ask for a large number of decimal digits, and then rtrim() an excess of 0 s. However, this idea is wrong, since the decimal extension often does not 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 the floating point number is of limited precision and is usually unable to maintain the exact literal value (for example: 1.0E+25 ). Instead, it retains the highest possible value that can be represented. number_format() expands these "coming approximations".

Timo Frenay Solution

I found this comment deeply immersed in sprintf() , surprisingly without magnification:

Here's how to print a floating point number with 16 significant digits, regardless of size:

 $result = sprintf(sprintf('%%.%dF', max(15 - floor(log10($value)), 0)), $value); 

The key part is to use log10() to determine the order of magnitude , then to calculate the number of decimal digits.

There are several errors that need to be fixed:

  • The code does not work for negative floats.
  • The code does not work for extremely small floats (for example: 1.0E-100 ). PHP reports this notification: " sprintf() : The requested precision of 116 digits was truncated to PHP no more than 53 digits"
  • If $value is 0.0 , then log10($value) is -INF .
  • Since the precision of the PHP float is "about 14 decimal digits", I think that instead of 16, 14 significant digits will be displayed.

My best attempt

This is the best solution I've come across. It is based on Timo Frenay's solution, fixes bugs, and uses regex ThiefMaster to trim 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 a Ideone demo with 200 random floats. The code seems to work correctly for all floats smaller than about 1.0E+15 .

It is interesting to see that number_format() works correctly even for extremely small floats:

 formatFloat(1.000001E-250); // "0.0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000001" 

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?

+7
floating-point php floating-point-conversion
source share
3 answers

This piece of code seems to do the job too . I don’t think I managed to make it 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; } 
+2
source share

I managed to create this (rather inelegant) solution.

If the float is less than 1.0E+14 , then it uses my "best try" code from my question. Otherwise, it rounds the integer to 14 significant digits.

Here's an Ideone demo with 500 random floats, and the code seems to work correctly for all of them.

As I said, this is not a very elegant implementation, so I’m still very interested to know if anyone can come up with a better solution.

 function formatFloat($value) { $phpPrecision = 14; if ($value == 0.0) return '0.0'; if (log10(abs($value)) < $phpPrecision) { $decimalDigits = max( ($phpPrecision - 1) - floor(log10(abs($value))), 0 ); $formatted = number_format($value, $decimalDigits); // Trim excess 0's $formatted = preg_replace('/(\.[0-9]+?)0*$/', '$1', $formatted); return $formatted; } $formattedWithoutCommas = number_format($value, 0, '.', ''); $sign = (strpos($formattedWithoutCommas, '-') === 0) ? '-' : ''; // Extract the unsigned integer part of the number preg_match('/^-?(\d+)(\.\d+)?$/', $formattedWithoutCommas, $components); $integerPart = $components[1]; // Split into significant and insignificant digits $significantDigits = substr($integerPart, 0, $phpPrecision); $insignificantDigits = substr($integerPart, $phpPrecision); // Round the significant digits (using the insignificant digits) $fractionForRounding = (float) ('0.' . $insignificantDigits); $rounding = (int) round($fractionForRounding); // Either 0 or 1 $rounded = $significantDigits + $rounding; // Pad on the right with zeros $formattingString = '%0-' . strlen($integerPart) . 's'; $formatted = sprintf($formattingString, $rounded); // Insert a comma between every group of thousands $formattedWithCommas = strrev( rtrim( chunk_split( strrev($formatted), 3, ',' ), ',' ) ); return $sign . $formattedWithCommas; } 
+1
source share
 number_format($result, 14, '.', ''); 
-one
source share

All Articles