Lua runner with rich builtin stdlib
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
a/lua/stdlib/math.lua

172 lines
5.6 KiB

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