[31.1] FormatNumber - A module for formatting numbers

This is an all-in-one number formatting module designed for displaying numbers in a more user-friendly way.

Why this module?

Aside from being the more known module (with over 100 likes), this is a solid tested module with many features included. Whether you insert a number large enough that some other number formatting module/snippets breaks, or you insert negative numbers or infinity, this module accounts for it.
As this is a module, you do not have to copy and paste any snippets, but you do need to know how to use a ModuleScript!

GitHub Repo

Here is the GitHub repo for the module:
Blockzez/RobloxFormatNumber: Number formatting module for Roblox Luau. (github.com)

Files

Current Version (31.1)

File: FormatNumber 31.1.rbxm (21.7 KiB)
Test: FormatNumberTest 31.rbxm (8.7 KiB)

Previous Versions

31.0

Licensing is included in DOCS ModuleScript
File:
FormatNumber 31.0.rbxm (24.4 KiB)
Test:
FormatNumberTest 31.rbxm (8.7 KiB)

31.0b2

Licensing is included in DOCS ModuleScript
File:
FormatNumber 31.0b2.rbxm (24.0 KiB)

31.0b1

Licensing is included in DOCS ModuleScript
File: FormatNumber 31.0b1.rbxm (23.5 KiB)

3.0.2

Double conversion vendor: v1.0.0b1
File: FormatNumber 3.0.2.rbxm (35.0 KiB)

3.0.1

File: FormatNumber 3.0.1.rbxm (23.9 KiB)

3.0.0

File: FormatNumber 3.0.0.rbxm (24.0 KiB)

How to use

Main API

It’s rather simple to use. First, you require the module (the Main ModuleScript inside the FormatNumber folder) then call the with constructor from the NumberFormatter class of it (and assign the result to a variable), then, optionally, add any settings (documented in the API), and then call the :Format(...) method and you get formatted number as a string (you can use that result to assign it to a Text propery of TextLabel for example).

local FormatNumber = require(--[[Enter the location of FormatNumber here]].Main)
local formatter = FormatNumber.NumberFormatter.with()
print(formatter:Format(1234)) --> 1,234

By default, it uses the standard formatting with grouping separators (commas) but you can change it to abbreviations (though it’ll still format with it comma separated on 5 digits or above unless you explicitly change this), and you can choose what abbreviations you want in thousands.

-- Add your abbreviations/compact notation suffixes here
local abbreviations = FormatNumber.Notation.compactWithSuffixThousands({
	"K", "M", "B", "T",
})
local formatter = FormatNumber.NumberFormatter.with()
    :Notation(abbreviations)
	-- Round to whichever results in longest out of integer and 3 significant digits.
	-- 1.23K  12.3K  123K
	-- If you prefer rounding to certain decimal places change it to something like Precision.maxFraction(1) to round it to 1 decimal place
	:Precision(FormatNumber.Precision.integer():WithMinDigits(3))

print(formatter:Format(1234)) --> 1.23K
print(formatter:Format(12345)) --> 12.3K
print(formatter:Format(123456)) --> 123K

You can create the NumberFormatter class many times with different settings.

Simple API

Though I try to make both APIs as simple as possible, this API is much more simpler to use for some people mainly because it’s just a function call of a method. You’ll get the same result as the Main API so there’s no advantage nor disadvantage of using this (aside from multiple instances of suffixes and symbols).

You just need to call the function with value provided, and you can use the result to assign it to Text property of a TextLabel.

print(FormatNumber.Format(1234.56)) --> 1,234.56
print(FormatNumber.Format(1234))

For FormatCompact, you have to change the COMPACT_SUFFIX from the Simple ModuleScript in the FormatNumber folder.
It looks like something like this

...
local COMPACT_SUFFIX = {
	...
}
...

then insert the suffixes for each power of thousands, something like

...
local COMPACT_SUFFIX = {
	"K", "M", "B", "T", ...
}
...

then you can now call FormatCompact.

print(FormatNumber.FormatCompact(12345)) --> 12K

Features

Since it’s a more known module, there’s lots of commonly used features mixed with more niche features. I won’t mention all features here but here are the some.

Grouping digits

This is most commonly used feature.
It adds grouping separators (commas) to the formatted value every 3 digits.

print(MainAPI.NumberFormatter.with():Format(1234)) --> 1,234
print(MainAPI.NumberFormatter.with():Format(12345)) --> 12,345
print(MainAPI.NumberFormatter.with():Format(1234567)) --> 1,234,567

print(SimpleAPI.Format(1234)) --> 1,234
print(SimpleAPI.Format(12345)) --> 12,345
print(SimpleAPI.Format(1234567)) --> 1,234,567 

Really just a function call for the Simple API, nothing more.
Accounts for negative numbers and decimals.

Abbreviations (or Compact Notation)

Another very commonly used feature.
It scales the number down and append the number with a suffix (and a prefix potentially in the future). e.g. 1234 → 1.2K
There’s no hard-coded suffixes but this is one of the suffixes you can use if you’re looking for one: Cash Suffixes | The Miner's Haven Wikia | Fandom.
For Simple API, you just call the FormatCompact function, but for the Main API it’s slightly more complicated (see the How to Use section).
It accounts for negative numbers and by default truncates (rounds down).

Precision (decimal places, significant digits, etc)

You can change the decimal places or the significant digits.
Very useful for abbreviations, since the default is whichever produces more digits out of truncating the integer and truncating two significant digits and I believe the majority of the users are looking for certain decimal places (most commonly 1 such as 1.2K and 12.3K and 2 such as 1.23K and 12.34K) rather than something more complex but it supports a more complex precision.

All-in-one settings

You only need to call one function Format (or FormatCompact for abbreviations if you’re on the Simple API) as it’s designed this way. There’s no FormatFraction, FormatCurrency, and the lots, just Format and FormatCompact in the Simple API.

This wouldn’t come as a surprise for those that are familiar with ICU’s NumberFormatter API but it’s a pretty cool feature.
The only exception to this is Format and FormatCompact for the Simple API but for the Main API, it’s only Format with all-in-one settings.

Features I will not add (for now)

Although this module has lots of features, I’m not likely going to add these features as I don’t think it makes sense for me to add it. None of my decision is final so it doesn’t mean there’s no chance that I’ll add it as I might change my mind, it’s just that it’s unlikely.

Parsing / Reversing Formatted String

I have tried to implement this before but I found implementing this too brittle even without internationlization constraint (like this module), alongside that I can’t think of any API that fits.
In addition, this just is not a good practice in my opinion - you should try to find an alternative for your use case if possible, if your use case generally is based around this then there’s likely another issue going on.

Padding

Integer width is the most likely of what you’re looking for and seems to cover most of the use cases of this.
However if integer width is not what you’re looking for and you still have a use case for this, then I believe that you ought to be able to implement this on your own.

Unit and Currency Formatting

If it’s designed for i18n, sure, but it’s not so I don’t think it makes sense for me to add it.
Use concatenation instead.

Some question answered

Before replying and asking any questions, please check if it’s covered here.

Is this for internatinalization?

No, though the API is based on, and the unit test script is taken from, an internationalization library (ICU) as it’s what I’m most familiar.
You can switch the decimal separator to a comma and the grouping separator to a full stop or a space with Symbols, and perhaps changing the grouping mode to MIN2 for some locales but the internationalization here - it cannot format 1 as one in English, uno in Spanish, and 侀 in Japanese; and it cannot format 2 as 2 inches in English for example.

Why does the Symbols option seem to be inconsistent with API?

This is not documented and I’ve just pretty much copied both the ICU’s NumberFormatter API and ICU’s old NumberFormat (yes NumberFormat and not NumberFormatter) API for this hence the inconsistency (ICU’s newer NumberFormatter API does this too).
Symbols is not really intended for internationlization (but can be used as one).
It’s just for those that’s not happy with “,” being the grouping symbol and “.” for the decimal symbol or if you prefer Infinity to print “Infinity” rather than “∞”.

Why is NumberFormatter immutable?

That’s just how it’s designed. Like ICU’s NumberFormatter, it’s based on fluent interface with copy-on-write semantics.
One benefit of this is that you can create multiple settings from a certain point without multiple formatters interfering from each other.

What is the difference between the Main API and the ICU’s NumberFormatter API?

This API might not have features from the newer versions of ICU’s NumberFormatter API.
There’s no unit (including currency) formatting or RBNF in this module.
The settings for compact notation (abbreviations) are different, there’s no Notation.compactShort() and Notation.compactLong() in this module.
The naming style in this module is different, instead of UNumberGroupingStrategy it’s GroupingStrategy.
DOWN is the default rounding mode for abbreviations/compact notation rather than HALF_EVEN in this module.
For the NumberFormatter class, for formatting there’s only the Format method and it returns a string rather than FormattedNumber for simplicity.

Why make it a class for the Main API?

You might want different multiple formatters. e.g. the one for separating thousands and the other for abbreviations.

Will you add this as a Roblox Model?

Maybe and I have two options.

  1. I could make it as a model for every major version - this doesn’t break compatibility but it’s quite tedious.
  2. Make the model rolling release and disregard compatibility.
    I haven’t decided which one of these yet.

I won’t replace this with the Alternative API (FormatNumber (Old Alternative API) - Roblox) as it leads to compatibility issues.

API Documentation

You will find this documentation in the DOCS ModuleScript (<31.0) or README.md in the GitHub Repo. (>=31.1)

Main API (FormatNumberFolder.Main)

NumberFormatter

The class to format the numbers, located in FormatNumber.NumberFormatter.

Static methods

function NumberFormatter.with(): NumberFormatter
Creates a new number formatter with the default setting.

function NumberFormatter.forSkeleton(skeleton: string): (boolean, NumberFormatter | string)
Tries to create a new number formatter with the skeleton string provided. If unsuccessful (e.g. the skeleton syntax is invalid) then it returns false and a message string, otherwise it returns true and the NumberFormatter.
See the Number Skeletons section of this API documentation for the skeleton syntax.

Methods

function NumberFormatter:Format(value: string): string
The number to format, it could be any Luau number. It accounts for negative numbers, infinities, and NaNs. It returns string instead of FormattedNumber to simplify the implementation of module.
function NumberFormatter:ToSkeleton(): (boolean, string)
Tries to convert it to skeleton. If it is unable to (like the settings having compact notation or symbols) then the first value will return false and a message stating that it is unsupported.
If it’s successful then the first value will return true and the second value will return the skeleton.

Settings chain methods

These are methods that returns NumberFormatter with the specific settings changed. Calling the methods doesn’t change the NumberFormatter object itself as it is immutable so you have to use the NumberFormatter that it returned.

function NumberFormatter:Notation(notation: FormatNumber.Notation): NumberFormatter
See Notation.
function NumberFormatter:Precision(precision: FormatNumber.Precision): NumberFormatter
See Precision.
function NumberFormatter:RoundingMode(roundingMode: FormatNumber.RoundingMode): NumberFormatter
See FormatNumber.RoundingMode enum.
function NumberFormatter:Grouping(strategy: FormatNumber.GroupingStrategy): NumberFormatter
See FormatNumber.GroupingStrategy enum.
function NumberFormatter:IntegerWidth(style: FormatNumber.IntegerWidth): NumberFormatter
See IntegerWidth.
function NumberFormatter:Sign(style: FormatNumber.SignDisplay): NumberFormatter
See FormatNumber.SignDisplay enum.
function NumberFormatter:Decimal(style: FormatNumber.DecimalSeparatorDisplay): NumberFormatter
See FormatNumber.DecimalSeparatorDisplay enum.

Notation

These specify how the number is rendered, located in FormatNumber.Notation.

Static methods

function Notation.scientific(): ScientificNotation
function Notation.engineering(): ScientificNotation
Scientific notation and the engineering version of it respectively. Uses E as the exponent separator but you can change this through the Symbols settings.

function Notation.compactWithSuffixThousands(suffixTable: {string}): CompactNotation
Basically abbreviations with suffix appended, scaling by every thousands as the suffix changes.
The suffixTable argument does not respect the __index metamethod nor the __len metamethod.

function Notation.simple(): SimpleNotation
The standard formatting without any scaling. The default.

ScientificNotation (methods)

ScientificNotation is a subclass of Notation.

function ScientificNotation:WithMinExponentDigits(minExponetDigits: number): ScientificNotation
The minimum, padding with zeroes if necessary.

function ScientificNotation:WithExponentSignDisplay(FormatNumber.SignDisplay exponentSignDisplay): ScientificNotation
See FormatNumber.SignDisplay enum.

CompactNotation (methods)

No methods currently but this is created just in case. This is a subclass of Notation.

SimpleNotation (methods)

No methods currently but this is created just in case. This is a subclass of Notation.

Precision

These are precision settings and changes to what places/figures the number rounds to, located in FormatNumber.Precision. The default is Precision.integer():WithMinDigits(2) for abbreviations and Precision.maxFraction(6) otherwise (for compatibility reasons).

Static methods

function Precision.integer(): FractionPrecision
Rounds the number to the nearest integer

function Precision.minFraction(minFractionDigits: number): FractionPrecision
function Precision.maxFraction(maxFractionDigits: number): FractionPrecision
function Precision.minMaxFraction(minFractionDigits: number, maxFractionDigits: number): FractionPrecision
function Precision.fixedFraction(fixedFractionDigits: number): FractionPrecision
Rounds the number to a certain fractional digits (or decimal places), min is the minimum fractional (decimal) digits to show, max is the fractional digits (decimal places) to round, fixed refers to both min and max.

function Precision.minSignificantDigits(minSignificantDigits: number): SignificantDigitsPrecision
function Precision.maxSignificantDigits(maxSignificantDigits: number): SignificantDigitsPrecision
function Precision.minMaxSignificantDigits(minSignificantDigits: number, maxSignificantDigits: number): SignificantDigitsPrecision
function Precision.fixedFraction(fixedSignificantDigits: number): SignificantDigitsPrecision
Round the number to a certain significant digits; min, max, and fixed are specified above but with significant digits.

function Precision.unlimited(): Precision
Show all available digits to its full precision.

FractionPrecision (methods)

FractionPrecision is subclass of Precision with more options for the fractional (decimal) digits precision. Calling these methods is not required.

function FractionPrecision:WithMinDigits(minSignificantDigits: number): Precision
Round to the decimal places specified by the FractionPrecision object but keep at least the amount of significant digit specified by the argument.

function FractionPrecision:WithMaxDigits(maxSignificantDigits: number): Precision
Round to the decimal places specified by the FractionPrecision object but don’t keep any more the amount of significant digit specified by the argument.

SignificantDigitsPrecision (methods)

No methods currently but this is created just in case. This is a subclass of Precision.

IntegerWidth

Static methods

function IntegerWidth.zeroFillTo(minInt: number): IntegerWidth
Zero fill numbers at the integer part of the number to guarantee at least certain digit in the integer part of the number.

Methods

function IntegerWidth:TruncateAt(maxInt: number): IntegerWidth
Truncates the integer part of the number to certain digits.

Enums

The associated numbers in all these enums are an implementation detail, please do not rely on them so instead of using 0, use FormatNumber.SignDisplay.AUTO.

FormatNumber.GroupingStrategy

This determines how the grouping separator (comma by default) is inserted - integer part only. There are three options.

  • OFF - no grouping.
  • MIN2 - grouping only on 5 digits or above. (default for compact notation - for compatibility reasons)
  • ON_ALIGNED - always group the value. (default unless it’s compact notation)

Example:

Grouping strategy 123 1234 12345 123456 1234567
OFF 123 1234 12345 123456 1234567
MIN2 123 1234 12,345 123,456 1,234,567
ON_ALIGNED 123 1,234 12,345 123,456 1,234,567

FormatNumber.SignDisplay

This determines how you display the plus sign (+) and the minus sign (-):

  • AUTO - Displays the minus sign only if the value is negative (that includes -0 and -NaN). (default)
  • ALWAYS - Displays the plus/minus sign on all values.
  • NEVER - Don’t display the plus/minus sign.
  • EXCEPT_ZERO - Display the plus/minus sign on all values except zero, numbers that round to zero and NaN.
  • NEGATIVE - Display the minus sign only if the value is negative but do not display the minus sign on -0 and -NaN.

Example:

Sign display +12 -12 +0 -0
AUTO 12 -12 0 -0
ALWAYS +12 -12 +0 -0
NEVER 12 12 0 0
EXCEPT_ZERO +12 -12 0 0
NEGATIVE 12 -12 0 0

FormatNumber.RoundingMode

This determines the rounding mode. I only documented three rounding modes but there are others undocumented if you need it.

  • HALF_EVEN - Round it to the nearest even if it’s in the midpoint, round it up if it’s above the midpoint and down otherwise. (default unless it’s compact or scientific/engineering notation)
  • HALF_UP - Round it away from zero if it’s in the midpoint or above, down otherwise. (most familiar, this is probably the method you are taught at school)
  • DOWN - Round the value towards zero (truncates the value). (default for compact and scientific/engineering notation)

Example:

Rounding mode 1.0 1.2 1.5 1.8 2.0 2.2 2.5 2.8
HALF_EVEN 1.0 1.0 2.0 2.0 2.0 2.0 2.0 3.0
HALF_UP 1.0 1.0 2.0 2.0 2.0 2.0 3.0 3.0
DOWN 1.0 1.0 1.0 1.0 2.0 2.0 2.0 2.0

FormatNumber.DecimalSeparatorDisplay

This determines how the decimal separator (. by default) is displayed.

  • AUTO - only show the decimal separators if there are at least one digits after it (default)
  • ALWAYS - always display the decimal separator, even if there’s no digits after it

Example:

Decimal separator display 1 1.5
AUTO 1 1.5
ALWAYS 1. 1.5
Simple API (FormatNumberFolder.Simple)

function FormatNumber.Format(value: number, skeleton: string?): string
Formats a number with the skeleton settings if provided.
See the Number Skeletons section of this API documentation for the skeleton syntax.

function FormatNumber.FormatCompact(value: number, skeleton: string?): string
Formats a number in compact notation.
You’ll need to provide the suffixes in the Simple ModuleScript. Multiple instances of suffixes are not supported
See the Number Skeletons section of this API documentation for the full skeleton syntax, but here’s the several skeleton syntax for quick reference if you want to change precision (e.g. decimal places)

Skeleton Precision description
precision-integer no decimal places
precision-integer/@@* whatever returns the longer result out of no decimal places and 2 significant digits (default)
.# 1 decimal place
.## 2 decimal places
.### 3 decimal places
@# 2 significant digits
@## 3 significant digits
Number Skeletons

This feature is introduced in version 31.
The syntax is identical to the one used in ICU, so you can use this page for reference: Number Skeletons - ICU Documentation
See the Main API documentation for the settings.
Do note that for this module, it only supports the following part of the Skeleton Stems and Options of the page linked:

  • Notation (but ignore compact-short/K and compact-long/KK as that’s not supported)
  • Precision (but ignore precision-increment/dddd, precision-currency, precision-currency-cash, and Trailing Zero Display as that’s not supported)
  • Rounding Mode (but ignore rounding-mode-unnecessary as that’s not supported)
  • Integer Width
  • Grouping (but ignore group-auto and group-thousands as that’s not supported)
  • Sign Display (but ignore any accounting sign display)
  • Decimal Separator Display

Bug Reports and Suggestions

As this module is most likely far from free of bugs so if you found one, please don’t hesitate to reply with the reproduction of the bug.

If you have any suggestions, please don’t hesitate to reply but do note that it does not mean that it’s guaranteed that I’ll add the feature you suggested and your suggestion does not break backwards compatibility too much and should fit the API well.

213 Likes

Good for simulators game, but isnt a good deal format on server on simulators games, cause you get stats per seconds, so the best idea is format the client vision

6 Likes

How does one even make a number look like the first example?? like 193202 —> 193,202 in script form? I can’t figure it out for the life of me

3 Likes

Use NumberFormatInfo

local en = FormatNumber.NumberFormatInfo.Preset.en;
print(FormatNumber.FormatDecimal(193202, en));

193,202

4 Likes

Majority of the Arabs just go with the English Numerals and I don’t think anyone even uses Eastern Arabic Numerals anymore.

3 Likes

I’d call it Western Arabic Numeral (despite the number system being orginated in India) instead of English Numerals, and yes while the majoirty of Arab world uses Western Arabic Numeral in some cases, CLDR defaults to Eastern Arabic Numerals in these locales, so I added it.

Not true (at least according to arabic locale on Facebook and Twitter):



image

Tip: Don’t assume everything when it comes to i18n.

2 Likes

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