问题出现:

在项目中之前完成的一个精灵汇总属性展示的需求,今天测试突然提了个bug:

某A号精灵 攻击力为 :20.50
某B号精灵 攻击力为 :17.98

汇总的总攻击力应该是: 38.48,但实际上得到的是 38.47
实现代码大概是这样的:

local num1 = 38.48		
print("num1 = "..num1)						-- num1 = 38.48

local num2 = math.floor(num1 * 100)/100
print("num2 = "..num2)						-- num2 = 38.47

Lua 版本:

lua取整数高位低位 lua 小数取整_Lua

由于Lua中的Number类型实际上是double(双精度浮点型),在为小数的时候应该特别注意:两个小数相等,不能用等号,而应该用它们的差的绝对值小于一个很小的数(比如math.abs(a - b) < 10e-6)!在程序中如何判断两个浮点数相等

因为电脑是用二进制存储数据的,而类似0.1这样的数字在电脑里存储起来其实是一个很长的数字,在二进制里0.1可能是一个无限不循环小数,所以我们会在一定程度上截取下来,截取之后就难免导致了实际值和我们存储的值有很小很小的差异!

这就导致了5562.9999999997和5563在我们看来是相等的两个数,在进行math.floor之后就出现了偏差,因为math.floor(5562.9999999997)会向下取整从而等于5562!

当时用的规避方式就是做这种运算的时候先保留2位有效数字 然后再做math.floor

思考:

一、浮点机制的理解偏差

看下面这段代码:

print("num1 = ",1.99999999999995)
print("num2 = ",math.floor(1.9999999999999999))

不过这里并不能说明math.floor是异常的。因为这里其实是浮点型的机制引起的"理解偏差",浮点型在内存中其实是一个2进制的分数(C#本质论,值类型那一章有详细讲解,一般的C++书应该也会讲),我们将其转换成十进制进行输出或者其他运算操作时,2进制分数和十进制小数无法严格一一对应,所以会有一个取近似值的操作,特别是精度值溢出后,甚至会直接做四舍五入和截取的操作。

所以这里其实不是math.floor问题,而是lua直接将1.99999999999999999,直接当成了2来处理(注意第一个tostring输出时,已经将原值认为成2了)。那么1.99999999999999999在lua中被识别成了2,math.floor截取也是2,结果还是正确的。

二、string.format()的 “四舍六入五成双”

(1)被修约的数字小于5时,该数字舍去;

(2)被修约的数字大于5时,则进位;

(3)被修约的数字等于5时,要看5前面的数字,若是奇数则进位,若是偶数则将5舍掉,即修约后末尾数字都成为偶数;若5的后面还有不为“0”的任何数,则此时无论5的前面是奇数还是偶数,均应进位。

local num1 = 1.551111111111111
local num2 = 1.550000000000000
local num3 = 1.650000000000000
local num4 = 1.661111111111111
local num5 = 1.666666666666666
print("原值:"..num1.."。取一位小数后:"..string.format("%.1f", num1))
print("原值:"..num2.."。取一位小数后:"..string.format("%.1f", num2))
print("原值:"..num3.."。取一位小数后:"..string.format("%.1f", num3))
print("原值:"..num4.."。取一位小数后:"..string.format("%.1f", num4))
print("原值:"..num5.."。取一位小数后:"..string.format("%.1f", num5))

-- 原值:1.5511111111111。取一位小数后:1.6
-- 原值:1.55。取一位小数后:1.6
-- 原值:1.65。取一位小数后:1.6
-- 原值:1.6611111111111。取一位小数后:1.7
-- 原值:1.6666666666667。取一位小数后:1.7

原值:1.5511111111111 处理后:1.6 分析 – 原值1.55,5后面有值,进了一位
原值:1.55 处理后:1.6 分析 – 1.55 – 5后面没有值,但是5前面还有个奇数5,进了一位。
原值:1.65 处理后:1.6 分析 – 1.65 – 5后面没有值,但是5前面有个偶数6,舍弃。

三、使用math.floor模拟进制转换的方案

-- 保留n位小数
function Format3(val, n)
    local n = math.pow(10, n or 1)
    val = tonumber(val)
    return math.floor(val * n) / n
end

print(Format3(math.pi,2))	-- 3.14