PHP 舍入浮点数

PHP Rounding Float

提问人:Guillermo Phillips 提问时间:1/22/2021 最后编辑:Guillermo Phillips 更新时间:1/22/2021 访问量:253

问:

我正在开发一个系统,我需要四舍五入到最接近的一分钱财务付款。我天真地以为我会乘以 100,发言,然后再再分。但是,以下示例行为不端:

echo 1298.34*100;

正确显示:

129834

echo floor(1298.34*100);

出乎意料地显示:

129833

例如,我遇到了同样的问题。intval

我怀疑乘法与浮点舍入相悖。但是,如果我不能依靠乘法,我该怎么做呢?我总是想可靠地四舍五入,我不需要考虑负数。

需要明确的是,我希望剥离任何零碎的便士金额:

1298.345 should give 1298.34
1298.349 should give 1298.34
1298.342 should give 1298.34
PHP 精度

评论

0赞 El_Vanja 1/22/2021
你可以把它变成一个字符串,如果你只想丢失超过小数点后第二点的任何内容,你可以把它变成一个字符串并截断。
0赞 Guillermo Phillips 1/22/2021
是的,这是可行的,我可能会这样做。但是,当我查询数据库中的金额时,它们不一定格式化为小数点后两位。因此,数值解是可取的。
0赞 El_Vanja 1/22/2021
不确定我是否正确理解了您的格式问题,但我想到了这样的事情
0赞 Guillermo Phillips 1/22/2021
@El_Vanja如果你能把这个作为答案发布,那么我会考虑接受它。

答:

-1赞 Hammad Ahmed khan 1/22/2021 #1

另一个可用的函数是 round(),它接受两个参数 - 要四舍五入的数字,以及要四舍五入到的小数位数。如果 一个数字正好在两个整数之间,round() 总是 四舍五入。

使用回合 :

echo round (1298.34*100);

结果:

129834

评论

0赞 Guillermo Phillips 1/22/2021
这是不可靠的。回声CEIL(1298.38*100);给129839。
1赞 LSerni 1/22/2021
别这样! 犯了地板完全相同的概念错误,只是颠倒了。使用或 - 如果你真的因为某种奇怪的原因不能 - 至少为你的秤添加一个可以忽略不计的数量(通常是一半)并使用.即使这样也不安全,因为四舍五入行为不是标准化的,你可以有“财务四舍五入”和“严格四舍五入”。ceilroundfloor
1赞 LSerni 1/22/2021 #2

您可以使用 round() 四舍五入到所需的精度,并在四舍五入最后 5 时具有预期的行为(这是您可能会遇到的另一个财务障碍)。

  $display = round(3895.0 / 3.0, 2);

另外,提醒一下,我习惯于总是用最后一个点或“.0”来写浮点整数。这可以防止某些语言推断错误的类型并进行整数除法,因此 5 / 3 将产生 1

如果你需要一个“自定义舍入”,并且想要确定,那么,它不起作用的原因是,并非所有浮点数都存在于机器表示中。1298.34不存在;取而代之的是1298.33999999999999999124。

因此,当您将其乘以 100 并得到 129833.9999999999999124 时,截断它当然会产生129833。

然后,您需要做的是添加少量,该数量必须足以弥补机器错误,不足以在财务计算中发挥作用。有一种算法可以确定这个数量,但你可能会得到“升级后的千分之一”。

所以:

 $display = floor((3895.0 / 3.0)*100.0 + 0.001);

请注意,您将“看到”为 1234.56 的这个数字可能再次不精确存在。它可能真的是 1234.56000000000000123 或 1234.5599999999999876。这可能会在复杂的复合计算中产生后果。

评论

0赞 KIKO Software 1/22/2021
我同意这应该可以完成这项工作。
0赞 Guillermo Phillips 1/22/2021
谢谢,但我总是想四舍五入。round() 只舍入到最接近的,这不是我想要的。
0赞 LSerni 1/22/2021
可以肯定的是,如果你有 12.339999,那一定是 12.33?
0赞 Guillermo Phillips 1/22/2021
是的,我已经修改了我的问题。
0赞 LSerni 1/22/2021
@GuillermoPhillips我添加了一个可能的解决方案。
0赞 Piotr 1/22/2021 #3

由于您正在与财务部门合作,因此您应该使用某种货币库(https://github.com/moneyphp/money)。几乎所有其他解决方案都在自找麻烦。


我不推荐的其他方法是:a) 仅使用整数,b) 计算或 c) 使用 Money 库中的 Number 类,例如:bcmath

function getMoneyValue($value): string
{
    if (!is_numeric($value)) {
        throw new \RuntimeException(sprintf('Money value has to be a numeric value, "%s" given', is_object($value) ? get_class($value) : gettype($value)));
    }

    $number = \Money\Number::fromNumber($value)->base10(-2);

    return $number->getIntegerPart();   
}

评论

0赞 Guillermo Phillips 1/22/2021
如果可以的话,我宁愿不添加另一个依赖项。但是,如果我找不到100%可靠的解决方案,我可能会走这条路。
1赞 El_Vanja 1/22/2021 #4

既然你提到你只用它来显示,你可以把这个金额变成一个字符串,然后截断任何超过小数点后第二位的东西。正则表达式可以完成这项工作:

preg_match('/\d+\.{0,1}\d{0,2}/', (string) $amount, $matches);

此表达式适用于任意数量的小数(包括零)。详细工作原理:

  • \d+匹配任意位数
  • \.{0,1}匹配 0 或 1 个文字点
  • \d{0,2}匹配点后的零位或两位数字

您可以运行以下代码来测试它:

$amounts = [
    1298,
    1298.3,
    1298.34,
    1298.341,
    1298.349279745,
];
foreach ($amounts as $amount) {
    preg_match('/\d+\.{0,1}\d{0,2}/', (string) $amount, $matches);
    var_dump($matches[0]);
}

也可作为此小提琴的现场测试。

评论

0赞 Guillermo Phillips 1/22/2021
谢谢。我使用了这个解决方案,因为它可以保证可靠地工作。之后填充到小数点后两位是相当简单的。
2赞 Hevez 11/24/2023 #5

首先,您需要阅读PHP官方页面上的RED部分 https://www.php.net/manual/en/language.types.float.php

浮点数的操作并不精确,我们需要使用特殊函数 https://www.php.net/manual/en/ref.bc.php

为什么?因为
// 输出浮点数(129833.99999999999)
var_dump(1298.34*100);

如何使用官方数学函数修复它?

捷径 - 使用 number_format()

number_format(1298.34*100, 0, '.', ''));

Long way - 使用 bc 函数
// 输出 “129834”
bcmul(1298.34, 100);

如果要设置小数的精度,请执行以下操作:
// 输出 “129834.00”
bcmul(1298.34, 100, 2);

最后,你可以把它投射到浮动状态

$result = (float)bcmul(1298.34, 100, 2);

小心 bcmul 中的 secound 参数,它不会向上或向下舍入,它会削减小数位数

四舍五入

number_format自动向上舍入,但如果要向下使用

round($result,2,PHP_ROUND_HALF_DOWN) //round DOWN to 2 decimals

评论

0赞 Guillermo Phillips 12/14/2023
这很清楚。但是,var_dump(1298.34*100) 显示 float(129834)。我认为 var_dump 和 echo 本身都可以在显示之前绕过浮点数的内部表示。
0赞 Guillermo Phillips 12/14/2023 #6

自从我问这个问题以来,我一直在想这个问题的纯数值解决方案。

答案是在发言之前在乘数上增加一小部分:

echo floor(1298.349 * (100 + 1/10000)) / 100;

给出预期的答案:

1298.34

要添加多少分数取决于要处理的最大数字大小。对于小数点后两位,一般公式为:

echo floor($amount * (100 + 1 / ($max_num * 10))) / 100;

这适用于浮点数中任意数量的小数位。$amount$amount

为了证明这可行,这里有一个小的测试程序。这将检查最多三个小数位的所有浮点数,截断到小数点后两位。它根据纯字符串解检查数值解,并输出第一个失败。

$increment = 1/((float) 100000);

$multiplier_float = (float) (100 + $increment);
for ($integer_part = 1; $integer_part <= 20000; $integer_part++) {
    $integer_part_string = (string) $integer_part;
    for ($decimal_part = 0; $decimal_part <= 999; $decimal_part++) {
        $decimal_part_string = str_pad($decimal_part, 3, '0', STR_PAD_LEFT);
        $truncated_decimal_part_string = substr($decimal_part_string, 0, 2);
        $number_string = $integer_part_string . '.' . $decimal_part_string;
        $number_float = (float) $number_string;

        $number_truncated_string = $integer_part_string . '.' . $truncated_decimal_part_string;
        $number_truncated_float = floor($number_float * $multiplier_float)/(float) 100;
        $number_truncated_float_as_string = (string) $number_truncated_float;

        if ($number_truncated_string != $number_truncated_float_as_string) {
            echo $number_string . ' ' . $number_truncated_string . ' ' . $number_truncated_float_as_string . "\n";
            exit;
        }
    }
}

我明确地将浮点数转换为浮点数以使其更清晰。

如果更改 您可以检查第一次故障发生的位置。该示例正在检查最多 20,000 个数字。您将看到第一个故障发生在 10000.009 上。$increment

实际上,在处理货币值时,将有最大数字大小(例如 6 个有效位置或更少)。因此,此解决方案的性能可能更高,并且是单行的。