-- Example locale-sensitive number formatting: -- https://docs.oracle.com/cd/E19455-01/806-0169/overview-9/index.html --[[ Locale specification: [DECIMAL_SEPARATOR] = string for decimal point, if needed [GROUP_DELIMITER] = string for groupings of numbers left of the decimal List section = abbreviations for language, in increasing order Missing features in this code: - No support for differences in number of digits per GROUP_DELIMITER. Some Chinese dialects group by 10000 instead of 1000. - No support for variable differences in number of digits per GROUP_DELIMITER. Indian natural language groups the first 3 to left of decimal, then every 2 after that. See https://en.wikipedia.org/wiki/Decimal_separator#Digit_grouping ]] local CorePackages = game:GetService("CorePackages") local Logging = require(CorePackages.Logging) local RoundingBehaviour = require(script.Parent.RoundingBehaviour) local localeInfos = {} local DEFAULT_LOCALE = "en-us" -- Separator aliases to help avoid spelling errors local DECIMAL_SEPARATOR = "decimalSeparator" local GROUP_DELIMITER = "groupDelimiter" localeInfos["en-us"] = { [DECIMAL_SEPARATOR] = ".", [GROUP_DELIMITER] = ",", { 1, "", }, { 1e3, "K", }, { 1e6, "M", }, { 1e9, "B", }, } localeInfos["es-es"] = { [DECIMAL_SEPARATOR] = ",", [GROUP_DELIMITER] = ".", { 1, "", }, { 1e3, " mil", }, { 1e6, " M", }, } localeInfos["fr-fr"] = { [DECIMAL_SEPARATOR] = ",", [GROUP_DELIMITER] = " ", { 1, "", }, { 1e3, " k", }, { 1e6, " M", }, { 1e9, " Md", }, } localeInfos["de-de"] = { [DECIMAL_SEPARATOR] = ",", [GROUP_DELIMITER] = " ", { 1, "", }, { 1e3, " Tsd.", }, { 1e6, " Mio.", }, { 1e9, " Mrd.", }, } localeInfos["pt-br"] = { [DECIMAL_SEPARATOR] = ",", [GROUP_DELIMITER] = ".", { 1, "", }, { 1e3, " mil", }, { 1e6, " mi", }, { 1e9, " bi", }, } localeInfos["zh-cn"] = { [DECIMAL_SEPARATOR] = ".", [GROUP_DELIMITER] = ",", -- Chinese commonly uses 3 digit groupings, despite 10000s rule { 1, "", }, { 1e3, "千", }, { 1e4, "万", }, { 1e8, "亿", }, } localeInfos["zh-tw"] = { [DECIMAL_SEPARATOR] = ".", [GROUP_DELIMITER] = ",", -- Chinese commonly uses 3 digit groupings, despite 10000s rule { 1, "", }, { 1e3, "千", }, { 1e4, "萬", }, { 1e8, "億", }, } localeInfos["ko-kr"] = { [DECIMAL_SEPARATOR] = ".", [GROUP_DELIMITER] = ",", { 1, "", }, { 1e3, "천", }, { 1e4, "만", }, { 1e8, "억", }, } localeInfos["ja-jp"] = { [DECIMAL_SEPARATOR] = ".", [GROUP_DELIMITER] = ",", { 1, "", }, { 1e3, "千", }, { 1e4, "万", }, { 1e8, "億", }, } localeInfos["it-it"] = { [DECIMAL_SEPARATOR] = ",", [GROUP_DELIMITER] = " ", { 1, "", }, { 1e3, " mila", }, { 1e6, " Mln", }, { 1e9, " Mld", }, } localeInfos["ru-ru"] = { [DECIMAL_SEPARATOR] = ",", [GROUP_DELIMITER] = ".", { 1, "", }, { 1e3, " тыс", }, { 1e6, " млн", }, { 1e9, " млрд", }, } localeInfos["id-id"] = { [DECIMAL_SEPARATOR] = ",", [GROUP_DELIMITER] = ".", { 1, "", }, { 1e3, " rb", }, { 1e6, " jt", }, { 1e9, " M", }, } localeInfos["vi-vn"] = { [DECIMAL_SEPARATOR] = ".", [GROUP_DELIMITER] = " ", { 1, "", }, { 1e3, " N", }, { 1e6, " Tr", }, { 1e9, " T", }, } localeInfos["th-th"] = { [DECIMAL_SEPARATOR] = ".", [GROUP_DELIMITER] = ",", { 1, "", }, { 1e3, " พ", }, { 1e4, " ม", }, { 1e5, " ส", }, { 1e6, " ล", }, } localeInfos["tr-tr"] = { [DECIMAL_SEPARATOR] = ",", [GROUP_DELIMITER] = ".", { 1, "", }, { 1e3, " B", }, { 1e6, " Mn", }, { 1e9, " Mr", }, } -- Aliases for languages that use the same mappings. localeInfos["en-gb"] = localeInfos["en-us"] localeInfos["es-mx"] = localeInfos["es-es"] local function findDecimalPointIndex(numberStr) return string.find(numberStr, "%.") or #numberStr + 1 end -- Find the base 10 offset needed to make 0.1 <= abs(number) < 1 local function findDecimalOffset(number) if number == 0 then return 0 end local offsetToOnesRange = math.floor(math.log10(math.abs(number))) return -(offsetToOnesRange + 1) -- Offset one more (or less) digit end local function roundToSignificantDigits(number, significantDigits, roundingBehaviour) local offset = findDecimalOffset(number) local multiplier = 10^(significantDigits + offset) local significand if roundingBehaviour == RoundingBehaviour.Truncate then significand = math.modf(number * multiplier) else significand = math.floor(number * multiplier + 0.5) end return significand / multiplier; end local function addGroupDelimiters(numberStr, delimiter) local formatted = numberStr local delimiterSubStr = string.format("%%1%s%%2", delimiter) while true do local lFormatted, k = string.gsub(formatted, "^(-?%d+)(%d%d%d)", delimiterSubStr) formatted = lFormatted if k == 0 then break end end return formatted end local function findDenominationEntry(localeInfo, number, roundingBehaviour) local denominationEntry = localeInfo[1] -- Default to base denominations local absOfNumber = math.abs(number) for i = #localeInfo, 2, -1 do local entry = localeInfo[i] local baseValue if roundingBehaviour == RoundingBehaviour.Truncate then baseValue = entry[1] else baseValue = entry[1] - (localeInfo[i - 1][1]) / 2 end if baseValue <= absOfNumber then denominationEntry = entry break end end return denominationEntry end local NumberLocalization = { } function NumberLocalization.localize(number, locale) if number == 0 then return "0" end local localeInfo = localeInfos[locale] if not localeInfo then localeInfo = localeInfos[DEFAULT_LOCALE] Logging.warn(string.format("Warning: Locale not found: '%s', reverting to '%s' instead.", tostring(locale), DEFAULT_LOCALE)) end if localeInfo.groupDelimiter then return addGroupDelimiters(number, localeInfo.groupDelimiter) end return number end function NumberLocalization.abbreviate(number, locale, roundingBehaviour) if number == 0 then return "0" end if roundingBehaviour == nil then roundingBehaviour = RoundingBehaviour.RoundToClosest end local localeInfo = localeInfos[locale] if not localeInfo then localeInfo = localeInfos[DEFAULT_LOCALE] Logging.warn(string.format("Warning: Locale not found: '%s', reverting to '%s' instead.", tostring(locale), DEFAULT_LOCALE)) end -- select which denomination we are going to use local denominationEntry = findDenominationEntry(localeInfo, number, roundingBehaviour) local baseValue = denominationEntry[1] local symbol = denominationEntry[2] -- Round to required significant digits local significantQuotient = roundToSignificantDigits(number / baseValue, 3, roundingBehaviour) -- trim to 1 decimal point local trimmedQuotient if roundingBehaviour == RoundingBehaviour.Truncate then trimmedQuotient = math.modf(significantQuotient * 10) / 10 else trimmedQuotient = math.floor(significantQuotient * 10 + 0.5) / 10 end local trimmedQuotientString = tostring(trimmedQuotient) -- Split the string into integer and fraction parts local decimalPointIndex = findDecimalPointIndex(trimmedQuotientString) local integerPart = string.sub(trimmedQuotientString, 1, decimalPointIndex - 1) local fractionPart = string.sub(trimmedQuotientString, decimalPointIndex + 1, #trimmedQuotientString) -- Add group delimiters to integer part if localeInfo.groupDelimiter then integerPart = addGroupDelimiters(integerPart, localeInfo.groupDelimiter) end if #fractionPart > 0 then return integerPart .. localeInfo.decimalSeparator .. fractionPart .. symbol else return integerPart .. symbol end end return NumberLocalization