我想创建一个函数 formatFloat()
,它接受任何 float 并将其格式化为十进制扩展字符串。例如:
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"
初步想法
简单的 casting float 到字符串是行不通的,因为对于大于 1.0E+14
或小于 1.0E-4
的 float ,PHP在 scientific notation 而不是 decimal expansion 中呈现它们。
number_format()
是可以尝试的明显 PHP 函数。但是,对于大的 float 会出现这个问题:
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"
对于小 float ,困难在于选择要求多少小数位。一种想法是要求大量的十进制数字,然后 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"
问题是 float 有 limited precision ,通常无法存储字面量的准确值(例如:1.0E+25
)。相反,它存储可以表示的最接近的可能值。 number_format()
揭示了这些“最接近的近似值”。
Timo Frenay 的解决方案
我发现 this comment 深埋在 sprintf()
页面中,令人惊讶的是没有投票:
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()
确定 float order of magnitude,进而计算出需要的小数位数。
有一些错误需要修复:
- 该代码不适用于负 float 。
- 该代码不适用于极小的 float (例如:
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 个随机 float 。对于小于 1.0E+15
的所有 float ,该代码似乎都能正常工作。
有趣的是 number_format()
即使对于极小的 float 也能正常工作:
formatFloat(1.000001E-250); // "0.0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000001"
问题
我对 formatFloat()
的最佳尝试仍然遇到这个问题:
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"
有没有一种优雅的方法来改进代码来解决这个问题?
最佳答案
我已经设法创建了这个(相当不优雅的)解决方案。
如果 float 小于 1.0E+14
,那么它会使用我问题中的“最佳尝试”代码。否则,它会将整数部分四舍五入为 14 位有效数字。
Here's an Ideone demo有 500 个随机 float ,代码似乎对所有这些都正确工作。
正如我所说,这不是一个非常优雅的实现,所以我仍然很想知道是否有人可以设计出更好的解决方案。
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;
}
关于PHP:将任何 float 格式化为十进制扩展,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/22274437/