8

我想创建一个函数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"

最初的想法

简单地将浮点数转换为字符串是行不通的,因为对于大于 about1.0E+14或小于 about的浮点数,PHP 会以科学记数法而不是十进制扩展1.0E-4来呈现它们。

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。然而,这个想法是有缺陷的,因为十进制扩展通常不以0s 结尾:

number_format(1.000001,     30);  // "1.000000999999999917733362053696"
number_format(1.000001E-10, 30);  // "0.000000000100000099999999996746"
number_format(1.000001E-11, 30);  // "0.000000000010000010000000000321"

问题是浮点数的精度有限,并且通常无法存储文字的确切值(例如:)1.0E+25。相反,它存储可以表示的最接近的可能值。 number_format()正在揭示这些“最接近的近似值”。

Timo Frenay 的解决方案

我发现这条评论深埋在sprintf()页面中,令人惊讶的是没有人赞成:

以下是如何打印具有 16 位有效数字的浮点数,无论大小如何:

$result = sprintf(sprintf('%%.%dF', max(15 - floor(log10($value)), 0)), $value);

关键部分是使用log10()来确定浮点数的数量级,然后计算所需的小数位数。

有几个bug需要修复:

  • 该代码不适用于负浮点数。
  • 该代码不适用于极小的浮点数(例如:)1.0E-100。PHP 报告此通知:“ sprintf():请求的 116 位精度被截断为 PHP 最大 53 位”
  • 如果$value0.0,那么log10($value)-INF
  • 由于PHP 浮点数的精度是“大约 14 位十进制数字”,我认为应该显示 14 位有效数字而不是 16 位。

我最好的尝试

这是我想出的最好的解决方案。它基于 Timo Frenay 的解决方案,修复了错误,并使用ThiefMaster 的正则表达式来修剪多余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;
}

这是一个带有 200 个随机浮点数的 Ideone 演示。该代码似乎适用于所有小于 about 的浮点数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"

有没有一种优雅的方法来改进代码来解决这个问题?

4

3 回答 3

2

这段代码似乎也可以完成这项工作。我不认为我设法让它比你的更优雅,但我花了很多时间在它上面,我不能把它扔掉:)

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;

}
于 2014-03-09T14:48:21.330 回答
1

我设法创建了这个(相当不雅的)解决方案。

如果浮点数小于1.0E+14,则它使用我的问题中的“最佳尝试”代码。否则,它将整数部分四舍五入为 14 位有效数字。

这是一个带有 500 个随机浮点数的 Ideone 演示,代码似乎对所有浮点数都正常工作。

正如我所说,这不是一个非常优雅的实现,所以我仍然很想知道是否有人可以设计出更好的解决方案。

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;
}
于 2014-03-13T20:42:08.660 回答
-2
number_format($result, 14, '.', '');
于 2014-11-11T17:56:37.557 回答