@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.
Files changed (64) hide show
  1. package/CHANGELOG.md +16 -54
  2. package/README.md +80 -2
  3. package/deploy.nevermore.json +10 -0
  4. package/package.json +22 -4
  5. package/src/Shared/Definitions/MetricUnitDefinitions.lua +1082 -0
  6. package/src/Shared/Definitions/MetricUnitDefinitions.spec.lua +55 -0
  7. package/src/Shared/Definitions/MetricUnitDimensionUtils.lua +164 -0
  8. package/src/Shared/Definitions/MetricUnitDimensionUtils.spec.lua +49 -0
  9. package/src/Shared/Definitions/MetricUnitKinds.lua +237 -0
  10. package/src/Shared/Definitions/MetricUnitKinds.spec.lua +51 -0
  11. package/src/Shared/Formatting/MetricUnitDisplayUnitUtils.lua +129 -0
  12. package/src/Shared/Formatting/MetricUnitFormatUtils.lua +924 -0
  13. package/src/Shared/Formatting/MetricUnitFormatUtils.spec.lua +280 -0
  14. package/src/Shared/Formatting/RxMetricUnitFormatUtils.lua +62 -0
  15. package/src/Shared/Localization/MetricUnitFallbackStrings.lua +289 -0
  16. package/src/Shared/Localization/MetricUnitFallbackStrings.spec.lua +82 -0
  17. package/src/Shared/Localization/MetricUnitTranslator/ar.json.txt +182 -0
  18. package/src/Shared/Localization/MetricUnitTranslator/de.json.txt +182 -0
  19. package/src/Shared/Localization/MetricUnitTranslator/en.json.txt +182 -0
  20. package/src/Shared/Localization/MetricUnitTranslator/es.json.txt +182 -0
  21. package/src/Shared/Localization/MetricUnitTranslator/fr.json.txt +182 -0
  22. package/src/Shared/Localization/MetricUnitTranslator/id.json.txt +182 -0
  23. package/src/Shared/Localization/MetricUnitTranslator/init.lua +92 -0
  24. package/src/Shared/Localization/MetricUnitTranslator/it.json.txt +182 -0
  25. package/src/Shared/Localization/MetricUnitTranslator/ja.json.txt +182 -0
  26. package/src/Shared/Localization/MetricUnitTranslator/ko.json.txt +182 -0
  27. package/src/Shared/Localization/MetricUnitTranslator/pl.json.txt +182 -0
  28. package/src/Shared/Localization/MetricUnitTranslator/pt.json.txt +182 -0
  29. package/src/Shared/Localization/MetricUnitTranslator/ru.json.txt +182 -0
  30. package/src/Shared/Localization/MetricUnitTranslator/th.json.txt +182 -0
  31. package/src/Shared/Localization/MetricUnitTranslator/tr.json.txt +182 -0
  32. package/src/Shared/Localization/MetricUnitTranslator/vi.json.txt +182 -0
  33. package/src/Shared/Localization/MetricUnitTranslator/zh-cn.json.txt +182 -0
  34. package/src/Shared/Localization/MetricUnitTranslator/zh-tw.json.txt +182 -0
  35. package/src/Shared/Localization/MetricUnitTranslator.spec.lua +309 -0
  36. package/src/Shared/Localization/PluralCategoryUtils.lua +103 -0
  37. package/src/Shared/Localization/PluralCategoryUtils.spec.lua +77 -0
  38. package/src/Shared/Logarithmic/DecibelUtils.lua +72 -0
  39. package/src/Shared/Logarithmic/DecibelUtils.spec.lua +35 -0
  40. package/src/Shared/MetricUnit.Angles.spec.lua +80 -0
  41. package/src/Shared/MetricUnit.Customary.spec.lua +104 -0
  42. package/src/Shared/MetricUnit.Derived.spec.lua +173 -0
  43. package/src/Shared/MetricUnit.Display.spec.lua +153 -0
  44. package/src/Shared/MetricUnit.Electrical.spec.lua +114 -0
  45. package/src/Shared/MetricUnit.Math.spec.lua +112 -0
  46. package/src/Shared/MetricUnit.Roblox.spec.lua +34 -0
  47. package/src/Shared/MetricUnit.SI.spec.lua +127 -0
  48. package/src/Shared/MetricUnit.Spring.spec.lua +94 -0
  49. package/src/Shared/MetricUnit.Temperature.spec.lua +212 -0
  50. package/src/Shared/MetricUnit.Time.spec.lua +202 -0
  51. package/src/Shared/MetricUnit.Units.spec.lua +210 -0
  52. package/src/Shared/MetricUnit.lua +1730 -0
  53. package/src/Shared/MetricUnit.spec.lua +362 -0
  54. package/src/Shared/MetricUtils.lua +47 -16
  55. package/src/Shared/MetricUtils.spec.lua +76 -0
  56. package/src/Shared/Parsing/MetricUnitParseUtils.lua +799 -0
  57. package/src/Shared/Parsing/MetricUnitParseUtils.spec.lua +189 -0
  58. package/src/Shared/Systems/MetricUnitSystem.lua +322 -0
  59. package/src/Shared/Systems/MetricUnitSystem.spec.lua +181 -0
  60. package/src/Shared/Systems/MetricUnitSystems.lua +225 -0
  61. package/src/jest.config.lua +3 -0
  62. package/src/node_modules.project.json +7 -0
  63. package/test/default.project.json +25 -0
  64. package/test/scripts/Server/ServerMain.server.lua +14 -0
@@ -0,0 +1,1730 @@
1
+ --!strict
2
+ --[=[
3
+ A measurement that knows what it measures: an amount in SI base units with integer exponents for each dimension.
4
+ Time is held in seconds, length in metres, mass in kilograms, memory in bytes, temperature in kelvin, angle in
5
+ radians, current in amperes, amount of substance in moles and luminous intensity in candelas: the SI base units,
6
+ with bytes and radians added. Derived
7
+ quantities such as speed, force, energy and power are products of those, so `MetricUnit.new(1, "newton")` and
8
+ `MetricUnit.kilograms(1) * MetricUnit.meters(1) / MetricUnit.pow(MetricUnit.seconds(1), 2)` are the same value.
9
+
10
+ Every unit has one id, such as `"kilometer"`, used to read and write it, and one constructor, such as
11
+ [MetricUnit.kilometers]. Read a value in any unit of its dimension with [MetricUnit.In].
12
+
13
+ Values are frozen, and every operation returns a new one. `*` and `/` combine dimensions, a plain number scales,
14
+ and `+`, `-`, `<`, `<=` and `==` need the same dimension on both sides. A plain `0` is zero of whatever dimension it
15
+ meets in `+`, `-` and the helpers; any other plain number is dimensionless.
16
+
17
+ A value is either a difference, which scales and adds freely, or absolute, a point on a scale such as a
18
+ temperature in °C or an instant in time. [MetricUnit.new] makes points on the temperature scales and differences
19
+ in every other unit; [MetricUnit.point] and [MetricUnit.difference] make either in any unit. Two absolutes subtract to a difference, an absolute and a difference add to an
20
+ absolute, and an absolute cannot be added to another, negated, multiplied or divided, kelvin included.
21
+
22
+ ```lua
23
+ local speed = MetricUnit.kilometers(36) / MetricUnit.hours(1)
24
+ speed:In("meterPerSecond") -- 10
25
+ speed:In("milePerHour") -- 22.37
26
+ speed:GetKind() -- "speed"
27
+ MetricUnit.format(speed) -- "10 m/s"
28
+
29
+ local power = MetricUnit.new(3, "kilowattHour") / MetricUnit.hours(1)
30
+ power:In("kilowatt") -- 3
31
+
32
+ local reach = MetricUnit.expect(MetricUnit.studs(100), "length", "reach") -- 28, in metres
33
+
34
+ local swing = MetricUnit.difference(12, "celsius")
35
+ (MetricUnit.celsius(15) + swing):In("celsius") -- 27
36
+ (MetricUnit.celsius(15) - MetricUnit.celsius(10)):In("kelvin") -- 5
37
+ ```
38
+
39
+ Time is elapsed time in exact seconds. [MetricUnit.years] and [MetricUnit.months] are averages for long-range math
40
+ (a Julian year of 365.25 days, and a twelfth of it), not calendar spans; move a date by calendar months with
41
+ `Time.add` from `@quenty/time`. [MetricUnit.InSeconds] hands exact seconds to `task.wait`, `os.time` and
42
+ `TimeDurationUtils`. [MetricUnit.instant] makes a point in time on any epoch, and [MetricUnit.fromDateTime] and
43
+ [MetricUnit.ToDateTime] cross to and from `DateTime`.
44
+
45
+ [MetricUnit.format] writes a value for players, localized, and [MetricUnit.parse] reads one back. A value is
46
+ written in the system of the unit it was made from (miles stay miles, studs stay studs), and metric when made
47
+ from a metric unit. Convert it ([MetricUnit.ToMetric], [MetricUnit.ToImperial]) or pin one unit
48
+ ([MetricUnit.As]) to write it otherwise; the locale never chooses. `tostring` is a fixed English debug string.
49
+
50
+ Luau cannot compare a table with a number, so `x < 0` and `x == 0` do not work; use [MetricUnit.sign] and
51
+ [MetricUnit.isZero].
52
+
53
+ @class MetricUnit
54
+ ]=]
55
+
56
+ local require = require(script.Parent.loader).load(script)
57
+
58
+ local MetricUnitDefinitions = require("MetricUnitDefinitions")
59
+ local MetricUnitDimensionUtils = require("MetricUnitDimensionUtils")
60
+ local MetricUnitDisplayUnitUtils = require("MetricUnitDisplayUnitUtils")
61
+ local MetricUnitFormatUtils = require("MetricUnitFormatUtils")
62
+ local MetricUnitKinds = require("MetricUnitKinds")
63
+ local MetricUnitParseUtils = require("MetricUnitParseUtils")
64
+ local MetricUnitSystem = require("MetricUnitSystem")
65
+ local MetricUnitSystems = require("MetricUnitSystems")
66
+
67
+ local TOSTRING_FORMAT_OPTIONS: MetricUnitFormatUtils.MetricUnitFormatOptions =
68
+ table.freeze({ maximumSignificantDigits = 4 })
69
+
70
+ local MetricUnit = {}
71
+ MetricUnit.ClassName = "MetricUnit"
72
+ MetricUnit.__index = MetricUnit
73
+
74
+ export type Dimensions = MetricUnitDimensionUtils.Dimensions
75
+ export type KindName = MetricUnitKinds.KindName
76
+
77
+ --[=[
78
+ See [MetricUnitDefinitions.UnitId].
79
+
80
+ @type UnitId MetricUnitDefinitions.UnitId
81
+ @within MetricUnit
82
+ ]=]
83
+ export type UnitId = MetricUnitDefinitions.UnitId
84
+
85
+ --[=[
86
+ See [MetricUnitDefinitions.UnitLike].
87
+
88
+ @type UnitLike MetricUnitDefinitions.UnitLike
89
+ @within MetricUnit
90
+ ]=]
91
+ export type UnitLike = MetricUnitDefinitions.UnitLike
92
+
93
+ --[=[
94
+ See [MetricUnitFormatUtils.MetricUnitFormatOptions].
95
+
96
+ @type FormatOptions MetricUnitFormatUtils.MetricUnitFormatOptions
97
+ @within MetricUnit
98
+ ]=]
99
+ export type FormatOptions = MetricUnitFormatUtils.MetricUnitFormatOptions
100
+
101
+ --[=[
102
+ See [MetricUnitFormatUtils.MetricUnitData].
103
+
104
+ @type MetricUnitData MetricUnitFormatUtils.MetricUnitData
105
+ @within MetricUnit
106
+ ]=]
107
+ export type MetricUnitData = MetricUnitFormatUtils.MetricUnitData
108
+ type System = MetricUnitSystem.MetricUnitSystem
109
+ type DisplayPreference = { system: System?, unit: MetricUnitDefinitions.UnitDefinition? }
110
+
111
+ -- Metatable typed as the whole class, not { __index = MetricUnit }, so the checker sees the metamethods
112
+ export type MetricUnit = typeof(setmetatable(
113
+ {} :: {
114
+ _amount: number,
115
+ _dimensions: Dimensions,
116
+ _absolute: boolean,
117
+ _display: DisplayPreference?,
118
+ },
119
+ {} :: typeof(MetricUnit)
120
+ ))
121
+
122
+ --[=[
123
+ `amount` of the unit `unit`: the constructor for every unit. The common ones also have their own, such as
124
+ [MetricUnit.meters]; the rest, and units chosen at runtime, use this. A temperature scale gives an absolute
125
+ temperature, as [MetricUnit.celsius] does.
126
+
127
+ ```lua
128
+ MetricUnit.new(5, "kilometer") == MetricUnit.kilometers(5) -- true
129
+ MetricUnit.new(90, "minutes") == MetricUnit.hours(1.5) -- true
130
+ MetricUnit.new(60, "milePerHour")
131
+ ```
132
+
133
+ @param amount number
134
+ @param unit UnitLike
135
+ @return MetricUnit
136
+ ]=]
137
+ function MetricUnit.new(amount: number, unit: UnitLike): MetricUnit
138
+ return MetricUnit._fromUnit(amount, unit, "new")
139
+ end
140
+ --[=[
141
+ A point `amount` of `unit` from that unit's zero: a reading on a scale rather than a change along it. A unit's zero
142
+ is where its scale starts, such as -273.15 °C for kelvin, and is 0 for every unit but the temperature scales; time
143
+ units count from an epoch the caller chooses, as [MetricUnit.instant] does.
144
+
145
+ ```lua
146
+ MetricUnit.point(20, "celsius") == MetricUnit.celsius(20) -- true
147
+ MetricUnit.point(90, "second") == MetricUnit.instant(90) -- true
148
+ ```
149
+
150
+ @param amount number
151
+ @param unit UnitLike
152
+ @return MetricUnit
153
+ ]=]
154
+ function MetricUnit.point(amount: number, unit: UnitLike): MetricUnit
155
+ local definition = MetricUnit._definitionFor(amount, unit, "point", 3)
156
+ return MetricUnit._new(
157
+ amount * definition.factor + (definition.zero or 0),
158
+ definition.dimensions,
159
+ true,
160
+ MetricUnit._displayOfUnit(definition)
161
+ )
162
+ end
163
+ --[=[
164
+ A difference of `amount` of `unit`: a change along a scale rather than a reading on it. Most units already make
165
+ differences with [MetricUnit.new]; this is for the temperature scales, whose readings are points.
166
+
167
+ ```lua
168
+ MetricUnit.difference(12, "celsius"):In("kelvin") -- 12
169
+ MetricUnit.difference(9, "fahrenheit"):In("kelvin") -- 5
170
+ (MetricUnit.celsius(15) + MetricUnit.difference(12, "celsius")):In("celsius") -- 27
171
+ ```
172
+
173
+ @param amount number
174
+ @param unit UnitLike
175
+ @return MetricUnit
176
+ ]=]
177
+ function MetricUnit.difference(amount: number, unit: UnitLike): MetricUnit
178
+ local definition = MetricUnit._definitionFor(amount, unit, "difference", 3)
179
+ return MetricUnit._new(
180
+ amount * definition.factor,
181
+ definition.dimensions,
182
+ false,
183
+ MetricUnit._displayOfUnit(definition)
184
+ )
185
+ end
186
+ --[=[
187
+ A dimensionless value.
188
+
189
+ @param n number
190
+ @return MetricUnit
191
+ ]=]
192
+ function MetricUnit.none(n: number): MetricUnit
193
+ MetricUnit._assertNumber(n, "none")
194
+ return MetricUnit._new(n, MetricUnitDimensionUtils.NONE, false)
195
+ end
196
+
197
+ --[=[
198
+ `n` milliseconds.
199
+
200
+ @param n number
201
+ @return MetricUnit
202
+ ]=]
203
+ function MetricUnit.milliseconds(n: number): MetricUnit
204
+ return MetricUnit._fromUnit(n, "millisecond", "milliseconds")
205
+ end
206
+
207
+ --[=[
208
+ `n` seconds.
209
+
210
+ @param n number
211
+ @return MetricUnit
212
+ ]=]
213
+ function MetricUnit.seconds(n: number): MetricUnit
214
+ return MetricUnit._fromUnit(n, "second", "seconds")
215
+ end
216
+
217
+ --[=[
218
+ `n` minutes.
219
+
220
+ @param n number
221
+ @return MetricUnit
222
+ ]=]
223
+ function MetricUnit.minutes(n: number): MetricUnit
224
+ return MetricUnit._fromUnit(n, "minute", "minutes")
225
+ end
226
+
227
+ --[=[
228
+ `n` hours.
229
+
230
+ @param n number
231
+ @return MetricUnit
232
+ ]=]
233
+ function MetricUnit.hours(n: number): MetricUnit
234
+ return MetricUnit._fromUnit(n, "hour", "hours")
235
+ end
236
+
237
+ --[=[
238
+ `n` days of 24 hours.
239
+
240
+ @param n number
241
+ @return MetricUnit
242
+ ]=]
243
+ function MetricUnit.days(n: number): MetricUnit
244
+ return MetricUnit._fromUnit(n, "day", "days")
245
+ end
246
+
247
+ --[=[
248
+ `n` weeks of seven days.
249
+
250
+ @param n number
251
+ @return MetricUnit
252
+ ]=]
253
+ function MetricUnit.weeks(n: number): MetricUnit
254
+ return MetricUnit._fromUnit(n, "week", "weeks")
255
+ end
256
+
257
+ --[=[
258
+ `n` averaged months, each exactly a twelfth of an averaged year. For calendar months, use `Time.add`.
259
+
260
+ @param n number
261
+ @return MetricUnit
262
+ ]=]
263
+ function MetricUnit.months(n: number): MetricUnit
264
+ return MetricUnit._fromUnit(n, "month", "months")
265
+ end
266
+
267
+ --[=[
268
+ `n` averaged years of 365.25 days (a Julian year), for long-range elapsed time. For calendar years, use `Time.add`.
269
+
270
+ @param n number
271
+ @return MetricUnit
272
+ ]=]
273
+ function MetricUnit.years(n: number): MetricUnit
274
+ return MetricUnit._fromUnit(n, "year", "years")
275
+ end
276
+
277
+ --[=[
278
+ `n` times a second.
279
+
280
+ @param n number
281
+ @return MetricUnit
282
+ ]=]
283
+ function MetricUnit.perSecond(n: number): MetricUnit
284
+ return MetricUnit._fromUnit(n, "perSecond", "perSecond")
285
+ end
286
+
287
+ --[=[
288
+ `n` times an hour.
289
+
290
+ @param n number
291
+ @return MetricUnit
292
+ ]=]
293
+ function MetricUnit.perHour(n: number): MetricUnit
294
+ return MetricUnit._fromUnit(n, "perHour", "perHour")
295
+ end
296
+
297
+ --[=[
298
+ `n` times a day.
299
+
300
+ @param n number
301
+ @return MetricUnit
302
+ ]=]
303
+ function MetricUnit.perDay(n: number): MetricUnit
304
+ return MetricUnit._fromUnit(n, "perDay", "perDay")
305
+ end
306
+
307
+ --[=[
308
+ `n` times a year.
309
+
310
+ @param n number
311
+ @return MetricUnit
312
+ ]=]
313
+ function MetricUnit.perYear(n: number): MetricUnit
314
+ return MetricUnit._fromUnit(n, "perYear", "perYear")
315
+ end
316
+
317
+ --[=[
318
+ `n` millimetres.
319
+
320
+ @param n number
321
+ @return MetricUnit
322
+ ]=]
323
+ function MetricUnit.millimeters(n: number): MetricUnit
324
+ return MetricUnit._fromUnit(n, "millimeter", "millimeters")
325
+ end
326
+
327
+ --[=[
328
+ `n` metres.
329
+
330
+ @param n number
331
+ @return MetricUnit
332
+ ]=]
333
+ function MetricUnit.meters(n: number): MetricUnit
334
+ return MetricUnit._fromUnit(n, "meter", "meters")
335
+ end
336
+
337
+ --[=[
338
+ `n` kilometres.
339
+
340
+ @param n number
341
+ @return MetricUnit
342
+ ]=]
343
+ function MetricUnit.kilometers(n: number): MetricUnit
344
+ return MetricUnit._fromUnit(n, "kilometer", "kilometers")
345
+ end
346
+
347
+ --[=[
348
+ `n` Roblox studs, 28 cm each, as Roblox measures them against the default avatar. For studs at a game's own
349
+ scale, use its system: `MINI:New(n, "stud")`.
350
+
351
+ ```lua
352
+ MetricUnit.studs(10):In("meter") -- 2.8
353
+ ```
354
+
355
+ @param n number
356
+ @return MetricUnit
357
+ ]=]
358
+ function MetricUnit.studs(n: number): MetricUnit
359
+ return MetricUnit._fromUnit(n, "stud", "studs")
360
+ end
361
+ --[=[
362
+ `n` square kilometres.
363
+
364
+ @param n number
365
+ @return MetricUnit
366
+ ]=]
367
+ function MetricUnit.squareKilometers(n: number): MetricUnit
368
+ return MetricUnit._fromUnit(n, "squareKilometer", "squareKilometers")
369
+ end
370
+
371
+ --[=[
372
+ `n` grams.
373
+
374
+ @param n number
375
+ @return MetricUnit
376
+ ]=]
377
+ function MetricUnit.grams(n: number): MetricUnit
378
+ return MetricUnit._fromUnit(n, "gram", "grams")
379
+ end
380
+
381
+ --[=[
382
+ `n` kilograms.
383
+
384
+ @param n number
385
+ @return MetricUnit
386
+ ]=]
387
+ function MetricUnit.kilograms(n: number): MetricUnit
388
+ return MetricUnit._fromUnit(n, "kilogram", "kilograms")
389
+ end
390
+
391
+ --[=[
392
+ `n` tonnes of 1,000 kg.
393
+
394
+ @param n number
395
+ @return MetricUnit
396
+ ]=]
397
+ function MetricUnit.tonnes(n: number): MetricUnit
398
+ return MetricUnit._fromUnit(n, "tonne", "tonnes")
399
+ end
400
+
401
+ --[=[
402
+ `n` bytes.
403
+
404
+ @param n number
405
+ @return MetricUnit
406
+ ]=]
407
+ function MetricUnit.bytes(n: number): MetricUnit
408
+ return MetricUnit._fromUnit(n, "byte", "bytes")
409
+ end
410
+
411
+ --[=[
412
+ `n` kilobytes of 1,000 bytes.
413
+
414
+ @param n number
415
+ @return MetricUnit
416
+ ]=]
417
+ function MetricUnit.kilobytes(n: number): MetricUnit
418
+ return MetricUnit._fromUnit(n, "kilobyte", "kilobytes")
419
+ end
420
+
421
+ --[=[
422
+ `n` megabytes of 1,000,000 bytes.
423
+
424
+ @param n number
425
+ @return MetricUnit
426
+ ]=]
427
+ function MetricUnit.megabytes(n: number): MetricUnit
428
+ return MetricUnit._fromUnit(n, "megabyte", "megabytes")
429
+ end
430
+
431
+ --[=[
432
+ `n` degrees, a 360th of a revolution.
433
+
434
+ @param n number
435
+ @return MetricUnit
436
+ ]=]
437
+ function MetricUnit.degrees(n: number): MetricUnit
438
+ return MetricUnit._fromUnit(n, "degree", "degrees")
439
+ end
440
+
441
+ --[=[
442
+ `n` radians, the unit `CFrame.Angles` and `math.sin` take.
443
+
444
+ @param n number
445
+ @return MetricUnit
446
+ ]=]
447
+ function MetricUnit.radians(n: number): MetricUnit
448
+ return MetricUnit._fromUnit(n, "radian", "radians")
449
+ end
450
+
451
+ --[=[
452
+ `n` percent, a hundredth each: `percent(45)` is 0.45.
453
+
454
+ @param n number
455
+ @return MetricUnit
456
+ ]=]
457
+ function MetricUnit.percent(n: number): MetricUnit
458
+ return MetricUnit._fromUnit(n, "percent", "percent")
459
+ end
460
+
461
+ --[=[
462
+ The absolute temperature `n` kelvin.
463
+
464
+ @param n number
465
+ @return MetricUnit
466
+ ]=]
467
+ function MetricUnit.kelvin(n: number): MetricUnit
468
+ return MetricUnit._fromUnit(n, "kelvin", "kelvin")
469
+ end
470
+
471
+ --[=[
472
+ The absolute temperature `n` °C.
473
+
474
+ @param n number
475
+ @return MetricUnit
476
+ ]=]
477
+ function MetricUnit.celsius(n: number): MetricUnit
478
+ return MetricUnit._fromUnit(n, "celsius", "celsius")
479
+ end
480
+
481
+ --[=[
482
+ The absolute temperature `n` °F.
483
+
484
+ @param n number
485
+ @return MetricUnit
486
+ ]=]
487
+ function MetricUnit.fahrenheit(n: number): MetricUnit
488
+ return MetricUnit._fromUnit(n, "fahrenheit", "fahrenheit")
489
+ end
490
+
491
+ --[=[
492
+ The instant `seconds` after an epoch: a point in time rather than a span of it. The epoch is whatever the caller
493
+ measures from, such as a simulation's start; [MetricUnit.fromDateTime] uses the Unix epoch. Instants follow the
494
+ same rules as absolute temperatures: two subtract to a span, an instant and a span add to an instant, and an
495
+ instant cannot be added to another, scaled or negated. Instants from different epochs must not be mixed; nothing
496
+ can check that for you.
497
+
498
+ ```lua
499
+ local worldStart = MetricUnit.instant(0)
500
+ local now = worldStart + MetricUnit.years(250)
501
+ (now - worldStart):In("year") -- 250
502
+ now:In("year") -- 250, time since its epoch
503
+ ```
504
+
505
+ @param seconds number -- since the epoch
506
+ @return MetricUnit
507
+ ]=]
508
+ function MetricUnit.instant(seconds: number): MetricUnit
509
+ MetricUnit._assertNumber(seconds, "instant")
510
+ return MetricUnit._new(seconds, MetricUnitKinds.TIME, true)
511
+ end
512
+
513
+ --[=[
514
+ The instant `dateTime` marks, measured from the Unix epoch, to the millisecond. Subtract two for the exact time
515
+ between them, which counts calendar months and leap days at their real length.
516
+
517
+ ```lua
518
+ local played = MetricUnit.fromDateTime(DateTime.now()) - MetricUnit.fromDateTime(joinedAt)
519
+ ```
520
+
521
+ @param dateTime DateTime
522
+ @return MetricUnit
523
+ ]=]
524
+ function MetricUnit.fromDateTime(dateTime: DateTime): MetricUnit
525
+ if typeof(dateTime) ~= "DateTime" then
526
+ error(string.format("MetricUnit.fromDateTime takes a DateTime, not %s", typeof(dateTime)), 2)
527
+ end
528
+ return MetricUnit._new(dateTime.UnixTimestampMillis / 1000, MetricUnitKinds.TIME, true)
529
+ end
530
+
531
+ --[=[
532
+ The speed of a Roblox velocity, such as `part.AssemblyLinearVelocity`, in studs per second by its magnitude. The
533
+ value converts to metric through the stud like any other: read it with `:In("kilometerPerHour")`.
534
+
535
+ @param velocity Vector3 -- in studs per second
536
+ @return MetricUnit
537
+ ]=]
538
+ function MetricUnit.fromStudVelocity(velocity: Vector3): MetricUnit
539
+ if typeof(velocity) ~= "Vector3" then
540
+ error(string.format("MetricUnit.fromStudVelocity takes a Vector3, not %s", typeof(velocity)), 2)
541
+ end
542
+ return MetricUnit.new(velocity.Magnitude, "studPerSecond")
543
+ end
544
+ --[=[
545
+ Reads `text` such as `"5 km"`, `"2.5 kilometers"`, `"1h30m"`, `"1:30:00"`, `"20 °C"`, `"60 mph"` or `"5 m/s"`,
546
+ for commands, admin tools, configs and text boxes. Returns nil and a message to show instead of erroring. See
547
+ [MetricUnitParseUtils.parse] for the grammar and how `kind` resolves ambiguous text such as `"m"`.
548
+
549
+ ```lua
550
+ local reach, err = MetricUnit.parse(textBox.Text, "length")
551
+ if not reach then
552
+ warn(err)
553
+ end
554
+ ```
555
+
556
+ @param text string
557
+ @param kind (KindName | Dimensions)?
558
+ @param options ParseOptions?
559
+ @return MetricUnit?
560
+ @return string? -- why it could not be read
561
+ ]=]
562
+ function MetricUnit.parse(
563
+ text: string,
564
+ kind: (KindName | Dimensions)?,
565
+ options: MetricUnitParseUtils.ParseOptions?
566
+ ): (MetricUnit?, string?)
567
+ return MetricUnit._parseInSystem(text, kind, options, nil)
568
+ end
569
+
570
+ function MetricUnit._parseInSystem(
571
+ text: string,
572
+ kind: (KindName | Dimensions)?,
573
+ options: MetricUnitParseUtils.ParseOptions?,
574
+ system: System?
575
+ ): (MetricUnit?, string?)
576
+ local data, err = MetricUnitParseUtils._parseInSystem(text, kind, options, system)
577
+ if not data then
578
+ return nil, err
579
+ end
580
+ local value = MetricUnit.fromData(data)
581
+ return if system then value:ToSystem(system) else value, nil
582
+ end
583
+
584
+ --[=[
585
+ Whether `value` is a [MetricUnit].
586
+
587
+ @param value any
588
+ @return boolean
589
+ ]=]
590
+ function MetricUnit.isUnit(value: any): boolean
591
+ return type(value) == "table" and getmetatable(value) == MetricUnit
592
+ end
593
+
594
+ --[=[
595
+ Whether `value` is a [MetricUnit] of `kind`, a [KindName] or a table of exponents. Safe to call on anything.
596
+ Errors on an unknown name, so a typo does not quietly answer false.
597
+
598
+ ```lua
599
+ MetricUnit.isKind(MetricUnit.kilometers(1) / MetricUnit.hours(1), "speed") -- true
600
+ MetricUnit.isKind(MetricUnit.celsius(5), "temperature") -- true, absolute
601
+ MetricUnit.isKind(MetricUnit.difference(5, "celsius"), "temperatureDifference") -- true
602
+ MetricUnit.isKind(5, "length") -- false
603
+ ```
604
+
605
+ @param value any
606
+ @param kind KindName | Dimensions
607
+ @return boolean
608
+ ]=]
609
+ function MetricUnit.isKind(value: any, kind: KindName | Dimensions): boolean
610
+ local dimensions, absolute = MetricUnitKinds.fromName(kind)
611
+ return MetricUnit.isUnit(value)
612
+ and value._absolute == absolute
613
+ and MetricUnitDimensionUtils.equal(value._dimensions, dimensions)
614
+ end
615
+
616
+ --[=[
617
+ Whether `a` and `b` have the same dimensions; a plain 0 has every dimension and any other number none.
618
+
619
+ @param a MetricUnit | number
620
+ @param b MetricUnit | number
621
+ @return boolean
622
+ ]=]
623
+ function MetricUnit.sameDimension(a: MetricUnit | number, b: MetricUnit | number): boolean
624
+ if a == 0 or b == 0 then
625
+ return true
626
+ end
627
+ return MetricUnitDimensionUtils.equal(MetricUnit._dimensionsOf(a), MetricUnit._dimensionsOf(b))
628
+ end
629
+
630
+ --[=[
631
+ The amount of `value` in SI base units, for checking arguments at an API boundary. Errors with `slot`, the
632
+ dimension expected and the one given if `value` is not of `kind`. A plain 0 is zero of any kind but an absolute
633
+ one.
634
+
635
+ ```lua
636
+ local function setReach(reach: MetricUnit.MetricUnit | number)
637
+ local meters = MetricUnit.expect(reach, "length", "reach")
638
+ end
639
+ ```
640
+
641
+ @param value MetricUnit | number
642
+ @param kind KindName | Dimensions
643
+ @param slot string -- what the value is for, used in the error
644
+ @return number -- in SI base units
645
+ ]=]
646
+ function MetricUnit.expect(value: MetricUnit | number, kind: KindName | Dimensions, slot: string): number
647
+ local dimensions, absolute = MetricUnitKinds.fromName(kind)
648
+ if type(value) == "number" then
649
+ if value == 0 and not absolute then
650
+ return 0
651
+ elseif not absolute and next(dimensions) == nil then
652
+ return value
653
+ end
654
+ error(
655
+ string.format(
656
+ "MetricUnit.expect: %s expects %s, got the plain number %s",
657
+ slot,
658
+ MetricUnitKinds.describeKind(dimensions, absolute),
659
+ tostring(value)
660
+ ),
661
+ 2
662
+ )
663
+ end
664
+ if not MetricUnit.isUnit(value) then
665
+ error(
666
+ string.format(
667
+ "MetricUnit.expect: %s expects %s, got %s",
668
+ slot,
669
+ MetricUnitKinds.describeKind(dimensions, absolute),
670
+ typeof(value)
671
+ ),
672
+ 2
673
+ )
674
+ end
675
+ local unit = value :: MetricUnit
676
+ if unit._absolute ~= absolute or not MetricUnitDimensionUtils.equal(unit._dimensions, dimensions) then
677
+ error(
678
+ string.format(
679
+ "MetricUnit.expect: %s expects %s, got %s",
680
+ slot,
681
+ MetricUnitKinds.describeKind(dimensions, absolute),
682
+ MetricUnitKinds.describeKind(unit._dimensions, unit._absolute)
683
+ ),
684
+ 2
685
+ )
686
+ end
687
+ return unit._amount
688
+ end
689
+
690
+ --[=[
691
+ Whether `value` is zero.
692
+
693
+ @param value MetricUnit | number
694
+ @return boolean
695
+ ]=]
696
+ function MetricUnit.isZero(value: MetricUnit | number): boolean
697
+ if type(value) == "number" then
698
+ return value == 0
699
+ end
700
+ MetricUnit._assertUnit(value, "isZero")
701
+ return value._amount == 0
702
+ end
703
+
704
+ --[=[
705
+ -1, 0 or 1 as `value` is negative, zero or positive; an absolute temperature is measured from absolute zero.
706
+
707
+ @param value MetricUnit | number
708
+ @return number
709
+ ]=]
710
+ function MetricUnit.sign(value: MetricUnit | number): number
711
+ if type(value) == "number" then
712
+ return math.sign(value)
713
+ end
714
+ MetricUnit._assertUnit(value, "sign")
715
+ return math.sign(value._amount)
716
+ end
717
+
718
+ --[=[
719
+ How many of `unit` this is. `unit` is a unit id, any of `@quenty/time`'s names for a time unit, or another
720
+ [MetricUnit] to take the ratio with. Errors, naming both dimensions, when they differ.
721
+
722
+ A point reads from the unit's zero, so a temperature reads on its scale and an instant as the time since its
723
+ epoch; a difference reads as it is. Rates read per a time unit with the `per` ids.
724
+
725
+ ```lua
726
+ MetricUnit.kilometers(2):In("meter") -- 2000
727
+ MetricUnit.minutes(90):In("hours") -- 1.5
728
+ MetricUnit.perDay(48):In("perHour") -- 2
729
+ MetricUnit.celsius(100):In("fahrenheit") -- 212
730
+ (MetricUnit.meters(10) / MetricUnit.seconds(1)):In(MetricUnit.kilometers(1) / MetricUnit.hours(1)) -- 36
731
+ ```
732
+
733
+ @param unit UnitLike | MetricUnit
734
+ @return number
735
+ ]=]
736
+ function MetricUnit.In(self: MetricUnit, unit: UnitLike | MetricUnit): number
737
+ MetricUnit._assertUnit(self, "In")
738
+ if not MetricUnit.isUnit(unit) then
739
+ local definition = MetricUnit._getUnit(unit :: UnitLike)
740
+ if not definition then
741
+ error(string.format("MetricUnit:In knows no unit %q", tostring(unit)), 2)
742
+ end
743
+ return MetricUnit._readUnit(self, definition)
744
+ end
745
+
746
+ MetricUnit._assertUnit(unit, "In")
747
+ local other = unit :: MetricUnit
748
+ MetricUnit._assertDifference(self, "take the ratio of")
749
+ MetricUnit._assertDifference(other, "take the ratio of")
750
+ if not MetricUnitDimensionUtils.equal(self._dimensions, other._dimensions) then
751
+ error(
752
+ string.format(
753
+ "MetricUnit:In cannot read %s in %s",
754
+ MetricUnitDimensionUtils.describe(self._dimensions),
755
+ MetricUnitDimensionUtils.describe(other._dimensions)
756
+ ),
757
+ 2
758
+ )
759
+ end
760
+ return self._amount / other._amount
761
+ end
762
+
763
+ --[=[
764
+ This time in exact seconds, the same as `:In("second")`: the number to hand to `task.wait`, `os.time` arithmetic
765
+ or `TimeDurationUtils`.
766
+
767
+ ```lua
768
+ task.wait(MetricUnit.minutes(2):InSeconds())
769
+ TimeDurationUtils.format(MetricUnit.hours(36):InSeconds(), "h:mm") --> 36:00
770
+ ```
771
+
772
+ @return number
773
+ ]=]
774
+ function MetricUnit.InSeconds(self: MetricUnit): number
775
+ MetricUnit._assertUnit(self, "InSeconds")
776
+ return MetricUnit._readUnit(self, (assert(MetricUnitDefinitions.getUnit("second"), "No second")))
777
+ end
778
+
779
+ --[=[
780
+ This instant as a `DateTime`, to the nearest millisecond, taking its epoch to be the Unix epoch as
781
+ [MetricUnit.fromDateTime] does. Errors on anything but an instant. To move a date by exact time, add a span:
782
+ `(MetricUnit.fromDateTime(dateTime) + MetricUnit.days(7)):ToDateTime()`. To move by calendar months, use
783
+ `Time.add` from `@quenty/time`.
784
+
785
+ @return DateTime
786
+ ]=]
787
+ function MetricUnit.ToDateTime(self: MetricUnit): DateTime
788
+ MetricUnit._assertUnit(self, "ToDateTime")
789
+ if not (self._absolute and MetricUnitDimensionUtils.equal(self._dimensions, MetricUnitKinds.TIME)) then
790
+ error(
791
+ string.format(
792
+ "MetricUnit:ToDateTime takes an instant, not %s",
793
+ MetricUnitKinds.describeKind(self._dimensions, self._absolute)
794
+ ),
795
+ 2
796
+ )
797
+ end
798
+ local seconds = self._amount
799
+ return DateTime.fromUnixTimestampMillis(math.floor(seconds * 1000 + 0.5))
800
+ end
801
+
802
+ --[=[
803
+ This value's amount in SI base units: seconds, metres, kilograms, bytes, kelvin, or their products.
804
+
805
+ @return number
806
+ ]=]
807
+ function MetricUnit.GetBaseAmount(self: MetricUnit): number
808
+ return self._amount
809
+ end
810
+
811
+ --[=[
812
+ This value's frozen dimension exponents, such as `{ length = 1, time = -1 }` for a speed.
813
+
814
+ @return Dimensions
815
+ ]=]
816
+ function MetricUnit.GetDimensions(self: MetricUnit): Dimensions
817
+ return self._dimensions
818
+ end
819
+
820
+ --[=[
821
+ What this value measures, such as `"length"`, `"speed"` or `"temperature"`, or nil for a compound without a
822
+ name, such as length to the fourth.
823
+
824
+ @return KindName?
825
+ ]=]
826
+ function MetricUnit.GetKind(self: MetricUnit): KindName?
827
+ return MetricUnitKinds.getKind(self._dimensions, self._absolute)
828
+ end
829
+
830
+ --[=[
831
+ Whether this value is of `kind`, a [KindName] or a table of exponents.
832
+
833
+ @param kind KindName | Dimensions
834
+ @return boolean
835
+ ]=]
836
+ function MetricUnit.IsKind(self: MetricUnit, kind: KindName | Dimensions): boolean
837
+ return MetricUnit.isKind(self, kind)
838
+ end
839
+
840
+ --[=[
841
+ Whether this value is absolute, a point on a scale such as a temperature, rather than a difference.
842
+
843
+ @return boolean
844
+ ]=]
845
+ function MetricUnit.IsAbsolute(self: MetricUnit): boolean
846
+ return self._absolute
847
+ end
848
+
849
+ --[=[
850
+ The ids of every unit this value can be read in or formatted as, for a settings menu to offer. Metric units come
851
+ first, then US customary ones, then Roblox studs.
852
+
853
+ ```lua
854
+ MetricUnit.meters(1):GetUnitIds() -- { "millimeter", "centimeter", "meter", "kilometer", "inch", ... }
855
+ ```
856
+
857
+ @return { UnitId }
858
+ ]=]
859
+ function MetricUnit.GetUnitIds(self: MetricUnit): { UnitId }
860
+ return MetricUnitDefinitions.getUnitIds(self._dimensions)
861
+ end
862
+
863
+ --[=[
864
+ This value, preferring United States customary units when it is formatted: miles, feet and inches; US gallons,
865
+ quarts and cups; short tons, pounds and ounces; mph, mpg (US), BTU, horsepower, psi and °F where its dimension
866
+ has them, and metric units elsewhere. The amount is unchanged, so arithmetic, comparison, [MetricUnit.In] and
867
+ saving behave exactly as before. A locale never chooses these units, not even `en-us`: call this from a
868
+ player's setting.
869
+
870
+ ```lua
871
+ MetricUnit.format(MetricUnit.kilometers(5):ToUSCustomary()) --> 3.107 mi
872
+ MetricUnit.format(MetricUnit.new(2, "liter"):ToUSCustomary()) --> 2.113 qt
873
+ local shown = if settings.useImperial then distance:ToUSCustomary() else distance
874
+ ```
875
+
876
+ @return MetricUnit
877
+ ]=]
878
+ function MetricUnit.ToUSCustomary(self: MetricUnit): MetricUnit
879
+ return MetricUnit.ToSystem(self, MetricUnitSystems.US_CUSTOMARY)
880
+ end
881
+ --[=[
882
+ This value, preferring British imperial units when it is formatted: imperial gallons, pints and fluid ounces;
883
+ stone, pounds and ounces; mpg (UK); and the units Britain shares with the United States (miles, mph, BTU,
884
+ horsepower, psi, °F). The amount is unchanged. See [MetricUnit.ToUSCustomary] for US units.
885
+
886
+ ```lua
887
+ MetricUnit.format(MetricUnit.new(2, "liter"):ToImperial()) --> 3.52 imp pt
888
+ MetricUnit.format(MetricUnit.kilograms(80):ToImperial()) --> 12.598 st
889
+ ```
890
+
891
+ @return MetricUnit
892
+ ]=]
893
+ function MetricUnit.ToImperial(self: MetricUnit): MetricUnit
894
+ return MetricUnit.ToSystem(self, MetricUnitSystems.IMPERIAL)
895
+ end
896
+ --[=[
897
+ This value, preferring metric units when it is formatted. Metric is already the default; this clears a US
898
+ customary or imperial preference or a pinned unit.
899
+
900
+ @return MetricUnit
901
+ ]=]
902
+ function MetricUnit.ToMetric(self: MetricUnit): MetricUnit
903
+ return MetricUnit.ToSystem(self, MetricUnitSystems.METRIC)
904
+ end
905
+
906
+ --[=[
907
+ This value, preferring `system`'s units when it is formatted. The amount is unchanged, so arithmetic,
908
+ comparison, [MetricUnit.In] and saving behave exactly as before. [MetricUnit.ToUSCustomary],
909
+ [MetricUnit.ToImperial] and [MetricUnit.ToMetric] are this with the well-known systems.
910
+
911
+ ```lua
912
+ MetricUnit.format(MetricUnit.meters(0.14):ToSystem(MINI)) --> 5 studs, in a game's own system
913
+ ```
914
+
915
+ @param system MetricUnitSystem
916
+ @return MetricUnit
917
+ ]=]
918
+ function MetricUnit.ToSystem(self: MetricUnit, system: System): MetricUnit
919
+ MetricUnit._assertUnit(self, "ToSystem")
920
+ if not MetricUnitSystem.isSystem(system) then
921
+ error(string.format("MetricUnit:ToSystem takes a MetricUnitSystem, not %s", typeof(system)), 2)
922
+ end
923
+ return MetricUnit._new(self._amount, self._dimensions, self._absolute, table.freeze({ system = system }))
924
+ end
925
+ --[=[
926
+ This value, always formatted in `unit`, which must measure the same thing. The amount is unchanged.
927
+
928
+ ```lua
929
+ MetricUnit.format(MetricUnit.kilometers(5):As("mile")) --> 3.107 mi
930
+ MetricUnit.format(MetricUnit.meters(28):As("stud")) --> 100 studs
931
+ MetricUnit.format(height:As(MINI:GetUnit("stud"))) --> a game's own stud
932
+ ```
933
+
934
+ @param unit UnitLike
935
+ @return MetricUnit
936
+ ]=]
937
+ function MetricUnit.As(self: MetricUnit, unit: UnitLike): MetricUnit
938
+ MetricUnit._assertUnit(self, "As")
939
+ local definition = MetricUnit._getUnit(unit)
940
+ if not definition then
941
+ error(string.format("MetricUnit:As knows no unit %q", tostring(unit)), 2)
942
+ elseif not MetricUnitDimensionUtils.equal(definition.dimensions, self._dimensions) then
943
+ error(
944
+ string.format(
945
+ "MetricUnit:As(%q) is %s, not %s",
946
+ definition.id,
947
+ MetricUnitDimensionUtils.describe(definition.dimensions),
948
+ MetricUnitKinds.describeKind(self._dimensions, self._absolute)
949
+ ),
950
+ 2
951
+ )
952
+ end
953
+ local display = table.freeze({
954
+ system = MetricUnitSystems.getSystemOfUnit(definition),
955
+ unit = definition,
956
+ })
957
+ return MetricUnit._new(self._amount, self._dimensions, self._absolute, display)
958
+ end
959
+
960
+ --[=[
961
+ The system this value prefers to be formatted in, from [MetricUnit.ToSystem] or [MetricUnit.As], or nil for the
962
+ default (metric). A display concern only: it is not saved by [MetricUnit.toData] and never affects arithmetic.
963
+
964
+ @return MetricUnitSystem?
965
+ ]=]
966
+ function MetricUnit.GetDisplaySystem(self: MetricUnit): System?
967
+ local display = self._display
968
+ return if display then display.system else nil
969
+ end
970
+
971
+ --[=[
972
+ The unit this value is always formatted in, from [MetricUnit.As], or nil.
973
+
974
+ @return UnitDefinition?
975
+ ]=]
976
+ function MetricUnit.GetDisplayUnit(self: MetricUnit): MetricUnitDefinitions.UnitDefinition?
977
+ local display = self._display
978
+ return if display then display.unit else nil
979
+ end
980
+
981
+ --[=[
982
+ Text for players, localized, in a unit that suits the size. See [MetricUnitFormatUtils.format] for the options.
983
+ An instant is written as the time since its epoch, since only the caller knows what the epoch is; for a calendar
984
+ date, use `Time.format(x:ToDateTime(), ...)` from `@quenty/time`.
985
+
986
+ ```lua
987
+ MetricUnit.format(MetricUnit.meters(2500)) --> 2.5 km
988
+ MetricUnit.format(MetricUnit.meters(2500), { unitDisplay = "long", locale = "en-gb" }) --> 2.5 kilometres
989
+ MetricUnit.format(MetricUnit.new(90, "kilometerPerHour"), { unit = "kilometerPerHour" }) --> 90 km/h
990
+ MetricUnit.format(MetricUnit.kilometers(5):ToUSCustomary()) --> 3.107 mi
991
+ MetricUnit.format(MetricUnit.years(1)) --> 1 year
992
+ MetricUnit.format(MetricUnit.minutes(90), { locale = "es-es" }) --> 1 hora, 30 minutos
993
+ ```
994
+
995
+ @param value MetricUnit
996
+ @param options FormatOptions?
997
+ @return string
998
+ ]=]
999
+ function MetricUnit.format(value: MetricUnit, options: FormatOptions?): string
1000
+ MetricUnit._assertUnit(value, "format")
1001
+ return MetricUnitFormatUtils.format(value, options)
1002
+ end
1003
+
1004
+ --[=[
1005
+ `value` as plain data, for saves.
1006
+
1007
+ @param value MetricUnit
1008
+ @return MetricUnitData
1009
+ ]=]
1010
+ function MetricUnit.toData(value: MetricUnit): MetricUnitData
1011
+ MetricUnit._assertUnit(value, "toData")
1012
+ return {
1013
+ amount = value._amount,
1014
+ dimensions = table.clone(value._dimensions),
1015
+ absolute = if value._absolute then true else nil,
1016
+ }
1017
+ end
1018
+
1019
+ --[=[
1020
+ The [MetricUnit] that `data` from [MetricUnit.toData] holds.
1021
+
1022
+ @param data MetricUnitData
1023
+ @return MetricUnit
1024
+ ]=]
1025
+ function MetricUnit.fromData(data: MetricUnitData): MetricUnit
1026
+ if
1027
+ type(data) ~= "table"
1028
+ or type(data.amount) ~= "number"
1029
+ or not MetricUnitDimensionUtils.isDimensions(data.dimensions)
1030
+ then
1031
+ error("MetricUnit.fromData takes { amount: number, dimensions: { [string]: integer }, absolute: boolean? }", 2)
1032
+ end
1033
+ return MetricUnit._new(data.amount, table.freeze(table.clone(data.dimensions)), data.absolute == true)
1034
+ end
1035
+
1036
+ --[=[
1037
+ The size of `value`, a difference.
1038
+
1039
+ @param value MetricUnit
1040
+ @return MetricUnit
1041
+ ]=]
1042
+ function MetricUnit.abs(value: MetricUnit): MetricUnit
1043
+ MetricUnit._assertUnit(value, "abs")
1044
+ MetricUnit._assertDifference(value, "take the size of")
1045
+ return MetricUnit._new(math.abs(value._amount), value._dimensions, false, value._display)
1046
+ end
1047
+
1048
+ --[=[
1049
+ The square root of `value`, halving its exponents, erroring if any is odd.
1050
+
1051
+ @param value MetricUnit
1052
+ @return MetricUnit
1053
+ ]=]
1054
+ function MetricUnit.sqrt(value: MetricUnit): MetricUnit
1055
+ MetricUnit._assertUnit(value, "sqrt")
1056
+ MetricUnit._assertDifference(value, "take the square root of")
1057
+ if value._amount < 0 then
1058
+ error(
1059
+ string.format("MetricUnit.sqrt of a negative %s", MetricUnitDimensionUtils.describe(value._dimensions)),
1060
+ 2
1061
+ )
1062
+ end
1063
+ return MetricUnit._new(
1064
+ math.sqrt(value._amount),
1065
+ MetricUnitDimensionUtils.scale(value._dimensions, 0.5),
1066
+ false,
1067
+ MetricUnit._systemOnly(value._display)
1068
+ )
1069
+ end
1070
+
1071
+ --[=[
1072
+ `value` to the whole power `power`, multiplying its exponents.
1073
+
1074
+ @param value MetricUnit
1075
+ @param power number -- a whole number
1076
+ @return MetricUnit
1077
+ ]=]
1078
+ function MetricUnit.pow(value: MetricUnit, power: number): MetricUnit
1079
+ MetricUnit._assertUnit(value, "pow")
1080
+ MetricUnit._assertDifference(value, "exponentiate")
1081
+ if type(power) ~= "number" or power ~= math.floor(power) then
1082
+ error(string.format("MetricUnit.pow takes a whole power, not %s", tostring(power)), 2)
1083
+ end
1084
+ return MetricUnit._new(
1085
+ value._amount ^ power,
1086
+ MetricUnitDimensionUtils.scale(value._dimensions, power),
1087
+ false,
1088
+ MetricUnit._systemOnly(value._display)
1089
+ )
1090
+ end
1091
+
1092
+ --[=[
1093
+ `value` rounded to a whole number of `step`, halves rounding up. `step` is a unit id, such as `"minute"`, or a
1094
+ value to snap to a multiple of. A temperature rounds on its own scale (`round(celsius(21.6), "celsius")` is
1095
+ 22 °C) and an instant rounds from its epoch.
1096
+
1097
+ ```lua
1098
+ MetricUnit.round(MetricUnit.seconds(95), "minute") -- 2 minutes
1099
+ MetricUnit.round(MetricUnit.meters(12.4), MetricUnit.meters(5)) -- 10 m
1100
+ ```
1101
+
1102
+ @param value MetricUnit
1103
+ @param step UnitLike | MetricUnit
1104
+ @return MetricUnit
1105
+ ]=]
1106
+ function MetricUnit.round(value: MetricUnit, step: UnitLike | MetricUnit): MetricUnit
1107
+ return MetricUnit._snap(value, step, function(count: number): number
1108
+ return math.floor(count + 0.5)
1109
+ end, "round")
1110
+ end
1111
+
1112
+ --[=[
1113
+ `value` rounded down to a whole number of `step`. See [MetricUnit.round].
1114
+
1115
+ @param value MetricUnit
1116
+ @param step UnitLike | MetricUnit
1117
+ @return MetricUnit
1118
+ ]=]
1119
+ function MetricUnit.floor(value: MetricUnit, step: UnitLike | MetricUnit): MetricUnit
1120
+ return MetricUnit._snap(value, step, math.floor, "floor")
1121
+ end
1122
+
1123
+ --[=[
1124
+ `value` rounded up to a whole number of `step`. See [MetricUnit.round].
1125
+
1126
+ @param value MetricUnit
1127
+ @param step UnitLike | MetricUnit
1128
+ @return MetricUnit
1129
+ ]=]
1130
+ function MetricUnit.ceil(value: MetricUnit, step: UnitLike | MetricUnit): MetricUnit
1131
+ return MetricUnit._snap(value, step, math.ceil, "ceil")
1132
+ end
1133
+
1134
+ --[=[
1135
+ The smallest of `...`, all of one dimension.
1136
+
1137
+ @param ... MetricUnit | number
1138
+ @return MetricUnit
1139
+ ]=]
1140
+ function MetricUnit.min(...: MetricUnit | number): MetricUnit
1141
+ return MetricUnit._extreme("min", math.min, ...)
1142
+ end
1143
+
1144
+ --[=[
1145
+ The largest of `...`, all of one dimension.
1146
+
1147
+ @param ... MetricUnit | number
1148
+ @return MetricUnit
1149
+ ]=]
1150
+ function MetricUnit.max(...: MetricUnit | number): MetricUnit
1151
+ return MetricUnit._extreme("max", math.max, ...)
1152
+ end
1153
+
1154
+ --[=[
1155
+ `value` held between `low` and `high`, all of one dimension.
1156
+
1157
+ @param value MetricUnit | number
1158
+ @param low MetricUnit | number
1159
+ @param high MetricUnit | number
1160
+ @return MetricUnit
1161
+ ]=]
1162
+ function MetricUnit.clamp(value: MetricUnit | number, low: MetricUnit | number, high: MetricUnit | number): MetricUnit
1163
+ return MetricUnit.min(MetricUnit.max(value, low), high)
1164
+ end
1165
+
1166
+ --[=[
1167
+ The value `t` of the way from `a` to `b`, of one dimension, for animation. Follows the absolute/difference rules,
1168
+ so lerping two temperatures or two instants gives a temperature or an instant.
1169
+
1170
+ @param a MetricUnit | number
1171
+ @param b MetricUnit | number
1172
+ @param t number -- 0 gives `a`, 1 gives `b`
1173
+ @return MetricUnit
1174
+ ]=]
1175
+ function MetricUnit.lerp(a: MetricUnit | number, b: MetricUnit | number, t: number): MetricUnit
1176
+ if type(t) ~= "number" then
1177
+ error(string.format("MetricUnit.lerp takes a number t, not %s", typeof(t)), 2)
1178
+ end
1179
+ return ((a :: any) + ((b :: any) - (a :: any)) * t) :: MetricUnit
1180
+ end
1181
+
1182
+ --[=[
1183
+ The sum of `values`, all of one dimension. Absolutes cannot be summed, as with `+`.
1184
+
1185
+ @param values { MetricUnit | number }
1186
+ @return MetricUnit
1187
+ ]=]
1188
+ function MetricUnit.sum(values: { MetricUnit | number }): MetricUnit
1189
+ if type(values) ~= "table" or #values == 0 then
1190
+ error("MetricUnit.sum takes a list of at least one value", 2)
1191
+ end
1192
+ local total: any = values[1]
1193
+ for index = 2, #values do
1194
+ total += values[index]
1195
+ end
1196
+ if type(total) == "number" then
1197
+ return MetricUnit.none(total)
1198
+ end
1199
+ return total
1200
+ end
1201
+
1202
+ --[=[
1203
+ The mean of `values`, all of one dimension. Absolutes average too: the mean of two temperatures is the
1204
+ temperature halfway between them. For the smallest or largest of a list, use
1205
+ `MetricUnit.min(table.unpack(values))`.
1206
+
1207
+ @param values { MetricUnit | number }
1208
+ @return MetricUnit
1209
+ ]=]
1210
+ function MetricUnit.mean(values: { MetricUnit | number }): MetricUnit
1211
+ if type(values) ~= "table" or #values == 0 then
1212
+ error("MetricUnit.mean takes a list of at least one value", 2)
1213
+ end
1214
+ local first: any = values[1]
1215
+ local sumOfDifferencesFromFirst: any = 0
1216
+ for index = 2, #values do
1217
+ sumOfDifferencesFromFirst += (values[index] :: any) - first
1218
+ end
1219
+ local result: any = first + sumOfDifferencesFromFirst / #values
1220
+ if type(result) == "number" then
1221
+ return MetricUnit.none(result)
1222
+ end
1223
+ return result
1224
+ end
1225
+
1226
+ --[=[
1227
+ Whether `a` and `b`, of one dimension, are within `epsilon` of each other. `epsilon` is a [MetricUnit] of their
1228
+ dimension, or a plain number in SI base units (1e-9 by default).
1229
+
1230
+ @param a MetricUnit | number
1231
+ @param b MetricUnit | number
1232
+ @param epsilon (MetricUnit | number)?
1233
+ @return boolean
1234
+ ]=]
1235
+ function MetricUnit.fuzzyEq(a: MetricUnit | number, b: MetricUnit | number, epsilon: (MetricUnit | number)?): boolean
1236
+ local aAmount, bAmount, dimensions = MetricUnit._pair(a, b, "compare")
1237
+ local tolerance = 1e-9
1238
+ if epsilon ~= nil then
1239
+ if type(epsilon) == "number" then
1240
+ tolerance = epsilon
1241
+ else
1242
+ tolerance = MetricUnit.expect(epsilon, dimensions, "fuzzyEq's epsilon")
1243
+ end
1244
+ end
1245
+ return math.abs(aAmount - bAmount) <= tolerance
1246
+ end
1247
+
1248
+ --[=[
1249
+ The length an angle sweeps along a circle of `radius`, or, for an angular velocity, the speed along it.
1250
+ Angles never mix with lengths on their own, so this is the one place they meet.
1251
+
1252
+ ```lua
1253
+ MetricUnit.arcLength(MetricUnit.degrees(180), MetricUnit.meters(2)):In("meter") -- 6.283
1254
+ MetricUnit.arcLength(MetricUnit.new(60, "revolutionPerMinute"), MetricUnit.meters(1)):In("meterPerSecond") -- 6.283
1255
+ ```
1256
+
1257
+ @param angular MetricUnit -- an angle, or an angle per time
1258
+ @param radius MetricUnit -- a length
1259
+ @return MetricUnit
1260
+ ]=]
1261
+ function MetricUnit.arcLength(angular: MetricUnit, radius: MetricUnit): MetricUnit
1262
+ MetricUnit._assertUnit(angular, "arcLength")
1263
+ MetricUnit._assertDifference(angular, "take the arc length of")
1264
+ if angular._dimensions.angle ~= 1 then
1265
+ error(
1266
+ string.format(
1267
+ "MetricUnit.arcLength takes an angle or an angle per time, not %s",
1268
+ MetricUnitDimensionUtils.describe(angular._dimensions)
1269
+ ),
1270
+ 2
1271
+ )
1272
+ end
1273
+ local meters = MetricUnit.expect(radius, "length", "arcLength's radius")
1274
+ local dimensions = MetricUnitDimensionUtils.combine(
1275
+ MetricUnitDimensionUtils.combine(angular._dimensions, MetricUnitKinds.ANGLE, -1),
1276
+ MetricUnitKinds.LENGTH,
1277
+ 1
1278
+ )
1279
+ return MetricUnit._new(
1280
+ angular._amount * meters,
1281
+ dimensions,
1282
+ false,
1283
+ MetricUnit._systemOnly(MetricUnit._displayOf(radius))
1284
+ )
1285
+ end
1286
+
1287
+ --[=[
1288
+ The sum of two values of one dimension; an absolute and a difference give an absolute.
1289
+
1290
+ @param a MetricUnit | number
1291
+ @param b MetricUnit | number
1292
+ @return MetricUnit
1293
+ ]=]
1294
+ function MetricUnit.__add(a: MetricUnit | number, b: MetricUnit | number): MetricUnit
1295
+ local aAmount, bAmount, dimensions, aAbsolute, bAbsolute = MetricUnit._pair(a, b, "add")
1296
+ if aAbsolute and bAbsolute then
1297
+ error(
1298
+ string.format(
1299
+ "MetricUnit: cannot add two %ss; subtract them for a difference, or add a difference to one",
1300
+ MetricUnitKinds.describeKind(dimensions, true)
1301
+ ),
1302
+ 2
1303
+ )
1304
+ end
1305
+ local display = MetricUnit._mergeDisplay(MetricUnit._displayOf(a), MetricUnit._displayOf(b))
1306
+ return MetricUnit._new(aAmount + bAmount, dimensions, aAbsolute or bAbsolute, display)
1307
+ end
1308
+
1309
+ --[=[
1310
+ The difference of two values of one dimension; two absolutes give a difference.
1311
+
1312
+ @param a MetricUnit | number
1313
+ @param b MetricUnit | number
1314
+ @return MetricUnit
1315
+ ]=]
1316
+ function MetricUnit.__sub(a: MetricUnit | number, b: MetricUnit | number): MetricUnit
1317
+ local aAmount, bAmount, dimensions, aAbsolute, bAbsolute = MetricUnit._pair(a, b, "subtract")
1318
+ if bAbsolute and not aAbsolute then
1319
+ error(
1320
+ string.format(
1321
+ "MetricUnit: cannot subtract an %s from a %s",
1322
+ MetricUnitKinds.describeKind(dimensions, true),
1323
+ MetricUnitKinds.describeKind(dimensions, false)
1324
+ ),
1325
+ 2
1326
+ )
1327
+ end
1328
+ local display = MetricUnit._mergeDisplay(MetricUnit._displayOf(a), MetricUnit._displayOf(b))
1329
+ return MetricUnit._new(aAmount - bAmount, dimensions, aAbsolute and not bAbsolute, display)
1330
+ end
1331
+
1332
+ --[=[
1333
+ The product, combining dimensions; a plain number scales.
1334
+
1335
+ @param a MetricUnit | number
1336
+ @param b MetricUnit | number
1337
+ @return MetricUnit
1338
+ ]=]
1339
+ function MetricUnit.__mul(a: MetricUnit | number, b: MetricUnit | number): MetricUnit
1340
+ local aAmount, aDimensions = MetricUnit._scalable(a, "multiply")
1341
+ local bAmount, bDimensions = MetricUnit._scalable(b, "multiply")
1342
+ local display = MetricUnit._productDisplay(a, b)
1343
+ return MetricUnit._new(
1344
+ aAmount * bAmount,
1345
+ MetricUnitDimensionUtils.combine(aDimensions, bDimensions, 1),
1346
+ false,
1347
+ display
1348
+ )
1349
+ end
1350
+
1351
+ --[=[
1352
+ The quotient, combining dimensions; a plain number scales, or over a unit inverts it.
1353
+
1354
+ @param a MetricUnit | number
1355
+ @param b MetricUnit | number
1356
+ @return MetricUnit
1357
+ ]=]
1358
+ function MetricUnit.__div(a: MetricUnit | number, b: MetricUnit | number): MetricUnit
1359
+ local aAmount, aDimensions = MetricUnit._scalable(a, "divide")
1360
+ local bAmount, bDimensions = MetricUnit._scalable(b, "divide")
1361
+ local display = MetricUnit._quotientDisplay(a, b)
1362
+ return MetricUnit._new(
1363
+ aAmount / bAmount,
1364
+ MetricUnitDimensionUtils.combine(aDimensions, bDimensions, -1),
1365
+ false,
1366
+ display
1367
+ )
1368
+ end
1369
+
1370
+ --[=[
1371
+ The negation of a difference.
1372
+
1373
+ @param a MetricUnit
1374
+ @return MetricUnit
1375
+ ]=]
1376
+ function MetricUnit.__unm(a: MetricUnit): MetricUnit
1377
+ MetricUnit._assertDifference(a, "negate")
1378
+ return MetricUnit._new(-a._amount, a._dimensions, false, a._display)
1379
+ end
1380
+
1381
+ --[=[
1382
+ Whether `a` equals `b`, both of one kind.
1383
+
1384
+ @param a MetricUnit
1385
+ @param b MetricUnit
1386
+ @return boolean
1387
+ ]=]
1388
+ function MetricUnit.__eq(a: MetricUnit, b: MetricUnit): boolean
1389
+ local aAmount, bAmount = MetricUnit._pair(a, b, "compare")
1390
+ return aAmount == bAmount
1391
+ end
1392
+
1393
+ --[=[
1394
+ Whether `a` is less than `b`, both of one kind.
1395
+
1396
+ @param a MetricUnit
1397
+ @param b MetricUnit
1398
+ @return boolean
1399
+ ]=]
1400
+ function MetricUnit.__lt(a: MetricUnit, b: MetricUnit): boolean
1401
+ local aAmount, bAmount = MetricUnit._pair(a, b, "compare")
1402
+ return aAmount < bAmount
1403
+ end
1404
+
1405
+ --[=[
1406
+ Whether `a` is at most `b`, both of one kind.
1407
+
1408
+ @param a MetricUnit
1409
+ @param b MetricUnit
1410
+ @return boolean
1411
+ ]=]
1412
+ function MetricUnit.__le(a: MetricUnit, b: MetricUnit): boolean
1413
+ local aAmount, bAmount = MetricUnit._pair(a, b, "compare")
1414
+ return aAmount <= bAmount
1415
+ end
1416
+
1417
+ --[=[
1418
+ [MetricUnit.format] in English with four significant digits, the same in every locale, for logs and test failures.
1419
+ An instant is written as its time since the epoch, such as "epoch + 1 minute, 30 seconds".
1420
+
1421
+ @return string
1422
+ ]=]
1423
+ function MetricUnit.__tostring(self: MetricUnit): string
1424
+ local text = MetricUnitFormatUtils.format(self, TOSTRING_FORMAT_OPTIONS)
1425
+ if self._absolute and MetricUnitDimensionUtils.equal(self._dimensions, MetricUnitKinds.TIME) then
1426
+ return "epoch + " .. text
1427
+ end
1428
+ return text
1429
+ end
1430
+
1431
+ function MetricUnit._new(
1432
+ amount: number,
1433
+ dimensions: Dimensions,
1434
+ absolute: boolean,
1435
+ display: DisplayPreference?
1436
+ ): MetricUnit
1437
+ local unit: MetricUnit = setmetatable({} :: any, MetricUnit)
1438
+ unit._amount = amount
1439
+ unit._dimensions = dimensions
1440
+ unit._absolute = absolute
1441
+ unit._display = display
1442
+ table.freeze(unit :: any)
1443
+ return unit
1444
+ end
1445
+ function MetricUnit._fromUnit(n: number, unit: string | MetricUnitDefinitions.UnitDefinition, name: string): MetricUnit
1446
+ local definition = MetricUnit._definitionFor(n, unit, name, 4)
1447
+ local display = MetricUnit._displayOfUnit(definition)
1448
+ local scaleZero = definition.zero
1449
+ if scaleZero ~= nil then
1450
+ return MetricUnit._new(n * definition.factor + scaleZero, definition.dimensions, true, display)
1451
+ end
1452
+ return MetricUnit._new(n * definition.factor, definition.dimensions, false, display)
1453
+ end
1454
+
1455
+ function MetricUnit._displayOfUnit(definition: MetricUnitDefinitions.UnitDefinition): DisplayPreference?
1456
+ local system = MetricUnitSystems.getSystemOfUnit(definition)
1457
+ if system == nil or rawequal(system, MetricUnitSystems.METRIC) then
1458
+ return nil
1459
+ end
1460
+ return table.freeze({ system = system })
1461
+ end
1462
+
1463
+ function MetricUnit._getUnit(
1464
+ unit: string | MetricUnitDefinitions.UnitDefinition,
1465
+ system: System?
1466
+ ): MetricUnitDefinitions.UnitDefinition?
1467
+ if system ~= nil and not MetricUnitSystem.isSystem(system) then
1468
+ error(string.format("Bad system, expected a MetricUnitSystem, not %s", typeof(system)), 4)
1469
+ end
1470
+ return if system then system:GetUnit(unit) else MetricUnitDefinitions.getUnit(unit)
1471
+ end
1472
+ function MetricUnit._definitionFor(
1473
+ n: number,
1474
+ unit: any,
1475
+ name: string,
1476
+ errorLevel: number
1477
+ ): MetricUnitDefinitions.UnitDefinition
1478
+ if type(n) ~= "number" then
1479
+ error(string.format("MetricUnit.%s takes a number, not %s", name, typeof(n)), errorLevel)
1480
+ end
1481
+ local definition = MetricUnit._getUnit(unit)
1482
+ if not definition then
1483
+ error(string.format("MetricUnit.%s knows no unit %q", name, tostring(unit)), errorLevel)
1484
+ end
1485
+ return definition
1486
+ end
1487
+
1488
+ function MetricUnit._assertNumber(n: any, name: string)
1489
+ if type(n) ~= "number" then
1490
+ error(string.format("MetricUnit.%s takes a number, not %s", name, typeof(n)), 4)
1491
+ end
1492
+ end
1493
+
1494
+ function MetricUnit._assertUnit(value: any, name: string)
1495
+ if not MetricUnit.isUnit(value) then
1496
+ error(string.format("MetricUnit.%s takes a MetricUnit, not %s", name, typeof(value)), 4)
1497
+ end
1498
+ end
1499
+
1500
+ function MetricUnit._assertDifference(value: MetricUnit, verb: string)
1501
+ if value._absolute then
1502
+ error(
1503
+ string.format(
1504
+ "MetricUnit: cannot %s an %s; subtract another to get a difference",
1505
+ verb,
1506
+ MetricUnitKinds.describeKind(value._dimensions, true)
1507
+ ),
1508
+ 4
1509
+ )
1510
+ end
1511
+ end
1512
+
1513
+ function MetricUnit._readUnit(self: MetricUnit, definition: MetricUnitDefinitions.UnitDefinition): number
1514
+ if not MetricUnitDimensionUtils.equal(self._dimensions, definition.dimensions) then
1515
+ error(
1516
+ string.format(
1517
+ "MetricUnit:In(%q) reads %s, not %s",
1518
+ definition.id,
1519
+ MetricUnitDimensionUtils.describe(definition.dimensions),
1520
+ MetricUnitKinds.describeKind(self._dimensions, self._absolute)
1521
+ ),
1522
+ 3
1523
+ )
1524
+ end
1525
+ local fromZero = if self._absolute then self._amount - (definition.zero or 0) else self._amount
1526
+ return fromZero / definition.factor
1527
+ end
1528
+
1529
+ function MetricUnit._snap(
1530
+ self: MetricUnit,
1531
+ step: UnitLike | MetricUnit,
1532
+ snap: (number) -> number,
1533
+ name: string
1534
+ ): MetricUnit
1535
+ MetricUnit._assertUnit(self, name)
1536
+ if not MetricUnit.isUnit(step) then
1537
+ local definition = MetricUnit._getUnit(step :: UnitLike, self:GetDisplaySystem())
1538
+ if not definition then
1539
+ error(string.format("MetricUnit.%s knows no unit %q", name, tostring(step)), 3)
1540
+ end
1541
+ local count = snap(MetricUnit._readUnit(self, definition))
1542
+ local amount = if self._absolute
1543
+ then count * definition.factor + (definition.zero or 0)
1544
+ else count * definition.factor
1545
+ return MetricUnit._new(amount, self._dimensions, self._absolute, self._display)
1546
+ end
1547
+
1548
+ MetricUnit._assertUnit(step, name)
1549
+ local unit = step :: MetricUnit
1550
+ MetricUnit._assertDifference(unit, "round to")
1551
+ if not MetricUnitDimensionUtils.equal(unit._dimensions, self._dimensions) or unit._amount <= 0 then
1552
+ error(
1553
+ string.format(
1554
+ "MetricUnit.%s takes a positive step of %s, not %s",
1555
+ name,
1556
+ MetricUnitDimensionUtils.describe(self._dimensions),
1557
+ MetricUnitDimensionUtils.describe(unit._dimensions)
1558
+ ),
1559
+ 3
1560
+ )
1561
+ end
1562
+ local scaleZero = if self._absolute then MetricUnit._displayScaleZero(self) else 0
1563
+ return MetricUnit._new(
1564
+ snap((self._amount - scaleZero) / unit._amount) * unit._amount + scaleZero,
1565
+ self._dimensions,
1566
+ self._absolute,
1567
+ self._display
1568
+ )
1569
+ end
1570
+
1571
+ function MetricUnit._displayScaleZero(self: MetricUnit): number
1572
+ local display = self._display
1573
+ local pinned = if display then display.unit else nil
1574
+ if pinned then
1575
+ return pinned.zero or 0
1576
+ end
1577
+ local units = MetricUnitDisplayUnitUtils.getDisplayUnits(self._dimensions, self:GetDisplaySystem(), true)
1578
+ local scale = if units then units[1] else nil
1579
+ return if scale and scale.zero then scale.zero else 0
1580
+ end
1581
+
1582
+ function MetricUnit._dimensionsOf(value: MetricUnit | number): Dimensions
1583
+ if type(value) == "number" then
1584
+ return MetricUnitDimensionUtils.NONE
1585
+ end
1586
+ MetricUnit._assertUnit(value, "sameDimension")
1587
+ return value._dimensions
1588
+ end
1589
+
1590
+ function MetricUnit._scalable(value: MetricUnit | number, verb: string): (number, Dimensions)
1591
+ if type(value) == "number" then
1592
+ return value, MetricUnitDimensionUtils.NONE
1593
+ end
1594
+ MetricUnit._assertUnit(value, verb)
1595
+ MetricUnit._assertDifference(value, verb)
1596
+ return value._amount, value._dimensions
1597
+ end
1598
+
1599
+ function MetricUnit._pair(
1600
+ a: MetricUnit | number,
1601
+ b: MetricUnit | number,
1602
+ verb: string
1603
+ ): (number, number, Dimensions, boolean, boolean)
1604
+ if type(a) == "number" and type(b) == "number" then
1605
+ return a, b, MetricUnitDimensionUtils.NONE, false, false
1606
+ elseif type(a) == "number" then
1607
+ MetricUnit._assertUnit(b, verb)
1608
+ local unit = b :: MetricUnit
1609
+ return a, unit._amount, MetricUnit._numberDimensions(a, unit, verb), false, unit._absolute
1610
+ elseif type(b) == "number" then
1611
+ MetricUnit._assertUnit(a, verb)
1612
+ return a._amount, b, MetricUnit._numberDimensions(b, a, verb), a._absolute, false
1613
+ end
1614
+ MetricUnit._assertUnit(a, verb)
1615
+ MetricUnit._assertUnit(b, verb)
1616
+ if not MetricUnitDimensionUtils.equal(a._dimensions, b._dimensions) then
1617
+ error(
1618
+ string.format(
1619
+ "MetricUnit: cannot %s %s and %s",
1620
+ verb,
1621
+ MetricUnitKinds.describeKind(a._dimensions, a._absolute),
1622
+ MetricUnitKinds.describeKind(b._dimensions, b._absolute)
1623
+ ),
1624
+ 3
1625
+ )
1626
+ end
1627
+ if verb == "compare" and a._absolute ~= b._absolute then
1628
+ error(
1629
+ string.format(
1630
+ "MetricUnit: cannot compare an %s and a %s",
1631
+ MetricUnitKinds.describeKind(a._dimensions, true),
1632
+ MetricUnitKinds.describeKind(a._dimensions, false)
1633
+ ),
1634
+ 3
1635
+ )
1636
+ end
1637
+ return a._amount, b._amount, a._dimensions, a._absolute, b._absolute
1638
+ end
1639
+
1640
+ function MetricUnit._numberDimensions(n: number, unit: MetricUnit, verb: string): Dimensions
1641
+ if n == 0 and not (verb == "compare" and unit._absolute) then
1642
+ return unit._dimensions
1643
+ elseif next(unit._dimensions) == nil and not unit._absolute then
1644
+ return unit._dimensions
1645
+ end
1646
+ error(
1647
+ string.format(
1648
+ "MetricUnit: cannot %s %s and the plain number %s",
1649
+ verb,
1650
+ MetricUnitKinds.describeKind(unit._dimensions, unit._absolute),
1651
+ tostring(n)
1652
+ ),
1653
+ 4
1654
+ )
1655
+ end
1656
+
1657
+ function MetricUnit._extreme(name: string, pick: (number, number) -> number, ...: MetricUnit | number): MetricUnit
1658
+ local count = select("#", ...)
1659
+ if count == 0 then
1660
+ error(string.format("MetricUnit.%s takes at least one value", name), 3)
1661
+ end
1662
+ local reference: MetricUnit | number = 0
1663
+ for index = 1, count do
1664
+ local value = select(index, ...)
1665
+ if value ~= 0 then
1666
+ reference = value
1667
+ break
1668
+ end
1669
+ end
1670
+ local bestAmount, _, dimensions, absolute = MetricUnit._pair(select(1, ...), reference, "compare")
1671
+ local display = MetricUnit._displayOf(select(1, ...))
1672
+ for index = 2, count do
1673
+ local value = select(index, ...)
1674
+ local amount = MetricUnit._pair(value, reference, "compare")
1675
+ bestAmount = pick(bestAmount, amount)
1676
+ display = MetricUnit._mergeDisplay(display, MetricUnit._displayOf(value))
1677
+ end
1678
+ return MetricUnit._new(bestAmount, dimensions, absolute, display)
1679
+ end
1680
+
1681
+ function MetricUnit._displayOf(value: any): DisplayPreference?
1682
+ return if MetricUnit.isUnit(value) then value._display else nil
1683
+ end
1684
+
1685
+ function MetricUnit._mergeDisplay(a: DisplayPreference?, b: DisplayPreference?): DisplayPreference?
1686
+ if a == nil then
1687
+ return b
1688
+ elseif b == nil or (a.system == b.system and a.unit == b.unit) then
1689
+ return a
1690
+ end
1691
+ return MetricUnit._sharedSystem(a, b)
1692
+ end
1693
+
1694
+ function MetricUnit._productDisplay(a: any, b: any): DisplayPreference?
1695
+ if type(b) == "number" then
1696
+ return MetricUnit._displayOf(a)
1697
+ elseif type(a) == "number" then
1698
+ return MetricUnit._displayOf(b)
1699
+ end
1700
+ return MetricUnit._newDimensionDisplay(a, b)
1701
+ end
1702
+
1703
+ function MetricUnit._quotientDisplay(a: any, b: any): DisplayPreference?
1704
+ if type(b) == "number" then
1705
+ return MetricUnit._displayOf(a)
1706
+ end
1707
+ return MetricUnit._newDimensionDisplay(a, b)
1708
+ end
1709
+
1710
+ function MetricUnit._newDimensionDisplay(a: any, b: any): DisplayPreference?
1711
+ local aDisplay = MetricUnit._systemOnly(MetricUnit._displayOf(a))
1712
+ local bDisplay = MetricUnit._systemOnly(MetricUnit._displayOf(b))
1713
+ if aDisplay == nil then
1714
+ return bDisplay
1715
+ elseif bDisplay == nil then
1716
+ return aDisplay
1717
+ end
1718
+ return MetricUnit._sharedSystem(aDisplay, bDisplay)
1719
+ end
1720
+
1721
+ function MetricUnit._sharedSystem(a: DisplayPreference, b: DisplayPreference): DisplayPreference?
1722
+ return if a.system ~= nil and a.system == b.system then table.freeze({ system = a.system }) else nil
1723
+ end
1724
+
1725
+ function MetricUnit._systemOnly(display: DisplayPreference?): DisplayPreference?
1726
+ local system = if display then display.system else nil
1727
+ return if system then table.freeze({ system = system }) else nil
1728
+ end
1729
+
1730
+ return MetricUnit