目录

PHP 中的浮点数

对浮点数进行比较运算是一个坑爹的事,由于栽在这个问题上的次数比较多,总是记吃不记打的,痛定思痛后打算整理一下,避免下次再犯。

浮点数的陷阱实例

浮点数计算错误实例

1
2
3
4
5
6
7
8
9
<?php

$float = 0.58;

var_dump($float * 100);
var_dump(intval($float * 100));

double(58)
int(57) // 居然是 57

浮点数比较错误实例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<?php

$a = 0.1;
$b = 0.2;

var_dump($a);
var_dump($b);
var_dump($a + $b);
var_dump($a + $b === 0.3);


// 结果
double(0.1)
double(0.2)
double(0.3)
bool(false) // 居然是 false
?>
警告

浮点数的精度

  • 浮点数的精度有限。尽管取决于系统,PHP 通常使用 IEEE 754 双精度格式,则由于取整而导致的最大相对误差为 1.11e-16。非基本数学运算可能会给出更大误差,并且要考虑到进行复合运算时的误差传递
  • 此外,以十进制能够精确表示的有理数如 0.10.7,无论有多少尾数都不能被内部所使用的二进制精确表示,因此不能在不丢失一点点精度的情况下转换为二进制的格式。这就会造成混乱的结果,例如:floor((0.1+0.7)*10 通常会返回 7 而不是预期中的 8,因为该结果内部的表示其实是类似 7.9999999999999991118...
  • 所以,永远不要相信浮点数结果精确到了最后一位,也永远不要比较两个浮点数是否相等。如果确实需要更高的精度,应该使用 任意精度数学函数 或者 gmp 函数

精度数学函数

名称说明
bcadd()任意精度数字的加法计算
bccomp()比较两个任意精度的数字
bcdiv()两个任意精度的数字除法计算
bcmod()对一个任意精度数字取模
bcmul()两个任意精度数字乘法计算
bcpow()任意精度数字的乘方
bcpowmod()任意精度数字乘方求模
bcscale()设置所有 bc 数学函数的默认小数点保留位数
bcsqrt()任意精度数字的二次方根
bcsub()两个任意精度数字的减法
警告
使用 bcscale() 设置的位数,超出部分是丢弃掉,而不是四舍五入
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?php

declare(strict_types=1);

$a = '0.19';
$b = '0.81';

$array = [
    'bcadd'    => bcadd($a, $b, 2),
    'bccomp'   => bccomp(bcadd($a, $b), '1', 1), // 两个数相等返回 0
    'bcdiv'    => bcdiv('100', '3', 4),
    'bcmod'    => bcmod(PHP_VERSION, '2'),
    'bcmul'    => bcmul($a, $b, 4),
    'bcpow'    => bcpow('2.2', '3', 3),
    'bcpowmod' => bcpowmod('2', '2', '3'),
    'bcscale'  => bcscale(6), // 设置新的小数点保留位数
    'bcsqrt'   => bcsqrt('4'),
    'bcsub'    => bcsub($b, $a),
];

print_r($array);

// 结果
Array
(
    [bcadd] => 1.00
    [bccomp] => 0
    [bcdiv] => 33.3333
    [bcmod] => 0
    [bcmul] => 0.1539
    [bcpow] => 10.648
    [bcpowmod] => 1
    [bcscale] => 1
    [bcsqrt] => 2.000000
    [bcsub] => 0.620000
)

正确的姿势

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?php

declare(strict_types=1);

$a = '0.185';
$b = '0.804';

$c = 0.185;
$d = 0.804;

$e = 101.1988654321;
$f = 101.1988456789;

$g = 1.23456789;
$h = 1.23456780;

$epsilon = 0.00001; // 机器极小值(epsilon)或最小单元取整数,是计算中所能接受的最小的差别值

var_dump(bcadd($a, $b, 2) === '0.98');
var_dump(round($c + $d, 2) === 0.99);
var_dump(bccomp((string)$e, (string)$f, 3) === 0);
var_dump(abs($g - $h) < $epsilon); // 误差小于这个值可以接受

// 结果
bool(true)
bool(true)
bool(true)
bool(true)