@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,799 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--[=[
|
|
3
|
+
Reads measurements from text, for chat commands, admin tools, configs and text boxes. Returns plain
|
|
4
|
+
[MetricUnitData]; [MetricUnit.parse] wraps it in a [MetricUnit].
|
|
5
|
+
|
|
6
|
+
Accepts:
|
|
7
|
+
|
|
8
|
+
- A number and a unit, with or without a space: `"5 km"`, `"5km"`, `"2.5 kilometers"`, `"45%"`, `"20 °C"`,
|
|
9
|
+
`"20C"`, `"-3 °F"`, `"60 mph"`, `"1.5 MiB"`. A unit is any unit id, constructor name, symbol, English name
|
|
10
|
+
(American or Commonwealth spelling), a common alias such as `"kph"` or `"Mbps"`, or one of `@quenty/time`'s
|
|
11
|
+
names for a time unit.
|
|
12
|
+
- Several of one dimension, added up: `"1h30m"`, `"1 hour, 30 minutes"`, `"5 ft 3 in"`.
|
|
13
|
+
- A composed unit: `"5 m/s"`, `"1.6 MB/s"`, `"6 m·kg/s"`, `"9.81 m/s^2"`, `"5/d"`.
|
|
14
|
+
- Clock text: `"1:30"` is one minute thirty (`m:ss`) and `"1:30:00"` one hour thirty (`h:mm:ss`), as
|
|
15
|
+
[MetricUnitFormatUtils] writes them with `durationStyle = "clock"`.
|
|
16
|
+
- An ISO 8601 duration without years or months, such as `"PT1H30M"`.
|
|
17
|
+
|
|
18
|
+
Text such as `"m"` can name more than one unit: the metre, and `@quenty/time`'s minute. The rule is:
|
|
19
|
+
|
|
20
|
+
1. With an expected `kind`, the unit of that kind wins: `parse("5 m", "time")` is five minutes.
|
|
21
|
+
2. Otherwise, in text with several parts, the dimension every part can share wins: `"1m30s"` is minutes.
|
|
22
|
+
3. Otherwise the SI symbol wins over `@quenty/time`'s alias: `"5 m"` is five metres.
|
|
23
|
+
|
|
24
|
+
Symbols match case exactly (`"MB"` is a megabyte, `"Mb"` is not); names match without case (`"Kilometers"`).
|
|
25
|
+
Numbers use the locale's decimal mark when `options.locale` is given (`"1,5 km"` in `de-de`), else `.`.
|
|
26
|
+
No-break spaces read as spaces, so formatted text reads back.
|
|
27
|
+
|
|
28
|
+
@class MetricUnitParseUtils
|
|
29
|
+
]=]
|
|
30
|
+
|
|
31
|
+
local require = require(script.Parent.loader).load(script)
|
|
32
|
+
|
|
33
|
+
local MetricUnitDefinitions = require("MetricUnitDefinitions")
|
|
34
|
+
local MetricUnitDimensionUtils = require("MetricUnitDimensionUtils")
|
|
35
|
+
local MetricUnitDisplayUnitUtils = require("MetricUnitDisplayUnitUtils")
|
|
36
|
+
local MetricUnitFallbackStrings = require("MetricUnitFallbackStrings")
|
|
37
|
+
local MetricUnitFormatUtils = require("MetricUnitFormatUtils")
|
|
38
|
+
local MetricUnitKinds = require("MetricUnitKinds")
|
|
39
|
+
local MetricUnitSystem = require("MetricUnitSystem")
|
|
40
|
+
local TimeDurationUtils = require("TimeDurationUtils")
|
|
41
|
+
|
|
42
|
+
local MetricUnitParseUtils = {}
|
|
43
|
+
|
|
44
|
+
--[=[
|
|
45
|
+
Options for [MetricUnitParseUtils.parse].
|
|
46
|
+
|
|
47
|
+
@interface ParseOptions
|
|
48
|
+
.locale string? -- Reads numbers with this locale's decimal mark and grouping, defaults to `.` and `,`
|
|
49
|
+
.defaultUnit UnitLike? -- The unit of a bare number, such as `"meter"` for a field of metres
|
|
50
|
+
@within MetricUnitParseUtils
|
|
51
|
+
]=]
|
|
52
|
+
export type ParseOptions = {
|
|
53
|
+
locale: string?,
|
|
54
|
+
defaultUnit: MetricUnitDefinitions.UnitLike?,
|
|
55
|
+
_system: MetricUnitSystem.MetricUnitSystem?,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
type Dimensions = MetricUnitDimensionUtils.Dimensions
|
|
59
|
+
type UnitDefinition = MetricUnitDefinitions.UnitDefinition
|
|
60
|
+
type MetricUnitData = MetricUnitFormatUtils.MetricUnitData
|
|
61
|
+
|
|
62
|
+
type Expectation = { dimensions: Dimensions?, absolute: boolean? }
|
|
63
|
+
type MeasuredUnit = { factor: number, dimensions: Dimensions, zero: number? }
|
|
64
|
+
type Term = { number: number, unitText: string, units: { MeasuredUnit } }
|
|
65
|
+
|
|
66
|
+
local NO_BREAK_SPACES = { "\u{00A0}", "\u{202F}" }
|
|
67
|
+
local MINUS_SIGN = "\u{2212}"
|
|
68
|
+
local LOOKALIKE_SYMBOLS = table.freeze({
|
|
69
|
+
{ "\u{03BC}", "\u{00B5}" }, -- Greek mu, as CLDR writes micro, to the micro sign
|
|
70
|
+
{ "\u{2126}", "\u{03A9}" }, -- ohm sign to Greek omega
|
|
71
|
+
})
|
|
72
|
+
local CLOCK_WITH_HOURS = "^([-+]?)(%d+):(%d%d):(%d%d%.?%d*)$"
|
|
73
|
+
local CLOCK_WITHOUT_HOURS = "^([-+]?)(%d+):(%d%d%.?%d*)$"
|
|
74
|
+
local ISO_DURATION_START = "^[-+]?P[%dT]"
|
|
75
|
+
local ISO_DATE_PART = "^[-+]?P([^T]*)"
|
|
76
|
+
local COMPOSITION_MARKS = table.freeze({ "/", "·", "*", "^", "²", "³" })
|
|
77
|
+
local SUPERSCRIPT_POWERS: { [string]: number } = { ["²"] = 2, ["³"] = 3 }
|
|
78
|
+
local UNSPACED_SYMBOLS: { [string]: true } = { ["°"] = true, ["%"] = true }
|
|
79
|
+
|
|
80
|
+
local AliasPriority = table.freeze({
|
|
81
|
+
ID_SYMBOL_OR_NAME = 1,
|
|
82
|
+
UNIT_ALIAS = 2,
|
|
83
|
+
SYMBOL_ANOTHER_UNIT_HAD_FIRST = 3,
|
|
84
|
+
TIME_ALIAS = 4,
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
type UnitMatch = { unit: UnitDefinition, priority: number }
|
|
88
|
+
|
|
89
|
+
type UnitIndex = {
|
|
90
|
+
exactText: { [string]: { UnitMatch } },
|
|
91
|
+
lowercaseNames: { [string]: { UnitMatch } },
|
|
92
|
+
fallback: UnitIndex?,
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
local function addMatch(matches: { [string]: { UnitMatch } }, text: string, unit: UnitDefinition, priority: number)
|
|
96
|
+
local found = matches[text]
|
|
97
|
+
if not found then
|
|
98
|
+
found = {}
|
|
99
|
+
matches[text] = found
|
|
100
|
+
end
|
|
101
|
+
for _, match in found do
|
|
102
|
+
if match.unit.id == unit.id then
|
|
103
|
+
match.priority = math.min(match.priority, priority)
|
|
104
|
+
return
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
table.insert(found, { unit = unit, priority = priority })
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
local function addSymbol(index: UnitIndex, symbol: string, unit: UnitDefinition, priority: number)
|
|
111
|
+
addMatch(index.exactText, symbol, unit, priority)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
local function addName(index: UnitIndex, name: string, unit: UnitDefinition, priority: number)
|
|
115
|
+
addMatch(index.exactText, name, unit, priority)
|
|
116
|
+
addMatch(index.lowercaseNames, string.lower(name), unit, priority)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
local function buildIndex(units: { UnitDefinition }, fallback: UnitIndex?): UnitIndex
|
|
120
|
+
local index: UnitIndex = { exactText = {}, lowercaseNames = {}, fallback = fallback }
|
|
121
|
+
for _, unit in units do
|
|
122
|
+
local isBareRate = unit.per ~= nil
|
|
123
|
+
|
|
124
|
+
addName(index, unit.id, unit, AliasPriority.ID_SYMBOL_OR_NAME)
|
|
125
|
+
if not isBareRate then
|
|
126
|
+
local symbolPriority = if index.exactText[unit.symbol]
|
|
127
|
+
then AliasPriority.SYMBOL_ANOTHER_UNIT_HAD_FIRST
|
|
128
|
+
else AliasPriority.ID_SYMBOL_OR_NAME
|
|
129
|
+
addSymbol(index, unit.symbol, unit, symbolPriority)
|
|
130
|
+
end
|
|
131
|
+
if unit.names then
|
|
132
|
+
for _, name in { unit.names.one, unit.names.other } do
|
|
133
|
+
addName(index, name, unit, AliasPriority.ID_SYMBOL_OR_NAME)
|
|
134
|
+
addName(
|
|
135
|
+
index,
|
|
136
|
+
MetricUnitFallbackStrings.toCommonwealthSpelling(name),
|
|
137
|
+
unit,
|
|
138
|
+
AliasPriority.ID_SYMBOL_OR_NAME
|
|
139
|
+
)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
local aliases: { string } = unit.aliases or {}
|
|
143
|
+
for _, alias in aliases do
|
|
144
|
+
addName(index, alias, unit, AliasPriority.UNIT_ALIAS)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
return index
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
type PrefixSpelling = { prefix: MetricUnitDefinitions.Prefix, text: string, isName: boolean }
|
|
151
|
+
|
|
152
|
+
local wellKnownUnits: UnitIndex? = nil
|
|
153
|
+
local prefixSpellingsLongestFirst: { PrefixSpelling }? = nil
|
|
154
|
+
|
|
155
|
+
local INDEX_BY_SYSTEM: { [MetricUnitSystem.MetricUnitSystem]: UnitIndex } = setmetatable({} :: any, { __mode = "k" })
|
|
156
|
+
|
|
157
|
+
local function trim(text: string): string
|
|
158
|
+
return string.match(text, "^%s*(.-)%s*$") :: string
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
--[=[
|
|
162
|
+
Reads `text` as a measurement. Returns the measurement as plain data, or nil and a message suitable to show a
|
|
163
|
+
player. `kind` is what the caller expects, such as `"length"`; it settles ambiguous units and turns a mismatch or
|
|
164
|
+
a bare number into a helpful message.
|
|
165
|
+
|
|
166
|
+
```lua
|
|
167
|
+
MetricUnitParseUtils.parse("5 km") -- { amount = 5000, dimensions = { length = 1 } }
|
|
168
|
+
MetricUnitParseUtils.parse("5", "length") -- nil, 'Add a unit, such as "5 m"'
|
|
169
|
+
MetricUnitParseUtils.parse("5", "length", { defaultUnit = "meter" }) -- { amount = 5, dimensions = { length = 1 } }
|
|
170
|
+
MetricUnitParseUtils.parse("20 C") -- an absolute temperature
|
|
171
|
+
MetricUnitParseUtils.parse("20 C", "temperatureDifference") -- a difference of 20 K
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
@param text string
|
|
175
|
+
@param kind (KindName | Dimensions)?
|
|
176
|
+
@param options ParseOptions?
|
|
177
|
+
@return MetricUnitData?
|
|
178
|
+
@return string?
|
|
179
|
+
]=]
|
|
180
|
+
function MetricUnitParseUtils.parse(
|
|
181
|
+
text: string,
|
|
182
|
+
kind: (MetricUnitKinds.KindName | Dimensions)?,
|
|
183
|
+
options: ParseOptions?
|
|
184
|
+
): (MetricUnitData?, string?)
|
|
185
|
+
if type(text) ~= "string" then
|
|
186
|
+
return nil, "Enter a value"
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
local expected: Expectation = {}
|
|
190
|
+
if kind ~= nil then
|
|
191
|
+
local dimensions, absolute = MetricUnitKinds.fromName(kind)
|
|
192
|
+
expected = { dimensions = dimensions, absolute = absolute }
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
local normalized = MetricUnitParseUtils._normalizeText(text)
|
|
196
|
+
if normalized == "" then
|
|
197
|
+
return nil, "Enter a value"
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
local data, err = MetricUnitParseUtils._readText(normalized, expected, options or {})
|
|
201
|
+
if not data then
|
|
202
|
+
return nil, err
|
|
203
|
+
end
|
|
204
|
+
return MetricUnitParseUtils._checkKind(data, expected, normalized)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
function MetricUnitParseUtils._parseInSystem(
|
|
208
|
+
text: string,
|
|
209
|
+
kind: (MetricUnitKinds.KindName | Dimensions)?,
|
|
210
|
+
options: ParseOptions?,
|
|
211
|
+
system: MetricUnitSystem.MetricUnitSystem?
|
|
212
|
+
): (MetricUnitData?, string?)
|
|
213
|
+
local resolved: ParseOptions = if options then table.clone(options) else {}
|
|
214
|
+
resolved._system = system
|
|
215
|
+
return MetricUnitParseUtils.parse(text, kind, resolved)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
function MetricUnitParseUtils._normalizeText(text: string): string
|
|
219
|
+
local normalized = text
|
|
220
|
+
for _, space in NO_BREAK_SPACES do
|
|
221
|
+
normalized = string.gsub(normalized, space, " ")
|
|
222
|
+
end
|
|
223
|
+
for _, pair in LOOKALIKE_SYMBOLS do
|
|
224
|
+
normalized = string.gsub(normalized, pair[1], pair[2])
|
|
225
|
+
end
|
|
226
|
+
normalized = trim(normalized)
|
|
227
|
+
return (string.gsub(normalized, MINUS_SIGN, "-"))
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
function MetricUnitParseUtils._readText(
|
|
231
|
+
text: string,
|
|
232
|
+
expected: Expectation,
|
|
233
|
+
options: ParseOptions
|
|
234
|
+
): (MetricUnitData?, string?)
|
|
235
|
+
local clockSeconds = MetricUnitParseUtils._readClockSeconds(text)
|
|
236
|
+
if clockSeconds then
|
|
237
|
+
return MetricUnitParseUtils._durationData(clockSeconds), nil
|
|
238
|
+
elseif string.match(text, ISO_DURATION_START) then
|
|
239
|
+
return MetricUnitParseUtils._readIsoDuration(text)
|
|
240
|
+
else
|
|
241
|
+
return MetricUnitParseUtils._readMeasurement(text, expected, options)
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
function MetricUnitParseUtils._readClockSeconds(text: string): number?
|
|
246
|
+
local sign, hours, minutes, seconds = string.match(text, CLOCK_WITH_HOURS)
|
|
247
|
+
if not hours then
|
|
248
|
+
sign, minutes, seconds = string.match(text, CLOCK_WITHOUT_HOURS)
|
|
249
|
+
end
|
|
250
|
+
if not minutes then
|
|
251
|
+
return nil
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
local total = (tonumber(hours) or 0) * 3600 + (tonumber(minutes) :: number) * 60 + (tonumber(seconds) :: number)
|
|
255
|
+
return if sign == "-" then -total else total
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
function MetricUnitParseUtils._readIsoDuration(text: string): (MetricUnitData?, string?)
|
|
259
|
+
local datePart = string.match(text, ISO_DATE_PART) or ""
|
|
260
|
+
if string.find(datePart, "[YM]") then
|
|
261
|
+
return nil, 'ISO years and months are calendar spans; write them as "1 year" or "2 months"'
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
local ok, seconds = pcall(TimeDurationUtils.toSeconds, text)
|
|
265
|
+
if not ok then
|
|
266
|
+
return nil, string.format("Could not read the duration %q", text)
|
|
267
|
+
end
|
|
268
|
+
return MetricUnitParseUtils._durationData(seconds :: number), nil
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
function MetricUnitParseUtils._durationData(seconds: number): MetricUnitData
|
|
272
|
+
return { amount = seconds, dimensions = { time = 1 } }
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
function MetricUnitParseUtils._readMeasurement(
|
|
276
|
+
text: string,
|
|
277
|
+
expected: Expectation,
|
|
278
|
+
options: ParseOptions
|
|
279
|
+
): (MetricUnitData?, string?)
|
|
280
|
+
local decimalMark = if options.locale then MetricUnitFormatUtils.getDecimalSeparator(options.locale) else "."
|
|
281
|
+
local index = MetricUnitParseUtils._indexFor(options._system)
|
|
282
|
+
local terms, err = MetricUnitParseUtils._splitTerms(text, decimalMark, index)
|
|
283
|
+
if not terms then
|
|
284
|
+
return nil, err
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
local isBareNumber = #terms == 1 and terms[1].unitText == ""
|
|
288
|
+
if isBareNumber then
|
|
289
|
+
return MetricUnitParseUtils._readBareNumber(terms[1].number, expected, options)
|
|
290
|
+
end
|
|
291
|
+
return MetricUnitParseUtils._combineTerms(terms, text, expected)
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
function MetricUnitParseUtils._splitTerms(text: string, decimalMark: string, index: UnitIndex): ({ Term }?, string?)
|
|
295
|
+
local terms: { Term } = {}
|
|
296
|
+
local position = 1
|
|
297
|
+
|
|
298
|
+
while position <= #text do
|
|
299
|
+
local number, afterNumber = MetricUnitParseUtils._readNumber(text, position, decimalMark)
|
|
300
|
+
local startsWithBareRate = #terms == 0 and string.sub(text, position, position) == "/"
|
|
301
|
+
if number == nil and startsWithBareRate then
|
|
302
|
+
number = 1
|
|
303
|
+
end
|
|
304
|
+
if number == nil then
|
|
305
|
+
return nil, string.format("Could not read %q", text)
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
local unitText, afterUnit = MetricUnitParseUtils._readUnitText(text, afterNumber, index)
|
|
309
|
+
local units = MetricUnitParseUtils._unitsNamedBy(unitText, index)
|
|
310
|
+
local isUnknownUnit = unitText ~= "" and #units == 0
|
|
311
|
+
if isUnknownUnit then
|
|
312
|
+
return nil, string.format("Unknown unit %q", unitText)
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
table.insert(terms, { number = number, unitText = unitText, units = units })
|
|
316
|
+
position = MetricUnitParseUtils._skipTermSeparator(text, afterUnit)
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
return terms, nil
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
function MetricUnitParseUtils._readNumber(text: string, position: number, decimalMark: string): (number?, number)
|
|
323
|
+
local sign = string.match(text, "^[-+]", position)
|
|
324
|
+
local afterSign = if sign then position + 1 else position
|
|
325
|
+
|
|
326
|
+
local groupMarks = MetricUnitParseUtils._groupMarkPatterns(decimalMark)
|
|
327
|
+
local integerDigits, afterInteger = MetricUnitParseUtils._readGroupedDigits(text, afterSign, groupMarks)
|
|
328
|
+
local fractionDigits, afterFraction = MetricUnitParseUtils._readFraction(text, afterInteger, decimalMark)
|
|
329
|
+
if integerDigits == "" and fractionDigits == "" then
|
|
330
|
+
return nil, position
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
local value = tonumber(
|
|
334
|
+
(if integerDigits == "" then "0" else integerDigits)
|
|
335
|
+
.. "."
|
|
336
|
+
.. (if fractionDigits == "" then "0" else fractionDigits)
|
|
337
|
+
)
|
|
338
|
+
assert(value, "Bad number")
|
|
339
|
+
return if sign == "-" then -value else value, afterFraction
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
function MetricUnitParseUtils._groupMarkPatterns(decimalMark: string): { string }
|
|
343
|
+
return if decimalMark == "," then { "%.", " " } else { "," }
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
function MetricUnitParseUtils._readGroupedDigits(
|
|
347
|
+
text: string,
|
|
348
|
+
position: number,
|
|
349
|
+
groupMarks: { string }
|
|
350
|
+
): (string, number)
|
|
351
|
+
local digits = string.match(text, "^%d+", position)
|
|
352
|
+
if not digits then
|
|
353
|
+
return "", position
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
local index = position + #digits
|
|
357
|
+
while true do
|
|
358
|
+
local groupDigits, afterGroup = MetricUnitParseUtils._readDigitGroup(text, index, groupMarks)
|
|
359
|
+
if not groupDigits then
|
|
360
|
+
break
|
|
361
|
+
end
|
|
362
|
+
digits ..= groupDigits
|
|
363
|
+
index = afterGroup
|
|
364
|
+
end
|
|
365
|
+
return digits, index
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
function MetricUnitParseUtils._readDigitGroup(text: string, position: number, groupMarks: { string }): (string?, number)
|
|
369
|
+
for _, mark in groupMarks do
|
|
370
|
+
local group, groupDigits = string.match(text, "^(" .. mark .. "(%d%d%d))", position)
|
|
371
|
+
local afterGroup = position + #(group or "")
|
|
372
|
+
local isExactlyThreeDigits = group ~= nil and not string.match(text, "^%d", afterGroup)
|
|
373
|
+
if isExactlyThreeDigits then
|
|
374
|
+
return groupDigits, afterGroup
|
|
375
|
+
end
|
|
376
|
+
end
|
|
377
|
+
return nil, position
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
function MetricUnitParseUtils._readFraction(text: string, position: number, decimalMark: string): (string, number)
|
|
381
|
+
local afterMark = position + #decimalMark
|
|
382
|
+
if string.sub(text, position, afterMark - 1) ~= decimalMark then
|
|
383
|
+
return "", position
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
local digits = string.match(text, "^%d+", afterMark)
|
|
387
|
+
if not digits then
|
|
388
|
+
return "", position
|
|
389
|
+
end
|
|
390
|
+
return digits, afterMark + #digits
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
function MetricUnitParseUtils._readUnitText(text: string, position: number, index: UnitIndex): (string, number)
|
|
394
|
+
local unitStart = position + #(string.match(text, "^%s*", position) :: string)
|
|
395
|
+
local unitText = string.sub(text, unitStart, MetricUnitParseUtils._findNextTermSeparator(text, unitStart) - 1)
|
|
396
|
+
if #MetricUnitParseUtils._unitsNamedBy(unitText, index) == 0 then
|
|
397
|
+
unitText = MetricUnitParseUtils._unitBeforeGluedNumber(unitText, index) or unitText
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
local afterUnit = unitStart + #unitText
|
|
401
|
+
return (string.gsub(unitText, "[%s,]+$", "")), afterUnit
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
function MetricUnitParseUtils._findNextTermSeparator(text: string, position: number): number
|
|
405
|
+
local endOfText = #text + 1
|
|
406
|
+
local beforeNumber = string.find(text, "[%s,]+[-+]?%d", position) or endOfText
|
|
407
|
+
local beforeAndNumber = string.find(text, "%s+and%s+[-+]?%d", position) or endOfText
|
|
408
|
+
return math.min(beforeNumber, beforeAndNumber)
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
function MetricUnitParseUtils._unitBeforeGluedNumber(unitText: string, index: UnitIndex): string?
|
|
412
|
+
local letters = string.match(unitText, "^([^%d%s]+)%d")
|
|
413
|
+
if letters and #MetricUnitParseUtils._findUnitsBestFirst(letters, index) > 0 then
|
|
414
|
+
return letters
|
|
415
|
+
end
|
|
416
|
+
return nil
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
function MetricUnitParseUtils._skipTermSeparator(text: string, position: number): number
|
|
420
|
+
local separator = string.match(text, "^[%s,]*and%s+", position) or string.match(text, "^[%s,]*", position)
|
|
421
|
+
return position + #(separator :: string)
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
function MetricUnitParseUtils._unitsNamedBy(unitText: string, index: UnitIndex): { MeasuredUnit }
|
|
425
|
+
local units: { MeasuredUnit } = {}
|
|
426
|
+
for _, unit in MetricUnitParseUtils._findUnitsBestFirst(unitText, index) do
|
|
427
|
+
table.insert(units, unit)
|
|
428
|
+
end
|
|
429
|
+
if #units > 0 then
|
|
430
|
+
return units
|
|
431
|
+
end
|
|
432
|
+
local composed = MetricUnitParseUtils._composeUnit(unitText, index)
|
|
433
|
+
return if composed then { composed } else {}
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
function MetricUnitParseUtils._wellKnownIndex(): UnitIndex
|
|
437
|
+
local found = wellKnownUnits
|
|
438
|
+
if found then
|
|
439
|
+
return found
|
|
440
|
+
end
|
|
441
|
+
local index = buildIndex(MetricUnitDefinitions.getDeclaredUnits(), nil)
|
|
442
|
+
for timeAlias, id in MetricUnitDefinitions.getTimeAliases() do
|
|
443
|
+
addSymbol(index, timeAlias, assert(MetricUnitDefinitions.getUnit(id), id), AliasPriority.TIME_ALIAS)
|
|
444
|
+
end
|
|
445
|
+
wellKnownUnits = index
|
|
446
|
+
return index
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
function MetricUnitParseUtils._indexFor(system: MetricUnitSystem.MetricUnitSystem?): UnitIndex
|
|
450
|
+
if not system then
|
|
451
|
+
return MetricUnitParseUtils._wellKnownIndex()
|
|
452
|
+
end
|
|
453
|
+
local index = INDEX_BY_SYSTEM[system]
|
|
454
|
+
if not index then
|
|
455
|
+
index = buildIndex(system:GetUnits(), MetricUnitParseUtils._indexFor(system:GetFallback()))
|
|
456
|
+
INDEX_BY_SYSTEM[system] = index
|
|
457
|
+
end
|
|
458
|
+
return index
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
function MetricUnitParseUtils._findUnitsBestFirst(text: string, index: UnitIndex): { UnitDefinition }
|
|
462
|
+
local spelledExactly = MetricUnitParseUtils._findUnitsNamed(text, index, true)
|
|
463
|
+
if #spelledExactly > 0 then
|
|
464
|
+
return spelledExactly
|
|
465
|
+
end
|
|
466
|
+
local prefixed = MetricUnitParseUtils._findPrefixedUnits(text, index)
|
|
467
|
+
if #prefixed > 0 then
|
|
468
|
+
return prefixed
|
|
469
|
+
end
|
|
470
|
+
return MetricUnitParseUtils._findUnitsNamed(text, index, false)
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
function MetricUnitParseUtils._findPrefixedUnits(text: string, index: UnitIndex): { UnitDefinition }
|
|
474
|
+
local lowercaseText = string.lower(text)
|
|
475
|
+
for _, spelling in MetricUnitParseUtils._prefixSpellingsLongestFirst() do
|
|
476
|
+
local start = if spelling.isName
|
|
477
|
+
then string.sub(lowercaseText, 1, #spelling.text)
|
|
478
|
+
else string.sub(text, 1, #spelling.text)
|
|
479
|
+
local rest = string.sub(text, #spelling.text + 1)
|
|
480
|
+
if start == spelling.text and rest ~= "" then
|
|
481
|
+
for _, base in MetricUnitParseUtils._findUnitsNamed(rest, index, false) do
|
|
482
|
+
local unit = MetricUnitDefinitions.getPrefixedUnit(spelling.prefix, base)
|
|
483
|
+
local isSpelledAsSiWritesIt = not spelling.isName
|
|
484
|
+
or (unit ~= nil and unit.id == spelling.text .. base.id)
|
|
485
|
+
if unit and isSpelledAsSiWritesIt then
|
|
486
|
+
return { unit }
|
|
487
|
+
end
|
|
488
|
+
end
|
|
489
|
+
end
|
|
490
|
+
end
|
|
491
|
+
return {}
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
function MetricUnitParseUtils._prefixSpellingsLongestFirst(): { PrefixSpelling }
|
|
495
|
+
local found = prefixSpellingsLongestFirst
|
|
496
|
+
if found then
|
|
497
|
+
return found
|
|
498
|
+
end
|
|
499
|
+
local spellings: { PrefixSpelling } = {}
|
|
500
|
+
for _, prefix in MetricUnitDefinitions.getPrefixes() do
|
|
501
|
+
table.insert(spellings, { prefix = prefix, text = prefix.symbol, isName = false })
|
|
502
|
+
table.insert(spellings, { prefix = prefix, text = prefix.name, isName = true })
|
|
503
|
+
local lastLetter = string.sub(prefix.name, -1)
|
|
504
|
+
if lastLetter == "a" or lastLetter == "o" then
|
|
505
|
+
table.insert(spellings, { prefix = prefix, text = string.sub(prefix.name, 1, -2), isName = true })
|
|
506
|
+
end
|
|
507
|
+
if prefix.name == "micro" then
|
|
508
|
+
table.insert(spellings, { prefix = prefix, text = "u", isName = false })
|
|
509
|
+
end
|
|
510
|
+
end
|
|
511
|
+
table.sort(spellings, function(a, b)
|
|
512
|
+
return #a.text > #b.text
|
|
513
|
+
end)
|
|
514
|
+
prefixSpellingsLongestFirst = spellings
|
|
515
|
+
return spellings
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
function MetricUnitParseUtils._findUnitsNamed(text: string, index: UnitIndex, matchCase: boolean): { UnitDefinition }
|
|
519
|
+
local lowercaseText = string.lower(text)
|
|
520
|
+
local matches: { UnitMatch }? = nil
|
|
521
|
+
local searched: UnitIndex? = index
|
|
522
|
+
while searched and not matches do
|
|
523
|
+
matches = searched.exactText[text] or (if matchCase then nil else searched.lowercaseNames[lowercaseText])
|
|
524
|
+
searched = searched.fallback
|
|
525
|
+
end
|
|
526
|
+
if not matches then
|
|
527
|
+
return {}
|
|
528
|
+
end
|
|
529
|
+
|
|
530
|
+
local ranked = table.clone(matches)
|
|
531
|
+
table.sort(ranked, function(a, b)
|
|
532
|
+
return a.priority < b.priority
|
|
533
|
+
end)
|
|
534
|
+
|
|
535
|
+
local units: { UnitDefinition } = {}
|
|
536
|
+
for _, match in ranked do
|
|
537
|
+
table.insert(units, match.unit)
|
|
538
|
+
end
|
|
539
|
+
return units
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
function MetricUnitParseUtils._hasCompositionMark(unitText: string): boolean
|
|
543
|
+
for _, mark in COMPOSITION_MARKS do
|
|
544
|
+
if string.find(unitText, mark, 1, true) then
|
|
545
|
+
return true
|
|
546
|
+
end
|
|
547
|
+
end
|
|
548
|
+
return false
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
function MetricUnitParseUtils._splitFactors(text: string): { string }
|
|
552
|
+
local factors: { string } = {}
|
|
553
|
+
for _, factorText in string.split((string.gsub(text, "·", "*")), "*") do
|
|
554
|
+
local trimmed = trim(factorText)
|
|
555
|
+
if trimmed ~= "" then
|
|
556
|
+
table.insert(factors, trimmed)
|
|
557
|
+
end
|
|
558
|
+
end
|
|
559
|
+
return factors
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
function MetricUnitParseUtils._composeUnit(unitText: string, index: UnitIndex): MeasuredUnit?
|
|
563
|
+
if not MetricUnitParseUtils._hasCompositionMark(unitText) then
|
|
564
|
+
return nil
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
local factor = 1
|
|
568
|
+
local dimensions: Dimensions = MetricUnitDimensionUtils.NONE
|
|
569
|
+
for position, part in string.split(unitText, "/") do
|
|
570
|
+
local isNumerator = position == 1
|
|
571
|
+
local exponentSign = if isNumerator then 1 else -1
|
|
572
|
+
local trimmedPart = trim(part)
|
|
573
|
+
if trimmedPart == "" then
|
|
574
|
+
if isNumerator then
|
|
575
|
+
continue
|
|
576
|
+
end
|
|
577
|
+
return nil
|
|
578
|
+
end
|
|
579
|
+
|
|
580
|
+
for _, factorText in MetricUnitParseUtils._splitFactors(trimmedPart) do
|
|
581
|
+
local unit, power = MetricUnitParseUtils._readUnitPower(factorText, index)
|
|
582
|
+
if not unit then
|
|
583
|
+
return nil
|
|
584
|
+
end
|
|
585
|
+
factor *= unit.factor ^ (exponentSign * power)
|
|
586
|
+
dimensions = MetricUnitDimensionUtils.combine(
|
|
587
|
+
dimensions,
|
|
588
|
+
MetricUnitDimensionUtils.scale(unit.dimensions, power),
|
|
589
|
+
exponentSign
|
|
590
|
+
)
|
|
591
|
+
end
|
|
592
|
+
end
|
|
593
|
+
return { factor = factor, dimensions = dimensions }
|
|
594
|
+
end
|
|
595
|
+
|
|
596
|
+
function MetricUnitParseUtils._readUnitPower(factorText: string, index: UnitIndex): (UnitDefinition?, number)
|
|
597
|
+
local unitText, power = MetricUnitParseUtils._splitPower(factorText)
|
|
598
|
+
local unit = MetricUnitParseUtils._findUnitsBestFirst(unitText, index)[1]
|
|
599
|
+
if unit and unit.per == nil then
|
|
600
|
+
return unit, power
|
|
601
|
+
end
|
|
602
|
+
return nil, power
|
|
603
|
+
end
|
|
604
|
+
|
|
605
|
+
function MetricUnitParseUtils._splitPower(factorText: string): (string, number)
|
|
606
|
+
for superscript, power in SUPERSCRIPT_POWERS do
|
|
607
|
+
if string.sub(factorText, -#superscript) == superscript then
|
|
608
|
+
return string.sub(factorText, 1, -#superscript - 1), power
|
|
609
|
+
end
|
|
610
|
+
end
|
|
611
|
+
local unitText, power = string.match(factorText, "^(.-)%^(%-?%d+)$")
|
|
612
|
+
if unitText then
|
|
613
|
+
return unitText, tonumber(power) :: number
|
|
614
|
+
end
|
|
615
|
+
return factorText, 1
|
|
616
|
+
end
|
|
617
|
+
|
|
618
|
+
function MetricUnitParseUtils._readBareNumber(
|
|
619
|
+
number: number,
|
|
620
|
+
expected: Expectation,
|
|
621
|
+
options: ParseOptions
|
|
622
|
+
): (MetricUnitData?, string?)
|
|
623
|
+
if options.defaultUnit then
|
|
624
|
+
local system = options._system
|
|
625
|
+
local unit = if system
|
|
626
|
+
then system:GetUnit(options.defaultUnit)
|
|
627
|
+
else MetricUnitDefinitions.getUnit(options.defaultUnit)
|
|
628
|
+
assert(unit, string.format("Bad defaultUnit %q", tostring(options.defaultUnit)))
|
|
629
|
+
return MetricUnitParseUtils._unitData(number, unit, expected.absolute), nil
|
|
630
|
+
end
|
|
631
|
+
|
|
632
|
+
local expectedDimensions = expected.dimensions
|
|
633
|
+
if expectedDimensions and next(expectedDimensions) ~= nil then
|
|
634
|
+
return nil, MetricUnitParseUtils._addUnitMessage(number, expectedDimensions)
|
|
635
|
+
end
|
|
636
|
+
return { amount = number, dimensions = {} }, nil
|
|
637
|
+
end
|
|
638
|
+
|
|
639
|
+
function MetricUnitParseUtils._combineTerms(
|
|
640
|
+
terms: { Term },
|
|
641
|
+
text: string,
|
|
642
|
+
expected: Expectation
|
|
643
|
+
): (MetricUnitData?, string?)
|
|
644
|
+
local dimensions = MetricUnitParseUtils._chooseDimensions(terms, expected.dimensions)
|
|
645
|
+
if #terms == 1 then
|
|
646
|
+
local unit, err = MetricUnitParseUtils._pickUnit(terms[1], dimensions, text)
|
|
647
|
+
if not unit then
|
|
648
|
+
return nil, err
|
|
649
|
+
end
|
|
650
|
+
return MetricUnitParseUtils._unitData(terms[1].number, unit, expected.absolute), nil
|
|
651
|
+
end
|
|
652
|
+
return MetricUnitParseUtils._sumTerms(terms, dimensions, text, expected)
|
|
653
|
+
end
|
|
654
|
+
|
|
655
|
+
function MetricUnitParseUtils._chooseDimensions(terms: { Term }, expectedDimensions: Dimensions?): Dimensions?
|
|
656
|
+
return expectedDimensions
|
|
657
|
+
or MetricUnitParseUtils._dimensionsEveryTermShares(terms)
|
|
658
|
+
or MetricUnitParseUtils._bestDimensionsOf(terms[1])
|
|
659
|
+
end
|
|
660
|
+
|
|
661
|
+
function MetricUnitParseUtils._dimensionsEveryTermShares(terms: { Term }): Dimensions?
|
|
662
|
+
for _, unit in terms[1].units do
|
|
663
|
+
if MetricUnitParseUtils._everyTermCanBe(terms, unit.dimensions) then
|
|
664
|
+
return unit.dimensions
|
|
665
|
+
end
|
|
666
|
+
end
|
|
667
|
+
return nil
|
|
668
|
+
end
|
|
669
|
+
|
|
670
|
+
function MetricUnitParseUtils._everyTermCanBe(terms: { Term }, dimensions: Dimensions): boolean
|
|
671
|
+
for _, term in terms do
|
|
672
|
+
if not MetricUnitParseUtils._termCanBe(term, dimensions) then
|
|
673
|
+
return false
|
|
674
|
+
end
|
|
675
|
+
end
|
|
676
|
+
return true
|
|
677
|
+
end
|
|
678
|
+
|
|
679
|
+
function MetricUnitParseUtils._termCanBe(term: Term, dimensions: Dimensions): boolean
|
|
680
|
+
for _, unit in term.units do
|
|
681
|
+
if MetricUnitDimensionUtils.equal(unit.dimensions, dimensions) then
|
|
682
|
+
return true
|
|
683
|
+
end
|
|
684
|
+
end
|
|
685
|
+
return false
|
|
686
|
+
end
|
|
687
|
+
|
|
688
|
+
function MetricUnitParseUtils._bestDimensionsOf(term: Term): Dimensions?
|
|
689
|
+
local best = term.units[1]
|
|
690
|
+
return if best then best.dimensions else nil
|
|
691
|
+
end
|
|
692
|
+
|
|
693
|
+
function MetricUnitParseUtils._pickUnit(term: Term, dimensions: Dimensions?, text: string): (MeasuredUnit?, string?)
|
|
694
|
+
for _, unit in term.units do
|
|
695
|
+
if dimensions == nil or MetricUnitDimensionUtils.equal(unit.dimensions, dimensions) then
|
|
696
|
+
return unit, nil
|
|
697
|
+
end
|
|
698
|
+
end
|
|
699
|
+
return nil, MetricUnitParseUtils._mismatchMessage(term.units[1].dimensions, dimensions :: Dimensions, text)
|
|
700
|
+
end
|
|
701
|
+
|
|
702
|
+
function MetricUnitParseUtils._sumTerms(
|
|
703
|
+
terms: { Term },
|
|
704
|
+
dimensions: Dimensions?,
|
|
705
|
+
text: string,
|
|
706
|
+
expected: Expectation
|
|
707
|
+
): (MetricUnitData?, string?)
|
|
708
|
+
local amount = 0
|
|
709
|
+
local sumDimensions: Dimensions? = nil
|
|
710
|
+
local signOfTheWhole = if terms[1].number < 0 then -1 else 1
|
|
711
|
+
for _, term in terms do
|
|
712
|
+
if term.unitText == "" then
|
|
713
|
+
return nil, string.format("Add a unit to every number in %q", text)
|
|
714
|
+
end
|
|
715
|
+
local unit, err = MetricUnitParseUtils._pickUnit(term, dimensions, text)
|
|
716
|
+
if not unit then
|
|
717
|
+
return nil, err
|
|
718
|
+
end
|
|
719
|
+
local readingsArePoints = unit.zero ~= nil
|
|
720
|
+
if readingsArePoints then
|
|
721
|
+
return nil, string.format("Write a temperature as one number and unit, not %q", text)
|
|
722
|
+
end
|
|
723
|
+
amount += signOfTheWhole * math.abs(term.number) * unit.factor
|
|
724
|
+
sumDimensions = unit.dimensions
|
|
725
|
+
end
|
|
726
|
+
|
|
727
|
+
return {
|
|
728
|
+
amount = amount,
|
|
729
|
+
dimensions = table.clone(sumDimensions :: Dimensions),
|
|
730
|
+
absolute = if expected.absolute then true else nil,
|
|
731
|
+
},
|
|
732
|
+
nil
|
|
733
|
+
end
|
|
734
|
+
|
|
735
|
+
function MetricUnitParseUtils._unitData(number: number, unit: MeasuredUnit, expectedAbsolute: boolean?): MetricUnitData
|
|
736
|
+
local dimensions = table.clone(unit.dimensions)
|
|
737
|
+
local zero = unit.zero
|
|
738
|
+
local isDifferenceExpected = expectedAbsolute == false
|
|
739
|
+
if zero ~= nil and not isDifferenceExpected then
|
|
740
|
+
return { amount = number * unit.factor + zero, dimensions = dimensions, absolute = true }
|
|
741
|
+
end
|
|
742
|
+
return { amount = number * unit.factor, dimensions = dimensions, absolute = if expectedAbsolute then true else nil }
|
|
743
|
+
end
|
|
744
|
+
|
|
745
|
+
function MetricUnitParseUtils._addUnitMessage(number: number, dimensions: Dimensions): string
|
|
746
|
+
local unit = MetricUnitParseUtils._suggestedUnit(dimensions)
|
|
747
|
+
if not unit then
|
|
748
|
+
return string.format("Add a unit of %s", MetricUnitDimensionUtils.describe(dimensions))
|
|
749
|
+
end
|
|
750
|
+
local example = if UNSPACED_SYMBOLS[unit.symbol] then unit.symbol else " " .. unit.symbol
|
|
751
|
+
return string.format('Add a unit, such as "%s%s"', MetricUnitFormatUtils.formatNumber(number), example)
|
|
752
|
+
end
|
|
753
|
+
|
|
754
|
+
function MetricUnitParseUtils._suggestedUnit(dimensions: Dimensions): UnitDefinition?
|
|
755
|
+
local displayUnits = MetricUnitDisplayUnitUtils.getDisplayUnits(dimensions)
|
|
756
|
+
if displayUnits then
|
|
757
|
+
return MetricUnitDisplayUnitUtils.getBaseUnit(displayUnits)
|
|
758
|
+
end
|
|
759
|
+
local ids = MetricUnitDefinitions.getUnitIds(dimensions)
|
|
760
|
+
return if ids[1] then MetricUnitDefinitions.getUnit(ids[1]) else nil
|
|
761
|
+
end
|
|
762
|
+
|
|
763
|
+
function MetricUnitParseUtils._checkKind(
|
|
764
|
+
data: MetricUnitData,
|
|
765
|
+
expected: Expectation,
|
|
766
|
+
text: string
|
|
767
|
+
): (MetricUnitData?, string?)
|
|
768
|
+
local expectedDimensions = expected.dimensions
|
|
769
|
+
if expectedDimensions == nil then
|
|
770
|
+
return data, nil
|
|
771
|
+
end
|
|
772
|
+
if not MetricUnitDimensionUtils.equal(data.dimensions, expectedDimensions) then
|
|
773
|
+
return nil, MetricUnitParseUtils._mismatchMessage(data.dimensions, expectedDimensions, text)
|
|
774
|
+
end
|
|
775
|
+
|
|
776
|
+
local isAbsolute = data.absolute == true
|
|
777
|
+
local isAbsoluteExpected = expected.absolute == true
|
|
778
|
+
if isAbsolute ~= isAbsoluteExpected then
|
|
779
|
+
return nil,
|
|
780
|
+
string.format(
|
|
781
|
+
"Expected %s, but %q is %s",
|
|
782
|
+
MetricUnitKinds.describeKind(expectedDimensions, isAbsoluteExpected),
|
|
783
|
+
text,
|
|
784
|
+
MetricUnitKinds.describeKind(data.dimensions, isAbsolute)
|
|
785
|
+
)
|
|
786
|
+
end
|
|
787
|
+
return data, nil
|
|
788
|
+
end
|
|
789
|
+
|
|
790
|
+
function MetricUnitParseUtils._mismatchMessage(got: Dimensions, expected: Dimensions, text: string): string
|
|
791
|
+
return string.format(
|
|
792
|
+
"Expected %s, but %q is %s",
|
|
793
|
+
MetricUnitDimensionUtils.describe(expected),
|
|
794
|
+
text,
|
|
795
|
+
MetricUnitDimensionUtils.describe(got)
|
|
796
|
+
)
|
|
797
|
+
end
|
|
798
|
+
|
|
799
|
+
return table.freeze(MetricUnitParseUtils)
|