Clients/Client2020/ExtraContent/LuaPackages/Localization/NumberLocalization.lua

300 lines
7.4 KiB
Lua

-- 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