BigNumber - Scalable Math System for Huge numbers in Roblox

Overview

Roblox’s default number type is based on double precision floating-point (64-bit), which limits you to roughly 1e+308 before the number overflows into infinity. This limitation can cause serious issues when your game involves currencies, stats, or progression systems that grow exponentially over time.

The BigNumber module provides a custom arithmetic system that allows you to represent and calculate values far beyond Roblox’s numeric boundaries.
It uses a mantissa + exponent structure similar to scientific notation (for example, 1.23e+100), ensuring your numbers remain mathematically valid even at extreme magnitudes.

This module focuses on stability, readability, and usability, and includes automatic normalization, string parsing, abbreviations, and compact formatting.

How It Works

For example, BigNumber.new(5.2, 6) is equal to 5.2 × 10^6 or 5,200,000.
The module ensures that:

  • The mantissa always remains between 1 and 10.
  • The exponent scales accordingly.
  • Trailing decimals and zeros are trimmed automatically to keep results clean.

This process is called normalization, and it ensures consistency across all calculations. Even if you multiply, divide, or raise to a power, the output will always remain properly normalized.

When NOT to Use It

While BigNumber is powerful, it is not a replacement for all numeric needs.
Here are the main limitations and things you should avoid:

  • Do not use NumberValue instances to store BigNumber results.
    Roblox’s NumberValue cannot store custom objects, and even if you store the mantissa or exponent separately, it will overflow once the exponent becomes too large.
    BigNumber exists specifically because Roblox’s number type cannot handle such scales.
  • Not designed for frequent per-frame operations.
    While efficient, BigNumber is written in pure Lua and does not match native arithmetic speed. Avoid using it inside physics or rendering loops.
  • No direct comparison to standard numbers without conversion.
    You must use provided methods (compare, toString, or from) to interact with regular numeric values.
  • This module does not support negative exponents for abbreviations.
    It handles positive exponents perfectly, but for very small decimals (less than 1), the display system switches to a scientific format automatically.

Note: code development is still open

--// BigNumber Module
--// Author: fariy's
--// Purpose: Allows calculations far beyond Roblox’s numeric limits using mantissa + exponent notation.

--[[
Example :
local a = BigNumber.new(3, 5)
local b = BigNumber.new(7, 5)

local sum = a:add(b)
print(sum:toString()) -- "1M"
--]]

local BigNumber = {}
BigNumber.__index = BigNumber

local TrimDecimalLevel = 4;
local Large_enough_to_shorten = 5; -- Determines how many digits the number must have in order to apply abbreviations.
local decimal_for_small_numbers = true; -- like 100'000

local Abbreviations = require(script:WaitForChild("Abbreviations"));

-- // Helper for trimming zeros
function BigNumber.TrimDecimal(num:number, IWantNumber:boolean?): number
	local multiplier = 10 ^ TrimDecimalLevel

	local trimmed = math.floor(num * multiplier) / multiplier

	-- Convert the number to a string and remove unnecessary zeros
	local str = string.format("%." .. TrimDecimalLevel .. "f", trimmed)
	str = str:gsub("0+$", "") -- remove trailing zeros
	str = str:gsub("%.$", "") -- if the last character is ‘.’, delete it too

	if IWantNumber then
		return tonumber(str) or 0;
	end;
	
	return str;
end;

-- // Constructor
function BigNumber.new(mantissa:number, exponent:number)
	local self = setmetatable({}, BigNumber)
	self.m = mantissa or 0
	self.e = exponent or 0
	self:normalize()
	return self
end

-- // Normalize (ensures mantissa is always < 10)
function BigNumber:normalize()
	-- Set the mantissa to the range 1–10
	while math.abs(self.m) >= 10 do
		self.m /= 10
		self.e += 1
	end
	while math.abs(self.m) < 1 and self.m ~= 0 do
		self.m *= 10
		self.e -= 1
	end

	-- Rounding: 3 decimal places
	self.m = BigNumber.TrimDecimal(self.m);

	-- If the mantissa is, for example, 1.000 → 1
	local mantissaStr = tostring(self.m)
	self.m = tonumber(mantissaStr)

	-- The same process for the exponent (rare but for stability)
	self.e = BigNumber.TrimDecimal(self.e);
	local expStr = tostring(self.e)
	self.e = tonumber(expStr) or 0;
end

-- // Clone
function BigNumber:clone()
	return BigNumber.new(self.m, self.e)
end

-- // Comparison (returns -1, 0, 1)
function BigNumber:compare(other)
	if self.e == other.e then
		if self.m == other.m then
			return 0
		elseif self.m > other.m then
			return 1
		else
			return -1
		end
	elseif self.e > other.e then
		return 1
	else
		return -1
	end
end

-- // Addition
function BigNumber:add(other)
	local a = self:clone()
	local b = other:clone()
	if math.abs(a.e - b.e) > 20 then
		-- Exponents are too far apart; smaller number is negligible
		return (a.e > b.e) and a or b
	end
	if a.e > b.e then
		b.m = b.m * 10^(b.e - a.e)
		b.e = a.e
	elseif b.e > a.e then
		a.m = a.m * 10^(a.e - b.e)
		a.e = b.e
	end
	local result = BigNumber.new(a.m + b.m, a.e)
	result:normalize()
	return result
end

-- // Subtraction
function BigNumber:sub(other)
	local negOther = BigNumber.new(-other.m, other.e)
	return self:add(negOther)
end

-- // Multiplication
function BigNumber:mul(other)
	local result = BigNumber.new(self.m * other.m, self.e + other.e)
	result:normalize()
	return result
end

-- // Division
function BigNumber:div(other)
	local result = BigNumber.new(self.m / other.m, self.e - other.e)
	result:normalize()
	return result
end

-- // Pow
function BigNumber:pow(power:number)
	if power == 0 then
		return BigNumber.new(1, 0)
	end

	if self.m == 0 then
		return BigNumber.new(0, 0)
	end

	-- The mantissa is taken, the exponent is multiplied
	local newMantissa = self.m ^ power
	local newExponent = self.e * power

	local result = BigNumber.new(newMantissa, newExponent)
	result:normalize()
	return result
end

-- // Convert to String (for printing)
function BigNumber:toString()
	-- Small numbers (below 10^6)
	if self.e < Large_enough_to_shorten then
		local num = self.m * 10 ^ self.e
		local str = BigNumber.TrimDecimal(num);
		
		if decimal_for_small_numbers then
			-- Flip the number over and divide it into groups of three, then flip it back over. Like my dad
			local formatted = string.reverse(str):gsub("(%d%d%d)", "%1'");
			-- If there is an unnecessary ' at the end, remove it.
			formatted = string.reverse(formatted):gsub("^'", "");
			return formatted
		end;
		
		return str
	end

	-- Abbreviation system
	for i = #Abbreviations, 1, -1 do
		local data = Abbreviations[i]
		if self.e >= data.Power then
			local scaled = BigNumber.new(self.m, self.e - data.Power);
			
			if not scaled.e or not scaled.m then error("attempt to perform arithmetic (pow) on number and nil") end;
			local num = scaled.m * (10 ^ scaled.e);

			-- If it's too big, switch to "e+" format
			if i == #Abbreviations and self.e - data.Power > 3 then
				local formatted = BigNumber.TrimDecimal(self.m)
				return formatted .. "e+" .. tostring(self.e):gsub("%.?0+$", "")
			end

			local formatted = BigNumber.TrimDecimal(num)
			return formatted .. data.Suffix
		end
	end

	-- Fallback: Scientific format (like 1e+30)
	local formatted = BigNumber.TrimDecimal(self.m)
	return formatted .. "e+" .. tostring(self.e):gsub("%.?0+$", "")
end


-- // Utility: Create from number or BigNumber
function BigNumber.from(value:any)
	if typeof(value) == "table" and value.m and value.e then
		return BigNumber.new(value.m, value.e)
	elseif typeof(value) == "number" then
		local e = 0
		local m = value
		if m == 0 then
			return BigNumber.new(0, 0)
		end
		while math.abs(m) >= 10 do
			m /= 10
			e += 1
		end
		while math.abs(m) < 1 do
			m *= 10
			e -= 1
		end
		return BigNumber.new(m, e)
	else
		error("Invalid value for BigNumber.from")
	end
end

function BigNumber.parse(str:string)
	-- Trim gaps
	str = tostring(str);
	str = string.gsub(str, "%s+", "")

	-- Scientific notation control (e.g., 1.23e+45)
	local mantissa, exponent = string.match(str, "([%d%.%-]+)e%+?(%-?%d+)")
	if mantissa and exponent then
		return BigNumber.new(tonumber(mantissa), tonumber(exponent))
	end

	-- Abbreviation (e.g., 9.87Dc)
	
	for _, data in ipairs(Abbreviations) do
		-- Let's ignore the case difference
		if string.lower(str):find(string.lower(data.Suffix) .. "$") then
			local num = string.gsub(str, data.Suffix, "")
			return BigNumber.new(tonumber(num), data.Power)
		end
	end

	-- Normal number (e.g., “12345”)
	if tonumber(str) then
		local num = tonumber(str)
		return BigNumber.from(num)
	end

	error("BigNumber.parse: Invalid number format -> " .. tostring(str))
end

-- // Easy Operations Helper
-- // Example: BigNumber.easy("1e+50") + BigNumber.easy("5e+40")
function BigNumber.easy(value)
	value = tostring(value);
	local mantissa, exponent = string.match(value, "([%d%.]+)e%+?(%-?%d+)")
	if mantissa and exponent then
		return BigNumber.new(tonumber(mantissa), tonumber(exponent))
	end
	error("Invalid BigNumber string format: " .. tostring(value))
end

-- // Quick String Formatter
-- // Converts a raw number directly into abbreviated string (e.g., 100000 -> "100K")
function BigNumber.easyString(value:number): string
	local exponent = math.floor(math.log10(value));
	local mantissa = value / (10 ^ exponent);
	
	local data = BigNumber.new(mantissa, exponent);
	return data:toString();
end

return BigNumber
2 Likes

Also Example for Abbreviations Module :

return {
	{Suffix = "K",  Power = 3},
	{Suffix = "M",  Power = 6},
	{Suffix = "B",  Power = 9},
	{Suffix = "T",  Power = 12},
	{Suffix = "q", Power = 15},
	{Suffix = "Q", Power = 18},
	{Suffix = "s", Power = 21},
	{Suffix = "S", Power = 24},
	{Suffix = "O", Power = 27},
	{Suffix = "N", Power = 30},
	{Suffix = "D", Power = 33},
	{Suffix = "UD",  Power = 36},
	{Suffix = "DD",  Power = 39},
	{Suffix = "TD",  Power = 42},
	{Suffix = "qD",  Power = 45},
	{Suffix = "QD", Power = 48},
	{Suffix = "sD", Power = 52},
	{Suffix = "SD", Power = 55},
	{Suffix = "OD", Power = 58},
	{Suffix = "ND", Power = 61},
	{Suffix = "V", Power = 64},
};