PHP:将任何 float 格式化为十进制扩展

标签 php floating-point floating-point-conversion

我想创建一个函数 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 位”
  • 如果 $value0.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/

相关文章:

c - C 中不同的浮点变量给出不同的大小

assembly - 使用按位运算将 Int 转换为 Float 或将 Float 转换为 Int(软件浮点)

c - C中 float 的平方

php - 如何获得单词字符的所有唯一组合?

javascript - 无法调用php函数

php - 查询耗时 30 秒

c++ - 浮点,相等比较是否足以防止被零除?

java - 乘以浮点值 "possible lossy conversion from double to float"

c++ - 跨编译器的浮点文字到 IEEE-754 二进制模式的一致性

php - 删除第一次出现的子字符串之前的文本