Module:Number

From Melvor Idle
Revision as of 19:44, 13 April 2024 by Ricewind (talk | contribs)

Documentation for this module may be created at Module:Number/doc

--
-- Module that contains functions related to numeric formatting or number manipulation
--

local p = {}

local numberSuffixes = {
        k = 1e3,
        m = 1e6,
        b = 1e9,
        t = 1e12,
        q = 1e15,
        s = 1e18,
        o = 1e21,
        n = 1e24,
        d = 1e30
    }

local function sigfig(x, y)
	local x_sign = x < 0 and -1 or 1
	local x = math.abs(x)
	local n = math.floor(math.log10(x)) + 1 - y
	return x_sign * math.pow(10, n) * p.round2(x / math.pow(10, n), 0)
end

-- Automatically rounds to 2 places from the significant figure.
-- Taken from RSWiki.
function p.autoround(x)
	x = tonumber(x) or 0
	local _x
	if x == 0 then
		_x = 0
	elseif math.abs(x) < 0.1 then
		_x = sigfig(x, 2)
	elseif math.abs(x) > 999 then
		_x = p.round2(x, 0)
	else
		_x = p.round2(x, 2)
	end
	return _x
end  

--- Formats a number by inserting commas as thousand separators.
-- @param number (number or string) The number to format.
-- @return (string) The formatted number with commas as thousand separators
function p.formatnum(number)
	if tonumber(number) == nil then
		return number
	end
	
	-- Find out of the number is using scientific notation.
	-- If it is, convert it to a string and remove the trailing zeroes.
	local result = tostring(number)
	if result:find("[eE]") ~= nil then
		result = string.format("%.20f", number)
		result = result:gsub("%.?0*$", "")
	end

	while true do
		-- Format in blocks of 3 digits at a time until formatting is complete
        local k
		result, k = string.gsub(result, "^(-?%d+)(%d%d%d)", '%1,%2')
		if k == 0 then
			break
		end
	end
	return result
end    

--- Rounds a number to a specified number of digits after the decimal point.
-- @param val (number or string) The value to round.
-- @param maxDigits (number) [optional] The maximum number of digits after the decimal point.
-- @param minDigits (number) [optional] The minimum number of digits after the decimal point.
-- @return (string) The rounded number with the specified number of digits after the decimal point, or the input unchanged if it's not a number.
function p.round(val, maxDigits, minDigits)
	if val == nil then
		return nil
	else
		if type(maxDigits) == "table" then
			minDigits = maxDigits[2]
			maxDigits = maxDigits[1]
		end

		local result = val..""
		local decimals = string.find(result, "%.")
		if decimals ~= nil then
			decimals = string.len(result) - decimals
		else
			decimals = 0
		end

		if maxDigits ~= nil and decimals > maxDigits then
			result = string.format("%."..maxDigits.."f", result)
		elseif minDigits ~= nil and decimals < minDigits then
			result = string.format("%."..minDigits.."f", result)
		end

		return result
	end
end

--- Rounds a number to a specified number of decimal places.
-- @param num (number) The number to round.
-- @param numDecimalPlaces (number) [optional] The number of decimal places to round to.
-- @return (number) The rounded number with the specified number of decimal places, or the nearest integer if numDecimalPlaces is not provided.
--From http://lua-users.org/wiki/SimpleRound
function p.round2(num, numDecimalPlaces)
	local mult = 10^(numDecimalPlaces or 0)
	return math.floor(num * mult + 0.5) / mult
end

-- Euclidean Greatest Common Divisor algorithm
function p.gcd(a, b)
	if b ~= 0 then
		return p.gcd(b, a % b)
	else
		return math.abs(a)
	end
end

--Formats a pair of numbers as a reduced fraction
function p.fraction(n, d)
	local gcd = p.gcd(n, d)
	return p.formatnum(n/gcd)..'/'..p.formatnum(d/gcd)
end

--Similar to p.fraction but returns the simplified numerator and denomerator separately without formatting
function p.fractionpair(n, d)
	local gcd = p.gcd(n, d)
	return n / gcd, d / gcd
end

--Returns a number including the sign, even if positive
function p.numStrWithSign(number)
	if number >= 0 then
		return '+'..p.formatnum(number)
	else
		return p.formatnum(number)
	end
end

--- Clamps a value between a minimum and maximum range.
-- @param value (number) The value to be clamped.
-- @param min (number) The minimum value in the range.
-- @param max (number) The maximum value in the range.
-- @return (number) The clamped value within the specified range.
function p.clamp(value, min, max)
    return math.min(math.max(value, min), max)
end

--- Parses a string representation of a number with suffixes
-- @param str (string) The string to parse.
-- @return (number) The numeric value represented by the input string or 0 if the string is NaN.
function p.parseNumber(str)
	-- Empty/nil string.
	if not str or str == "" then 
		return 0 
	end

	-- If the input is already a number, return it.
	local number = tonumber(str)
    if number then
        return number
    end
    
    local numStr = str:sub(1, -2)
    local suffix = str:sub(-1):lower()
    
    if tonumber(numStr) then
    	local multiplier = numberSuffixes[suffix] or 1
    	return numStr * multiplier
    else
    	return 0
    end
end

--- Format a number with optional suffixes based on its magnitude.
-- @param number (number) The number to be formatted.
-- @param decimals (number) [optional] The number of decimals to include in the formatted output. Default is 2.
-- @return (string) The formatted number as a string
function p.abbreviateNumber(number, decimals)
	if type(number) ~= "number" then
        error("NaN")
    end
	-- Round to two decimals by default.
    decimals = decimals or 2

	for suffix, value in pairs(numberSuffixes) do
        if number >= value then
            if number % 1 ~= 0 then
                return string.format("%." .. decimals .. "f%s", number / value, suffix)
            else
                return string.format("%.0f%s", number / value, suffix)
            end
        end
    end

    return tostring(number)
end

--- Converts a string to a number or returns a default value if conversion fails.
-- @param str (string) The string to convert to a number.
-- @param def (number) The default value to return if conversion fails or the input is nil.
-- @return (number) The numeric value represented by the input string or the default value if conversion fails.
function p.toNumberOrDefault(str, def)
	local num = tonumber(str)
	if num then
		return num
	else
		return def
	end
end

--- Converts a string to a number or throws an error with a specified message if conversion fails.
-- @param str (string) The string to convert to a number.
-- @param errorMsg (string) [optional] The error message to throw if conversion fails. Defaults to "NaN".
-- @return (number) The numeric value represented by the input string.
-- @throws Error with specified error message if conversion fails.
function p.toNumberOrError(str, errorMsg)
	local num = tonumber(str)
	local msg = errorMsg
	
	if msg == nil then
		msg = "NaN"
	end
	
	if num then
		return num
	else
		error(msg)
	end
end

return p