@quenty/metricutils 2.4.4 → 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 +6 -0
- 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 +46 -15
- 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,924 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--[=[
|
|
3
|
+
Writes measurements for people, in the style of JavaScript's `Intl.NumberFormat` with `style: "unit"`: a
|
|
4
|
+
localized number, then the unit's symbol or name. The unit is picked to suit the value's size unless one is
|
|
5
|
+
given, so 1,500 N is `1.5 kN`. Takes a [MetricUnit] or its plain [MetricUnitData].
|
|
6
|
+
|
|
7
|
+
Numbers go through [NumberLocalizationUtils], so digit grouping and the decimal mark follow the locale. Unit
|
|
8
|
+
names come from [MetricUnitFallbackStrings], and duration words from [TimeLocalizationUtils].
|
|
9
|
+
|
|
10
|
+
A duration is broken into years, months, days, hours, minutes and seconds with [MetricUnit]'s own averaged year
|
|
11
|
+
and month (a Julian year, and a twelfth of it), not `@quenty/time`'s 365 and 30 days, so `MetricUnit.years(1)`
|
|
12
|
+
reads `1 year` and `MetricUnit.months(1)` reads `1 month`. Only `@quenty/time`'s words are used. For a clock-style
|
|
13
|
+
`h:mm:ss`, or calendar months, hand exact seconds to `TimeDurationUtils` or `Time` yourself.
|
|
14
|
+
|
|
15
|
+
```lua
|
|
16
|
+
MetricUnitFormatUtils.format(MetricUnit.meters(1500)) --> 1.5 km
|
|
17
|
+
MetricUnitFormatUtils.format(MetricUnit.meters(1500), { unitDisplay = "long" }) --> 1.5 kilometers
|
|
18
|
+
MetricUnitFormatUtils.format(MetricUnit.meters(1500):ToUSCustomary()) --> 4,921.26 ft
|
|
19
|
+
MetricUnitFormatUtils.format({ amount = 1234.5, dimensions = { length = 1 } }, { unit = "meter", locale = "de-de" }) --> 1 234,5 m
|
|
20
|
+
MetricUnitFormatUtils.format(MetricUnit.minutes(90)) --> 1 hour, 30 minutes
|
|
21
|
+
MetricUnitFormatUtils.formatUnit("kilowattHour", 3) --> 3 kWh
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
@class MetricUnitFormatUtils
|
|
25
|
+
]=]
|
|
26
|
+
|
|
27
|
+
local require = require(script.Parent.loader).load(script)
|
|
28
|
+
|
|
29
|
+
local MetricUnitDefinitions = require("MetricUnitDefinitions")
|
|
30
|
+
local MetricUnitDimensionUtils = require("MetricUnitDimensionUtils")
|
|
31
|
+
local MetricUnitDisplayUnitUtils = require("MetricUnitDisplayUnitUtils")
|
|
32
|
+
local MetricUnitFallbackStrings = require("MetricUnitFallbackStrings")
|
|
33
|
+
local MetricUnitKinds = require("MetricUnitKinds")
|
|
34
|
+
local MetricUnitSystem = require("MetricUnitSystem")
|
|
35
|
+
local NumberLocalizationUtils = require("NumberLocalizationUtils")
|
|
36
|
+
local PluralCategoryUtils = require("PluralCategoryUtils")
|
|
37
|
+
local Time = require("Time")
|
|
38
|
+
local TimeDurationUtils = require("TimeDurationUtils")
|
|
39
|
+
local TimeLocalizationUtils = require("TimeLocalizationUtils")
|
|
40
|
+
|
|
41
|
+
local MetricUnitFormatUtils = {}
|
|
42
|
+
|
|
43
|
+
--[=[
|
|
44
|
+
The no-break space (U+00A0) that joins a number to its unit, so a line never wraps between them.
|
|
45
|
+
|
|
46
|
+
@prop NO_BREAK_SPACE string
|
|
47
|
+
@within MetricUnitFormatUtils
|
|
48
|
+
]=]
|
|
49
|
+
MetricUnitFormatUtils.NO_BREAK_SPACE = MetricUnitFallbackStrings.NO_BREAK_SPACE
|
|
50
|
+
|
|
51
|
+
local DEFAULT_LOCALE = "en-us"
|
|
52
|
+
local DEFAULT_MAXIMUM_FRACTION_DIGITS = 3
|
|
53
|
+
local DEFAULT_DURATION_PARTS = 2
|
|
54
|
+
local LARGEST_GROUPED_INTEGER = 1e15
|
|
55
|
+
local DECIMAL_SEPARATOR_PROBE = 0.5
|
|
56
|
+
local ENGLISH_PLURAL_RULES = "en"
|
|
57
|
+
local YEAR_OR_MONTH_CLOCK_TOKENS = "[yM]"
|
|
58
|
+
-- Keeps twelve averaged months, summed in floating point, a whole year
|
|
59
|
+
local WHOLE_UNIT_TOLERANCE = 1e-9
|
|
60
|
+
local NUMBER_MARK = "\u{E000}"
|
|
61
|
+
local UNIT_MARK = "\u{E001}"
|
|
62
|
+
|
|
63
|
+
--[=[
|
|
64
|
+
`"short"` writes the unit's symbol (`5 km`), `"long"` its name (`5 kilometers`).
|
|
65
|
+
|
|
66
|
+
@type UnitDisplay "short" | "long"
|
|
67
|
+
@within MetricUnitFormatUtils
|
|
68
|
+
]=]
|
|
69
|
+
export type UnitDisplay = "short" | "long"
|
|
70
|
+
|
|
71
|
+
--[=[
|
|
72
|
+
A measurement as plain data, for saves and for formatting without a [MetricUnit]: its amount in SI base units,
|
|
73
|
+
its exponents, and whether it is absolute. [MetricUnit.toData] and [MetricUnit.fromData] convert to and from it.
|
|
74
|
+
|
|
75
|
+
@interface MetricUnitData
|
|
76
|
+
.amount number
|
|
77
|
+
.dimensions { [string]: number }
|
|
78
|
+
.absolute boolean?
|
|
79
|
+
@within MetricUnitFormatUtils
|
|
80
|
+
]=]
|
|
81
|
+
export type MetricUnitData = {
|
|
82
|
+
amount: number,
|
|
83
|
+
dimensions: { [string]: number },
|
|
84
|
+
absolute: boolean?,
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
--[=[
|
|
88
|
+
How a duration is written: in words through `@quenty/time`'s duration phrases, or as a clock.
|
|
89
|
+
|
|
90
|
+
@type DurationStyle "words" | "clock"
|
|
91
|
+
@within MetricUnitFormatUtils
|
|
92
|
+
]=]
|
|
93
|
+
export type DurationStyle = "words" | "clock"
|
|
94
|
+
|
|
95
|
+
--[=[
|
|
96
|
+
What the formatter needs of [MetricUnitTranslator]: a `JSONTranslator`'s `FormatByKey`, `GetLocaleId` and,
|
|
97
|
+
for [RxMetricUnitFormatUtils.observeFormat], `ObserveFormatByKey`.
|
|
98
|
+
|
|
99
|
+
@interface MetricUnitTranslatorLike
|
|
100
|
+
.FormatByKey (self, key: string, args: { [string]: any }?) -> string
|
|
101
|
+
.GetLocaleId (self) -> string
|
|
102
|
+
.ObserveFormatByKey (self, key: string, args: { [string]: any }?) -> Observable<string>
|
|
103
|
+
@within MetricUnitFormatUtils
|
|
104
|
+
]=]
|
|
105
|
+
export type MetricUnitTranslatorLike = {
|
|
106
|
+
FormatByKey: (any, string, { [string]: any }?) -> string,
|
|
107
|
+
GetLocaleId: (any) -> string,
|
|
108
|
+
ObserveFormatByKey: (any, string, { [string]: any }?) -> any,
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
--[=[
|
|
112
|
+
Options for [MetricUnitFormatUtils.format], modelled on `Intl.NumberFormat`.
|
|
113
|
+
|
|
114
|
+
@interface MetricUnitFormatOptions
|
|
115
|
+
.locale string? -- Picks the number format and unit names, defaults to `en-us`
|
|
116
|
+
.unitDisplay UnitDisplay? -- `short` symbols (the default) or `long` names
|
|
117
|
+
.unit UnitLike? -- Always write this unit; for a rate, the whole rate's (`kilometerPerHour`, `perHour`) or the numerator's (`kilometer`)
|
|
118
|
+
.perUnit TimeUnit? -- Always write a rate per this time unit, such as `"hour"` or `"h"`
|
|
119
|
+
.autoUnit boolean? -- Pick the unit that suits the size (the default), or always write the SI base unit
|
|
120
|
+
.minimumFractionDigits number? -- Pad with zeros to at least this many decimals, defaults to 0
|
|
121
|
+
.maximumFractionDigits number? -- Round to at most this many decimals, defaults to 3
|
|
122
|
+
.maximumSignificantDigits number? -- Round to this many significant digits instead of fraction digits
|
|
123
|
+
.useGrouping boolean? -- Group thousands for the locale, defaults to true
|
|
124
|
+
.strings UnitStringOverrides? -- Overrides a unit's phrase, e.g. `{ kilometer = { short = "%s klicks" } }`
|
|
125
|
+
.perStrings PerStringOverrides? -- Overrides a per phrase, e.g. `{ hour = { short = "%s an hour" } }`
|
|
126
|
+
.durationStrings DurationStringOverrides? -- Overrides a duration word, as in `TimeDurationUtils`
|
|
127
|
+
.maxParts number? -- How many non-zero units a duration is written in, largest first, defaults to 2
|
|
128
|
+
.separator string? -- Between the parts of a duration, defaults to `, `
|
|
129
|
+
.durationStyle DurationStyle? -- `words` (the default, `1 hour, 30 minutes`) or `clock` (`1:30:00`, or `1:30` under an hour)
|
|
130
|
+
.durationTemplate string? -- A `TimeDurationUtils.format` template such as `hh:mm:ss`; years and months are refused
|
|
131
|
+
.translator MetricUnitTranslatorLike? -- [MetricUnitTranslator], for long names in the player's language
|
|
132
|
+
@within MetricUnitFormatUtils
|
|
133
|
+
]=]
|
|
134
|
+
export type MetricUnitFormatOptions = {
|
|
135
|
+
locale: string?,
|
|
136
|
+
unitDisplay: UnitDisplay?,
|
|
137
|
+
unit: MetricUnitDefinitions.UnitLike?,
|
|
138
|
+
perUnit: Time.TimeUnit?,
|
|
139
|
+
autoUnit: boolean?,
|
|
140
|
+
minimumFractionDigits: number?,
|
|
141
|
+
maximumFractionDigits: number?,
|
|
142
|
+
maximumSignificantDigits: number?,
|
|
143
|
+
useGrouping: boolean?,
|
|
144
|
+
strings: MetricUnitFallbackStrings.UnitStringOverrides?,
|
|
145
|
+
perStrings: MetricUnitFallbackStrings.PerStringOverrides?,
|
|
146
|
+
durationStrings: TimeLocalizationUtils.DurationStringOverrides?,
|
|
147
|
+
maxParts: number?,
|
|
148
|
+
separator: string?,
|
|
149
|
+
durationStyle: DurationStyle?,
|
|
150
|
+
durationTemplate: string?,
|
|
151
|
+
translator: MetricUnitTranslatorLike?,
|
|
152
|
+
_system: MetricUnitSystem.MetricUnitSystem?,
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
type UnitDefinition = MetricUnitDefinitions.UnitDefinition
|
|
156
|
+
type DurationUnit = { field: string, factor: number }
|
|
157
|
+
|
|
158
|
+
local DURATION_FIELDS_LARGEST_FIRST: { { field: string, unitId: string } } = table.freeze({
|
|
159
|
+
{ field = "years", unitId = "year" },
|
|
160
|
+
{ field = "months", unitId = "month" },
|
|
161
|
+
{ field = "days", unitId = "day" },
|
|
162
|
+
{ field = "hours", unitId = "hour" },
|
|
163
|
+
{ field = "minutes", unitId = "minute" },
|
|
164
|
+
{ field = "seconds", unitId = "second" },
|
|
165
|
+
{ field = "milliseconds", unitId = "millisecond" },
|
|
166
|
+
})
|
|
167
|
+
local durationUnitsLargestFirst: { DurationUnit }? = nil
|
|
168
|
+
|
|
169
|
+
local TIME_LOCALIZATION_FIELD_BY_UNIT_ID: { [string]: string } = {
|
|
170
|
+
millisecond = "milliseconds",
|
|
171
|
+
second = "seconds",
|
|
172
|
+
minute = "minutes",
|
|
173
|
+
hour = "hours",
|
|
174
|
+
day = "days",
|
|
175
|
+
week = "weeks",
|
|
176
|
+
month = "months",
|
|
177
|
+
year = "years",
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
local SUPERSCRIPTS: { [number]: string } = { [2] = "²", [3] = "³" }
|
|
181
|
+
|
|
182
|
+
local decimalSeparators: { [string]: string } = {}
|
|
183
|
+
|
|
184
|
+
--[=[
|
|
185
|
+
Writes a measurement for the locale in a unit that suits its size, or the one `options.unit` names. Durations are
|
|
186
|
+
broken into parts with localized words, temperatures keep whether they are absolute (`5 °C`) or a difference
|
|
187
|
+
(`5 K`), and a rate is written per the shortest time unit it fills at least once (`1.6 MB/s`).
|
|
188
|
+
|
|
189
|
+
A value is written in the system of the unit it was made from, so `MetricUnit.new(5, "mile")` writes miles and
|
|
190
|
+
`MetricUnit.kilometers(8)` kilometres. Convert it to write another ([MetricUnit.ToMetric],
|
|
191
|
+
[MetricUnit.ToImperial]), or use [MetricUnitSystem.Format]. The locale never chooses a measurement system.
|
|
192
|
+
|
|
193
|
+
```lua
|
|
194
|
+
local length = MetricUnit.meters(2500)
|
|
195
|
+
MetricUnitFormatUtils.format(length) --> 2.5 km
|
|
196
|
+
MetricUnitFormatUtils.format(length, { unit = "meter" }) --> 2,500 m
|
|
197
|
+
MetricUnitFormatUtils.format(length, { unit = "stud" }) --> 8,928.571 studs
|
|
198
|
+
MetricUnitFormatUtils.format(length, { unitDisplay = "long", locale = "en-gb" }) --> 2.5 kilometres
|
|
199
|
+
MetricUnitFormatUtils.format(length, { maximumSignificantDigits = 1 }) --> 3 km
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
@param value MetricUnit | MetricUnitData
|
|
203
|
+
@param options MetricUnitFormatOptions?
|
|
204
|
+
@return string
|
|
205
|
+
]=]
|
|
206
|
+
function MetricUnitFormatUtils.format(
|
|
207
|
+
-- any: MetricUnit requires this module, so its type cannot be named here; _readValue checks it at runtime
|
|
208
|
+
value: any,
|
|
209
|
+
options: MetricUnitFormatOptions?
|
|
210
|
+
): string
|
|
211
|
+
local amount, dimensions, absolute, displaySystem, displayUnit = MetricUnitFormatUtils._readValue(value)
|
|
212
|
+
local resolved = MetricUnitFormatUtils._resolveOptionsWithValuePreference(options, displaySystem, displayUnit)
|
|
213
|
+
|
|
214
|
+
if MetricUnitDimensionUtils.equal(dimensions, MetricUnitKinds.TIME) then
|
|
215
|
+
return MetricUnitFormatUtils._formatDuration(amount, resolved)
|
|
216
|
+
end
|
|
217
|
+
return MetricUnitFormatUtils._formatRelative(amount, dimensions, resolved, resolved.unit, absolute)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
function MetricUnitFormatUtils._formatInSystem(
|
|
221
|
+
value: any,
|
|
222
|
+
system: MetricUnitSystem.MetricUnitSystem,
|
|
223
|
+
options: MetricUnitFormatOptions?
|
|
224
|
+
): string
|
|
225
|
+
local resolved: MetricUnitFormatOptions = if options then table.clone(options) else {}
|
|
226
|
+
resolved._system = system
|
|
227
|
+
return MetricUnitFormatUtils.format(value, resolved)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
--[=[
|
|
231
|
+
Writes `amount` of the unit `unit`, as [MetricUnitFormatUtils.format] would, without picking another unit. A
|
|
232
|
+
temperature is absolute on its own scale, so `formatUnit("celsius", 20)` is `20 °C`.
|
|
233
|
+
|
|
234
|
+
```lua
|
|
235
|
+
MetricUnitFormatUtils.formatUnit("kilometerPerHour", 90) --> 90 km/h
|
|
236
|
+
MetricUnitFormatUtils.formatUnit("liter", 1, { unitDisplay = "long" }) --> 1 liter
|
|
237
|
+
MetricUnitFormatUtils.formatUnit("hours", 1.5) --> 1.5 hours
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
@param unit UnitLike
|
|
241
|
+
@param amount number -- in `unit`
|
|
242
|
+
@param options MetricUnitFormatOptions?
|
|
243
|
+
@return string
|
|
244
|
+
]=]
|
|
245
|
+
function MetricUnitFormatUtils.formatUnit(
|
|
246
|
+
unit: MetricUnitDefinitions.UnitLike,
|
|
247
|
+
amount: number,
|
|
248
|
+
options: MetricUnitFormatOptions?
|
|
249
|
+
): string
|
|
250
|
+
assert(type(amount) == "number", "Bad amount")
|
|
251
|
+
local resolved = MetricUnitFormatUtils._resolveOptionsWithValuePreference(options, nil, nil)
|
|
252
|
+
local definition = MetricUnitFormatUtils._getUnit(resolved, unit)
|
|
253
|
+
assert(definition, string.format("No unit %q", tostring(unit)))
|
|
254
|
+
|
|
255
|
+
if definition.duration then
|
|
256
|
+
return MetricUnitFormatUtils._durationUnitPhrase(definition, amount, resolved)
|
|
257
|
+
elseif definition.per then
|
|
258
|
+
local per = MetricUnitFormatUtils._perUnit(definition.per)
|
|
259
|
+
return MetricUnitFormatUtils._perPhrase(per, MetricUnitFormatUtils._formatNumber(amount, resolved), resolved)
|
|
260
|
+
end
|
|
261
|
+
return MetricUnitFormatUtils._unitPhrase(definition, amount, resolved)
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
--[=[
|
|
265
|
+
Writes a plain number for the locale: grouped thousands, the locale's decimal mark, and the digit options of
|
|
266
|
+
[MetricUnitFormatOptions].
|
|
267
|
+
|
|
268
|
+
```lua
|
|
269
|
+
MetricUnitFormatUtils.formatNumber(1234.5678) --> 1,234.568
|
|
270
|
+
MetricUnitFormatUtils.formatNumber(1234.5, { locale = "de-de" }) --> 1 234,5
|
|
271
|
+
MetricUnitFormatUtils.formatNumber(2, { minimumFractionDigits = 2 }) --> 2.00
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
@param value number
|
|
275
|
+
@param options MetricUnitFormatOptions?
|
|
276
|
+
@return string
|
|
277
|
+
]=]
|
|
278
|
+
function MetricUnitFormatUtils.formatNumber(value: number, options: MetricUnitFormatOptions?): string
|
|
279
|
+
assert(type(value) == "number", "Bad value")
|
|
280
|
+
local text = MetricUnitFormatUtils._formatNumber(value, options or {})
|
|
281
|
+
return text
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
function MetricUnitFormatUtils._readValue(value: any): (
|
|
285
|
+
number,
|
|
286
|
+
MetricUnitDimensionUtils.Dimensions,
|
|
287
|
+
boolean,
|
|
288
|
+
MetricUnitSystem.MetricUnitSystem?,
|
|
289
|
+
UnitDefinition?
|
|
290
|
+
)
|
|
291
|
+
assert(type(value) == "table", "Bad value")
|
|
292
|
+
if type(value.amount) == "number" then
|
|
293
|
+
assert(MetricUnitDimensionUtils.isDimensions(value.dimensions), "Bad dimensions")
|
|
294
|
+
return value.amount, value.dimensions, value.absolute == true, nil, nil
|
|
295
|
+
end
|
|
296
|
+
assert(value.ClassName == "MetricUnit", "Bad value, expected a MetricUnit or MetricUnitData")
|
|
297
|
+
return value:GetBaseAmount(),
|
|
298
|
+
value:GetDimensions(),
|
|
299
|
+
value:IsAbsolute(),
|
|
300
|
+
value:GetDisplaySystem(),
|
|
301
|
+
value:GetDisplayUnit()
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
function MetricUnitFormatUtils._resolveOptionsWithValuePreference(
|
|
305
|
+
options: MetricUnitFormatOptions?,
|
|
306
|
+
displaySystem: MetricUnitSystem.MetricUnitSystem?,
|
|
307
|
+
displayUnit: UnitDefinition?
|
|
308
|
+
): MetricUnitFormatOptions
|
|
309
|
+
local resolved: MetricUnitFormatOptions = if options then table.clone(options) else {}
|
|
310
|
+
if resolved.locale == nil and resolved.translator then
|
|
311
|
+
resolved.locale = resolved.translator:GetLocaleId()
|
|
312
|
+
end
|
|
313
|
+
if resolved._system == nil then
|
|
314
|
+
resolved._system = displaySystem
|
|
315
|
+
end
|
|
316
|
+
if resolved.unit == nil and displayUnit then
|
|
317
|
+
resolved.unit = displayUnit
|
|
318
|
+
end
|
|
319
|
+
return resolved
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
function MetricUnitFormatUtils._getUnit(
|
|
323
|
+
options: MetricUnitFormatOptions,
|
|
324
|
+
unit: string | UnitDefinition
|
|
325
|
+
): UnitDefinition?
|
|
326
|
+
local system = options._system
|
|
327
|
+
return if system then system:GetUnit(unit) else MetricUnitDefinitions.getUnit(unit)
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
function MetricUnitFormatUtils._tryTranslate(
|
|
331
|
+
options: MetricUnitFormatOptions,
|
|
332
|
+
keys: { string },
|
|
333
|
+
args: { [string]: string }
|
|
334
|
+
): string?
|
|
335
|
+
local translator = options.translator
|
|
336
|
+
if not translator then
|
|
337
|
+
return nil
|
|
338
|
+
end
|
|
339
|
+
for _, key in keys do
|
|
340
|
+
local ok, text = pcall(translator.FormatByKey, translator, key, args)
|
|
341
|
+
if ok and type(text) == "string" and text ~= key then
|
|
342
|
+
return text
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
return nil
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
function MetricUnitFormatUtils._formatNumber(value: number, options: MetricUnitFormatOptions): (string, number, string)
|
|
349
|
+
if value ~= value or value == math.huge or value == -math.huge then
|
|
350
|
+
local noFractionDigits = ""
|
|
351
|
+
return tostring(value), value, noFractionDigits
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
local locale = options.locale or DEFAULT_LOCALE
|
|
355
|
+
local minimumFractionDigits = options.minimumFractionDigits or 0
|
|
356
|
+
|
|
357
|
+
local rounded, decimals
|
|
358
|
+
if options.maximumSignificantDigits then
|
|
359
|
+
local significant = options.maximumSignificantDigits
|
|
360
|
+
if value == 0 then
|
|
361
|
+
rounded, decimals = 0, 0
|
|
362
|
+
else
|
|
363
|
+
local places = significant - (math.floor(math.log10(math.abs(value))) + 1)
|
|
364
|
+
rounded = MetricUnitFormatUtils._roundHalfAwayFromZero(value, places)
|
|
365
|
+
decimals = math.max(0, places)
|
|
366
|
+
end
|
|
367
|
+
else
|
|
368
|
+
decimals = options.maximumFractionDigits or DEFAULT_MAXIMUM_FRACTION_DIGITS
|
|
369
|
+
rounded = MetricUnitFormatUtils._roundHalfAwayFromZero(value, decimals)
|
|
370
|
+
end
|
|
371
|
+
decimals = math.max(decimals, minimumFractionDigits)
|
|
372
|
+
|
|
373
|
+
local digits = string.format("%." .. tostring(decimals) .. "f", math.abs(rounded))
|
|
374
|
+
local integerPart, shownFractionDigits = string.match(digits, "^(%d+)%.?(%d*)$")
|
|
375
|
+
assert(integerPart and shownFractionDigits, "Bad number")
|
|
376
|
+
while #shownFractionDigits > minimumFractionDigits and string.sub(shownFractionDigits, -1) == "0" do
|
|
377
|
+
shownFractionDigits = string.sub(shownFractionDigits, 1, -2)
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
local integerValue = tonumber(integerPart) :: number
|
|
381
|
+
if options.useGrouping ~= false and integerValue < LARGEST_GROUPED_INTEGER then
|
|
382
|
+
integerPart = NumberLocalizationUtils.localize(integerValue, locale)
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
local text = (if rounded < 0 then "-" else "") .. integerPart
|
|
386
|
+
if #shownFractionDigits > 0 then
|
|
387
|
+
text ..= MetricUnitFormatUtils.getDecimalSeparator(locale) .. shownFractionDigits
|
|
388
|
+
end
|
|
389
|
+
return text, rounded, shownFractionDigits
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
function MetricUnitFormatUtils._roundHalfAwayFromZero(value: number, decimalPlacesOrNegativeForTens: number): number
|
|
393
|
+
local factor = 10 ^ decimalPlacesOrNegativeForTens
|
|
394
|
+
local rounded = math.floor(math.abs(value) * factor + 0.5) / factor
|
|
395
|
+
return if value < 0 and rounded ~= 0 then -rounded else rounded
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
--[=[
|
|
399
|
+
The decimal mark `locale` writes numbers with, such as `"."` or `","`.
|
|
400
|
+
|
|
401
|
+
@param locale string
|
|
402
|
+
@return string
|
|
403
|
+
]=]
|
|
404
|
+
function MetricUnitFormatUtils.getDecimalSeparator(locale: string): string
|
|
405
|
+
local cached = decimalSeparators[locale]
|
|
406
|
+
if cached then
|
|
407
|
+
return cached
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
local separator = MetricUnitFormatUtils._readDecimalSeparatorFromAbbreviate(locale)
|
|
411
|
+
decimalSeparators[locale] = separator
|
|
412
|
+
return separator
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
-- NumberLocalizationUtils.localize always writes "." as the decimal mark; abbreviate writes the locale's
|
|
416
|
+
function MetricUnitFormatUtils._readDecimalSeparatorFromAbbreviate(locale: string): string
|
|
417
|
+
local probe = NumberLocalizationUtils.abbreviate(DECIMAL_SEPARATOR_PROBE, locale, nil, 1)
|
|
418
|
+
return string.match(probe, "^0(.-)5$") or "."
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
function MetricUnitFormatUtils._getUnitLocale(options: MetricUnitFormatOptions): MetricUnitFallbackStrings.UnitLocale
|
|
422
|
+
return MetricUnitFallbackStrings.getLocale(MetricUnitFormatUtils._fallbackStringsLocale(options))
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
function MetricUnitFormatUtils._fallbackStringsLocale(options: MetricUnitFormatOptions): string?
|
|
426
|
+
local writesSymbolsThatAreTheSameInEveryLocale = options.unitDisplay ~= "long"
|
|
427
|
+
if writesSymbolsThatAreTheSameInEveryLocale and not MetricUnitFallbackStrings.hasLocale(options.locale) then
|
|
428
|
+
return DEFAULT_LOCALE
|
|
429
|
+
end
|
|
430
|
+
return options.locale
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
function MetricUnitFormatUtils._pickPluralTemplate(
|
|
434
|
+
phrase: { one: string, other: string },
|
|
435
|
+
pluralRulesLocale: string?,
|
|
436
|
+
rounded: number,
|
|
437
|
+
fractionDigits: string
|
|
438
|
+
): string
|
|
439
|
+
local category = PluralCategoryUtils.getPluralCategory(pluralRulesLocale, rounded, fractionDigits)
|
|
440
|
+
return if category == "one" then phrase.one else phrase.other
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
function MetricUnitFormatUtils._applyUnitPhrase(
|
|
444
|
+
phrase: MetricUnitFallbackStrings.UnitPhrase,
|
|
445
|
+
pluralRulesLocale: string?,
|
|
446
|
+
rounded: number,
|
|
447
|
+
numberText: string,
|
|
448
|
+
fraction: string
|
|
449
|
+
): string
|
|
450
|
+
if type(phrase) == "function" then
|
|
451
|
+
return phrase(rounded, numberText)
|
|
452
|
+
end
|
|
453
|
+
local template = MetricUnitFormatUtils._pickPluralTemplate(phrase, pluralRulesLocale, rounded, fraction)
|
|
454
|
+
return (string.gsub(template, "%%s", function()
|
|
455
|
+
return numberText
|
|
456
|
+
end))
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
function MetricUnitFormatUtils._unitPhrase(
|
|
460
|
+
definition: UnitDefinition,
|
|
461
|
+
amount: number,
|
|
462
|
+
options: MetricUnitFormatOptions,
|
|
463
|
+
display: UnitDisplay?
|
|
464
|
+
): string
|
|
465
|
+
local numberText, rounded, fraction = MetricUnitFormatUtils._formatNumber(amount, options)
|
|
466
|
+
local resolvedDisplay: UnitDisplay = display or options.unitDisplay or "short"
|
|
467
|
+
|
|
468
|
+
local override = if options.strings then options.strings[definition.id] else nil
|
|
469
|
+
local phrase = if override then (override :: any)[resolvedDisplay] else nil
|
|
470
|
+
if phrase == nil and options.translator then
|
|
471
|
+
local category = PluralCategoryUtils.getPluralCategory(options.locale, rounded, fraction)
|
|
472
|
+
local keys = {
|
|
473
|
+
MetricUnitFormatUtils._unitTranslationKey(definition.id, resolvedDisplay, category),
|
|
474
|
+
MetricUnitFormatUtils._unitTranslationKey(definition.id, resolvedDisplay, "other"),
|
|
475
|
+
}
|
|
476
|
+
local translated = MetricUnitFormatUtils._tryTranslate(options, keys, { n = numberText })
|
|
477
|
+
or MetricUnitFormatUtils._translatePrefixed(options, definition, resolvedDisplay, category, numberText)
|
|
478
|
+
if translated then
|
|
479
|
+
return translated
|
|
480
|
+
end
|
|
481
|
+
end
|
|
482
|
+
local pluralRulesLocale = options.locale
|
|
483
|
+
if phrase == nil then
|
|
484
|
+
local locale = MetricUnitFormatUtils._fallbackStringsLocale(options)
|
|
485
|
+
phrase = (MetricUnitFallbackStrings.getUnitNames(locale, definition) :: any)[resolvedDisplay]
|
|
486
|
+
pluralRulesLocale = ENGLISH_PLURAL_RULES
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
return MetricUnitFormatUtils._applyUnitPhrase(phrase, pluralRulesLocale, rounded, numberText, fraction)
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
function MetricUnitFormatUtils._translatePrefixed(
|
|
493
|
+
options: MetricUnitFormatOptions,
|
|
494
|
+
definition: UnitDefinition,
|
|
495
|
+
display: UnitDisplay,
|
|
496
|
+
category: string,
|
|
497
|
+
numberText: string
|
|
498
|
+
): string?
|
|
499
|
+
local prefix = definition.prefix
|
|
500
|
+
if not prefix then
|
|
501
|
+
return nil
|
|
502
|
+
end
|
|
503
|
+
local baseText = MetricUnitFormatUtils._tryTranslate(options, {
|
|
504
|
+
MetricUnitFormatUtils._unitTranslationKey(prefix.base, display, category),
|
|
505
|
+
MetricUnitFormatUtils._unitTranslationKey(prefix.base, display, "other"),
|
|
506
|
+
}, { n = NUMBER_MARK })
|
|
507
|
+
local prefixPattern = MetricUnitFormatUtils._tryTranslate(
|
|
508
|
+
options,
|
|
509
|
+
{ "prefixes." .. prefix.name .. "." .. display },
|
|
510
|
+
{ unit = UNIT_MARK }
|
|
511
|
+
)
|
|
512
|
+
if not (baseText and prefixPattern) then
|
|
513
|
+
return nil
|
|
514
|
+
end
|
|
515
|
+
|
|
516
|
+
local unitText = MetricUnitFormatUtils._withoutNumber(baseText)
|
|
517
|
+
local start, finish = string.find(baseText, unitText, 1, true)
|
|
518
|
+
if unitText == "" or not start then
|
|
519
|
+
return nil
|
|
520
|
+
end
|
|
521
|
+
local prefixIsACapitalizedWordBeforeTheUnit = display == "long"
|
|
522
|
+
and string.match(prefixPattern, "^%u") ~= nil
|
|
523
|
+
and string.sub(prefixPattern, -#UNIT_MARK) == UNIT_MARK
|
|
524
|
+
local unitInPrefix = if prefixIsACapitalizedWordBeforeTheUnit
|
|
525
|
+
then string.lower(string.sub(unitText, 1, 1)) .. string.sub(unitText, 2)
|
|
526
|
+
else unitText
|
|
527
|
+
local prefixedUnit = string.gsub(prefixPattern, UNIT_MARK, function()
|
|
528
|
+
return unitInPrefix
|
|
529
|
+
end)
|
|
530
|
+
local text = string.sub(baseText, 1, start - 1) .. prefixedUnit .. string.sub(baseText, (finish :: number) + 1)
|
|
531
|
+
return (string.gsub(text, NUMBER_MARK, function()
|
|
532
|
+
return numberText
|
|
533
|
+
end))
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
function MetricUnitFormatUtils._withoutNumber(text: string): string
|
|
537
|
+
local unitText = string.gsub(text, NUMBER_MARK, "")
|
|
538
|
+
for _, space in { " ", MetricUnitFormatUtils.NO_BREAK_SPACE } do
|
|
539
|
+
while string.sub(unitText, 1, #space) == space do
|
|
540
|
+
unitText = string.sub(unitText, #space + 1)
|
|
541
|
+
end
|
|
542
|
+
while #unitText >= #space and string.sub(unitText, -#space) == space do
|
|
543
|
+
unitText = string.sub(unitText, 1, -#space - 1)
|
|
544
|
+
end
|
|
545
|
+
end
|
|
546
|
+
return unitText
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
function MetricUnitFormatUtils._durationUnitsLargestFirst(): { DurationUnit }
|
|
550
|
+
local found = durationUnitsLargestFirst
|
|
551
|
+
if found then
|
|
552
|
+
return found
|
|
553
|
+
end
|
|
554
|
+
local units: { DurationUnit } = {}
|
|
555
|
+
for _, duration in DURATION_FIELDS_LARGEST_FIRST do
|
|
556
|
+
local definition = assert(MetricUnitDefinitions.getUnit(duration.unitId), duration.unitId)
|
|
557
|
+
table.insert(units, { field = duration.field, factor = definition.factor })
|
|
558
|
+
end
|
|
559
|
+
local frozen = table.freeze(units)
|
|
560
|
+
durationUnitsLargestFirst = frozen
|
|
561
|
+
return frozen
|
|
562
|
+
end
|
|
563
|
+
|
|
564
|
+
function MetricUnitFormatUtils._unitTranslationKey(unitId: string, display: UnitDisplay, pluralCategory: string): string
|
|
565
|
+
return "units." .. unitId .. "." .. display .. "." .. pluralCategory
|
|
566
|
+
end
|
|
567
|
+
|
|
568
|
+
function MetricUnitFormatUtils._perUnit(unit: string): UnitDefinition
|
|
569
|
+
local per = MetricUnitDefinitions.getUnit(unit)
|
|
570
|
+
assert(
|
|
571
|
+
per and TIME_LOCALIZATION_FIELD_BY_UNIT_ID[per.id] and per.id ~= "millisecond",
|
|
572
|
+
string.format("Bad per unit %q, expected a second, minute, hour, day, week, month or year", unit)
|
|
573
|
+
)
|
|
574
|
+
return per
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
function MetricUnitFormatUtils._perPhrase(
|
|
578
|
+
per: UnitDefinition,
|
|
579
|
+
numeratorText: string,
|
|
580
|
+
options: MetricUnitFormatOptions
|
|
581
|
+
): string
|
|
582
|
+
local display: UnitDisplay = options.unitDisplay or "short"
|
|
583
|
+
|
|
584
|
+
local override = if options.perStrings then options.perStrings[per.id] else nil
|
|
585
|
+
local template = if override then (override :: any)[display] else nil
|
|
586
|
+
if template == nil and options.translator then
|
|
587
|
+
local translated = MetricUnitFormatUtils._tryTranslate(
|
|
588
|
+
options,
|
|
589
|
+
{ "per." .. per.id .. "." .. display },
|
|
590
|
+
{ n = numeratorText }
|
|
591
|
+
)
|
|
592
|
+
if translated then
|
|
593
|
+
return translated
|
|
594
|
+
end
|
|
595
|
+
end
|
|
596
|
+
if template == nil then
|
|
597
|
+
local names = (MetricUnitFormatUtils._getUnitLocale(options).per :: any)[per.id]
|
|
598
|
+
assert(names, string.format("No per phrase for %q", per.id))
|
|
599
|
+
template = names[display]
|
|
600
|
+
end
|
|
601
|
+
|
|
602
|
+
return (string.gsub(template, "%%s", function()
|
|
603
|
+
return numeratorText
|
|
604
|
+
end))
|
|
605
|
+
end
|
|
606
|
+
|
|
607
|
+
function MetricUnitFormatUtils._formatRelative(
|
|
608
|
+
amount: number,
|
|
609
|
+
dimensions: MetricUnitDimensionUtils.Dimensions,
|
|
610
|
+
options: MetricUnitFormatOptions,
|
|
611
|
+
unit: (string | UnitDefinition)?,
|
|
612
|
+
absolute: boolean?
|
|
613
|
+
): string
|
|
614
|
+
if next(dimensions) == nil and unit == nil then
|
|
615
|
+
return (MetricUnitFormatUtils._formatNumber(amount, options))
|
|
616
|
+
end
|
|
617
|
+
|
|
618
|
+
if unit then
|
|
619
|
+
local definition = MetricUnitFormatUtils._getUnit(options, unit)
|
|
620
|
+
if not definition then
|
|
621
|
+
error(
|
|
622
|
+
string.format("MetricUnitFormatUtils: no unit %q", if type(unit) == "string" then unit else unit.id),
|
|
623
|
+
3
|
|
624
|
+
)
|
|
625
|
+
end
|
|
626
|
+
if MetricUnitDimensionUtils.equal(definition.dimensions, dimensions) then
|
|
627
|
+
if definition.per then
|
|
628
|
+
return MetricUnitFormatUtils._formatRate(amount, dimensions, options, nil, definition.per)
|
|
629
|
+
end
|
|
630
|
+
return MetricUnitFormatUtils._unitPhrase(
|
|
631
|
+
definition,
|
|
632
|
+
MetricUnitFormatUtils._amountIn(amount, definition, absolute),
|
|
633
|
+
options
|
|
634
|
+
)
|
|
635
|
+
elseif not MetricUnitFormatUtils._isPerTime(dimensions) then
|
|
636
|
+
error(
|
|
637
|
+
string.format(
|
|
638
|
+
"MetricUnitFormatUtils: %s is %s, not %s",
|
|
639
|
+
definition.id,
|
|
640
|
+
MetricUnitDimensionUtils.describe(definition.dimensions),
|
|
641
|
+
MetricUnitDimensionUtils.describe(dimensions)
|
|
642
|
+
),
|
|
643
|
+
3
|
|
644
|
+
)
|
|
645
|
+
end
|
|
646
|
+
end
|
|
647
|
+
|
|
648
|
+
local isRateWrittenAsAsked = (unit ~= nil or options.perUnit ~= nil)
|
|
649
|
+
and MetricUnitFormatUtils._isPerTime(dimensions)
|
|
650
|
+
if isRateWrittenAsAsked then
|
|
651
|
+
return MetricUnitFormatUtils._formatRate(amount, dimensions, options, unit, options.perUnit)
|
|
652
|
+
end
|
|
653
|
+
|
|
654
|
+
local units = MetricUnitDisplayUnitUtils.getDisplayUnits(dimensions, options._system, absolute)
|
|
655
|
+
if units then
|
|
656
|
+
local definition
|
|
657
|
+
if options.autoUnit == false then
|
|
658
|
+
definition = MetricUnitDisplayUnitUtils.getBaseUnit(
|
|
659
|
+
MetricUnitDisplayUnitUtils.getDisplayUnits(dimensions, nil, absolute) or units
|
|
660
|
+
)
|
|
661
|
+
else
|
|
662
|
+
definition = MetricUnitDisplayUnitUtils.pickDisplayUnit(amount, units)
|
|
663
|
+
end
|
|
664
|
+
return MetricUnitFormatUtils._unitPhrase(
|
|
665
|
+
definition,
|
|
666
|
+
MetricUnitFormatUtils._amountIn(amount, definition, absolute),
|
|
667
|
+
options
|
|
668
|
+
)
|
|
669
|
+
end
|
|
670
|
+
|
|
671
|
+
if MetricUnitFormatUtils._isPerTime(dimensions) then
|
|
672
|
+
return MetricUnitFormatUtils._formatRate(amount, dimensions, options, unit, options.perUnit)
|
|
673
|
+
end
|
|
674
|
+
|
|
675
|
+
return MetricUnitFormatUtils._formatInBaseUnitSymbols(amount, dimensions, options)
|
|
676
|
+
end
|
|
677
|
+
|
|
678
|
+
function MetricUnitFormatUtils._amountIn(amount: number, definition: UnitDefinition, absolute: boolean?): number
|
|
679
|
+
local fromZero = if absolute then amount - (definition.zero or 0) else amount
|
|
680
|
+
return fromZero / definition.factor
|
|
681
|
+
end
|
|
682
|
+
|
|
683
|
+
function MetricUnitFormatUtils._isPerTime(dimensions: MetricUnitDimensionUtils.Dimensions): boolean
|
|
684
|
+
if dimensions.time == -1 then
|
|
685
|
+
return true
|
|
686
|
+
end
|
|
687
|
+
local numerator = MetricUnitDimensionUtils.combine(dimensions, MetricUnitKinds.TIME, 1)
|
|
688
|
+
return MetricUnitDisplayUnitUtils.getDisplayUnits(numerator) ~= nil
|
|
689
|
+
end
|
|
690
|
+
|
|
691
|
+
function MetricUnitFormatUtils._formatRate(
|
|
692
|
+
amount: number,
|
|
693
|
+
dimensions: MetricUnitDimensionUtils.Dimensions,
|
|
694
|
+
options: MetricUnitFormatOptions,
|
|
695
|
+
numeratorUnit: (string | UnitDefinition)?,
|
|
696
|
+
perUnit: string?
|
|
697
|
+
): string
|
|
698
|
+
local per = if perUnit
|
|
699
|
+
then MetricUnitFormatUtils._perUnit(perUnit)
|
|
700
|
+
else MetricUnitDisplayUnitUtils.pickDisplayPerUnit(amount)
|
|
701
|
+
|
|
702
|
+
local numerator = MetricUnitDimensionUtils.combine(dimensions, MetricUnitKinds.TIME, 1)
|
|
703
|
+
local numeratorText = MetricUnitFormatUtils._formatRelative(amount * per.factor, numerator, options, numeratorUnit)
|
|
704
|
+
return MetricUnitFormatUtils._perPhrase(per, numeratorText, options)
|
|
705
|
+
end
|
|
706
|
+
|
|
707
|
+
function MetricUnitFormatUtils._formatInBaseUnitSymbols(
|
|
708
|
+
amount: number,
|
|
709
|
+
dimensions: MetricUnitDimensionUtils.Dimensions,
|
|
710
|
+
options: MetricUnitFormatOptions
|
|
711
|
+
): string
|
|
712
|
+
local names: { string } = {}
|
|
713
|
+
for name in dimensions do
|
|
714
|
+
table.insert(names, name)
|
|
715
|
+
end
|
|
716
|
+
table.sort(names)
|
|
717
|
+
|
|
718
|
+
local over: { string } = {}
|
|
719
|
+
local under: { string } = {}
|
|
720
|
+
for _, name in names do
|
|
721
|
+
local exponent = dimensions[name]
|
|
722
|
+
local symbol = MetricUnitDisplayUnitUtils.getBaseSymbol(name)
|
|
723
|
+
local power = math.abs(exponent)
|
|
724
|
+
local part = if power == 1 then symbol else symbol .. (SUPERSCRIPTS[power] or "^" .. tostring(power))
|
|
725
|
+
table.insert(if exponent > 0 then over else under, part)
|
|
726
|
+
end
|
|
727
|
+
|
|
728
|
+
local text = MetricUnitFormatUtils._formatNumber(amount, options)
|
|
729
|
+
if #over > 0 then
|
|
730
|
+
text ..= MetricUnitFormatUtils.NO_BREAK_SPACE .. table.concat(over, "·")
|
|
731
|
+
end
|
|
732
|
+
for _, part in under do
|
|
733
|
+
text ..= "/" .. part
|
|
734
|
+
end
|
|
735
|
+
return text
|
|
736
|
+
end
|
|
737
|
+
|
|
738
|
+
function MetricUnitFormatUtils._formatDuration(seconds: number, options: MetricUnitFormatOptions): string
|
|
739
|
+
if options.unit then
|
|
740
|
+
local definition = MetricUnitFormatUtils._getUnit(options, options.unit)
|
|
741
|
+
assert(
|
|
742
|
+
definition and definition.duration,
|
|
743
|
+
string.format("MetricUnitFormatUtils: %s is not a time unit", tostring(options.unit))
|
|
744
|
+
)
|
|
745
|
+
return MetricUnitFormatUtils._durationUnitPhrase(definition, seconds / definition.factor, options)
|
|
746
|
+
end
|
|
747
|
+
|
|
748
|
+
if options.durationTemplate or options.durationStyle == "clock" then
|
|
749
|
+
return MetricUnitFormatUtils._formatClock(seconds, options)
|
|
750
|
+
end
|
|
751
|
+
|
|
752
|
+
local magnitude = math.abs(seconds)
|
|
753
|
+
if magnitude == 0 then
|
|
754
|
+
return MetricUnitFormatUtils._durationPhrase("seconds", 0, options)
|
|
755
|
+
elseif magnitude < 1e-3 then
|
|
756
|
+
local microsecond = assert(MetricUnitDefinitions.getUnit("microsecond"), "No microsecond")
|
|
757
|
+
return MetricUnitFormatUtils._durationUnitPhrase(microsecond, seconds / microsecond.factor, options)
|
|
758
|
+
end
|
|
759
|
+
|
|
760
|
+
local parts = MetricUnitFormatUtils._breakDuration(magnitude, options.maxParts or DEFAULT_DURATION_PARTS)
|
|
761
|
+
local pieces: { string } = {}
|
|
762
|
+
for _, part in parts do
|
|
763
|
+
local count = if #pieces == 0 and seconds < 0 then -part.count else part.count
|
|
764
|
+
table.insert(pieces, MetricUnitFormatUtils._durationPhrase(part.field, count, options))
|
|
765
|
+
end
|
|
766
|
+
return table.concat(pieces, options.separator or ", ")
|
|
767
|
+
end
|
|
768
|
+
|
|
769
|
+
function MetricUnitFormatUtils._durationUnitPhrase(
|
|
770
|
+
definition: UnitDefinition,
|
|
771
|
+
amount: number,
|
|
772
|
+
options: MetricUnitFormatOptions
|
|
773
|
+
): string
|
|
774
|
+
local timeLocalizationField = TIME_LOCALIZATION_FIELD_BY_UNIT_ID[definition.id]
|
|
775
|
+
if timeLocalizationField then
|
|
776
|
+
return MetricUnitFormatUtils._durationPhrase(timeLocalizationField, amount, options)
|
|
777
|
+
elseif definition.id == "microsecond" then
|
|
778
|
+
local hasLongNames = MetricUnitFallbackStrings.hasLocale(options.locale)
|
|
779
|
+
local display: UnitDisplay = if hasLongNames then "long" else "short"
|
|
780
|
+
return MetricUnitFormatUtils._unitPhrase(definition, amount, options, display)
|
|
781
|
+
end
|
|
782
|
+
error(string.format("MetricUnitFormatUtils: no duration word for %q", definition.id))
|
|
783
|
+
end
|
|
784
|
+
|
|
785
|
+
function MetricUnitFormatUtils._formatClock(seconds: number, options: MetricUnitFormatOptions): string
|
|
786
|
+
local template = options.durationTemplate or "h:*mm:ss"
|
|
787
|
+
local tokensOutsideBracketedLiterals = string.gsub(template, "%b[]", "")
|
|
788
|
+
if string.find(tokensOutsideBracketedLiterals, YEAR_OR_MONTH_CLOCK_TOKENS) then
|
|
789
|
+
error(
|
|
790
|
+
string.format(
|
|
791
|
+
'MetricUnitFormatUtils: durationTemplate %q counts years or months, which TimeDurationUtils sizes differently from MetricUnit; use durationStyle = "words"',
|
|
792
|
+
template
|
|
793
|
+
),
|
|
794
|
+
4
|
|
795
|
+
)
|
|
796
|
+
end
|
|
797
|
+
return TimeDurationUtils.format(seconds, template)
|
|
798
|
+
end
|
|
799
|
+
|
|
800
|
+
type DurationPart = { field: string, count: number }
|
|
801
|
+
|
|
802
|
+
function MetricUnitFormatUtils._breakDuration(seconds: number, maxParts: number): { DurationPart }
|
|
803
|
+
assert(maxParts >= 1, "Bad maxParts")
|
|
804
|
+
|
|
805
|
+
local roundedParts = MetricUnitFormatUtils._breakDurationRoundingSmallestPart(seconds, maxParts)
|
|
806
|
+
local roundedTotal = MetricUnitFormatUtils._totalSeconds(roundedParts)
|
|
807
|
+
if roundedTotal == 0 then
|
|
808
|
+
return roundedParts
|
|
809
|
+
end
|
|
810
|
+
|
|
811
|
+
local partsWithRoundingCarried = MetricUnitFormatUtils._breakDurationRoundingSmallestPart(roundedTotal, maxParts)
|
|
812
|
+
return partsWithRoundingCarried
|
|
813
|
+
end
|
|
814
|
+
|
|
815
|
+
function MetricUnitFormatUtils._totalSeconds(parts: { DurationPart }): number
|
|
816
|
+
local total = 0
|
|
817
|
+
for _, part in parts do
|
|
818
|
+
total += part.count * MetricUnitFormatUtils._durationScale(part.field)
|
|
819
|
+
end
|
|
820
|
+
return total
|
|
821
|
+
end
|
|
822
|
+
|
|
823
|
+
function MetricUnitFormatUtils._breakDurationRoundingSmallestPart(seconds: number, maxParts: number): { DurationPart }
|
|
824
|
+
local smallest = MetricUnitFormatUtils._smallestKeptUnitIndex(seconds, maxParts)
|
|
825
|
+
local counts = MetricUnitFormatUtils._countWholeUnitsRoundingTheSmallest(seconds, smallest)
|
|
826
|
+
local parts: { DurationPart } = {}
|
|
827
|
+
for index = 1, smallest do
|
|
828
|
+
if counts[index] ~= 0 then
|
|
829
|
+
table.insert(
|
|
830
|
+
parts,
|
|
831
|
+
{ field = MetricUnitFormatUtils._durationUnitsLargestFirst()[index].field, count = counts[index] }
|
|
832
|
+
)
|
|
833
|
+
end
|
|
834
|
+
end
|
|
835
|
+
if #parts == 0 then
|
|
836
|
+
table.insert(parts, { field = MetricUnitFormatUtils._durationUnitsLargestFirst()[smallest].field, count = 0 })
|
|
837
|
+
end
|
|
838
|
+
return parts
|
|
839
|
+
end
|
|
840
|
+
|
|
841
|
+
function MetricUnitFormatUtils._smallestKeptUnitIndex(seconds: number, maxParts: number): number
|
|
842
|
+
local exactCounts = MetricUnitFormatUtils._countWholeUnitsRoundingTheSmallest(
|
|
843
|
+
seconds,
|
|
844
|
+
#MetricUnitFormatUtils._durationUnitsLargestFirst()
|
|
845
|
+
)
|
|
846
|
+
|
|
847
|
+
local smallest = #MetricUnitFormatUtils._durationUnitsLargestFirst()
|
|
848
|
+
local nonZeroUnitsKept = 0
|
|
849
|
+
for index, count in exactCounts do
|
|
850
|
+
if count ~= 0 then
|
|
851
|
+
nonZeroUnitsKept += 1
|
|
852
|
+
smallest = index
|
|
853
|
+
if nonZeroUnitsKept >= maxParts then
|
|
854
|
+
break
|
|
855
|
+
end
|
|
856
|
+
end
|
|
857
|
+
end
|
|
858
|
+
return smallest
|
|
859
|
+
end
|
|
860
|
+
|
|
861
|
+
function MetricUnitFormatUtils._countWholeUnitsRoundingTheSmallest(seconds: number, smallest: number): { number }
|
|
862
|
+
local counts: { number } = {}
|
|
863
|
+
local remaining = seconds
|
|
864
|
+
for index, unit in MetricUnitFormatUtils._durationUnitsLargestFirst() do
|
|
865
|
+
local count = 0
|
|
866
|
+
if index < smallest then
|
|
867
|
+
count = math.floor(remaining / unit.factor + WHOLE_UNIT_TOLERANCE)
|
|
868
|
+
elseif index == smallest then
|
|
869
|
+
count = math.floor(remaining / unit.factor + 0.5)
|
|
870
|
+
end
|
|
871
|
+
remaining -= count * unit.factor
|
|
872
|
+
counts[index] = count
|
|
873
|
+
end
|
|
874
|
+
return counts
|
|
875
|
+
end
|
|
876
|
+
|
|
877
|
+
function MetricUnitFormatUtils._durationScale(field: string): number
|
|
878
|
+
for _, unit in MetricUnitFormatUtils._durationUnitsLargestFirst() do
|
|
879
|
+
if unit.field == field then
|
|
880
|
+
return unit.factor
|
|
881
|
+
end
|
|
882
|
+
end
|
|
883
|
+
error(string.format("No duration unit %q", field))
|
|
884
|
+
end
|
|
885
|
+
|
|
886
|
+
function MetricUnitFormatUtils._joinNumberToUnitWithNoBreakSpace(text: string, numberText: string): string
|
|
887
|
+
local start, finish = string.find(text, numberText .. " ", 1, true)
|
|
888
|
+
if start and finish then
|
|
889
|
+
return string.sub(text, 1, finish - 1) .. MetricUnitFormatUtils.NO_BREAK_SPACE .. string.sub(text, finish + 1)
|
|
890
|
+
end
|
|
891
|
+
return text
|
|
892
|
+
end
|
|
893
|
+
|
|
894
|
+
function MetricUnitFormatUtils._replaceFirst(text: string, plain: string, replacement: string): string
|
|
895
|
+
local start, finish = string.find(text, plain, 1, true)
|
|
896
|
+
if start and finish then
|
|
897
|
+
return string.sub(text, 1, start - 1) .. replacement .. string.sub(text, finish + 1)
|
|
898
|
+
end
|
|
899
|
+
return text
|
|
900
|
+
end
|
|
901
|
+
|
|
902
|
+
function MetricUnitFormatUtils._durationPhrase(field: string, amount: number, options: MetricUnitFormatOptions): string
|
|
903
|
+
local numberText, rounded, fraction = MetricUnitFormatUtils._formatNumber(amount, options)
|
|
904
|
+
|
|
905
|
+
local overrides = options.durationStrings
|
|
906
|
+
local phrase = if overrides then overrides[field] else nil
|
|
907
|
+
if phrase == nil then
|
|
908
|
+
phrase = (TimeLocalizationUtils.getDurationStringsForLocale(options.locale) :: any)[field]
|
|
909
|
+
end
|
|
910
|
+
assert(phrase, string.format("No duration phrase for %q", field))
|
|
911
|
+
|
|
912
|
+
if type(phrase) == "function" then
|
|
913
|
+
local textWithTostringNumber = phrase(rounded)
|
|
914
|
+
local text = MetricUnitFormatUtils._replaceFirst(textWithTostringNumber, tostring(rounded), numberText)
|
|
915
|
+
return MetricUnitFormatUtils._joinNumberToUnitWithNoBreakSpace(text, numberText)
|
|
916
|
+
end
|
|
917
|
+
|
|
918
|
+
local template = MetricUnitFormatUtils._pickPluralTemplate(phrase, options.locale, rounded, fraction)
|
|
919
|
+
return MetricUnitFormatUtils._joinNumberToUnitWithNoBreakSpace((string.gsub(template, "%%d", function()
|
|
920
|
+
return numberText
|
|
921
|
+
end)), numberText)
|
|
922
|
+
end
|
|
923
|
+
|
|
924
|
+
return table.freeze(MetricUnitFormatUtils)
|