@quenty/metricutils 2.4.3 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -54
- package/README.md +80 -2
- package/deploy.nevermore.json +10 -0
- package/package.json +22 -4
- package/src/Shared/Definitions/MetricUnitDefinitions.lua +1082 -0
- package/src/Shared/Definitions/MetricUnitDefinitions.spec.lua +55 -0
- package/src/Shared/Definitions/MetricUnitDimensionUtils.lua +164 -0
- package/src/Shared/Definitions/MetricUnitDimensionUtils.spec.lua +49 -0
- package/src/Shared/Definitions/MetricUnitKinds.lua +237 -0
- package/src/Shared/Definitions/MetricUnitKinds.spec.lua +51 -0
- package/src/Shared/Formatting/MetricUnitDisplayUnitUtils.lua +129 -0
- package/src/Shared/Formatting/MetricUnitFormatUtils.lua +924 -0
- package/src/Shared/Formatting/MetricUnitFormatUtils.spec.lua +280 -0
- package/src/Shared/Formatting/RxMetricUnitFormatUtils.lua +62 -0
- package/src/Shared/Localization/MetricUnitFallbackStrings.lua +289 -0
- package/src/Shared/Localization/MetricUnitFallbackStrings.spec.lua +82 -0
- package/src/Shared/Localization/MetricUnitTranslator/ar.json.txt +182 -0
- package/src/Shared/Localization/MetricUnitTranslator/de.json.txt +182 -0
- package/src/Shared/Localization/MetricUnitTranslator/en.json.txt +182 -0
- package/src/Shared/Localization/MetricUnitTranslator/es.json.txt +182 -0
- package/src/Shared/Localization/MetricUnitTranslator/fr.json.txt +182 -0
- package/src/Shared/Localization/MetricUnitTranslator/id.json.txt +182 -0
- package/src/Shared/Localization/MetricUnitTranslator/init.lua +92 -0
- package/src/Shared/Localization/MetricUnitTranslator/it.json.txt +182 -0
- package/src/Shared/Localization/MetricUnitTranslator/ja.json.txt +182 -0
- package/src/Shared/Localization/MetricUnitTranslator/ko.json.txt +182 -0
- package/src/Shared/Localization/MetricUnitTranslator/pl.json.txt +182 -0
- package/src/Shared/Localization/MetricUnitTranslator/pt.json.txt +182 -0
- package/src/Shared/Localization/MetricUnitTranslator/ru.json.txt +182 -0
- package/src/Shared/Localization/MetricUnitTranslator/th.json.txt +182 -0
- package/src/Shared/Localization/MetricUnitTranslator/tr.json.txt +182 -0
- package/src/Shared/Localization/MetricUnitTranslator/vi.json.txt +182 -0
- package/src/Shared/Localization/MetricUnitTranslator/zh-cn.json.txt +182 -0
- package/src/Shared/Localization/MetricUnitTranslator/zh-tw.json.txt +182 -0
- package/src/Shared/Localization/MetricUnitTranslator.spec.lua +309 -0
- package/src/Shared/Localization/PluralCategoryUtils.lua +103 -0
- package/src/Shared/Localization/PluralCategoryUtils.spec.lua +77 -0
- package/src/Shared/Logarithmic/DecibelUtils.lua +72 -0
- package/src/Shared/Logarithmic/DecibelUtils.spec.lua +35 -0
- package/src/Shared/MetricUnit.Angles.spec.lua +80 -0
- package/src/Shared/MetricUnit.Customary.spec.lua +104 -0
- package/src/Shared/MetricUnit.Derived.spec.lua +173 -0
- package/src/Shared/MetricUnit.Display.spec.lua +153 -0
- package/src/Shared/MetricUnit.Electrical.spec.lua +114 -0
- package/src/Shared/MetricUnit.Math.spec.lua +112 -0
- package/src/Shared/MetricUnit.Roblox.spec.lua +34 -0
- package/src/Shared/MetricUnit.SI.spec.lua +127 -0
- package/src/Shared/MetricUnit.Spring.spec.lua +94 -0
- package/src/Shared/MetricUnit.Temperature.spec.lua +212 -0
- package/src/Shared/MetricUnit.Time.spec.lua +202 -0
- package/src/Shared/MetricUnit.Units.spec.lua +210 -0
- package/src/Shared/MetricUnit.lua +1730 -0
- package/src/Shared/MetricUnit.spec.lua +362 -0
- package/src/Shared/MetricUtils.lua +47 -16
- package/src/Shared/MetricUtils.spec.lua +76 -0
- package/src/Shared/Parsing/MetricUnitParseUtils.lua +799 -0
- package/src/Shared/Parsing/MetricUnitParseUtils.spec.lua +189 -0
- package/src/Shared/Systems/MetricUnitSystem.lua +322 -0
- package/src/Shared/Systems/MetricUnitSystem.spec.lua +181 -0
- package/src/Shared/Systems/MetricUnitSystems.lua +225 -0
- package/src/jest.config.lua +3 -0
- package/src/node_modules.project.json +7 -0
- package/test/default.project.json +25 -0
- package/test/scripts/Server/ServerMain.server.lua +14 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--[[
|
|
3
|
+
@class MetricUnitFormatUtils.spec.lua
|
|
4
|
+
]]
|
|
5
|
+
|
|
6
|
+
local require = require(script.Parent.loader).load(script)
|
|
7
|
+
|
|
8
|
+
local Jest = require("Jest")
|
|
9
|
+
local MetricUnit = require("MetricUnit")
|
|
10
|
+
local MetricUnitFormatUtils = require("MetricUnitFormatUtils")
|
|
11
|
+
|
|
12
|
+
local describe = Jest.Globals.describe
|
|
13
|
+
local expect = Jest.Globals.expect
|
|
14
|
+
local it = Jest.Globals.it
|
|
15
|
+
|
|
16
|
+
-- Formatting puts a no-break space between a number and its unit; these compare it as a plain space
|
|
17
|
+
local function plain(text: string): string
|
|
18
|
+
return (string.gsub(text, "\u{00A0}", " "))
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
local function formatUnit(unit: any, amount: number, options: any?): string
|
|
22
|
+
return plain(MetricUnitFormatUtils.formatUnit(unit, amount, options))
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
local function formatData(value: any, options: any?): string
|
|
26
|
+
return plain(MetricUnitFormatUtils.format(value, options))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
local function format(value: any, options: MetricUnit.FormatOptions?): string
|
|
30
|
+
return plain(MetricUnit.format(value, options))
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
describe("MetricUnitFormatUtils.formatNumber", function()
|
|
34
|
+
it("rounds to three decimals and groups thousands by default", function()
|
|
35
|
+
expect(MetricUnitFormatUtils.formatNumber(1234.5678)).toBe("1,234.568")
|
|
36
|
+
expect(MetricUnitFormatUtils.formatNumber(-1234.5)).toBe("-1,234.5")
|
|
37
|
+
expect(MetricUnitFormatUtils.formatNumber(2)).toBe("2")
|
|
38
|
+
expect(MetricUnitFormatUtils.formatNumber(-0.0004)).toBe("0")
|
|
39
|
+
end)
|
|
40
|
+
|
|
41
|
+
it("uses the locale's grouping and decimal mark", function()
|
|
42
|
+
expect(MetricUnitFormatUtils.formatNumber(1234.5, { locale = "de-de" })).toBe("1 234,5")
|
|
43
|
+
expect(MetricUnitFormatUtils.formatNumber(1234.5, { locale = "es-es" })).toBe("1.234,5")
|
|
44
|
+
expect(MetricUnitFormatUtils.formatNumber(1234.5, { locale = "en-gb" })).toBe("1,234.5")
|
|
45
|
+
end)
|
|
46
|
+
|
|
47
|
+
it("takes Intl-style digit options", function()
|
|
48
|
+
expect(MetricUnitFormatUtils.formatNumber(2, { minimumFractionDigits = 2 })).toBe("2.00")
|
|
49
|
+
expect(MetricUnitFormatUtils.formatNumber(2.345, { maximumFractionDigits = 1 })).toBe("2.3")
|
|
50
|
+
expect(MetricUnitFormatUtils.formatNumber(2.35, { maximumFractionDigits = 0 })).toBe("2")
|
|
51
|
+
expect(MetricUnitFormatUtils.formatNumber(123456, { maximumSignificantDigits = 2 })).toBe("120,000")
|
|
52
|
+
expect(MetricUnitFormatUtils.formatNumber(0.012345, { maximumSignificantDigits = 2 })).toBe("0.012")
|
|
53
|
+
expect(MetricUnitFormatUtils.formatNumber(1234.5, { useGrouping = false })).toBe("1234.5")
|
|
54
|
+
end)
|
|
55
|
+
end)
|
|
56
|
+
|
|
57
|
+
describe("MetricUnitFormatUtils.format units", function()
|
|
58
|
+
it("picks the unit that suits the size, or the one given", function()
|
|
59
|
+
local length = MetricUnit.meters(2500)
|
|
60
|
+
expect(format(length)).toBe("2.5 km")
|
|
61
|
+
expect(format(length, { unit = "meter" })).toBe("2,500 m")
|
|
62
|
+
expect(format(length, { autoUnit = false })).toBe("2,500 m")
|
|
63
|
+
expect(format(length, { maximumSignificantDigits = 1 })).toBe("3 km")
|
|
64
|
+
expect(format(MetricUnit.meters(0))).toBe("0 m")
|
|
65
|
+
end)
|
|
66
|
+
|
|
67
|
+
it("writes long names, pluralized, in American or Commonwealth spelling", function()
|
|
68
|
+
expect(format(MetricUnit.meters(2500), { unitDisplay = "long" })).toBe("2.5 kilometers")
|
|
69
|
+
expect(format(MetricUnit.kilometers(1), { unitDisplay = "long" })).toBe("1 kilometer")
|
|
70
|
+
expect(format(MetricUnit.kilometers(1), { unitDisplay = "long", minimumFractionDigits = 1 })).toBe(
|
|
71
|
+
"1.0 kilometers"
|
|
72
|
+
)
|
|
73
|
+
expect(format(MetricUnit.meters(2500), { unitDisplay = "long", locale = "en-gb" })).toBe("2.5 kilometres")
|
|
74
|
+
expect(format(MetricUnit.new(2, "liter"), { unitDisplay = "long", locale = "en-au" })).toBe("2 litres")
|
|
75
|
+
expect(format(MetricUnit.tonnes(2), { unitDisplay = "long" })).toBe("2 metric tons")
|
|
76
|
+
expect(format(MetricUnit.tonnes(2), { unitDisplay = "long", locale = "en-gb" })).toBe("2 tonnes")
|
|
77
|
+
end)
|
|
78
|
+
|
|
79
|
+
it("writes SI symbols with the locale's numbers in any locale", function()
|
|
80
|
+
expect(format(MetricUnit.meters(1234.5), { unit = "meter", locale = "de-de" })).toBe("1 234,5 m")
|
|
81
|
+
expect(format(MetricUnit.kilograms(2.5), { locale = "fr-fr" })).toBe("2,5 kg")
|
|
82
|
+
end)
|
|
83
|
+
|
|
84
|
+
it("writes derived units", function()
|
|
85
|
+
expect(format(MetricUnit.new(1500, "newton"))).toBe("1.5 kN")
|
|
86
|
+
expect(format(MetricUnit.new(1500, "newton"), { unitDisplay = "long" })).toBe("1.5 kilonewtons")
|
|
87
|
+
expect(format(MetricUnit.new(2500, "watt"), { unitDisplay = "long" })).toBe("2.5 kilowatts")
|
|
88
|
+
expect(format(MetricUnit.new(3, "kilowattHour"), { unit = "kilowattHour" })).toBe("3 kWh")
|
|
89
|
+
expect(format(MetricUnit.new(25000, "squareMeter"), { unit = "hectare" })).toBe("2.5 ha")
|
|
90
|
+
expect(format(MetricUnit.new(0.25, "liter"))).toBe("250 mL")
|
|
91
|
+
expect(format(MetricUnit.new(4096, "kibibyte"), { unit = "mebibyte" })).toBe("4 MiB")
|
|
92
|
+
expect(format(MetricUnit.new(9.81, "meterPerSecondSquared"), { unitDisplay = "long" })).toBe(
|
|
93
|
+
"9.81 meters per second squared"
|
|
94
|
+
)
|
|
95
|
+
end)
|
|
96
|
+
|
|
97
|
+
it("takes phrase overrides", function()
|
|
98
|
+
local length = MetricUnit.meters(2500)
|
|
99
|
+
expect(format(length, { strings = { kilometer = { short = { one = "%s klick", other = "%s klicks" } } } })).toBe(
|
|
100
|
+
"2.5 klicks"
|
|
101
|
+
)
|
|
102
|
+
expect(format(length, {
|
|
103
|
+
strings = {
|
|
104
|
+
kilometer = {
|
|
105
|
+
short = function(_amount: number, numberText: string): string
|
|
106
|
+
return numberText .. "!"
|
|
107
|
+
end,
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
})).toBe("2.5!")
|
|
111
|
+
end)
|
|
112
|
+
|
|
113
|
+
it("errors when the unit given is of another dimension", function()
|
|
114
|
+
expect(function()
|
|
115
|
+
format(MetricUnit.meters(1), { unit = "kilogram" })
|
|
116
|
+
end).toThrow("kilogram is mass, not length")
|
|
117
|
+
end)
|
|
118
|
+
|
|
119
|
+
it("writes dimensionless values and unnamed compounds", function()
|
|
120
|
+
expect(format(MetricUnit.none(0.25))).toBe("0.25")
|
|
121
|
+
expect(format(MetricUnit.kilograms(2) * MetricUnit.new(3, "meterPerSecond"))).toBe("6 m·kg/s")
|
|
122
|
+
expect(format(MetricUnit.kilograms(2) / MetricUnit.kilometers(1))).toBe("0.002 kg/m")
|
|
123
|
+
end)
|
|
124
|
+
end)
|
|
125
|
+
|
|
126
|
+
describe("MetricUnitFormatUtils.format rates", function()
|
|
127
|
+
it("writes the numerator in its own display unit, per the shortest time unit it fills", function()
|
|
128
|
+
expect(format(MetricUnit.new(5, "meterPerSecond"))).toBe("5 m/s")
|
|
129
|
+
expect(format(MetricUnit.new(5, "meterPerSecond"), { unitDisplay = "long" })).toBe("5 meters per second")
|
|
130
|
+
expect(format(MetricUnit.new(1600, "kilobytePerSecond"))).toBe("1.6 MB/s")
|
|
131
|
+
expect(format(MetricUnit.new(1600, "kilobytePerSecond"), { unitDisplay = "long" })).toBe(
|
|
132
|
+
"1.6 megabytes per second"
|
|
133
|
+
)
|
|
134
|
+
expect(format(MetricUnit.perDay(5))).toBe("5/d")
|
|
135
|
+
expect(format(MetricUnit.perDay(5), { unitDisplay = "long" })).toBe("5 per day")
|
|
136
|
+
end)
|
|
137
|
+
|
|
138
|
+
it("takes a whole rate unit, a numerator unit or a per unit", function()
|
|
139
|
+
expect(format(MetricUnit.new(18, "kilometerPerHour"), { unit = "kilometerPerHour" })).toBe("18 km/h")
|
|
140
|
+
expect(format(MetricUnit.new(18, "kilometerPerHour"), { unit = "kilometerPerHour", unitDisplay = "long" })).toBe(
|
|
141
|
+
"18 kilometers per hour"
|
|
142
|
+
)
|
|
143
|
+
expect(format(MetricUnit.new(1600, "kilobytePerSecond"), { unit = "kilobyte" })).toBe("1,600 kB/s")
|
|
144
|
+
expect(format(MetricUnit.new(1600, "kilobytePerSecond"), { perUnit = "minute" })).toBe("96 MB/min")
|
|
145
|
+
expect(format(MetricUnit.new(18, "kilometerPerHour"), { unit = "kilometer", perUnit = "hour" })).toBe("18 km/h")
|
|
146
|
+
expect(format(MetricUnit.new(60, "hertz"), { unit = "hertz" })).toBe("60 Hz")
|
|
147
|
+
end)
|
|
148
|
+
|
|
149
|
+
it("takes per phrase overrides", function()
|
|
150
|
+
expect(format(MetricUnit.perHour(3), { perStrings = { hour = { short = "%s an hour" } } })).toBe("3 an hour")
|
|
151
|
+
end)
|
|
152
|
+
end)
|
|
153
|
+
|
|
154
|
+
describe("MetricUnitFormatUtils.format temperatures", function()
|
|
155
|
+
it("writes an absolute temperature on a scale, °C by default", function()
|
|
156
|
+
expect(format(MetricUnit.celsius(21.5))).toBe("21.5 °C")
|
|
157
|
+
expect(format(MetricUnit.celsius(100), { unit = "fahrenheit" })).toBe("212 °F")
|
|
158
|
+
expect(format(MetricUnit.celsius(0), { unit = "kelvin" })).toBe("273.15 K")
|
|
159
|
+
expect(format(MetricUnit.celsius(1), { unitDisplay = "long" })).toBe("1 degree Celsius")
|
|
160
|
+
expect(format(MetricUnit.celsius(20), { unitDisplay = "long" })).toBe("20 degrees Celsius")
|
|
161
|
+
end)
|
|
162
|
+
|
|
163
|
+
it("writes a difference without an offset, K by default", function()
|
|
164
|
+
expect(format(MetricUnit.difference(3, "celsius"))).toBe("3 K")
|
|
165
|
+
expect(format(MetricUnit.difference(3, "celsius"), { unit = "celsius" })).toBe("3 °C")
|
|
166
|
+
expect(format(MetricUnit.difference(5, "celsius"), { unit = "fahrenheit" })).toBe("9 °F")
|
|
167
|
+
expect(format(MetricUnit.celsius(25) - MetricUnit.celsius(20), { unitDisplay = "long" })).toBe("5 kelvins")
|
|
168
|
+
end)
|
|
169
|
+
|
|
170
|
+
it("errors when the unit given is not a temperature", function()
|
|
171
|
+
expect(function()
|
|
172
|
+
format(MetricUnit.celsius(5), { unit = "meter" })
|
|
173
|
+
end).toThrow("meter is length, not temperature")
|
|
174
|
+
end)
|
|
175
|
+
end)
|
|
176
|
+
|
|
177
|
+
describe("MetricUnitFormatUtils.formatUnit", function()
|
|
178
|
+
it("writes an amount already in the unit", function()
|
|
179
|
+
expect(formatUnit("kilometerPerHour", 90)).toBe("90 km/h")
|
|
180
|
+
expect(formatUnit("liter", 1, { unitDisplay = "long" })).toBe("1 liter")
|
|
181
|
+
expect(formatUnit("celsius", 20)).toBe("20 °C")
|
|
182
|
+
expect(formatUnit("hour", 1.5)).toBe("1.5 hours")
|
|
183
|
+
expect(formatUnit("day", 1.5, { locale = "es-es" })).toBe("1,5 días")
|
|
184
|
+
end)
|
|
185
|
+
end)
|
|
186
|
+
|
|
187
|
+
describe("MetricUnitFormatUtils.format with a translator", function()
|
|
188
|
+
local function fakeTranslator(locale: string, entries: { [string]: string }): any
|
|
189
|
+
return {
|
|
190
|
+
GetLocaleId = function()
|
|
191
|
+
return locale
|
|
192
|
+
end,
|
|
193
|
+
FormatByKey = function(_self: any, key: string, args: { [string]: any }?): string
|
|
194
|
+
local text = entries[key]
|
|
195
|
+
if not text then
|
|
196
|
+
error("No key " .. key)
|
|
197
|
+
end
|
|
198
|
+
return (string.gsub(text, "{n}", function()
|
|
199
|
+
return (args :: any).n
|
|
200
|
+
end))
|
|
201
|
+
end,
|
|
202
|
+
ObserveFormatByKey = function()
|
|
203
|
+
error("unused")
|
|
204
|
+
end,
|
|
205
|
+
}
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
local spanish = fakeTranslator("es-es", {
|
|
209
|
+
["units.mile.long.one"] = "{n} milla",
|
|
210
|
+
["units.mile.long.other"] = "{n} millas",
|
|
211
|
+
["units.mile.short.other"] = "{n} mi",
|
|
212
|
+
["units.kilometer.long.other"] = "{n} kilómetros",
|
|
213
|
+
["per.hour.long"] = "{n} por hora",
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
it("writes long names in the translator's language, with its plural rules and numbers", function()
|
|
217
|
+
expect(format(MetricUnit.new(3, "mile"):ToUSCustomary(), { unitDisplay = "long", translator = spanish })).toBe(
|
|
218
|
+
"3 millas"
|
|
219
|
+
)
|
|
220
|
+
expect(format(MetricUnit.new(1, "mile"):ToUSCustomary(), { unitDisplay = "long", translator = spanish })).toBe(
|
|
221
|
+
"1 milla"
|
|
222
|
+
)
|
|
223
|
+
expect(format(MetricUnit.new(1.5, "mile"):ToUSCustomary(), { unitDisplay = "long", translator = spanish })).toBe(
|
|
224
|
+
"1,5 millas"
|
|
225
|
+
)
|
|
226
|
+
expect(format(MetricUnit.new(5, "kilometerPerHour"), {
|
|
227
|
+
unitDisplay = "long",
|
|
228
|
+
unit = "kilometer",
|
|
229
|
+
perUnit = "hour",
|
|
230
|
+
translator = spanish,
|
|
231
|
+
})).toBe("5 kilómetros por hora")
|
|
232
|
+
end)
|
|
233
|
+
|
|
234
|
+
it("keeps symbols for short display, and English when a key is missing", function()
|
|
235
|
+
expect(format(MetricUnit.new(3, "mile"):ToUSCustomary(), { translator = spanish })).toBe("3 mi")
|
|
236
|
+
expect(format(MetricUnit.kilograms(3), { unitDisplay = "long", translator = spanish })).toBe("3 kilograms")
|
|
237
|
+
local broken = fakeTranslator("es-es", {})
|
|
238
|
+
expect(format(MetricUnit.new(3, "mile"):ToUSCustomary(), { unitDisplay = "long", translator = broken })).toBe(
|
|
239
|
+
"3 miles"
|
|
240
|
+
)
|
|
241
|
+
end)
|
|
242
|
+
end)
|
|
243
|
+
|
|
244
|
+
describe("MetricUnitFormatUtils.format input", function()
|
|
245
|
+
it("takes a MetricUnit or its plain data", function()
|
|
246
|
+
expect(formatData(MetricUnit.kilometers(1))).toBe("1 km")
|
|
247
|
+
expect(formatData(MetricUnit.toData(MetricUnit.kilometers(1)))).toBe("1 km")
|
|
248
|
+
expect(formatData({ amount = 278.15, dimensions = { temperature = 1 }, absolute = true })).toBe("5 °C")
|
|
249
|
+
expect(function()
|
|
250
|
+
formatData({ hello = true })
|
|
251
|
+
end).toThrow("expected a MetricUnit or MetricUnitData")
|
|
252
|
+
end)
|
|
253
|
+
|
|
254
|
+
it("writes bare rates by their per id, and frequencies in hertz", function()
|
|
255
|
+
expect(format(MetricUnit.perHour(3), { unit = "perHour" })).toBe("3/h")
|
|
256
|
+
expect(format(MetricUnit.perHour(3), { unit = "perHour", unitDisplay = "long" })).toBe("3 per hour")
|
|
257
|
+
expect(format(MetricUnit.perHour(3), { perUnit = "h" })).toBe("3/h")
|
|
258
|
+
expect(formatUnit("perDay", 5)).toBe("5/d")
|
|
259
|
+
end)
|
|
260
|
+
end)
|
|
261
|
+
|
|
262
|
+
describe("MetricUnitFormatUtils.format rates written as asked", function()
|
|
263
|
+
it("writes a rate in the numerator unit asked for, whatever the value's system shows", function()
|
|
264
|
+
expect(format(MetricUnit.new(60, "milePerHour"), { unit = "kilometer" })).toBe("0.027 km/s")
|
|
265
|
+
expect(format(MetricUnit.new(60, "milePerHour"), { unit = "kilometer", perUnit = "hour" })).toBe("96.561 km/h")
|
|
266
|
+
expect(format(MetricUnit.new(10, "studPerSecond"), { unit = "meter" })).toBe("2.8 m/s")
|
|
267
|
+
end)
|
|
268
|
+
|
|
269
|
+
it("writes a rate per the time unit asked for, whatever the value's system shows", function()
|
|
270
|
+
expect(format(MetricUnit.new(60, "milePerHour"), { perUnit = "second" })).toBe("88 ft/s")
|
|
271
|
+
expect(format(MetricUnit.new(60, "milePerHour"), { unit = "meter", perUnit = "second" })).toBe("26.822 m/s")
|
|
272
|
+
end)
|
|
273
|
+
end)
|
|
274
|
+
|
|
275
|
+
describe("MetricUnitFormatUtils plural forms by locale", function()
|
|
276
|
+
it("picks duration words by the locale's plural rule", function()
|
|
277
|
+
expect(format(MetricUnit.hours(1.5), { locale = "fr-fr", unit = "hour" })).toBe("1,5 heure")
|
|
278
|
+
expect(format(MetricUnit.hours(1.5), { locale = "en-us", unit = "hour" })).toBe("1.5 hours")
|
|
279
|
+
end)
|
|
280
|
+
end)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--[=[
|
|
3
|
+
Observable formatting, for UI that should follow the player's language. Kept apart from
|
|
4
|
+
[MetricUnitFormatUtils] so formatting itself needs no Rx and runs anywhere, servers and specs included.
|
|
5
|
+
|
|
6
|
+
@class RxMetricUnitFormatUtils
|
|
7
|
+
]=]
|
|
8
|
+
|
|
9
|
+
local require = require(script.Parent.loader).load(script)
|
|
10
|
+
|
|
11
|
+
local MetricUnitFormatUtils = require("MetricUnitFormatUtils")
|
|
12
|
+
local Observable = require("Observable")
|
|
13
|
+
local Rx = require("Rx")
|
|
14
|
+
|
|
15
|
+
local RxMetricUnitFormatUtils = {}
|
|
16
|
+
|
|
17
|
+
local ANY_TRANSLATION_KEY = "units.meter.other"
|
|
18
|
+
|
|
19
|
+
--[=[
|
|
20
|
+
Observes [MetricUnitFormatUtils.format] in the translator's language: emits again when the player's locale
|
|
21
|
+
changes or its translations finish streaming in, with English names until then. Numbers follow the translator's
|
|
22
|
+
locale unless `options.locale` is given.
|
|
23
|
+
|
|
24
|
+
```lua
|
|
25
|
+
local translator = serviceBag:GetService(require("MetricUnitTranslator"))
|
|
26
|
+
maid:GiveTask(RxMetricUnitFormatUtils.observeFormat(translator, MetricUnit.new(3, "mile"), {
|
|
27
|
+
unitDisplay = "long",
|
|
28
|
+
}):Subscribe(function(text)
|
|
29
|
+
label.Text = text -- "3 miles", "3 millas", "3 Meilen"
|
|
30
|
+
end))
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
@param translator MetricUnitTranslatorLike -- [MetricUnitTranslator]
|
|
34
|
+
@param value MetricUnit | MetricUnitData
|
|
35
|
+
@param options MetricUnitFormatOptions?
|
|
36
|
+
@return Observable<string>
|
|
37
|
+
]=]
|
|
38
|
+
function RxMetricUnitFormatUtils.observeFormat(
|
|
39
|
+
translator: MetricUnitFormatUtils.MetricUnitTranslatorLike,
|
|
40
|
+
value: any,
|
|
41
|
+
options: MetricUnitFormatUtils.MetricUnitFormatOptions?
|
|
42
|
+
): Observable.Observable<string>
|
|
43
|
+
assert(type(translator) == "table" and translator.ObserveFormatByKey, "Bad translator")
|
|
44
|
+
|
|
45
|
+
local formatOptions: MetricUnitFormatUtils.MetricUnitFormatOptions = table.clone(options or {})
|
|
46
|
+
formatOptions.translator = translator
|
|
47
|
+
|
|
48
|
+
return RxMetricUnitFormatUtils._observeTranslationChanges(translator):Pipe({
|
|
49
|
+
Rx.map(function()
|
|
50
|
+
return MetricUnitFormatUtils.format(value, formatOptions)
|
|
51
|
+
end) :: any,
|
|
52
|
+
Rx.distinct() :: any,
|
|
53
|
+
}) :: any
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
function RxMetricUnitFormatUtils._observeTranslationChanges(
|
|
57
|
+
translator: MetricUnitFormatUtils.MetricUnitTranslatorLike
|
|
58
|
+
): Observable.Observable<string>
|
|
59
|
+
return translator:ObserveFormatByKey(ANY_TRANSLATION_KEY, { n = "" })
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
return table.freeze(RxMetricUnitFormatUtils)
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--[=[
|
|
3
|
+
English unit names for [MetricUnitFormatUtils] when no translator is given. They need no translator, so specs,
|
|
4
|
+
servers and anything formatting before a translator loads always get readable text. Translated names stream to clients through [MetricUnitTranslator], a
|
|
5
|
+
`JSONTranslator` holding the same keys.
|
|
6
|
+
|
|
7
|
+
Lookups resolve through [ResolveLocaleUtils], so `en-us` lands on American English and `en-au` on Commonwealth
|
|
8
|
+
English. Without a translator, short phrases are the symbols in [MetricUnitDefinitions]; with one, they are the player's language's
|
|
9
|
+
(`5 км`, `5公里`). A no-break space joins a number to its unit either way.
|
|
10
|
+
|
|
11
|
+
@class MetricUnitFallbackStrings
|
|
12
|
+
]=]
|
|
13
|
+
|
|
14
|
+
local require = require(script.Parent.loader).load(script)
|
|
15
|
+
|
|
16
|
+
local MetricUnitDefinitions = require("MetricUnitDefinitions")
|
|
17
|
+
local ResolveLocaleUtils = require("ResolveLocaleUtils")
|
|
18
|
+
|
|
19
|
+
local MetricUnitFallbackStrings = {}
|
|
20
|
+
|
|
21
|
+
local DEFAULT_LOCALE = "en-us"
|
|
22
|
+
|
|
23
|
+
--[=[
|
|
24
|
+
A unit phrase with `%s` for the already formatted number: a singular and plural pair, or a function of the
|
|
25
|
+
rounded amount and the formatted number for languages with more plural forms.
|
|
26
|
+
|
|
27
|
+
@type UnitPhrase { one: string, other: string } | (amount: number, numberText: string) -> string
|
|
28
|
+
@within MetricUnitFallbackStrings
|
|
29
|
+
]=]
|
|
30
|
+
export type UnitPhrase = { one: string, other: string } | (number, string) -> string
|
|
31
|
+
|
|
32
|
+
--[=[
|
|
33
|
+
The short and long phrase for one unit, such as `"%s km"` and `"%s kilometers"`.
|
|
34
|
+
|
|
35
|
+
@interface UnitNames
|
|
36
|
+
.short UnitPhrase
|
|
37
|
+
.long UnitPhrase
|
|
38
|
+
@within MetricUnitFallbackStrings
|
|
39
|
+
]=]
|
|
40
|
+
export type UnitNames = { short: UnitPhrase, long: UnitPhrase }
|
|
41
|
+
|
|
42
|
+
--[=[
|
|
43
|
+
[UnitNames] for every unit written by name, keyed by unit id: every unit [MetricUnitDefinitions] gives `names`. Time
|
|
44
|
+
units are written with [TimeLocalizationUtils]' duration words instead, and bare rates with [PerStrings].
|
|
45
|
+
|
|
46
|
+
@type UnitStrings { [UnitId]: UnitNames }
|
|
47
|
+
@within MetricUnitFallbackStrings
|
|
48
|
+
]=]
|
|
49
|
+
export type UnitStrings = { [string]: UnitNames }
|
|
50
|
+
|
|
51
|
+
--[=[
|
|
52
|
+
How to write something per a time unit, with `%s` for the numerator already written: `"%s/h"` and
|
|
53
|
+
`"%s per hour"`.
|
|
54
|
+
|
|
55
|
+
@interface PerNames
|
|
56
|
+
.short string
|
|
57
|
+
.long string
|
|
58
|
+
@within MetricUnitFallbackStrings
|
|
59
|
+
]=]
|
|
60
|
+
export type PerNames = { short: string, long: string }
|
|
61
|
+
|
|
62
|
+
--[=[
|
|
63
|
+
[PerNames] for every time unit a rate can be written per.
|
|
64
|
+
|
|
65
|
+
@interface PerStrings
|
|
66
|
+
.second PerNames
|
|
67
|
+
.minute PerNames
|
|
68
|
+
.hour PerNames
|
|
69
|
+
.day PerNames
|
|
70
|
+
.week PerNames
|
|
71
|
+
.month PerNames
|
|
72
|
+
.year PerNames
|
|
73
|
+
@within MetricUnitFallbackStrings
|
|
74
|
+
]=]
|
|
75
|
+
export type PerStrings = {
|
|
76
|
+
second: PerNames,
|
|
77
|
+
minute: PerNames,
|
|
78
|
+
hour: PerNames,
|
|
79
|
+
day: PerNames,
|
|
80
|
+
week: PerNames,
|
|
81
|
+
month: PerNames,
|
|
82
|
+
year: PerNames,
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
--[=[
|
|
86
|
+
Unit phrases a caller passes to override a locale's, keyed by unit id: `{ kilometer = { short = "%s klicks" } }`.
|
|
87
|
+
|
|
88
|
+
@type UnitStringOverrides { [string]: { short: UnitPhrase?, long: UnitPhrase? } }
|
|
89
|
+
@within MetricUnitFallbackStrings
|
|
90
|
+
]=]
|
|
91
|
+
export type UnitStringOverrides = { [string]: { short: UnitPhrase?, long: UnitPhrase? } }
|
|
92
|
+
|
|
93
|
+
--[=[
|
|
94
|
+
Per phrases a caller passes to override a locale's, keyed by time unit id.
|
|
95
|
+
|
|
96
|
+
@type PerStringOverrides { [string]: { short: string?, long: string? } }
|
|
97
|
+
@within MetricUnitFallbackStrings
|
|
98
|
+
]=]
|
|
99
|
+
export type PerStringOverrides = { [string]: { short: string?, long: string? } }
|
|
100
|
+
|
|
101
|
+
--[=[
|
|
102
|
+
Everything one locale needs.
|
|
103
|
+
|
|
104
|
+
@interface UnitLocale
|
|
105
|
+
.units UnitStrings
|
|
106
|
+
.per PerStrings
|
|
107
|
+
@within MetricUnitFallbackStrings
|
|
108
|
+
]=]
|
|
109
|
+
export type UnitLocale = {
|
|
110
|
+
units: UnitStrings,
|
|
111
|
+
per: PerStrings,
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
-- U+00A0, not CLDR's narrow U+202F: create.roblox.com does not document how TextLabels render U+202F
|
|
115
|
+
local NO_BREAK_SPACE = "\u{00A0}"
|
|
116
|
+
MetricUnitFallbackStrings.NO_BREAK_SPACE = NO_BREAK_SPACE
|
|
117
|
+
|
|
118
|
+
local function withNoBreakSpaceAfterNumber(template: string): string
|
|
119
|
+
return (string.gsub(template, "^%%s ", "%%s" .. NO_BREAK_SPACE))
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
type Spelling = (string) -> string
|
|
123
|
+
|
|
124
|
+
local function american(text: string): string
|
|
125
|
+
return text
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
--[=[
|
|
129
|
+
`text` in Commonwealth English spelling: metre for meter, litre for liter, tonne for metric ton.
|
|
130
|
+
|
|
131
|
+
@param text string
|
|
132
|
+
@return string
|
|
133
|
+
]=]
|
|
134
|
+
function MetricUnitFallbackStrings.toCommonwealthSpelling(text: string): string
|
|
135
|
+
text = string.gsub(text, "metric tons?", { ["metric ton"] = "tonne", ["metric tons"] = "tonnes" })
|
|
136
|
+
text = string.gsub(text, "meter", "metre")
|
|
137
|
+
text = string.gsub(text, "liter", "litre")
|
|
138
|
+
return text
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
function MetricUnitFallbackStrings._lazyEnglishUnits(spelling: Spelling): UnitStrings
|
|
142
|
+
return setmetatable({}, {
|
|
143
|
+
__index = function(self, id: string): UnitNames?
|
|
144
|
+
local unit = MetricUnitDefinitions.getUnit(id)
|
|
145
|
+
if not (unit and unit.names) then
|
|
146
|
+
return nil
|
|
147
|
+
end
|
|
148
|
+
local names = MetricUnitFallbackStrings._englishNamesOf(unit, spelling)
|
|
149
|
+
rawset(self, id, names)
|
|
150
|
+
return names
|
|
151
|
+
end,
|
|
152
|
+
}) :: any
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
function MetricUnitFallbackStrings._englishNamesOf(
|
|
156
|
+
unit: MetricUnitDefinitions.UnitDefinition,
|
|
157
|
+
spelling: Spelling
|
|
158
|
+
): UnitNames
|
|
159
|
+
local short = "%s" .. NO_BREAK_SPACE .. unit.symbol
|
|
160
|
+
local templates = unit.shortTemplates
|
|
161
|
+
local names = unit.names or { one = unit.symbol, other = unit.symbol }
|
|
162
|
+
return table.freeze({
|
|
163
|
+
short = table.freeze({
|
|
164
|
+
one = if templates then withNoBreakSpaceAfterNumber(templates.one) else short,
|
|
165
|
+
other = if templates then withNoBreakSpaceAfterNumber(templates.other) else short,
|
|
166
|
+
}),
|
|
167
|
+
long = table.freeze({
|
|
168
|
+
one = "%s" .. NO_BREAK_SPACE .. spelling(names.one),
|
|
169
|
+
other = "%s" .. NO_BREAK_SPACE .. spelling(names.other),
|
|
170
|
+
}),
|
|
171
|
+
})
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
local PER_TIME_UNIT_IDS: { [string]: boolean } =
|
|
175
|
+
table.freeze({ second = true, minute = true, hour = true, day = true, week = true, month = true, year = true })
|
|
176
|
+
|
|
177
|
+
function MetricUnitFallbackStrings._lazyEnglishPerStrings(): PerStrings
|
|
178
|
+
return setmetatable({}, {
|
|
179
|
+
__index = function(self, id: string): PerNames?
|
|
180
|
+
local unit = if PER_TIME_UNIT_IDS[id] then MetricUnitDefinitions.getUnit(id) else nil
|
|
181
|
+
if not unit then
|
|
182
|
+
return nil
|
|
183
|
+
end
|
|
184
|
+
local names = table.freeze({ short = "%s/" .. unit.symbol, long = "%s per " .. id })
|
|
185
|
+
rawset(self, id, names)
|
|
186
|
+
return names
|
|
187
|
+
end,
|
|
188
|
+
}) :: any
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
local AMERICAN_ENGLISH: UnitLocale = table.freeze({
|
|
192
|
+
units = MetricUnitFallbackStrings._lazyEnglishUnits(american),
|
|
193
|
+
per = MetricUnitFallbackStrings._lazyEnglishPerStrings(),
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
local COMMONWEALTH_ENGLISH: UnitLocale = table.freeze({
|
|
197
|
+
units = MetricUnitFallbackStrings._lazyEnglishUnits(MetricUnitFallbackStrings.toCommonwealthSpelling),
|
|
198
|
+
per = MetricUnitFallbackStrings._lazyEnglishPerStrings(),
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
local LOCALES: { [string]: UnitLocale } = table.freeze({
|
|
202
|
+
en = AMERICAN_ENGLISH,
|
|
203
|
+
["en-gb"] = COMMONWEALTH_ENGLISH,
|
|
204
|
+
["en-au"] = COMMONWEALTH_ENGLISH,
|
|
205
|
+
["en-ca"] = COMMONWEALTH_ENGLISH,
|
|
206
|
+
["en-ie"] = COMMONWEALTH_ENGLISH,
|
|
207
|
+
["en-nz"] = COMMONWEALTH_ENGLISH,
|
|
208
|
+
}) :: any
|
|
209
|
+
|
|
210
|
+
local warnedLocales: { [string]: boolean } = {}
|
|
211
|
+
|
|
212
|
+
--[=[
|
|
213
|
+
Whether a locale has its own unit names here, directly or through a locale of the same language.
|
|
214
|
+
|
|
215
|
+
@param locale string?
|
|
216
|
+
@return boolean
|
|
217
|
+
]=]
|
|
218
|
+
function MetricUnitFallbackStrings.hasLocale(locale: string?): boolean
|
|
219
|
+
return ResolveLocaleUtils.resolveClosestKey(locale or DEFAULT_LOCALE, LOCALES) ~= nil
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
--[=[
|
|
223
|
+
Returns the read-only [UnitLocale] for a locale, defaulting to American English. Warns once per unknown locale.
|
|
224
|
+
|
|
225
|
+
@param locale string?
|
|
226
|
+
@return UnitLocale
|
|
227
|
+
]=]
|
|
228
|
+
function MetricUnitFallbackStrings.getLocale(locale: string?): UnitLocale
|
|
229
|
+
local key = ResolveLocaleUtils.resolveClosestKey(locale or DEFAULT_LOCALE, LOCALES)
|
|
230
|
+
if key then
|
|
231
|
+
return LOCALES[key]
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
local name = tostring(locale)
|
|
235
|
+
if not warnedLocales[name] then
|
|
236
|
+
warnedLocales[name] = true
|
|
237
|
+
warn(
|
|
238
|
+
string.format(
|
|
239
|
+
"[MetricUnitFallbackStrings] - No unit names for locale '%s', reverting to '%s' instead.",
|
|
240
|
+
name,
|
|
241
|
+
DEFAULT_LOCALE
|
|
242
|
+
)
|
|
243
|
+
)
|
|
244
|
+
end
|
|
245
|
+
return LOCALES[ResolveLocaleUtils.resolveClosestKey(DEFAULT_LOCALE, LOCALES) :: string]
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
--[=[
|
|
249
|
+
The [UnitNames] of `unit` in a locale: the locale's own, or, for a unit a game's own [MetricUnitSystem] declares,
|
|
250
|
+
its English names and symbol from its definition.
|
|
251
|
+
|
|
252
|
+
@param locale string?
|
|
253
|
+
@param unit UnitDefinition
|
|
254
|
+
@return UnitNames
|
|
255
|
+
]=]
|
|
256
|
+
function MetricUnitFallbackStrings.getUnitNames(locale: string?, unit: MetricUnitDefinitions.UnitDefinition): UnitNames
|
|
257
|
+
local unitLocale = MetricUnitFallbackStrings.getLocale(locale)
|
|
258
|
+
local names = unitLocale.units[unit.id]
|
|
259
|
+
if names then
|
|
260
|
+
return names
|
|
261
|
+
end
|
|
262
|
+
local spelling = if unitLocale == COMMONWEALTH_ENGLISH
|
|
263
|
+
then MetricUnitFallbackStrings.toCommonwealthSpelling
|
|
264
|
+
else american
|
|
265
|
+
return MetricUnitFallbackStrings._englishNamesOf(unit, spelling)
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
--[=[
|
|
269
|
+
Returns the read-only [UnitStrings] for a locale, defaulting to American English. Each unit's names are built the
|
|
270
|
+
first time they are read.
|
|
271
|
+
|
|
272
|
+
@param locale string?
|
|
273
|
+
@return UnitStrings
|
|
274
|
+
]=]
|
|
275
|
+
function MetricUnitFallbackStrings.getUnitStringsForLocale(locale: string?): UnitStrings
|
|
276
|
+
return MetricUnitFallbackStrings.getLocale(locale).units
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
--[=[
|
|
280
|
+
Returns the read-only [PerStrings] for a locale, defaulting to American English.
|
|
281
|
+
|
|
282
|
+
@param locale string?
|
|
283
|
+
@return PerStrings
|
|
284
|
+
]=]
|
|
285
|
+
function MetricUnitFallbackStrings.getPerStringsForLocale(locale: string?): PerStrings
|
|
286
|
+
return MetricUnitFallbackStrings.getLocale(locale).per
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
return table.freeze(MetricUnitFallbackStrings)
|