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;
}