PHP:将任何浮点数格式化为十进制扩展
Posted
技术标签:
【中文标题】PHP:将任何浮点数格式化为十进制扩展【英文标题】:PHP: format any float as a decimal expansion 【发布时间】:2014-03-08 20:09:24 【问题描述】:我想创建一个函数formatFloat()
,它接受任何浮点数并将其格式化为十进制扩展字符串。例如:
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 将浮点数转换为字符串将不起作用,因为对于大于大约1.0E+14
或小于大约1.0E-4
的浮点数,php 将它们呈现在scientific notation 而不是decimal expansion。
number_format()
是显而易见的 PHP 函数。但是,大浮点数会出现此问题:
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"
对于小浮点数,困难在于选择要请求多少个十进制数字。一种想法是要求大量的十进制数字,然后是rtrim()
多余的0
s。然而,这个想法是有缺陷的,因为十进制扩展通常不会以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"
问题是浮点数有limited precision,并且通常无法存储文字的确切值(例如:1.0E+25
)。相反,它存储可以表示的最接近的可能值。 number_format()
揭示了这些“最接近的近似值”。
Timo Frenay 的解决方案
我发现this comment 深埋在sprintf()
页面中,令人惊讶的是没有点赞:
以下是如何打印具有 16 位有效数字的浮点数,无论大小如何:
$result = sprintf(sprintf('%%.%dF', max(15 - floor(log10($value)), 0)), $value);
关键部分是使用log10()
确定float的order of magnitude,然后计算出所需的小数位数。
有几个错误需要修复:
该代码不适用于负浮点数。 该代码不适用于极小的浮点数(例如:1.0E-100
)。 PHP 报告此通知:“sprintf()
: 请求的 116 位精度被截断为 PHP 最大 53 位”
如果$value
是0.0
,那么log10($value)
是-INF
。
由于 PHP float 的精度是“大约 14 位十进制数字”,我认为应该显示 14 位有效数字而不是 16 位。
我的最佳尝试
这是我想出的最佳解决方案。它基于 Timo Frenay 的解决方案,修复了 bug,并使用 ThiefMaster's regex 修剪多余的 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 an Ideone demo 带有 200 个随机浮点数。该代码似乎适用于小于大约1.0E+15
的所有浮点数。
有趣的是,number_format()
即使对于非常小的浮点数也能正常工作:
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"
有没有优雅的方法来改进代码来解决这个问题?
【问题讨论】:
您正在寻找的东西类似于 bcmath ;我自己为 php 写了一个 decimal_number 接口; 15年前 ;仍然有代码,但我不知道它是否仍然准确......;通过 log10 只会让你回到你的第一个问题。最好的。 【参考方案1】:这段代码seems to do the job too。我不认为我设法让它比你的更优雅,但我花了这么多时间,我不能把它扔掉:)
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;
【讨论】:
是的,我意识到 PHP 浮点数不能精确地表示1.0E+25
。所以需要一种算法,它采用1.0E+25
的不精确浮点表示,并将其转换为四舍五入为 14 位有效数字的字符串表示。 是可以创建一个函数来执行此操作并且只接受原生 PHP 浮点数作为输入参数(如 my inelegant answer 所示)。
@TachyonVortex ...我意识到我一开始并不理解你的问题。现在我从你的代码中理解了。我非常努力地寻找一种更好的方法来做到这一点,但这是我能做的所有事情(见编辑)。
非常感谢您的代码,以及您投入的所有时间和精力 - 非常感谢!我有兴趣了解您的方法,使用number_format()
进行预格式化,然后从尾数复制数字,或者如果没有尾数数字,则复制0
s。
链接页面上的脚本存在一些问题:警告在第 83 行除以零警告 str_repeat() 期望参数 2 为整数,在第 110 行上给出浮点数 0 --> 0.0 --> 0 .NAN WARNING 在第 83 行被零除 WARNING str_repeat() 期望参数 2 为整数,在第 110 行给出浮点数 0 --> 0.0 --> 0.NAN WARNING 在第 83 行被零除 WARNING str_repeat()期望参数 2 为整数,在第 110 行给出浮点数
我没有太多时间来编辑和测试它是否已完美纠正。如果你有解决的版本,我会感谢你的分享。【参考方案2】:
我设法创建了这个(相当不优雅的)解决方案。
如果浮点数小于1.0E+14
,则它使用我的问题中的“最佳尝试”代码。否则,它将整数部分四舍五入为 14 位有效数字。
Here's an Ideone demo 带有 500 个随机浮点数,并且代码似乎对所有浮点数都正常工作。
正如我所说,这不是一个非常优雅的实现,所以我仍然很想知道是否有人可以设计出更好的解决方案。
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;
【讨论】:
【参考方案3】:number_format($result, 14, '.', '');
【讨论】:
有一个简短的解释通常比只有代码的答案更好。 @IdeaHat:非常正确。斯科特:你的回答似乎没有解决我原来的问题。请您添加解释。以上是关于PHP:将任何浮点数格式化为十进制扩展的主要内容,如果未能解决你的问题,请参考以下文章
如何在没有不必要的十进制 0 的情况下很好地将浮点数格式化为字符串