formatFloat(1.0E+25); // "10,000,000" formatFloat(1.0E+24); // "1,000" formatFloat(1.000001); // "1.000001" formatFloat(1.000001E-10); // "0.0000000001000001" formatFloat(1.000001E-11); // "0.00000000001000001"
初步想法
简单地说casting浮点到一个字符串将不起作用,因为对于大于约1.0E 14或小于约1.0E-4的浮点数,PHP在scientific notation而不是decimal expansion中呈现它们.
number_format()
是一个明显的PHP函数.但是,大浮动会出现此问题:
number_format(1.0E+25); // "10,905,969,664" number_format(1.0E+24); // "999,999,983,222,784"
对于小浮点数,难点在于选择要求的小数位数.一个想法是要求大量的十进制数字,然后rtrim()
超过0.但是,这个想法是有缺陷的,因为十进制扩展通常不会以0结尾:
number_format(1.000001,30); // "1.000000999999999917733362053696" number_format(1.000001E-10,30); // "0.000000000100000099999999996746" number_format(1.000001E-11,30); // "0.000000000010000010000000000321"
问题是浮点数具有limited precision,并且通常无法存储文字的确切值(例如:1.0E 25).相反,它存储可以表示的最接近的可能值. number_format()揭示了这些“最接近的近似值”.
Timo Frenay的解决方案
我在sprintf()
页面深处埋没了this comment,令人惊讶的是没有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);
关键部分是使用log10()
来确定浮点数的order of magnitude,然后计算所需的小数位数.
有一些需要修复的错误:
>该代码不适用于负浮动.
>该代码不适用于极小的浮点数(例如:1.0E-100). PHP报告此通知:“sprintf():请求的116位数的精度被截断为PHP最多53位”
>如果$value为0.0,则log10($value)为-INF.
>由于PHP float的精度是“大约14位小数”,我认为应该显示14位有效数字而不是16位数.
我最好的尝试
这是我提出的最佳解决方案.它基于Timo Frenay的解决方案,修复了错误,并使用ThiefMaster’s regex修剪多余的0:
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有200个随机浮点数.代码似乎适用于小于约1.0E 15的所有浮点数.
有趣的是,即使是非常小的浮点数,number_format()也能正常工作:
formatFloat(1.000001E-250); // "0.0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000001"
这个问题
我对formatFloat()的最佳尝试仍然遇到这个问题:
formatFloat(1.0E+25); // "10,664" formatFloat(1.0E+24); // "999,784"
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; }