【问题标题】:Lua: Rounding numbers and then truncateLua:四舍五入然后截断
【发布时间】:2013-08-21 05:06:20
【问题描述】:

将数字四舍五入然后截断它(四舍五入后删除小数位)的最佳有效方法是什么?

例如,如果小数大于 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)

【问题讨论】:

  • 我知道这已经很晚了,但是您还记得如果您打算 truncate 也用于负数,IOW 向零舍入一半吗?我的答案应该在 FPU 舍入模式相应调整后执行此操作。

标签: lua rounding truncate


【解决方案1】:

如果它对任何人有用,我已经散列出LUA's 逻辑的通用版本,但这次是truncate()

**强调文本预先为不知道 lua 语法而道歉,所以这是在 AWK/lua 混合中,但希望它应该足够直观

-- due to lua-magic alrdy in 2^(52-to-53) zone,
-- has to use a more coarse-grained delta than
-- true IEEE754 double machineepsilon of 2^-52
function trunc_lua(x,s) {
    return \
      ((x*(s=(-1)^(x<-x)) \
           - 2^-1 + 2^-50 \  -- can also be written as
                          \  --    2^-50-5^0/2
           -  _LUAMAGIC  \   -- if u like symmetric 
                         \   --    code for fun
           +  _LUAMAGIC \
      )  *(s) };

它本质上与舍入的概念相同,但强制处理正值区域中的所有输入,偏移量为 -1*(0.5-delta)。我能达到的最小增量是2^-52 ~ 2.222e-16

lua-magic 值必须在所有这些预处理步骤之后出现,否则可能会出现精度损失。最后,恢复输入的原始符号。

这两个“乘法”只是低开销的符号翻转。对于最初的负值,符号翻转 4 次(2 次手动翻转和往返尾数末尾的往返),而任何 x &gt;= 0,包括 -0.0,只翻转两次。避免了所有三次函数调用、浮点除法和整数模数,仅对 x

使用说明:

  • (1) 不对输入执行无效或恶意负载检查,
  • (2) 不使用快速检查零,
  • (3) 不检查可能导致此逻辑无效的极端输入,并且
  • (4) 不会尝试格式化值

【讨论】:

    【解决方案2】:

    尝试使用math.ceil(number + 0.5) 这是根据Wikipedia page. 如果我是正确的,这只是舍入正整数。你需要为否定做math.floor(number - 0.5)

    【讨论】:

    • 这基本上是@toma91 答案的另一个版本。我发帖之前没看到。
    【解决方案3】:

    为了四舍五入到给定数量的小数(也可以是负数),我建议使用以下解决方案,该解决方案结合了已经作为答案提供的结果,尤其是 the inspiring one given by Pedro Gimeno。我测试了一些我感兴趣的极端案例,但不能声称这使得该功能 100% 可靠:

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

    这些案例说明了 round-half-to-even 属性(这应该是大多数机器上的默认值):

    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 being the default 设置的当前舍入模式。这就是为什么在设置FE_TOWARDZERO(但是你可以在 Lua 中这样做)之后,这个解决方案很可能会返回问题中要求的结果。

    【讨论】:

      【解决方案4】:

      这是一个灵活的函数,可以四舍五入到不同的位置。我用负数、大数、小数和各种边缘情况对其进行了测试,它有用且可靠:

      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) dpdps 之间的差异可能令人困惑,(2) 对于正输入,从零开始舍入,对于负输入,舍入到零(我认为这应该包括在内...(或重新实现?))
      【解决方案5】:

      如果您的 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,它同样适用于您的测试用例。当然,我可以自己计算,这当然是值得的,但也许你可以在这里与更多的观众分享棘手的细节和边缘案例。
      • @RolandIllig 我的测试套件不是很详尽;它不会检查所有极端情况。您的电话号码失败,例如2.75,四舍五入为 2.0。在 0 和 2^52-1 之间的任何数字上添加和减去 2^52 会通过将其推入 2^52..2^53 范围来对其进行舍入,其中双精度浮点数在调整回来之前会丢失所有小数。为了弥补负面影响,我添加了 2^51 以确保中间结果仍在 2^52..2^53 范围内。如果你像你的号码经常做的那样把它推到 2^53 以上,你也会失去偶数;例如2^53+1 不能用双精度精确表示。
      • “你也输了偶数”——我的意思是你输了奇数个数字。
      【解决方案6】:

      我喜欢上面 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
      

      【讨论】:

      • 不幸的是,string.format() 会根据使用的 Lua 版本进行不同的舍入。请参阅my question about LuaJIT,它更喜欢四舍五入,而大多数其他人在面对“精确”一半时会向下舍入。
      【解决方案7】:

      对于糟糕的舍入(切断末端):

      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))
      

      预期结果:

      334255

      【讨论】:

        【解决方案8】:

        这是一个四舍五入到任意位数的数字(如果未定义,则为 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
        

        【讨论】:

        • 这适用于 Lua,但不适用于 LuaJIT。如果你让x=32.90625n=4,那么大多数Luas 会给你32.9062,但LuaJIT 会给你23.0963。请参阅my question 了解如何避免这种情况。
        【解决方案9】:

        为了也支持负数,使用这个:

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

        【讨论】:

        • 对于 x = 0.49999999999999994 返回 1,对于 -0.49999999999999994 返回 -1;它应该返回 0。请参阅我的答案以获取正确四舍五入到最接近或什至的版本。
        【解决方案10】:

        对整数以外的十进制数字舍入有用的技巧是通过格式化的 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 的符号)以获得您想要的边缘情况。

        【讨论】:

        • 不幸的是,string.format() 会根据所使用的 Lua 版本进行不同的舍入。请参阅my question about LuaJIT,它更喜欢四舍五入,而大多数其他人在面对“精确”一半时会向下舍入。
        • 至于第二部分,为什么不干脆(exact + quantum*0.5) // quantum * quantum
        【解决方案11】:

        应该是math.ceil(a-0.5) 才能正确处理半整数

        【讨论】:

        • 取决于“正确”的含义。人们必须仔细决定如何处理半步,以及如何处理负值。加半和floor 和减半和ceil 都会对精确一半的情况引入一致的偏差。假设截断通常向零舍入,两者都不同于加半和截断。从某种意义上说,实现四舍五入更“公平”。四舍五入充满了微妙之处。
        【解决方案12】:

        Lua 中没有内置的 math.round() 函数,但您可以执行以下操作: print(math.floor(a+0.5)).

        【讨论】:

        • a 应该是什么?
        • @averwhy a 是您要四舍五入的值。
        • 这将返回 1 代表 a = 0.49999999999999994;它应该返回 0。
        • @PedroGimeno 这是唯一错误的情况,还是其他情况?我试过1.49999999999999988 + 0.5,效果很好。 2.49999999999999977 + 0.5 也一样。
        • @RolandIllig 据我所知,这是唯一的案例。由于四舍五入,0.49999999999999994 和 0.5 相加返回 1;对于较大的数字,四舍五入不会引起问题,因为它们没有足够的有效数字来影响四舍五入。这个数字的特别之处在于 0.5 比它大。
        猜你喜欢
        • 1970-01-01
        • 2011-03-09
        • 2011-12-27
        • 1970-01-01
        • 1970-01-01
        • 2017-01-29
        • 2010-09-24
        • 1970-01-01
        相关资源
        最近更新 更多