lua工具函数

lua 工具函数,版本Lua 5.3+

--[[
    util.lua —— 数值 / 位运算 / 采样 工具集

    依赖:
      * Lua 5.3+(使用了原生位运算符 & 、>> 、<<)
      * 若在 Lua 5.1 / 5.2 使用,请引入 bit 库并替换位运算,
        或只使用其中不依赖位运算的函数(Round / RoundXDecimal / Log2 / SampleArray)。

    约定:
      * 所有位相关函数仅对「正整数」有意义,非法输入返回 nil。
      * FindSingleBitPosition / Log2Exact 仅接受 2 的幂,否则返回 nil。
      * Round 系列对正负数的舍入方向统一为「远离零」。
      * SampleArray 返回新数组,不修改入参数组。
--]]

local util = util or {}
util = util

local LN2 = math.log(2)

--------------------------------------------------------------------------------
-- 舍入
--------------------------------------------------------------------------------

--- 四舍五入到整数(正负数都正确,舍入方向为「远离零」)
--- @param num number 待舍入的数
--- @return integer
function util.Round(num)
    if num >= 0 then
        return math.floor(num + 0.5)
    else
        return math.ceil(num - 0.5)
    end
end

--- 四舍五入到指定小数位
--- @param n number          待舍入的数
--- @param decimals integer? 小数位数,默认 0(即舍入到整数)
--- @return number
function util.RoundXDecimal(n, decimals)
    local mult = 10 ^ (decimals or 0)
    return util.Round(n * mult) / mult
end

--- 四舍五入到 1 位小数
--- @param n number
--- @return number
function util.Round1Decimal(n)
    return util.RoundXDecimal(n, 1)
end

--------------------------------------------------------------------------------
-- 对数
--------------------------------------------------------------------------------

--- 计算任意正数的以 2 为底的对数(浮点结果)
--- 说明:使用 math.log 直接换算;x <= 0 时返回 nil。
---      若需要「最高位 bit 位置」请用 Log2Exact 或另行添加 Log2Floor。
--- @param x number 必须 > 0
--- @return number|nil
function util.Log2(x)
    if x <= 0 then
        return nil
    end
    return math.log(x) / LN2
end

--- 计算以 2 为底的对数,仅当 x 是 2 的幂时返回精确整数
--- 例:Log2Exact(8) --> 3,Log2Exact(12) --> nil
--- 实现:先校验 2 的幂,再用二分位掩码定位,无浮点误差,覆盖 64 位整数。
--- @param x integer 必须为正整数且是 2 的幂
--- @return integer|nil
function util.Log2Exact(x)
    -- 必须为正整数
    if x <= 0 or x % 1 ~= 0 then
        return nil
    end

    -- 必须是 2 的幂(二进制只有一个 1)
    if (x & (x - 1)) ~= 0 then
        return nil
    end

    local n = 0
    if x & 0xFFFFFFFF00000000 ~= 0 then n = n + 32; x = x >> 32 end
    if x & 0x00000000FFFF0000 ~= 0 then n = n + 16; x = x >> 16 end
    if x & 0x000000000000FF00 ~= 0 then n = n + 8;  x = x >> 8  end
    if x & 0x00000000000000F0 ~= 0 then n = n + 4;  x = x >> 4  end
    if x & 0x000000000000000C ~= 0 then n = n + 2;  x = x >> 2  end
    if x & 0x0000000000000002 ~= 0 then n = n + 1 end

    return n
end

--------------------------------------------------------------------------------
-- 位操作
--------------------------------------------------------------------------------

--- 找出一个数中唯一那个二进制位的位置(从 1 开始计数)
--- 仅接受 2 的幂,否则返回 nil。
--- 例:
---   FindSingleBitPosition(1)    --> 1
---   FindSingleBitPosition(2)    --> 2
---   FindSingleBitPosition(4)    --> 3
---   FindSingleBitPosition(8)    --> 4
---   FindSingleBitPosition(1024) --> 11
---   FindSingleBitPosition(12)   --> nil  (不是 2 的幂)
---   FindSingleBitPosition(0)    --> nil
---   FindSingleBitPosition(-4)   --> nil
--- @param num integer 必须为正整数且是 2 的幂
--- @return integer|nil
function util.FindSingleBitPosition(num)
    if num <= 0 or num % 1 ~= 0 then
        return nil
    end
    if (num & (num - 1)) ~= 0 then
        return nil
    end

    -- 直接复用 Log2Exact,零误差且覆盖 64 位
    return util.Log2Exact(num) + 1
end

--------------------------------------------------------------------------------
-- 采样 / 洗牌
--------------------------------------------------------------------------------

--- Fisher-Yates 洗牌采样。洗的是索引,不改动入参数组
--- (入参常是配置表缓存,原地洗牌会污染缓存)
--- 说明:
---   * 从 n 个元素中等概率抽取 sampleNum 个(无放回)。
---   * 只洗前 sampleNum 个位置,复杂度 O(sampleNum) 而非 O(n)。
---   * 返回新数组,不修改入参。
---   * 调用前请确保 math.randomseed 已执行(Lua 5.4 自动播种)。
--- @param array any[]     输入数组
--- @param num   integer   希望抽取的数量
--- @return any[]          抽取结果(新数组)
function util.SampleArray(array, num)
    if array == nil then
        return {}
    end

    local n = #array
    if num <= 0 or n == 0 then
        return {}
    end

    local sampleNum = math.min(math.floor(num), n)

    -- 索引表
    local indexes = {}
    for i = 1, n do
        indexes[i] = i
    end

    -- 只洗前 sampleNum 个位置:j ∈ [i, n]
    for i = 1, sampleNum do
        local j = math.random(i, n)
        indexes[i], indexes[j] = indexes[j], indexes[i]
    end

    -- 取前 sampleNum 个
    local result = {}
    for i = 1, sampleNum do
        result[i] = array[indexes[i]]
    end

    return result
end

--- Fisher-Yates 原地采样(会修改入参数组!)
--- 说明:
---   * 直接把前 sampleNum 个位置洗成随机样本,省掉索引表。
---   * 入参数组的前 sampleNum 个元素会被打乱,请勿用于共享/缓存数组。
---   * 返回的是新数组(从入参拷贝出的样本),不是入参本身。
---   * 调用前请确保 math.randomseed 已执行(Lua 5.4 自动播种)。
--- @param array any[]    输入数组(会被修改)
--- @param num   integer  希望抽取的数量
--- @return any[]         抽取结果(新数组)
function util.SampleArrayInPlace(array, num)
    if array == nil then
        return {}
    end

    local n = #array
    if num <= 0 or n == 0 then
        return {}
    end

    local sampleNum = math.min(math.floor(num), n)

    -- 原地 Fisher-Yates:只洗前 sampleNum 个位置
    for i = 1, sampleNum do
        local j = math.random(i, n)
        array[i], array[j] = array[j], array[i]
    end

    -- 拷贝前 sampleNum 个作为结果
    local result = {}
    for i = 1, sampleNum do
        result[i] = array[i]
    end

    return result
end

--- Fisher-Yates 极致省内存版(会修改入参数组!)
--- 说明:
---   * 只洗牌,不拷贝样本,零额外分配(除入参本身)。
---   * 洗完后样本位于 array[1 .. 返回的 sampleNum]。
---   * 入参数组前 sampleNum 个元素会被打乱,请勿用于共享/缓存数组。
---   * 调用前请确保 math.randomseed 已执行(Lua 5.4 自动播种)。
--- @param array any[]    输入数组(会被修改)
--- @param num   integer  希望抽取的数量
--- @return integer       实际样本数量 sampleNum,样本在 array[1..sampleNum]
function util.ShuffleInPlace(array, num)
    if array == nil then
        return 0
    end

    local n = #array
    if num <= 0 or n == 0 then
        return 0
    end

    local sampleNum = math.min(math.floor(num), n)

    for i = 1, sampleNum do
        local j = math.random(i, n)
        array[i], array[j] = array[j], array[i]
    end

    return sampleNum
end

--------------------------------------------------------------------------------
-- 便捷导出(可选)
--------------------------------------------------------------------------------

return util