-- Round a finite float half away from zero, exactly. -- math.modf is exact, unlike the naive floor(x + 0.5) which goes wrong -- for values like 0.49999999999999994 (x + 0.5 rounds up to 1.0 in the float domain). local function roundHalfAwayFromZero(x) local i, f = math.modf(x) if f >= 0.5 then return i + 1 elseif f <= -0.5 then return i - 1 end return i end --- @brief Round a number to the nearest integer, or to N decimal places. --- --- round(value) and round(value, 0) return an integer; round(value, places > 0) returns a float. --- Rounds half away from zero. Errors on non-numbers, NaN and infinity, on negative or --- non-integer places, and (in the integer form) on results outside the Lua integer range. --- If value * 10^places overflows, rounding cannot change the value and it is returned unchanged. --- --- @param value number: Value to round --- @param places number|nil: Optional number of decimal places, default 0 (must be a non-negative integer) --- @return number: The rounded value function math.round(value, places) if type(value) ~= "number" then error("round() called with a non-number value") end if places == nil then places = 0 else places = math.tointeger(places) if places == nil then error("round() places must be an integer number") end if places < 0 then error("round() called with negative places") end end if math.type(value) == "integer" then if places == 0 then return value end return value + 0.0 -- an integer has no decimals to round, but the contract says float end if not math.isFinite(value) then error("round() called with a non-finite value (NaN or infinity)") end if places == 0 then local int = math.tointeger(roundHalfAwayFromZero(value)) if int == nil then error("round() called with value out of range of Lua integer") end return int end local mul = 10.0 ^ places local scaled = value * mul if not math.isFinite(scaled) then -- rounding cannot change a value of this magnitude return value end return roundHalfAwayFromZero(scaled) / mul end --- @brief Clamp number to an interval, optionally half-open. --- If both bounds are nil, just returns the value. --- Errors if any of the params is in wrong type (value must be number, min and max must be number or nil), --- or if min is greater than max. --- @param value number: Value to clamp --- @param min number|nil: Lower bound, or nil for no lower bound --- @param max number|nil: Upper bound, or nil for no upper bound --- @return number: Value clamped to the interval function math.clamp(value, min, max) if type(value) ~= "number" then error("Non-number passed to clamp() as value") end if min ~= nil and type(min) ~= "number" then error("Non-number passed to clamp() as minimum") end if max ~= nil and type(max) ~= "number" then error("Non-number passed to clamp() as maximum") end if min ~= nil and max ~= nil and min > max then error("clamp() minimum is greater than maximum") end if min ~= nil and value < min then return min end if max ~= nil and value > max then return max end return value end --- @brief Check if a value is a finite number (not NaN or infinity) --- @param value any: Value to check --- @return boolean: True if the value is a finite number; false for NaN, infinity and non-number values function math.isFinite(value) if type(value) ~= "number" then return false end -- NaN is the only value not equal to itself -- math.huge represents infinity return value == value and value ~= math.huge and value ~= -math.huge end --- @brief Get the sign of a number: -1 for negative, 0 for zero, 1 for positive. --- Note: NaN has no sign and yields 0. --- @param value number: Value to take the sign of --- @return number: -1, 0 or 1 function math.sign(value) if type(value) ~= "number" then error("Non-number passed to sign()") end if value > 0 then return 1 elseif value < 0 then return -1 else return 0 end end --- @brief Scale a value from one range to another (linear interpolation). --- @param value number: The input value to scale --- @param inMin number: The minimum of the input range --- @param inMax number: The maximum of the input range --- @param outMin number: The minimum of the output range --- @param outMax number: The maximum of the output range --- @param clamp boolean|nil: If true, clamp the result to [outMin, outMax]. Default is false. --- @return number: The scaled value function math.scale(value, inMin, inMax, outMin, outMax, clamp) if type(value) ~= "number" then error("Non-number passed to scale() as value") end if type(inMin) ~= "number" or type(inMax) ~= "number" then error("Non-number passed to scale() as input range") end if type(outMin) ~= "number" or type(outMax) ~= "number" then error("Non-number passed to scale() as output range") end if inMin == inMax then error("Input range cannot be zero (inMin == inMax)") end local ratio = (value - inMin) / (inMax - inMin) local result = outMin + ratio * (outMax - outMin) if clamp then local lo, hi = outMin, outMax if lo > hi then lo, hi = hi, lo end if result < lo then return lo elseif result > hi then return hi end end return result end