Various mathematical functions.
function export.tonumber_extended(x, base, finite_real, no_prefix)
An extended version of tonumber()
, which attempts to convert x
to a number. Like tonumber()
, it will convert from base 10 by default, and the optional parameter base
can be used to specify a different base between 2 and 36, with the letters A-Z
(case-insensitive) representing additional digits beyond 0-9
. When strings contain hexadecimal notation (e.g. "0x100"
), base 16 is used as the default instead, but this is overridden if base
is set to anything other than 16.
This function differs from tonumber()
in the following ways:
finite_real
is set, then the function will only return finite real numbers; inputs which would normally produce ±infinity or NaN will instead produce nil
.no_prefix
is set, then strings which start with "0x"
will not be interpreted as containing hexadecimal notation, resulting in nil
.base
is explicitly set to 10
, then strings in hexadecimal notation will always return nil
. This fixes a bug in tonumber()
, which treats base=10
the same as base
being unset, causing base 16 to be used if x
contains hexadecimal notation (e.g. tonumber("0x10", 10)
returns 16
, whereas tonumber_extended("0x10", 10)
returns nil
).function export.to_integer(x)
Converts x
to an integer by removing the fractional portion (e.g. 3.5
becomes 3
, and -2.9
becomes -2
). This is equivalent to rounding down positive numbers and rounding up negative numbers. If conversion is not possible, returns nil
.
function export.sign(x, signed_0)
Returns 1
if x
is positive, -1
if x
is negative, or 0
if x
is 0
.
If signed_0
is set, this function will only return either 1
or -1
, and will make a distinction between signed zeroes (+0
and -0
). This is useful when a 0
result could be disruptive (e.g. x % 0
).
function export.is_finite_real_number(x)
Returns true
if x
is a finite real number, or false
if not.
function export.is_integer(x)
Returns true
if x
is an integer, or false
if not.
function export.is_positive_integer(x, include_0)
Returns true
if x
is a positive integer (or zero if the include_0
flag is set), or false
if not.
function export.is_NaN(x)
Returns true
is x
is NaN (Not a Number), or false
if not.
NaN is a value that has the type "number", but does not represent an actual numeric value; it has the unique property that if x
is NaN, x ~= x
evaluates to true
.
function export.log10(x)
Returns the base-10 logarithm of x
.
This function should be used instead of math.log10
, which is deprecated and may stop working if Scribunto is updated to a more recent Lua version.
function export.to_hex(dec, include_prefix)
Converts a decimal number to hexadecimal. If include_prefix
is set, the returned number will include the 0x prefix.
function export.gcd(x, ...)
Returns the greatest common divisor of an arbitrary number of input numbers.
function export.lcm(x, ...)
Returns the least common multiple of an arbitrary number of input numbers.
local export = {}
local byte = string.byte
local ceil = math.ceil
local floor = math.floor
local format = string.format
local is_integer -- defined below
local match = string.match
local select = select
local tonumber = tonumber
local tonumber_ext -- defined below
local tostring = tostring
local type = type
local INF = math.huge
local function sign(x, signed_0)
if x > 0 then
return 1
elseif x < 0 then
return -1
elseif x == 0 then
-- 1/(+0) is infinity and 1/(-0) is -infinity.
return signed_0 and (1 / x > 0 and 1 or -1) or 0
end
-- NaN: convert to string with a forced sign prefix, and grab the first byte.
local sign = byte(format("%+f", x))
return sign == 0x2B and 1 or -- +
sign == 0x2D and -1 or -- -
-- If there's no sign, throw an error. This shouldn't be possible, but
-- avoids silent errors if it does happen.
error("Internal error: cannot determine sign of " .. x)
end
--[==[
An extended version of {tonumber()}, which attempts to convert `x` to a number. Like {tonumber()}, it will convert from base 10 by default, and the optional parameter `base` can be used to specify a different base between 2 and 36, with the letters {A-Z} (case-insensitive) representing additional digits beyond {0-9}. When strings contain hexadecimal notation (e.g. {"0x100"}), base 16 is used as the default instead, but this is overridden if `base` is set to anything other than 16.
This function differs from {tonumber()} in the following ways:
* If `finite_real` is set, then the function will only return finite real numbers; inputs which would normally produce ±infinity or NaN will instead produce {nil}.
* If `no_prefix` is set, then strings which start with {"0x"} will not be interpreted as containing hexadecimal notation, resulting in {nil}.
* If `base` is explicitly set to {10}, then strings in hexadecimal notation will always return {nil}. This fixes a bug in {tonumber()}, which treats {base=10} the same as {base} being unset, causing base 16 to be used if `x` contains hexadecimal notation (e.g. {tonumber("0x10", 10)} returns {16}, whereas {tonumber_extended("0x10", 10)} returns {nil}).]==]
function export.tonumber_extended(x, base, finite_real, no_prefix)
-- TODO: tonumber() maxes out at 2^64 if the base is anything other than 10.
-- TODO: support binary (0b) and octal (0o) prefixes.
local n = tonumber(x, base)
if not n or finite_real and (n ~= n or n == INF or n == -INF) then
return nil
-- If `base` is explicitly set to 10 (not simply nil), or `no_prefix` is set
-- and `base` is nil or 16, filter out inputs that started with hexadecimal
-- prefixes. Note that if `base` is anything else, the initial "0x" will
-- have been interpreted as digits by tonumber() instead of a prefix (as "x"
-- can be a digit from base 34 upwards), so there's no prefix to check for.
elseif base == 10 or no_prefix and (base == nil or base == 16) then
return not match(x, "^%s*?0()") and n or nil
end
return n
end
tonumber_ext = export.tonumber_extended
--[==[
Converts `x` to an integer by removing the fractional portion (e.g. {3.5} becomes {3}, and {-2.9} becomes {-2}). This is equivalent to rounding down positive numbers and rounding up negative numbers. If conversion is not possible, returns {nil}.]==]
function export.to_integer(x)
x = tonumber(x)
if not (x and x == x and x ~= INF and x ~= -INF) then
return nil
elseif x % 1 == 0 then
return x
-- Round-down positives.
elseif x >= 0 then
return floor(x)
end
--Round-up negatives.
return ceil(x)
end
--[==[
Returns {1} if `x` is positive, {-1} if `x` is negative, or {0} if `x` is {0}.
If `signed_0` is set, this function will only return either {1} or {-1}, and will make a distinction between ] ({+0} and {-0}). This is useful when a {0} result could be disruptive (e.g. {x % 0}).]==]
function export.sign(x, signed_0)
return sign(
tonumber(x) or
error(format("bad argument #1 to 'sign' (number expected, got %s)", type(x)), 2),
signed_0
)
end
--[==[
Returns {true} if `x` is a finite real number, or {false} if not.]==]
function export.is_finite_real_number(x)
return x and x == x and not (x == INF or x == -INF) and type(x) == "number"
end
--[==[
Returns {true} if `x` is an integer, or {false} if not.]==]
function export.is_integer(x)
return x and type(x) == "number" and x % 1 == 0 or false
end
is_integer = export.is_integer
--[==[
Returns {true} if `x` is a positive integer (or zero if the `include_0` flag is set), or {false} if not.]==]
function export.is_positive_integer(x, include_0)
return x and type(x) == "number" and (x > 0 or include_0 and x == 0) and x % 1 == 0 or false
end
--[==[
Returns {true} is `x` is ] (Not a Number), or {false} if not.
NaN is a value that has the type "number", but does not represent an actual numeric value; it has the unique property that if {x} is NaN, {x ~= x} evaluates to {true}.]==]
function export.is_NaN(x)
return x ~= x
end
--[==[
Returns the base-10 logarithm of `x`.
This function should be used instead of {math.log10}, which is deprecated and may stop working if Scribunto is updated to a more recent Lua version.]==]
function export.log10(x) -- Structured like this so that module documentation works.
local log10 = math.log10
if log10 ~= nil then
return log10
end
local log = math.log
return log(10, 10) == 1 and function(x) -- Lua 5.2
return log(x, 10)
end or function(x) -- Lua 5.1
return log(x) * 0.43429448190325182765112891891660508229439700580367 -- log10(e)
end
end
export.log10 = export.log10() -- Sets the actual returned function.
local function integer_error(x, param, func_name)
local type_x = type(x)
error(format(
"bad argument #%d to '%s' (integer expected, got %s)",
param, func_name, type_x == "number" and tostring(x) or type_x
), 3)
end
--[==[
Converts a decimal number to hexadecimal. If `include_prefix` is set, the returned number will include the 0x prefix.]==]
function export.to_hex(dec, include_prefix)
dec = tonumber(dec) or dec
if not is_integer(dec) then
integer_error(dec, 1, "to_hex")
end
local neg = dec < 0
if neg then
dec = -dec
end
-- Inputs >= 2^64 cause string.format to return "0".
if dec >= 0x1p64 then
error("integer overflow in 'to_hex': cannot convert inputs with a magnitude greater than or equal to 2^64 (18446744073709551616)", 2)
end
-- string.format treats hex numbers as unsigned, so any sign must be added manually.
return format("%s%s%X", neg and "-" or "", include_prefix and "0x" or "", dec)
end
--[==[
Returns the greatest common divisor of an arbitrary number of input numbers.]==]
function export.gcd(x, ...)
x = tonumber(x) or x
if not is_integer(x) then
integer_error(x, 1, "gcd")
end
local q, args_len, integers = ..., select("#", ...)
-- Compute p_1 = gcd(n_1, n_2), p_2 = gcd(p_1, n_3), ... i.e. compute GCD by Euclid's algorithm for the current result and the next number.
for i = 2, args_len + 1 do
q = tonumber(q) or q
if not is_integer(q) then
integer_error(q, i, "gcd")
elseif x ~= 1 then -- If x is 1, validate remaining inputs.
-- GCD of two integers x, q with Euclid's algorithm.
while q ~= 0 do
x, q = q, x % q
end
end
if i <= args_len then
-- Only create a table if absolutely necessary, as it's inefficient.
if i == 2 then
integers = {...}
end
q = integers
end
end
return x < 0 and -x or x
end
--[==[
Returns the least common multiple of an arbitrary number of input numbers.]==]
function export.lcm(x, ...)
x = tonumber(x) or x
if not is_integer(x) then
integer_error(x, 1, "lcm")
end
local q, args_len, integers = ..., select("#", ...)
-- Compute the product of all inputs as p and GCD as x.
for i = 2, args_len + 1 do
q = tonumber(q) or q
if not is_integer(q) then
integer_error(q, i, "lcm")
elseif x ~= 0 then -- If x is 0, validate remaining inputs.
-- Compute the product.
local p = x * q
-- GCD of two integers x, q with Euclid's algorithm.
while q ~= 0 do
x, q = q, x % q
end
-- Divide product by the GCD to get new LCM.
x = p / x
end
if i <= args_len then
-- Only create a table if absolutely necessary, as it's inefficient.
if i == 2 then
integers = {...}
end
q = integers
end
end
return x < 0 and -x or x
end
return export