[31.1] FormatNumber - A module for formatting numbers

Yea my bad should’ve read properly I I thought of Western Arabic Numerals as the “Arabic Numbers” and Eastern were the ones on the clock in the wiki you linked as it showed up first.

Also you made a typo

1 Like

Thank you so much :)) I greatly appreciate this!

1 Like

now, how would you make 1e+17 for example, become 100,000,000,000,000,000 via script?? lol

1 Like

i figured it out, was a mistake in my code lol

1 Like

Having some trouble figuring out how to convert a number into a format such as 150K. I’ve read the documentation, but it really isn’t making sense to me. Could somebody provide an example?

1 Like

Not the best at documentation. Maybe use NumberFormatInfo?

FormatNumber.FormatCompact(150000, NumberFormatInfo.presets.en)

150K

Number Default NumberFormatInfo.presets.en
1,234 1234 (But will return 1 234) 1.2K
12,345 12 345 12K
123,456 123 456 123K
1,234,567 1,2 M 1.2M
12,345,678 12 M 12M
123,456,789 123 M 123M
1,234,567,890 1 G 1B

Float types can only reach up to 9 long-scale quadrillion (2^53 or 9 007 199 254 740 992) without being weird, I’ve made BigInteger module similar to C#'s BigInteger somewhere to bypass this.

You could do this but this only works for positive integers (just replace spaces with commas, I use spaces to prevent ambiguity)

("%.0f"):format(1e17):reverse():gsub("%d%d%d"):gsub(" $", ''):reverse();

or you could use the untested CLDRTools

CLDR.Numbers.FormatDecimal(CLDR.Locale.new('en'), BigInteger.new('100000000000000000'))

100,000,000,000,000,000

3 Likes

Remade the module.
Source:

--[=[
	Version 2.0.1
	This is intended for Roblox ModuleScripts
	BSD 2-Clause Licence
	Copyright ©, 2020 - Blockzez (devforum.roblox.com/u/Blockzez and github.com/Blockzez)
	All rights reserved.
	
	Redistribution and use in source and binary forms, with or without
	modification, are permitted provided that the following conditions are met:
	
	1. Redistributions of source code must retain the above copyright notice, this
	   list of conditions and the following disclaimer.
	
	2. Redistributions in binary form must reproduce the above copyright notice,
	   this list of conditions and the following disclaimer in the documentation
	   and/or other materials provided with the distribution.
	
	THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
	AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
	IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
	DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
	FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
	DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
	SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
	CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
	OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
	OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
]=]--
local f = { };

local function to_literal(str)
	return "'" .. str .. "'";
end;

function f.AbbreviationToCLDR(abbreviations, include_currency)
	local ret = { };
	for _, p in ipairs(abbreviations) do
		for zcount = 1, 3 do
			if p == '' or not p then
				table.insert(ret, '0');
			else
				table.insert(ret, (include_currency and '¤' or '') .. ('0'):rep(zcount) .. p:gsub('¤', "'¤'"):gsub('‰', "'‰'"):gsub("[.;,#%%]", to_literal):gsub("'", "''"));
			end;
		end;
	end;
	return ret;
end;

-- Berezaa's suffix
local defaultCompactPattern = f.AbbreviationToCLDR {
	-- https://minershaven.fandom.com/wiki/Cash_Suffixes
	"k", "M", "B", "T", "qd", "Qn", "sx", "Sp", "O", "N", "de", "Ud", "DD", "tdD",
	"qdD", "QnD", "sxD", "SpD", "OcD", "NvD", "Vgn", "UVg", "DVg", "TVg", "qtV",
	"QnV", "SeV", "SPG", "OVG", "NVG", "TGN", "UTG", "DTG", "tsTG", "qtTG", "QnTG",
	"ssTG", "SpTG", "OcTG", "NoTG", "QdDR", "uQDR", "dQDR", "tQDR", "qdQDR", "QnQDR",
	"sxQDR", "SpQDR", "OQDDr", "NQDDr", "qQGNT", "uQGNT", "dQGNT", "tQGNT", "qdQGNT",
	"QnQGNT", "sxQGNT", "SpQGNT", "OQQGNT", "NQQGNT", "SXGNTL", "USXGNTL", "DSXGNTL",
	"TSXGNTL", "QTSXGNTL", "QNSXGNTL", "SXSXGNTL", "SPSXGNTL", "OSXGNTL", "NVSXGNTL",
	"SPTGNTL", "USPTGNTL", "DSPTGNTL", "TSPTGNTL", "QTSPTGNTL", "QNSPTGNTL", "SXSPTGNTL",
	"SPSPTGNTL", "OSPTGNTL", "NVSPTGNTL", "OTGNTL", "UOTGNTL", "DOTGNTL", "TOTGNTL", "QTOTGNTL",
	"QNOTGNTL", "SXOTGNTL", "SPOTGNTL", "OTOTGNTL", "NVOTGNTL", "NONGNTL", "UNONGNTL", "DNONGNTL",
	"TNONGNTL", "QTNONGNTL", "QNNONGNTL", "SXNONGNTL", "SPNONGNTL", "OTNONGNTL", "NONONGNTL", "CENT", "UNCENT",
};

local sym_tables = { '¤', '%', '-', '+', 'E', '', '‰', '*' };
local function generatecompact(ptn)
	local org_ptn = ptn;
	if type(ptn) ~= "string" then
		error("Compact patterns must be a table of string", 4);
	end;
	local ret, size, i0 = { }, 0, 1;
	while i0 do
		local i1 = math.min(ptn:find("[0-9@#.,+%%;*'-]", i0) or #ptn + 1, ptn:find('¤', i0) or #ptn + 1, ptn:find('‰', i0) or #ptn + 1);
		local i2 = i1 + ((ptn:sub(i1, i1 + 1) == '¤' or ptn:sub(i1, i1 + 1) == '‰') and 1 or 0);
		local chr = ptn:sub(i1, i2);
		-- Literal charaters
		if chr == "'" then
			local r = ptn:sub(i0, i1 - 1);
			i0 = ptn:find("'", i1 + 1);
			if i0 == i1 + 1 then
				r = r .. "'";
			elseif i0 then
				r = r .. ptn:sub(i1 + 1, i0 - 1);
			else
				error("'" .. org_ptn .. "' is not a valid pattern", 2);
			end;
			table.insert(ret, r);
			i0 = i0 + 1;
		-- This is the rounding, which we're not using
		elseif chr:match('[1-9]') then
			error("The rounding (1-9) pattern isn't supported", 4);
		elseif chr == '¤' or chr == '‰' or chr:match('[%%*+%-]') then
			error("The '" .. chr .. "' pattern isn't supported", 4);
		elseif chr == '0' then
			table.insert(ret, ptn:sub(i0, i1 - 1));
			table.insert(ret, 0);
			
			i0 = ptn:find('[^0]', i1);
			local int = ptn:sub(i1, (i0 or #ptn + 1) - 1);
			
			if (not int:match('^0+$')) or size > 0 then
				error("'" .. org_ptn .. "' is not a valid pattern", 4);
			end;
			
			size = #int;
		else
			table.insert(ret, ptn:sub(i0));
			i0 = nil;
		end;
	end;
	
	return ret, size;
end;

-- From International, modified
local valid_value_property =
{
	groupSymbol = "f/str",
	decimalSymbol = "f/str",
	compactPattern = "f/table",
	
	style = { "decimal", "currency", "percent" },
	useGrouping = { "min2", "always", "never" },
	minimumIntegerDigits = "f/1..",
	maximumIntegerDigits = "f/minimumIntegerDigits..inf",
	minimumFractionDigits = "f/0..",
	maximumFractionDigits = "f/minimumFractionDigits..inf",
	minimumSignificantDigits = "f/1..",
	maximumSignificantDigits = "f/minimumSignificantDigits..inf",
	currency = "f/str",
	rounding = { "halfUp", "halfEven", "halfDown", "ceiling", "floor" },
};

local function check_property(tbl_out, tbl_to_check, property, default)
	local check_values = valid_value_property[property];
	if not check_values then
		return;
	end;
	
	local value = rawget(tbl_to_check, property);
	local valid = false;
	if type(check_values) == "table" then
		valid = table.find(check_values, value);
	elseif check_values == 'f/bool' then
		valid = (type(value) == "boolean");
	elseif check_values == 'f/str' then
		valid = (type(value) == "string");
	elseif check_values == 'f/table' then
		valid = (type(value) == "table");
	elseif not check_values then
		valid = true;
	elseif type(value) == "number" and (value % 1 == 0) or (value == math.huge) then
		local min, max = check_values:match("f/(%w*)%.%.(%w*)");
		valid = (value >= (tbl_out[min] or tonumber(min) or 0)) and ((max == '' and value ~= math.huge) or (value <= tonumber(max)));
	end;
	if valid then
		tbl_out[property] = value;
		return;
	elseif value == nil then
		if type(default) == "string" and (default:sub(1, 7) == 'error: ') then
			error(default:sub(8), 4);
		end;
		tbl_out[property] = default;
		return;
	end;
	error(property .. " value is out of range.", 4);
end;
local function check_options(ttype, options)
	local ret = { };
	if type(options) ~= "table" then
		options = { };
	end;
	check_property(ret, options, "groupSymbol", ',');
	check_property(ret, options, "decimalSymbol", '.');
	check_property(ret, options, 'useGrouping', (ttype == "compact") and "min2" or "always");
	check_property(ret, options, 'style', 'decimal');
	if ttype == "compact" then
		check_property(ret, options, 'compactPattern', defaultCompactPattern);
	end;
	
	if ret.style == "currency" then
		check_property(ret, options, 'currency', 'error: Currency is required with currency style');
	end;
	
	check_property(ret, options, 'rounding', 'halfEven');
	ret.isSignificant = not not (rawget(options, 'minimumSignificantDigits') or rawget(options, 'maximumSignificantDigits'));
	if ret.isSignificant then
		check_property(ret, options, 'minimumSignificantDigits', 1);
		check_property(ret, options, 'maximumSignificantDigits');
	else
		check_property(ret, options, 'minimumIntegerDigits', 1);
		check_property(ret, options, 'maximumIntegerDigits');
		check_property(ret, options, 'minimumFractionDigits');
		check_property(ret, options, 'maximumFractionDigits');
		
		if not (ret.minimumFractionDigits or ret.maximumFractionDigits) then
			if ret.style == "percent" then
				ret.minimumFractionDigits = 0;
				ret.maximumFractionDigits = 0;
			elseif ttype ~= "compact" then
				ret.minimumFractionDigits = 0;
				ret.maximumFractionDigits = 3;
			end;
		end;
	end;
	return ret;
end;

local function quantize(val, exp, rounding)
	local d, e = ('0' .. val):gsub('%.', ''), (val:find('%.') or (#val + 1)) + 1;
	local pos = e + exp;
	if pos > #d then
		return val:match("^(%d*)%.?(%d*)$");
	end;
	d = d:split('');
	local add = rounding == 'ceiling';
	if rounding ~= "ceiling" and rounding ~= "floor" then
		add = d[pos]:match(((rounding == "halfEven" and (d[pos - 1] or '0'):match('[02468]')) or rounding == "halfDown") and '[6-9]' or '[5-9]');
	end;
	for p = pos, #d do
		d[p] = 0
	end;
	if add then
		repeat
			if d[pos] == 10 then
				d[pos] = 0;
			end;
			pos = pos - 1;
			d[pos] = tonumber(d[pos]) + 1;
		until d[pos] ~= 10;
	end;
	return table.concat(d, '', 1, e - 1), table.concat(d, '', e);
end;
local function scale(val, exp)
	val = ('0'):rep(-exp) .. val .. ('0'):rep(exp);
	local unscaled = (val:gsub("[.,]", ''));
	local len = #val;
	local dpos = (val:find("[.,]") or (len + 1)) + exp;
	return unscaled:sub(1, dpos - 1) .. '.' .. unscaled:sub(dpos);
end;
local function compact(val, size)
	val = (val:gsub('%.', ''));
	return val:sub(1, size) .. '.' .. val:sub(size + 1);
end;
local function raw_format(val, minintg, maxintg, minfrac, maxfrac, rounding)
	local intg, frac;
	if maxfrac and maxfrac ~= math.huge then
		intg, frac = quantize(val, maxfrac, rounding);
	else
		intg, frac = val:match("^(%d*)%.?(%d*)$");
	end;
	intg = intg:gsub('^0+', '');
	frac = frac:gsub('0+$', '');
	local intglen = #intg;
	local fraclen = #frac;
	if minintg and (intglen < minintg) then
		intg = ('0'):rep(minintg - intglen) .. intg;
	end;
	if minfrac and (fraclen < minfrac) then
		frac = frac .. ('0'):rep(minfrac - fraclen);
	end;
	if maxintg and (intglen > maxintg) then
		intg = intg:sub(-maxintg);
	end;
	if frac == '' then
		return intg;
	end;
	return intg .. '.' .. frac;
end;
local function raw_format_sig(val, min, max, rounding)
	local intg, frac;
	if max and max ~= math.huge then
		intg, frac = quantize(val, max - ((val:find('%.') or (#val + 1)) - 1), rounding);
	else
		intg, frac = val:match("^(%d*)%.?(%d*)$");
	end;
	intg = intg:gsub('^0+', '');
	frac = frac:gsub('0+$', '');
	if min then
		min = math.max(min - #val:gsub('%.%d*$', ''), 0);
		if #frac < min then
			frac = frac .. ('0'):rep(min - #frac);
		end;
	end;
	if frac == '' then
		return intg;
	end;
	return intg .. '.' .. frac;
end;
local function parse_exp(val)
	if not val:find('[eE]') then
		return val;
	end;
	local negt, val, exp = val:match('^([+%-]?)(%d*%.?%d*)[eE]([+%-]?%d+)$');
	if val then
		exp = tonumber(exp);
		if not exp then
			return nil;
		end;
		if val == '' then
			return nil;
		end;
		return negt .. scale(val, exp);
	end;
	return nil;
end;
local function num_to_str(value, scale_v)
	local value_type = typeof(value);
	if value_type == "number" then
		value = ('%.17f'):format(value);
	else
		value = tostring(value);
		value = parse_exp(value) or value:lower();
	end;
	if scale_v then
		value = scale(value, scale_v);
	end;
	return value;
end;

local function format(ttype, ...)
	if select('#', ...) == 0 then
		error("missing argument #1", 3);
	end;
	local value, options = ...;
	options = check_options(ttype, options);
	value = num_to_str(value, options.style == "percent" and 2);
	
	-- from International, modified
	local negt, post = value:match("^([+%-]?)(.+)$");
	local tokenized_compact;
	if post:match("^[%d.]*$") and select(2, post:gsub('%.', '')) < 2 then
		local minfrac, maxfrac = options.minimumFractionDigits, options.maximumFractionDigits;
		if ttype == "compact" then
			post = post:gsub('^0+$', '');
			local intlen = #post:gsub('%..*', '') - 3;
			-- Just in case, that pattern is '0'
			if (options.compactPattern[math.min(intlen, #options.compactPattern)] or '0') ~= '0' then
				local size;
				tokenized_compact, size = generatecompact(options.compactPattern[math.min(intlen, #options.compactPattern)]);
				post = compact(post, size + math.max(intlen - #options.compactPattern, 0));
			-- The '0' pattern indicates no compact number available
			end;
			if not (minfrac or maxfrac) then
				maxfrac = ((#post:gsub('%.%d*$', '') < 2) and 1 or 0);
			end;
		end;
		
		if options.isSignificant then
			post = raw_format_sig(post, options.minimumSignificantDigits, options.maximumSignificantDigits, options.rounding);
		else
			post = raw_format(post, options.minimumIntegerDigits, options.maximumIntegerDigits, minfrac, maxfrac, options.rounding);
		end;
	elseif (post == "inf") or (post == "infinity") then
		return negt == '-' and '-∞' or '∞';
	else
		return 'NaN';
	end;
	negt = (negt == '-');
	
	local ret;
	local first, intg, frac = post:match("^(%d)(%d*)%.?(%d*)$");
	if (options.useGrouping ~= "never") and (#intg > (options.useGrouping == "min2" and 3 or 2)) then
		intg = intg:reverse():gsub("%d%d%d", "%1" .. options.groupSymbol:reverse()):reverse();
	end;
	ret = (negt and '-' or '') .. ((options.currency and (options.currency .. (options.currency:match("%a$") and ' ' or ''))) or '') .. first .. intg .. (frac == '' and '' or (options.decimalSymbol .. frac)) .. (options.style == "percent" and '%' or '');
	if tokenized_compact then
		local value_pos = table.find(tokenized_compact, 0);
		if value_pos then
			tokenized_compact[value_pos] = ret;
		end;
		return table.concat(tokenized_compact);
	end;
	return ret;
end;

function f.FormatStandard(...)
	return format('standard', ...);
end;

function f.FormatCompact(...)
	return format('compact', ...);
end;

return setmetatable({ }, { __metatable = "The metatable is locked", __index = f,
	__newindex = function()
		error("Attempt to modify a readonly table", 2);
	end,
});
Version 1 text

Current Version 1.1.0: FormatNumber.rbxm (6,3 KB)

Previous versions
1.0.0: FormatNumber.rbxm (5,5 KB)

What’s this module?

It’s a module that’s capable for formatting/shortening numbers
image
into something like this:
image

As you can see, this is more visually appealing than the first one.

Features

It supports, decimals, negatives and exponents as well as currencies and percentages. It also supports formatting array-like numeric tables like { 1, 2, 3 }.

Functions

FormatNumber.FormatCustom

Returns the formatted number in the specified pattern

Overloads
FormatNumber.FormatCustom(value, pattern, currency_symbol, nfi, ignore_minimum_grouping_digits, decimal_quantization)
FormatNumber.FormatCustom(value, pattern, currency_symbol, nfi, ignore_minimum_grouping_digits)
FormatNumber.FormatCustom(value, pattern, currency_symbol, nfi)
FormatNumber.FormatCustom(value, pattern, nfi, ignore_minimum_grouping_digits, decimal_quantization)
FormatNumber.FormatCustom(value, pattern, nfi, ignore_minimum_grouping_digits)
FormatNumber.FormatCustom(value, pattern, nfi)
FormatNumber.FormatCustom(value, pattern)

Parameters

  • value – The number to format
  • pattern
  • currency_symbol - The currency symbol that the pattern substitutes to.
  • nfi – The NumberFormatInfo
  • ignore_minimum_grouping_digits – Ignores the NumberFormatInfo’s MinimumGroupingDigits, so it’ll group numbers like 1000 regardless of MinimumGroupingDigits value
  • decimal_quantization – Round high-percision numbers to the format pattern. Defaults to true
FormatNumber.FormatDecimal

Returns the formatted number in the pattern of a specified locale

Overloads
FormatNumber.FormatDecimal(value, nfi, ignore_minimum_grouping_digits, decimal_quantization)
FormatNumber.FormatDecimal(value, nfi, ignore_minimum_grouping_digits)
FormatNumber.FormatDecimal(value, nfi)
FormatNumber.FormatDecimal(value)

Parameters

  • value – The number to format
  • nfi – The NumberFormatInfo
  • ignore_minimum_grouping_digits – Ignores the NumberFormatInfo’s MinimumGroupingDigits, so it’ll group numbers like 1000 regardless of MinimumGroupingDigits value
  • decimal_quantization – Round high-percision numbers to the format pattern. Defaults to true
FormatNumber.FormatCurrency

Returns the formatted Currencyial number in the pattern of a specified NumberFormatInfo.

Overloads
FormatNumber.FormatCurrency(value, currency_symbol, nfi, ignore_minimum_grouping_digits, decimal_quantization)
FormatNumber.FormatCurrency(value, currency_symbol, nfi, ignore_minimum_grouping_digits)
FormatNumber.FormatCurrency(value, currency_symbol, nfi)
FormatNumber.FormatCurrency(value, currency_symbol)

Parameters

  • value – The number to format
  • currency_symbol - The currency symbol that the pattern substitutes to.
  • nfi – The NumberFormatInfo
  • ignore_minimum_grouping_digits – Ignores the NumberFormatInfo’s MinimumGroupingDigits, so it’ll group numbers like 1000 regardless of MinimumGroupingDigits value
  • decimal_quantization – Round high-percision numbers to the format pattern. Defaults to true
FormatNumber.FormatExponent

Returns the formatted exponential number in the pattern of a specified NumberFormatInfo.
Overloads
FormatNumber.FormatExponent(value, nfi, ignore_minimum_grouping_digits, decimal_quantization)
FormatNumber.FormatExponent(value, nfi, ignore_minimum_grouping_digits)
FormatNumber.FormatExponent(value, nfi)
FormatNumber.FormatExponent(value)

Parameters

  • value – The number to format
  • nfi – The NumberFormatInfo
  • ignore_minimum_grouping_digits – Ignores the NumberFormatInfo’s MinimumGroupingDigits, so it’ll group numbers like 1000 regardless of MinimumGroupingDigits value
  • decimal_quantization – Round high-percision numbers to the format pattern. Defaults to true
FormatNumber.FormatPercent

Returns the formatted number in the pattern of a specified locale

Overloads
FormatNumber.FormatPercent(value, nfi, ignore_minimum_grouping_digits, decimal_quantization)
FormatNumber.FormatPercent(value, nfi, ignore_minimum_grouping_digits)
FormatNumber.FormatPercent(value, nfi)
FormatNumber.FormatPercent(value)

Parameters

  • value – The number to format
  • nfi – The NumberFormatInfo
  • ignore_minimum_grouping_digits – Ignores the NumberFormatInfo’s MinimumGroupingDigits, so it’ll group numbers like 1000 regardless of MinimumGroupingDigits value
  • decimal_quantization – Round high-percision numbers to the format pattern. Defaults to true
FormatNumber.FormatList

(Partially undocumented)
Format lists of numbers, e.g. { 1500, 3000, 4500 } would return 1,500, 3,000, 4,500, supports Vector2, Vector3, Vector2int16, Vector3int16, UDim, UDim2, Color3 and Rect.

FormatNumber.FormatCompactCustom

Returns the formatted compacted number like 1.5K
Overloads
FormatNumber.FormatCompactCustom(value, compactlist, currency_symbol, nfi, ignore_minimum_grouping_digits)
FormatNumber.FormatCompactCustom(value, compactlist, currency_symbol, nfi)
FormatNumber.FormatCompactCustom(value, compactlist, nfi, ignore_minimum_grouping_digits)
FormatNumber.FormatCompactCustom(value, compactlist, nfi)
FormatNumber.FormatCompactCustom(value, compactlist)

Parameters

  • value – The number to format
  • compactlist – An array of patterns, the function will get the index the length of the value of this array, '0' or '' indicates there aren’t any compact number format available thus, will just return the formatted number
  • currency_symbol – The currency symbol that the pattern substitutes to.
  • nfi – The NumberFormatInfo
  • ignore_minimum_grouping_digits – Ignores the NumberFormatInfo’s MinimumGroupingDigits, so it’ll group numbers like 1000 regardless of MinimumGroupingDigits value
FormatNumber.FormatCompactDecimal

Returns the formatted compacted number like 1.5K depending on the NumberFormatInfo’s pattern
Overloads
FormatNumber.FormatCompactDecimal(value, nfi, ignore_minimum_grouping_digits)
FormatNumber.FormatCompactDecimal(value, nfi)
FormatNumber.FormatCompactDecimal(value)

Parameters

  • value – The number to format
  • nfi – The NumberFormatInfo
  • ignore_minimum_grouping_digits – Ignores the NumberFormatInfo’s MinimumGroupingDigits, so it’ll group numbers like 1000 regardless of MinimumGroupingDigits value
FormatNumber.FormatCompactCurrency

Returns the currency formatted compacted number like 1.5K depending on the NumberFormatInfo’s pattern
Overloads
FormatNumber.FormatCompactCurrency(value, currency_symbol, nfi, ignore_minimum_grouping_digits)
FormatNumber.FormatCompactCurrency(value, currency_symbol, nfi)
FormatNumber.FormatCompactCurrency(value, nfi, ignore_minimum_grouping_digits)
FormatNumber.FormatCompactCurrency(value, nfi)
FormatNumber.FormatCompactCurrency(nfi)

Parameters

  • value – The number to format
  • currency_symbol – The currency symbol that the pattern substitutes to.
  • nfi – The NumberFormatInfo
  • ignore_minimum_grouping_digits – Ignores the NumberFormatInfo’s MinimumGroupingDigits, so it’ll group numbers like 1000 regardless of MinimumGroupingDigits value
FormatNumber.ParseFloat

Converts formatted string to lua’s number type.

Overloads
FormatNumber.ParseFloat(str, nfi, strict)
FormatNumber.ParseFloat(str, nfi)
FormatNumber.ParseFloat(str)

Parameters

  • str – The string to parse
  • nfi – The NumberFormatInfo
  • strict

FormatNumber.NumberFormatInfo

FormatNumber.NumberFormatInfo.new

Create a new NumberFormat, with the properties as its argument e.g. FormatNumber.NumberFormat.new{ DecimalSymbol = '.' } creates a new NumberFormatInfo where the DecimalSymbol are .
The default are

{
	DecimalSymbol = ',';
	GroupingSymbol = ' ';
	NaNSymbol = 'NaN';
	NegativeSign = '-';
	PositiveSign = '+';
	InfinitySymbol = '∞';
	PercentSymbol = '%';
	PerMilleSymbol = '‰';
	ExponentSymbol = 'E';
	ListSymbol = ';';
	
	DecimalFormat = "#.###,###";
	CurrencyFormat = "#.###,00 ¤";
	ExponentFormat = "#E0";
	PercentFormat = "#.### %";
	
	DecimalCompact = { '0', '0', '0', '0', '0', '0', '0,# M', '00 M', '000 M', '0,# G', '00 G', '000 G', '0,# T', '00 T', '000 T' };
	CurrencyCompact = { '0', '0', '0', '0', '0', '0', '0,# M ¤', '00 M ¤', '000 M ¤', '0,# G ¤', '00 G ¤', '000 G ¤', '0,# T ¤', '00 T ¤', '000 T ¤' };
	
	NativeDigits = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };
	MinimumGroupingDigits = 1;

	ReadOnly = false;
}
FormatNumber.NumberFormatInfo:Clone

Clones a non read-only version of NumberFormatInfo
Overloads
NumberFormatInfo:Clone()
FormatNumber.NumberFormatInfo.Clone(self)

FormatNumber.NumberFormatInfo.Preset

A table of already done NumberFormatInfo in the following langauges:

  • English (en)
  • French (fr)
  • Spanish (es)
  • Latin American Spanish (es_MX (1.0.0); es_419 (≥1.1.0))
  • German (de)
  • Japanese (ja)
  • Portuguese (pt) (≥1.1.0)
  • Portugal Portuguese (pt_PT) (≥1.1.0)
  • Russian (ru) (≥1.1.0)
  • Simplified Chinese (zh) (≥1.1.0)
Properties
  • DecimalSymbol – Gets the decimal separator.

  • GroupingSymbol – Gets the digit grouping separator.

  • NaNSymbol – Gets the NaN symbol. (Not substituted in 1.0.0)

  • NegativeSign – Gets the negative sign symbol.

  • PositiveSign – Gets the positive sign symbol.

  • InfinitySymbol – Gets the infinity symbol. (Not substituted in 1.0.0)

  • PercentSymbol – Gets the percent symbol.

  • PerMilleSymbol – Gets the per-mille symbol.

  • ExponentSymbol – Gets the exponent symbol.

  • ListSymbol – Gets the list separator. (The term list came from C# TextInfo.ListSeparator)

  • DecimalFormat – FormatNumber.FormatDecimal pattern

  • CurrencyFormat – FormatNumber.FormatCurrencyl pattern

  • ExponentFormat – FormatNumber.FormatExponentl pattern

  • PercentFormat – FormatNumber.FormatPercent pattern

  • DecimalCompact – FormatNumber.FormatCompactDecimal pattern

  • CurrencyCompact – FormatNumber.FormatCompactCurrency pattern

  • MinimumGroupingDigits – Don’t group when a number is below certain value. This is intended for languages such as Polish and Spanish where one would only group on values over 9999. Example:

Minimum­GroupingDigits Pattern Value Formatted
1 #.##0 1 000 1,000
1 #.##0 10000 10,000
2 #.##0 1000 1000
2 #.##0 10000 10,000
  • ReadOnly – Gets the boolean if NumberFormatInfo can be modified, returns true if it can’t and returns false if it can

Patterns

For FormatNumber.FormatCustom and FormatNumber.FormatCompactCustom pattern parameter.
If you just want a decimal number without digit grouping or want to have a custom negative-number format then this is useful. This is bascially the CLDR number format pattern except , for decimal and . for digit grouping.

Format specifier Name Description Examples
0 Zero placeholder Replaces digits with 0 if there aren’t enough digits 1234 (“00000”) → 01234; 24.5 (“000,00”) → 024.50
# Digit placeholder Remove the digit if it’s a non-significant 0 0.5 ("#,#") → .5; 200.00 ("#,##") → 200
, Decimal separator The location of the decimal 0.1234 (“0,0000”) → 0.1234
. Grouping separator The location of grouping and how big the grouping digit is 1234567 ("#.##0") → 1,234,567; 123456 ("##.##.##0") → 1,23,456
% Percentage placeholder Substitutes into NumberFormatInfo’s PercentSymbol note that value will be multiplied by 100 if this is included 0.666666 ("#.##0,##%") → 66.67%
‰ (U+2030) PerMille placeholder Substitutes into NumberFormatInfo’s PerMilleSymbol note that value will be multiplied by 1000 if this is included 0.666666 ("#.##0,##%") → 666.67‰
E Exponent placeholder Substitutes into NumberFormatInfo’s ExponenetSymbol note that value will be converted to exponents if this is included
¤ (U+00A4) Currency placeholder Substitutes into `the currency symbol if there isn’t one, this will be ignored
Literal string delimiter Used to get literal characters of 0, #, ,, ., %, , ¤, E and ;, '' for literal ' 1234 ("#’,’") → 1234,
; Section separator Separate positive and negative pattern sections -1234 ("#;(#)") → (1234)

FormatCompactCustom pattern

It’s bascially the same but provided in a table, the FormatCompactCustom will get the index (the length of the value) of the table e.g. if the value is 1000 it’ll the get 4th index of { '0', '0', '0', '0K' } as the value have 4 digits, if the number of digits gets over the length, it’ll just get the last index of the table.
The length of the shortened number will depend on how many 0s are there in the format. e.g. if the format is 00K and the value is 12345, it’ll return 12K, but if the format is 0K and the value is 12345 it’ll return 1K instead.

But why?

Why not do something like { 'K', 'M', 'B', 'T' }?
Some countries don’t even have the same system, in East Asian cultures, they don’t abbreviate 1,000,000 as 1 but as 100萬 so doing { "萬" ,"億", "兆" } assumes you abbreivate numbers in 3s thus 1000 would be wrongly return as 1萬. If that wasn’t enough, some languages like German don’t even abbreivate numbers until one million, and some languages like Indian English, don’t even have the same number size e.g. 1,234,567 is 12L and 12,345,678 is 1Cr but 1,234,567,890 is 123Cr, and some langauges like Spanish, abbreivate 1,000,000,000 as 1000 M but 10,000,000,000 as 10 MRD. thus I prefer to doing it by { '0', '0', '0', '0K', '00K', '000K', '0M', '00M', etc. }.

Why NumberFormatInfo?

Why can’t you just format 1000 to 1,000 and million to 1M, so I don’t have do NumberFormatInfo.new?
Not everyone writes numbers in the same way, here’s the table of number formats used in certain places.

Number format Used in Notes
1,234,567.89 U.S., English-speaking Canada, Latin America, UK, China, Korea and Japan
1.234.567,89 Spain, Portugal, Majority of South America, Germany, Indonesia, France at one point
1 234 567,89 Frence, French-speaking Canada, Russia Thin spaces! (U+2009)
12,34,567.89 India in some cases Grouped by 3, 2, 2, 3, 2, 2 etc.
1’234’567.89 Switzerland and Liechtenstein for computing
1_234_567.89 Syntax of numbers in Python and Lua
1,234,567·89 United Kingdom at one point
1.234.567’89 Spain(?) Yes a ' for decimal.
1,234,567 89 Literally nowhere
١٬٢٣٤٬٥٦٧٫٨٩ Majority of the Arab world Not on the same number system

So I added NumberFormatInfo so some don’t misinterpret 1,250 as 1 point 250 and to make sure it’ll format the number correctly.

In some of my functions, inserting NumberFormatInfo is optional, and if you didn’t insert it, it’ll just format using the default properties of NumberFormatInfo, which indicates spaces as a digit grouping symbol and comma as decimal to reduce ambiguity.

In future updates I might (but not likely) even add Eastern Arabic Numerals and Thai Numerials

Changelog

Version 1.1.0

17 April 2020, 12:06:52

  • Fixed the FormatCompactNumber issue, now “0,0” will return correctly
  • Now substitutes NaN (e.g. 0/0) and Infinity, both positive and negative (e.g. math.huge)

Edit 1 on 2020-04-16T23:20:46Z: Updated my documentation
Edit 2 on 2020-04-17T12:06:19Z

6 Likes

How would I use FormatCompact to accomplish this:

12,340 → 12.3k
1,639 → 1.6k
103,304 → 103.3k

Also, is there a way to disable rounding?

1 Like

For the first, try maximumFractionDigits = 1 to always have 1 fractional digits (expect when it ends with 0), the default behaviour is round to nearest integer, keep 2 significant digits, no trailing zeroes.

Also what do you mean by disabling rounding? Is rounding = "floor" what you’re looking for (e.g. 1.9 rounds down to 1)? or maybe the “unnecessary” rounding mode which errors when it can’t be represented exactly without rounding (which apologies this module don’t support)?

2 Likes

I still haven’t thought much about the rounding, I might change the default precision (doesn’t affect International, only this module) for the compact notation. For this module, the compact notation has few options you can pick:

  • Nearest integer but keep 2 significant digits (currently the default): 1.2 12 123 1234 12,345 123,456 1,234,567
  • Round to the nearest 1 fractional digits: 1.2 12.3 123.4 1234.5 12,345.6 123,456.7 1,234,567.8
  • Maximum of 3 significant digits: 1.23 12.3 123 1230 12,300 123,000 1,230,000

Which one do you prefer? (Some not available for now)

  • 1.2 12 123 1234 12,345 123,456 1,234,567
  • 1.2 12.3 123.4 1234.5 12,345.6 123,456.7 1,234,567.8
  • 1.23 12.3 123 1230 12,300 123,000 1,230,000
  • 1 12 123 1234 12,345 123,456 1,234,567
  • 1.23 12.34 123.45 1234.56 12,345.67 123,456.78 1,234,567.89
  • 1.23 12.3 123 1234 12,345 123,456 1,234,567

0 voters

And I might change the default rounding for compact notation for this module.
Which rounding for compact notation do you prefer?

  • Half even - 1.9 → 2, 2.5 → 2 (default)
  • Down - 1.9 → 1, 2.5 → 2

0 voters

As for grouping, the default for compact notation will stay as "min2".

Edit 14.08.2020: another decision I’m unsure
Should I merge FormatStandard and FormatCompact into a “notation” option (like International does)

  • Merge them into the notation option
  • Keep it the way it is

0 voters

And here’s stuff from International I might add to this module. Which one should I add?

  • Range formatting
  • Formatting to parts
  • Unit formatting
  • Number formatter class
  • Long compact notation
  • Scientific/engineering notation

0 voters

Feel free to post more suggestions

1 Like

Update 2.1

  • Added FormatStandardRange and FormatCompactRange. But it’s still experemental (in both International and this module), I haven’t used the NumberRangeFormatter from Unicode ICU and I know hardly anything about it yet :frowning:
  • Fixed the rounding bug, now “floor” and “ceiling” has been replaced by “down” and “up” (the term was misleading as “floor” rounds up for negative numbers but down for positive numbers) and 999,999 will no longer return 1000K for halfUp/halfEven/up/halfDown rounding.
  • Rounding now defaults to down for FormatCompact.

Next update, I might be adding:

  • ECMA 402 “styled” FormatToParts
  • Scientific/engineering notation
  • Different (non-algoirthmic) numbering system option

I won’t be adding these because these require plural rules and I’m not adding plural rules, if you want these, you can use International:

  • Unit formatting
  • Long compact notation (you can slightly achieve this via compactPattern option)/pluralised compact notation

And I won’t be adding a NumberFormatter class because I can’t find a use for this for non-i18n situation. (NumberFormatter class was added in International so it doesn’t have to go through the CLDR tree every time it formats the number)

You can get it here: FormatNumber.rbxm (7.8 KB)

4 Likes

Bit late, but what are the permissions of use?
@Blockzez

1 Like

It’s under BSD 2-clause licence, you can pretty much do whatever you want with it as long you include the copyright notice in it.

1 Like

Thank you.
I’ll see if I can use this in my game

1 Like

Features

I decided to keep track of the features for this module, here’s the features I might add:

:heavy_check_mark: - the feature is available in this module
:x: - the feature is not available in this moudle because I don’t believe it makes sense to include it
:soon: - the feature is not available in this module yet but I’m working on it
:woman_shrugging: - the feature is not available in this module yet, but we might include it at some point.

International (NumberFormat and PluralRules)

Feature Status Notes
Formatting to parts :heavy_check_mark: Is isn’t easy to implement but if there are use cases I might add it
Formatting range :heavy_check_mark: But it’s experemental
Number formatter class :x: I don’t think there’s a use case for this. The purpose of NumberFormat class for International is so that it doesn’t have to go to the CLDR tree every time just to format the number. Doesn’t apply here. I also want this to be easy to use for scripters, just call the function and you get the number formatted. If you really like this style, use International or Unicode ICU.
Scientific/Engineering notation :heavy_check_mark:
Unit formatting :x: Not adding plural rules and this requires plural rules.
Plural rules :x: Aside from decimal places, workaround is trivial for English n == 1 and "one" or "other"
Treat currency as ISO 4217 code instead of a symbol :x: Do you have a use case for this on Roblox? and I’m considering custom currency option for International :slight_smile:
Compact notation :heavy_check_mark: (short with no plurals), :x: We did add compactPattern. For pluralised compact pattern like 1 Million but 2 Millionen, it’s better left to International and no use case here.
Locale argument :x: This isn’t locale-based
“compact” as a notation option :woman_shrugging: Previous version of FormatNumber have it as separate function, but we’ll see
More numbering system :heavy_check_mark:
Sign display option :woman_shrugging:
Currency sign display option :woman_shrugging:

Unicode ICU

Feature Status Notes
More fraction-significant rounding :heavy_check_mark: Experemental
Rule based number formatting :x: I want this moudle to be simple as possible. RBNF is a complex thing to implement and was experemental in International but got removed (but might bring it back).
Add options from NumberRangeFormatter to this modules :woman_shrugging: Haven’t touched the API yet.

ECMA 402: Intl.NumberFormat v3

Features Status Notes
Rounding modes :heavy_check_mark:
Enumerated "useGrouping" option :heavy_check_mark:
roundingIncrement option :x: Can’t find a use case here.
Interpreting strings as decimal :heavy_check_mark:

Other suggestions

Feature Status Notes
Change the default compact rounding :woman_shrugging: The default seems to work, but most other number abbreviation system does it differently
Change the default compact pattern :woman_shrugging: Suffixes from Miner’s Haven works, unlikely but we’ll see.
Option to change the minimumGroupingDigits :x: (for values over 4), :woman_shrugging: (for 3 and 4), :heavy_check_mark: (for 1 and 2) Can’t find a case where 1 or 2 is not the answer. You can use useGrouping = "min2" for it to be 2 and "always" for it to be 1. Might add an option to set it to 3 or 4 (through "min3" and "min4" value for the useGrouping option) but we’ll see.
NaN and Infinity symbol option :woman_shrugging:
Abbreviations fallback to scientifc notation if there isn’t any available :heavy_check_mark: MoneyLib does this but I can’t find a use case for this
Custom number patterns :woman_shrugging: Does anyone use FormatCustom in the previous version of FormatNumber?
Option argument respect the __index metamethod if the table has it :woman_shrugging: Unlikely but if there are cases, I’ll add this.
Option to change "0" pattern rounding behaviour :soon: Yep most abbreviation module rounds it differently when it the special "0" pattern. Usually rounding down to the nearest integer for "0" patterns.
3 Likes

Update 2.2 upcoming
Here are the upcoming features:

More rounding options for FormatCompact

Experemental, might change in the future
Now with minimum signfiicant digits to keep and trailing zeroes if rounded
{ minimumSignificantDigitsToKeep = 3 } → 1.23 12.3 123 1234 12,345 123,456
{ trailingZeroesIfRounded = 1 } → 1.0 1.2 10 12 100 123 1000 1234 10,000 12,345 100,000 123,456

End fallback

You can now fallback if the end doesn’t have any abbreviations available.
Whether be like MoneyLib and fallback to scientific notation.
You have three options of fallbacks:

  • Last abbreviation with more digits added (default)
  • Scientific notation (10 ^ 309 and 10 ^ 310 returns 1E309 and 1E310 instead of 1000 UNCENT and 10,000 UNCENT)
  • Standard notation (can be solved by adding "0" at the end)

Scientific notation

FormatScientific function added, with an option of 1E1 or 1e1 defaulting to 1E1.

Minor changes:

  • useGrouping now accepts booleans. true is "always" for FormatStandard and "min2" for FormatCompact and false is "never"

Release is upcoming.
(Random tirival info: The option argument doesn’t respect the __index metamethod :see_no_evil:)

1 Like

How does one go about this ie. 123,456.7890 into → 123,456 as seen here with this gatekeeper boss UI.

Sorry, I’m not much of a documentation person --I did try reading it though, and looking through the thread but couldn’t get what I wanted to achieve, although I saw the first reply placing it in didn’t work. :stuck_out_tongue:

P.S. For a higher level boss I was trying to make it display as 30.1M / 59.99M (or 25M / 59.99M) so how would I’d go about doing that too (using rounding), thanks for the this anyways. :+1:

1 Like

The maximum fraction digits does this, as it looks like you want to truncate the fractional digit with rounding mode set to "down".
(or you could use math.floor if you want to round down the value before formatting as well)

2 Likes

2.2 released, you can get it here: FormatNumber.rbxm (8.9 KiB)

Added feautres:

  • More compact rounding options, the default is the same as before
  • Scientific notation.
  • useGrouping boolean implicit conversion, true = “always” / “min2” for compact notation, false = “never”.

Update 2.3 planned features

The features I’m plannning to add in the next update.

Different numbering systems support

From 𝟏𝟐𝟑𝟒𝟓𝟔𝟕𝟖𝟗 to 一二三四五六七八九 to ١٢٣٬٤٥٦٬٧٨٩. Here are the list that are planned to be supported:

Code Numbering system
latn Western Digits (default)
arab Arabic-Indic Digits
beng Bangla Digits
tibt Tibetan Digits
arabext Extended Arabic-Indic Digits
deva Devanagari Digits
mymr Myanmar Digits
olck Ol Chiki Digits
hanidec Chinese Decimal Numerals
thai Thai Digits
tamldec Tamil Digits

Format to parts

A part formatting in “ECMA 402 style”, similar to Intl.NumberFormat.formatToParts (ECMA 402) and International.NumberFormat:FormatToParts (International).


to

{
	{ type = "currency", value = "¤" },
	{ type = "literal", value = " " },
	{ type = "integer", value = "12" },
	{ type = "group", value = "," },
	{ type = "integer", value = "345" },
	{ type = "decimal", value = "." },
	{ type = "fraction", value = "68" },
	{ type = "literal", value = " " },
	{ type = "compact", value = "T" }
}

Better range formatting API

Range formatting option based on Unicode ICU, will be experemental, options:

rangeIdentityFallback
The behaviour when two numbers are similar after it had been rounded:

  • "singleValue" show it as a single value rather than the range (default)
  • "approximately" show the value using approximation symbol
  • "approximatelyOrSingleValue"show the value using approximation symbol expect if the numbers are the same before it was rounded, which shows it as a single value instead.
  • "range" show the number in range

rangeCollapse
Collapse the range by notation level, can either be true or false (or nil), defaults to false

print(FormatNumber.FormatCompactRange(1000, 2000, { rangeCollapse = true })) --> 1–2k
print(FormatNumber.FormatScientificRange(1000, 2000, { rangeCollapse = true })) --> 1–2E3

Note
Format range functions will also return 2 values, the first is the formatted and the second returns either of these three value:

  • "notEqual" two numbers in the range doesn’t equal to each other
  • "equalAfterRounding" two numbers in the range equals to each other after it was rounded
  • "equalBeforeRounding" two numbers in the range equals to each other even before it was rounded
4 Likes

Update 2.3

The rangeCollapse option are now the following and no longer booleans as planned:

  • "none" never collapse (default)
  • "unit" collapse range by unit level (currency, and percent)
  • "all" collapse range by notation and unit level.

(FormatCompactRange will not return 2 value now because it isn’t easy to implement, it might in the future)

File:
FormatNumber.rbxm (12.3 KiB)

Why formatting to parts?

While it’s more useful and there are more use cases in International, this still have a use without it e.g. decorate certain part. (For example 1.5k instead of 1.5k). Implementing it yourself isn’t easy and can be a mess.
International has this so you can customise formatted strings while preserving locale-based components (espacially decimal and grouping symbols).
It might replace decimalSymbol and groupSymbol in the future.

How does the FormatCompact work?

It’s based on powers of 10 that gets the pattern depending on the power of 10 with 0 as the size.
I made a mistake by scaling it (depending on the size of the 0) then the rounding it when I’ve should’ve gone signfiicant digit rounding first, then I substitute the 0 with. I also take grouping into consideraton as it’s just a scaled-down number.
If the pattern is a single 0 without any other characters ("0") it won’t scale the number.
International also takes plural rules into account.

E.g. The number is 1,234,567

  1. Get the length of it (and remove leading zeroes first), the length is 7.
  2. Get the 7th value of the pattern (let’s say the 7th pattern is 0000K)
  3. Check if that pattern equals to "0" if so skip to step 4 and ignore step 5 otherwise scale the value so it’ll show 4 digits (because the size of 0 is 4) so it’ll be 1234.567
  4. Format that value by itself so 1234.567 becomes 1,234.56 (assuming rounding by 2 decimal places and grouping strategy is "always").
  5. Substitute the zero so that 0000K becomes 1,234.56K

So the output would be 1,234.56K.
(In reality, it’s more complex than that as it also checks to see if the value rounded to a value with a digit higher than the original value if so re-scale it, and for International plural rules etc)

1 Like