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.
172 lines
5.6 KiB
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
|
|
|