Lua:四舍五入并截断数字

41

哪种方法最有效地将数字四舍五入并截断(在四舍五入后删除小数位)?

例如,如果小数大于0.5(即0.6、0.7等),我想要先将其四舍五入再截断(情况1)。否则,我想要直接截断(情况2)。

for example:
232.98266601563 => after rounding and truncate = 233 (case 1)
232.49445450000 => after rounding and truncate = 232 (case 2)
232.50000000000 => after rounding and truncate = 232 (case 2)

我知道现在已经很晚了,但是你是否记得你是否也想要对负数进行截断,即四舍五入到零的一半?我的答案应该在调整FPU舍入模式后实现这一点。 - Wolf
14个回答

53

Lua中没有内置的math.round()函数,但是可以使用以下方法: print(math.floor(a+0.5))


a 应该是什么? - averwhy
@averwhy a 是你想要四舍五入的值。 - Nifim
5
对于 a = 0.49999999999999994,该函数返回值为1,但正确结果应该是0。 - Pedro Gimeno
1
@PedroGimeno 这是唯一的错误案例吗,还是还有其他的?我尝试了 1.49999999999999988 + 0.5,结果很好。同样适用于 2.49999999999999977 + 0.5 - Roland Illig
1
据我所知,这是唯一的情况。由于四舍五入,0.49999999999999994和0.5的相加结果为1;对于更大的数字,四舍五入不会造成问题,因为它们没有足够的有效数字来影响四舍五入。该数字很特殊,因为0.5比它大。 - Pedro Gimeno
@PedroGimeno 如果将0.49999999999999994替换为0.5,应该可以解决这个问题,并且我相信对于其他所有数字也会起作用(因为它们会因为同样的原因四舍五入)。 - undefined

29

一个有用的技巧,用于将十进制数值四舍五入到小数位而不是整数位,是将数值传递到格式化的ASCII文本中,并使用%f格式字符串来指定所需的舍入方式。例如:

mils = tonumber(string.format("%.3f", exact))

exact中的任意值四舍五入为0.001的倍数。

使用math.floor()math.ceil()之前和之后进行比例缩放可以得到类似的结果,但是根据您对边缘情况处理的期望获取正确的细节可能会很棘手。虽然string.format()也存在此问题,但已经投入了大量工作使其产生“预期”的结果。

将值四舍五入为除十的幂以外的倍数仍需要进行缩放,并且仍具有所有棘手的边缘情况。一种简单表达且具有稳定行为的方法是编写:

function round(exact, quantum)
    local quant,frac = math.modf(exact/quantum)
    return quantum * (quant + (frac > 0.5 and 1 or 0))
end

您可以微调 frac 的确切条件(可能还包括exact的符号)来获得您想要的边缘情况。


2
不幸的是,string.format() 根据使用的 Lua 版本而有所不同。请参见我的关于 LuaJIT 的问题,它更喜欢向上舍入,而大多数其他情况下会向下舍入,当面对“精确”的一半时。 - Caleb
至于第二部分,为什么不直接使用(exact + quantum*0.5) // quantum * quantum呢? - AF7

21

要支持负数,可以使用以下代码:

function round(x)
  return x>=0 and math.floor(x+0.5) or math.ceil(x-0.5)
end

4
当x = 0.49999999999999994时,此函数返回1,当x = -0.49999999999999994时,返回-1;应该返回0。请参考我的答案,其中包含一个正确舍入到最接近或偶数的版本。 - Pedro Gimeno

12
如果你的 Lua 使用双精度 IEC-559(也称为 IEEE-754)浮点数,大多数情况下都是如此,并且你的数字相对较小(该方法保证可用于 -251 到 251 之间的输入),则以下高效代码将使用你的 FPU 当前的舍入模式执行舍入,通常是四舍五入到最近的偶数:
local function round(num)
  return num + (2^52 + 2^51) - (2^52 + 2^51)
end

注意,括号中的数字是在编译时计算的,不影响运行时。

例如,当FPU设置为四舍五入到最近偶数时,该单元测试将打印“所有测试都通过”:

local function testnum(num, expected)
  if round(num) ~= expected then
    error(("Failure rounding %.17g, expected %.17g, actual %.17g")
          :format(num+0, expected+0, round(num)+0))
  end
end

local function test(num, expected)
  testnum(num, expected)
  testnum(-num, -expected)
end

test(0, 0)
test(0.2, 0)
test(0.4, 0)
-- Most rounding algorithms you find on the net, including Ola M's answer,
-- fail this one:
test(0.49999999999999994, 0)
-- Ties are rounded to the nearest even number, rather than always up:
test(0.5, 0)
test(0.5000000000000001, 1)
test(1.4999999999999998, 1)
test(1.5, 2)
test(2.5, 2)
test(3.5, 4)
test(2^51-0.5, 2^51)
test(2^51-0.75, 2^51-1)
test(2^51-1.25, 2^51-1)
test(2^51-1.5, 2^51-2)
print("All tests passed")

这里有另一种算法(当然不够高效),它可以对所有数字执行相同的FPU舍入:

local function round(num)
  local ofs = 2^52
  if math.abs(num) > ofs then
    return num
  end
  return num < 0 and num - ofs + ofs or num + ofs - ofs
end

你能解释一下为什么修正值需要是 2^52 + 2^51 吗?我尝试了 2^53 - 2,在你的测试用例中同样有效。当然,我可以自己计算,这肯定是值得的,但也许你可以在这里与更广泛的受众分享一些棘手的细节和边缘情况。 - Roland Illig
3
我的测试套件不够详尽,它没有检查所有边角情况。例如,对于 2.75 这个数字,它会被四舍五入为 2.0。将 2^52 添加到 0 和 2^52-1 之间的任何数字中,并从中减去 2^52,可以通过将其推入范围 2^52..2^53 来进行舍入,在调整回来之前,双精度浮点数丢失了所有小数位。为了涵盖负数,我添加了 2^51,以确保中间结果仍在 2^52..2^53 范围内。如果你像你那个数字一样将其推到 2^53 之外,你甚至会失去偶数;例如,2^53+1 无法用双精度表示。 - Pedro Gimeno
2
你失去了偶数,而我指的是你失去了奇数 - Pedro Gimeno

6

以下代码可将数字四舍五入至任意位数(如果未定义,则为0):

function round(x, n)
    n = math.pow(10, n or 0)
    x = x * n
    if x >= 0 then x = math.floor(x + 0.5) else x = math.ceil(x - 0.5) end
    return x / n
end

1
这适用于Lua,但不适用于LuaJIT。如果让x=32.90625n=4,则大多数Luas将给出32.9062,但LuaJIT将给出23.0963。请参见我的问题以了解如何避免此问题。 - Caleb
1
回答不错,但是在此处发布代码时,您应该更加小心。n或0没有涵盖定义数字数目的情况(使用函数中第二个参数...并通过arg[2]进行引用可以实现这一点),更不用说可能会出现除以0的情况。我也可以提到少见的-0发生情况,但你明白我的意思:只要正确处理异常情况,方法就是好的。 - Yin Cognyto

5

对于不良的四舍五入(截断结尾):

function round(number)
  return number - (number % 1)
end

如果你愿意的话,你可以扩展这个内容以便更好地概括。

function round(number)
  if (number - (number % 0.1)) - (number - (number % 1)) < 0.5 then
    number = number - (number % 1)
  else
    number = (number - (number % 1)) + 1
  end
 return number
end

print(round(3.1))
print(round(math.pi))
print(round(42))
print(round(4.5))
print(round(4.6))

预期结果: 3, 3, 42, 5, 5

3

我喜欢RBerteig上面的回答:mils = tonumber(string.format("%.3f", exact))。将其扩展为函数调用并添加了精度值。

function round(number, precision)
   local fmtStr = string.format('%%0.%sf',precision)
   number = string.format(fmtStr,number)
   return number
end

1
不幸的是,string.format() 根据使用的 Lua 版本而有所不同。请参见我的关于 LuaJIT 的问题,它更喜欢向上舍入,而大多数其他情况下会向下舍入,当面对“精确”的一半时。 - Caleb

1

如果不存在math.round

function math.round(x, n)
    return tonumber(string.format("%." .. n .. "f", x))
end

1

这里有一个灵活的函数,可以将数字四舍五入到不同的位数。我已经测试过它适用于负数、大数、小数以及各种边缘情况,它非常实用可靠:

function Round(num, dp)
    --[[
    round a number to so-many decimal of places, which can be negative, 
    e.g. -1 places rounds to 10's,  
    
    examples
        173.2562 rounded to 0 dps is 173.0
        173.2562 rounded to 2 dps is 173.26
        173.2562 rounded to -1 dps is 170.0
    ]]--
    local mult = 10^(dp or 0)
    return math.floor(num * mult + 0.5)/mult
end

API文档:(1)“dp”和“dps”的偏差可能会令人困惑;(2)针对正数输入,四舍五入的方式是远离零点,而针对负数则朝向零点(我认为应该加以说明...或重新实施?) - Wolf

0

对于保留给定小数位数(也可以是负数)的四舍五入,我建议采用以下解决方案,它结合了已经提供的答案,特别是Pedro Gimeno提供的启发式答案。我测试了一些我感兴趣的边角情况,但不能保证这使得该函数100%可靠:

function round(number, decimals)
  local scale = 10^decimals
  local c = 2^52 + 2^51
  return ((number * scale + c ) - c) / scale
end

这些案例说明了"四舍五入到最近的偶数"属性(这应该是大多数机器的默认设置):

assert(round(0.5, 0) == 0)
assert(round(-0.5, 0) == 0)
assert(round(1.5, 0) == 2)
assert(round(-1.5, 0) == -2)
assert(round(0.05, 1) == 0)
assert(round(-0.05, 1) == 0)
assert(round(0.15, 1) == 0.2)
assert(round(-0.15, 1) == -0.2)

我知道我的回答没有处理实际问题的第三种情况,但为了符合IEEE-754标准,我的方法是有意义的。因此,我期望结果取决于FPU中设置的当前舍入模式,其中FE_TONEAREST是默认值。这就是为什么在设置FE_TOWARDZERO之后(无论您如何在Lua中设置),此解决方案似乎极有可能返回与问题所要求的完全相同的结果。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接