[31.1] FormatNumber - A module for formatting numbers

Oh, someone showed me how to format numbers with International anyways, ill stick with that

I haven’t read sufficiently into the original post to determine an answer - but for the locale-aware version, have you implemented Vedic digit grouping (1,00,00,000 = 1 crore) and the myriad system in East Asia (1,0000,0000 = 1亿 / 1億). I noticed a some hints of the myriad system in a few diagrams, but there wasn’t any mention of these groupings explicitly.

There is myriad compact notation (e.g. 1万) but there isn’t a myraid digit grouping (no locale have myraid grouping including the east asian locale) and there’s vedic digit grouping (e.g. 1,23,456) but not vedic compact notation outside of currency (e.g. 1 lakh) until IIRC CLDR 39.

I only needed to deal with two (previously three) different grouping size (thousands [3, 3], lakhs/crores[3, 2], ceb percent until CLDR 38 [2, 2]), although International can handle any grouping size with [x, y] in the format of ...y,y,y,x (e.g. [4, 3] is 100,000,000,0000).

Do you have a use case for myriad grouping sizes, East Asian locales seems to use thousands for grouping sizes?

1 Like

If you look in Wikipedia (or even any website that relays the basics of Mandarin), you’ll see otherwise.

Neither of the page mentions the grouping size being [4, 4], it only mentioned compact notation in myraids?

File a bug report if you disagree the zh locale standard decimal formatting being #,##0.###:
https://st.unicode.org/cldr-apps/v#/zh/Number_Formatting_Patterns/24a93b3d14ba17b2

3.0.0 update

Completely remade the module.
You can safely ignore many of my old statement, these no longer apply in version 3, things have been changed.
I based it around the ICU’s NumberFormatter API and simplified the module, removing features that complicates the implementation (formatting to parts, number range formatting) with not many use cases outside of i18n.

Why NumberFormatter class?

There isn’t any reason behind it, just a design decision and doesn’t really make the module more complicated. I’d say it’d make the implementation easier as it doesn’t have to resolve options and check types every time the function calls.
This way, you can have multiple formatters with different settings. It’s easier to change settings this way.

Old post

I’ve remade this module. With more user-friendly functions, and it’s easy to use, just call the function with the value as the argument, with an optional option argument if you’re not satsified with the default!

2.3.0
FormatNumber.rbxm (12.3 KB)

2.2.0
FormatNumber.rbxm

2.1.0
FormatNumber.rbxm (7.8 KB)

2.0.1
FormatNumber.rbxm (7.3 KB)

2.0.0
FormatNumber.rbxm (7.3 KB)

Features

  • Negaive number, decimal, infinity and NaN support.
  • BigNum/BigInt and numeric string support
  • Range formatting
  • Formatting to parts.

Does not include

  • Unit formatting.
  • Pluralised number abbreviation.
  • Currency names

Functions

string FormatNumber.FormatStandard(number/BigNum/BigInt value, table options)
Format numbers in the standard pattern.

Example

print(FormatNumber.FormatStandard(1234.56)) --> 1,234.56
print(FormatNumber.FormatStandard(-1234.56)) --> -1,234.56
print(FormatNumber.FormatStandard(0/0)) --> NaN

string FormatNumber.FormatCompact(number/BigNum/BigInt value, table options)
Want to abbreviate numbers? No problem. Uses @berezaa’s suffixes (MoneyLib) and up to 10^308.

Example

print(FormatNumber.FormatCompact(1234)) --> 1.2k
print(FormatNumber.FormatCompact(12345)) --> 12k

string FormatNumber.FormatScientific(number/BigNum/BigInt start_value, number/BigNum/BigInt, end_value, table options)
Show numbers in scientific notation. Supports engineering

string, string FormatNumber.FormatStandardRange(number/BigNum/BigInt start_value, number/BigNum/BigInt end_value, table options)
string FormatNumber.FormatCompactRange(number/BigNum/BigInt start_value, number/BigNum/BigInt end_value, table options)
string, string FormatNumber.FormatScientificRange(number/BigNum/BigInt start_value, number/BigNum/BigInt end_value, table options)
Format numbers in range, experimental. (Expect for FormatCompactRange which only returns 1 value (don’t ask), it returns 2 value, the 1st is the formatted value while the 2nd is the rounding result)

print((FormatNumber.FormatStandardRange(1234, 12345))) --> 1,234–12,345
print((FormatNumber.FormatStandardRange(-math.huge, math.huge))) --> -∞–∞
print((FormatNumber.FormatCompactRange(1000, 2000))) --> 1k–2k

table FormatNumber.FormatStandardToParts(number/BigNum/BigInt value, table options)
table FormatNumber.FormatCompactToParts(number/BigNum/BigInt value, table options)
table FormatNumber.FormatScientificToParts(number/BigNum/BigInt value, table options)
Format numbers to parts, experimental.

type meaning
integer The integral part of the value
decimal The decimal symbol
group The grouping symbol
fraction The fraction part of the value
minusSign The minus sign
plusSign The plus sign
currency The currency value
literal The literal value
nan The Not-a-Number value
infinity The infinity value

table Format
Number
.AbbreviationToCLDR(table abbreviations, boolean include_currency)
Converts abbreviation suffixes of 3 digits to Unicode CLDR compact number pattern so you can use it on compactPattern as that only accept that “CLDR”-styled pattern (no plurals though).

Example FormatNumber.AbbreviationsToCLDR{'k', 'M', 'B', 'T'} converts it to {'0k', '00k', '000k', '0M', '00M', '000M', '0B', '00B', '000B', '0T', '00T', '000T'}

Options

Applies to FormatStandard and FormatCompact
groupSymbol
The grouping symbol, the default is ,

decimalSymbol
The decimal symbol, the default is .

useGrouping
Determine the number should be grouped. This modules assumes the default minimum grouping digits is 1 so:

  • "always" group the number (default for FormatStandard) (Trivial info: It also ignores locales that disables grouping like bg currency and the en-US-POSIX locale and still group the number)
  • "min2" group the number only if it has 5 digits or over (default for FormatCompact) (Trivial info: The actual function is locale dependant and it actually means the minimum grouping digits will be overrided to 2 if it’s lower than 2, so if the default minimum grouping digits is 3 like locale ee or 4 like locale hu before CLDR 36, it still won’t group values below 6 digits, this info does not matter here and this can be safely ignored)
  • "never", don’t group the number

Trivial info: The "auto" value isn’t supported because that’s locale dependant, and is redundant because the default minimum grouping digits is assumed to be 1 and “always” sets the minimum grouping digits to 1

style
The formatting style to use

  • "decimal" plain number formatting
  • "currency" currency number formatting
  • "percent" percent formatting

Trivial info: The "unit" value isn’t supported because it doesn’t support unit formatting

currency
The currency to format, (For this module, this is a currency symbol, while in International, this is the ISO 4217 currency code)

rounding
Rounding types

  • "halfEven" rounds to the nearest even in when it’s halfway or over (default)
  • "halfUp" rounds up the value when it’s halfway or over
  • "halfDown" rounds down the value when it’s halfway or over
  • "down" rounds down the value
  • "up" rounds up the value

only available on FormatCompact and FormatCompactRange
compactPattern
The compact pattern in tables, it starts at thousands so if the value is 1000 and the pattern is {'0K'}, it’ll format it as 1K

The pattern string is based around Unicode CLDR’s, ; for separation of positive and negative, ¤ for currency placeholder, etc. Keep in mind 0 fallbacks to FormatStandard and any value below 1000 have a pattern of 0 and only the 0 (size) and literal pattern (’) are suppored.
For example if I insert the value of 12345

pattern formatted
0 12,345
0 ten thousand 1.2 ten thousand
00k 12k

The default is the abbreviation pattern is ber’s Miner Haven, it’ll format values similarly to MoneyLib (except that values over 10^309 won’t be in scientific notation)

If at least one of the minimumSignificantDigits and maximumSignificantDigits option is not nil, minimumFractionDigits, maximumFractionDigits, minimumIntegerDigits and maximumIntegerDigits are ignored, and (Only applies to FormatCompact and FormatCompactRange) if at least one of the minimumSignificantDigits, maximumSignificantDigits, minimumFractionDigits, maximumFractionDigits, minimumIntegerDigits and maximumIntegerDigits option is not nil, minimumSignificantDigitsToKeep and trailingZeroesIfRounded are ignored.

minimumFractionDigits
The minimum of fractional digit to use, defaults to 0 (unlike International.NumberFormat which defaults depending on the ISO 4217 currency entered if the style is currency, this still defaults to 0 if the style is currency)

maximumFractionDigits
The maximum of fracitonal digit to use, defaults to 3, use math.huge for unlimited maximum fraciton digits.

minimumIntegerDigits
The minimum of integral digits to use (Zero padded), for example if the minimum integer digit is 2 and 1 is entered, it’ll format it as 01, defaults to 1

maximumIntegerDigits
(undocumented)

minimumSignificantDigits
The minimum of significant digits to use.

maximumSignificantDigits
The maximum of significant digits to use.

experemental
only available on FormatCompact and FormatCompactRange
minimumSignificantDigitsToKeep
The minimum significant digits to keep, and don’t round those, defaults to 2.

only available on FormatCompact and FormatCompactRange
trailingZeroesIfRounded
The minimum trailing zeroes if the FormatCompact/FormatCompactRange is rounded, defaults to 0.

Example
value trailingZeroesIfRounded minimumSignificantDigitsToKeep rounded/trauncated
1 0 2 1
1.2 0 2 1.2
9.87 0 2 9.8
10 0 2 10
12.3 0 2 12
98.76 0 2 98.7
100 0 2 100
123.4 0 2 123
987.65 0 2 987
1000 0 2 1000
1234.5 0 2 1234
9876.54 0 2 9876
10,000 0 2 10,000
98,765.43 0 2 98,765
12,345.6 0 2 12,345
1 0 3 1
1.2 0 3 1.2
9.87 0 3 9.87
10 0 3 10
12.3 0 3 12.3
98.76 0 3 98.7
100 0 3 100
123.4 0 3 123
987.65 0 3 987
1000 0 3 1000
1234.5 0 3 1234
9876.54 0 3 9876
10,000 0 3 10,000
98,765.43 0 3 98,765
12,345.6 0 3 12,345
1 1 2 1.0
1.2 1 2 1.2
9.87 1 2 9.8
10 1 2 10
12.3 1 2 12
98.76 1 2 98.7
1 2 3 1.00
1.2 2 3 1.20
9.87 2 3 9.87
10 2 3 10.0
12.3 2 3 12.3
98.76 2 3 98.7
100 2 3 100
123.4 2 3 123
987.65 2 3 987
1 1 3 1.0
1.2 1 3 1.2
9.87 1 3 9.87
10 1 3 10
12.3 1 3 12.3
98.76 1 3 98.7

Only available on FormatScientific and FormatScientificRange
engineering
Show in exponent in 10s only when it’s divisble by three, defaults to false

exponentLowercased
experemental, might change
Shows the exponent symbol as e instead of E, defaults to false

Question

Is it really easy to use?

Yep, all you need to is just call the function, with the value argument and if you’re not happy you can create a dictionary for the option argument with options you can change.

print(FormatNumber.FormatStandard(1000)) --> 1,000
print(FormatNumber.FormatCompact(1234)) --> 1.2k

Why was min2 the default value for useGrouping for numbers abbreviations?

International originally have two options for grouping: true and false, true maps to "auto" and false maps to "never" for standard notation however true maps to "min2" for compact notation/number abbreviation, the reason for this is was back then I was trying to replicate ECMA 402 behaviour. On the 2.1 update, I added more grouping options, and min2 became the default for grouping for compact notation and that’s the hang over from it.
You might also notice ECMA 402 also default min2 as grouping for compact notation (and still uses booleans for useGrouping option):
image
ECMA 402 does this because Unicode ICU defaulting grouping for compact notation to be min2.

That’s the way it is and it had always been that way. In summary, the behaviour is a hangover from International which is a hangover from ECMA 402 which uses Unicode ICU default grouping for compact notation.

Is this locale-aware?

No. If you want a locale aware version, see International. This is a subset of International so it might feel similar.

2 Likes

Sounds like something many people with stats systems would use. I hope this gets updated more in the future.

Fantastic work.
This is incredible, i’ve tried a few solutions for number formatting but they all seem to fail after 100 trillion.

Thank you very much.

I remember using this module at around versions 2.0.0 and up, and it was very useful. I came back to check for updates recently, and in my opinion, it’s gotten incredibly more complex to use after versions 3.0.0+. I’m sure people have their use cases, but as an average developer, the newer versions are not for me.

1 Like

yeah, i’d much rather use the old ones. FormatCompact felt a lot easier than, whatever we do now

1 Like

(This has been superseded by the Simple API though this is not deprecated and is here for backwards compatibility - I’ll still provide bug fixes for now but no new features will likely be added)

I decided to create a separate API that doesn’t contain NumberFormatter class if that’s not your thing.
This is slightly simpler with one function call rather than creating class than call the format method.
The alternative API will be just as supported as the NumberFormatter API.
It loses many features with use cases, but if you do not need it, you might prefer this over the NumberFormatter API.

All functions support negative numbers and all functions except FormatInt supports pretty much every number (including decimals).

You would need to use the files provided to use the alternative API
Alternatively, I’ve uploaded a model:FormatNumber (Old Alternative API) - Roblox

Module

3.0.0b3

File FormatNumberAlt 3.0.0b3.rbxm (28.4 KiB)

3.0.0b2

File: FormatNumber Alt 3.0.0b2.rbxm (28.0 KiB)

3.0.0b1

File: FormatNumber Alt 3.0.0b1.rbxm (27.9 KiB)

API

All of these function formats so the integer part of the formatted number are separated by the grouping separator every 3 digits.

function FormatNumber.FormatInt(value: number): string
Formats an integer.

function FormatNumber.FormatStandard(value: number): string
Formats a number.

function FormatNumber.FormatFixed(value: number, digits: number?): string
Formats a number rounded to the certain decimal places.
The default is 6 decimal places.

function FormatNumber.FormatPrecision(value: number, digits: number?): string
Formats a number rounded to the certain significant digits.
The default is 6 significant digits.

function FormatNumber.FormatCompact(value: number, fractionDigits: number?): string
Formats a number so it is in compact form (abbreviated such as “1000” to “1K”).
The significand (referring to 1.2 in “1.2K”) is truncated to the fractionDigits specified in the argument. If not, it is truncated to integers but keeping 2 significant digits.
You can change the suffix by changing the compactSuffix field from the config ModuleScript included in the module.

4 Likes

Don’t know why but when I use your module it uses a lot of memory, problem with my use of the module, code or the module itself?

What version are you on? (Yes I’m aware my modules are fragmented)
How much memory (in KiB or MiB) does the module take? I haven’t checked the memory consumption.

I’m on the latest version and it makes memory raise about 1MB per second until the game just breaks.

I like this one, but there is a small bug to fix: if you use FormatPrecision with 0, it results as “0.”
image
An easy fix is to check if the value is 0 and return “0”.

How would you reproduce this bug? I cannot reproduce it on my end.

print(FormatNumber.FormatPrecision(0)) --> 0.00000

What argument(s) did you insert for the function?

FormatNumber.FormatPrecision(0, 1) --> 0.

First one “0” is the value before abbreviation

Here’s my module:

Also, when you try to shorten numbers above 10, it doesn’t show the other digits.
Real value:
image
image


What’s being shown:
image
image

The first bug should be fixed. Thanks for catching it.

FormatPrecision rounds the value by significant digits (significant figures). So in your case, it rounds to 1 significant digits so it doesn’t show any digits other than the 1 digit on the left.

Hey, do you have a list of abbreviations? “K”, “M”, “B”, “T”, e.t.c

I recommend this if you’re looking for English abbreviations up to 10308 (~21024): Cash Suffixes | The Miner's Haven Wikia | Fandom

2 Likes