@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,362 @@
1
+ --!strict
2
+ --[[
3
+ @class MetricUnit.spec.lua
4
+ ]]
5
+
6
+ local require = require(script.Parent.loader).load(script)
7
+
8
+ local Jest = require("Jest")
9
+ local MetricUnit = require("MetricUnit")
10
+
11
+ local DAY = MetricUnit.days(1):InSeconds()
12
+ local MONTH = MetricUnit.months(1):InSeconds()
13
+ local YEAR = MetricUnit.years(1):InSeconds()
14
+
15
+ local describe = Jest.Globals.describe
16
+ local expect = Jest.Globals.expect
17
+ local it = Jest.Globals.it
18
+
19
+ type MetricUnit = MetricUnit.MetricUnit
20
+
21
+ local DAYS_A_YEAR = YEAR / DAY
22
+
23
+ describe("MetricUnit constructors and readers", function()
24
+ it("reads every time unit back", function()
25
+ expect(MetricUnit.seconds(90):InSeconds()).toBeCloseTo(90, 9)
26
+ expect(MetricUnit.minutes(90):In("minute")).toBeCloseTo(90, 9)
27
+ expect(MetricUnit.hours(36):In("hour")).toBeCloseTo(36, 9)
28
+ expect(MetricUnit.days(3):In("day")).toBe(3)
29
+ expect(MetricUnit.weeks(2):In("week")).toBe(2)
30
+ expect(MetricUnit.months(18):In("month")).toBeCloseTo(18, 9)
31
+ expect(MetricUnit.years(2):In("year")).toBe(2)
32
+ end)
33
+
34
+ it("counts a year and a month by their definitions", function()
35
+ expect(MetricUnit.hours(36):In("day")).toBeCloseTo(1.5, 9)
36
+ expect(MetricUnit.seconds(86_400):In("day")).toBeCloseTo(1, 9)
37
+ expect(MetricUnit.minutes(60):In("hour")).toBeCloseTo(1, 9)
38
+ expect(MetricUnit.weeks(1):In("day")).toBe(7)
39
+ expect(MetricUnit.years(1):In("day")).toBe(DAYS_A_YEAR)
40
+ expect(MetricUnit.months(12):In("year")).toBeCloseTo(1, 9)
41
+ end)
42
+
43
+ it("makes twelve averaged months exactly one averaged year", function()
44
+ expect(MetricUnit.months(12) == MetricUnit.years(1)).toBe(true)
45
+ expect(MetricUnit.years(1) / 12 == MetricUnit.months(1)).toBe(true)
46
+ expect(MONTH * 12).toBe(YEAR)
47
+ end)
48
+
49
+ it("reads every length unit back", function()
50
+ expect(MetricUnit.millimeters(5):In("millimeter")).toBeCloseTo(5, 9)
51
+ expect(MetricUnit.new(5, "centimeter"):In("centimeter")).toBeCloseTo(5, 9)
52
+ expect(MetricUnit.meters(500):In("meter")).toBeCloseTo(500, 9)
53
+ expect(MetricUnit.kilometers(2):In("kilometer")).toBe(2)
54
+ expect(MetricUnit.meters(500):In("kilometer")).toBeCloseTo(0.5, 9)
55
+ expect(MetricUnit.new(100, "centimeter"):In("meter")).toBeCloseTo(1, 9)
56
+ expect(MetricUnit.millimeters(10):In("centimeter")).toBeCloseTo(1, 9)
57
+ end)
58
+
59
+ it("reads every memory unit back, a kilobyte being 1,000 bytes", function()
60
+ expect(MetricUnit.bytes(512):In("byte")).toBe(512)
61
+ expect(MetricUnit.kilobytes(1_600):In("kilobyte")).toBe(1_600)
62
+ expect(MetricUnit.megabytes(1.6):In("megabyte")).toBeCloseTo(1.6, 9)
63
+ expect(MetricUnit.kilobytes(1_600):In("megabyte")).toBeCloseTo(1.6, 9)
64
+ expect(MetricUnit.kilobytes(1):In("byte")).toBe(1_000)
65
+ end)
66
+
67
+ it("reads every mass unit back", function()
68
+ expect(MetricUnit.grams(250):In("gram")).toBeCloseTo(250, 9)
69
+ expect(MetricUnit.kilograms(3):In("kilogram")).toBe(3)
70
+ expect(MetricUnit.tonnes(2):In("tonne")).toBe(2)
71
+ expect(MetricUnit.tonnes(2):In("kilogram")).toBe(2_000)
72
+ expect(MetricUnit.grams(500):In("kilogram")).toBeCloseTo(0.5, 9)
73
+ end)
74
+
75
+ it("reads every rate back", function()
76
+ expect(MetricUnit.perSecond(2):In("perSecond")).toBeCloseTo(2, 9)
77
+ expect(MetricUnit.perHour(2):In("perHour")).toBeCloseTo(2, 9)
78
+ expect(MetricUnit.perDay(2):In("perDay")).toBe(2)
79
+ expect(MetricUnit.new(2, "perWeek"):In("perWeek")).toBeCloseTo(2, 9)
80
+ expect(MetricUnit.new(2, "perMonth"):In("perMonth")).toBeCloseTo(2, 9)
81
+ expect(MetricUnit.perYear(0.03):In("perYear")).toBeCloseTo(0.03, 12)
82
+ expect(MetricUnit.perYear(DAYS_A_YEAR):In("perDay")).toBeCloseTo(1, 9)
83
+ expect(MetricUnit.perDay(1):In("perHour")).toBeCloseTo(1 / 24, 9)
84
+ end)
85
+
86
+ it("errors when a reader meets another dimension, naming both", function()
87
+ expect(function()
88
+ MetricUnit.kilometers(1):In("day")
89
+ end).toThrow('In("day") reads time, not length')
90
+ expect(function()
91
+ MetricUnit.days(1):In("perYear")
92
+ end).toThrow('In("perYear") reads 1/time, not time')
93
+ expect(function()
94
+ (MetricUnit.kilometers(1) / MetricUnit.days(1)):In("kilometer")
95
+ end).toThrow('In("kilometer") reads length, not length/time')
96
+ expect(function()
97
+ MetricUnit.kilograms(1):In("kilobyte")
98
+ end).toThrow('In("kilobyte") reads memory, not mass')
99
+ end)
100
+
101
+ it("errors when a constructor is given something other than a number", function()
102
+ expect(function()
103
+ MetricUnit.kilometers("2" :: any)
104
+ end).toThrow("MetricUnit.kilometers takes a number, not string")
105
+ end)
106
+ end)
107
+
108
+ describe("MetricUnit composition", function()
109
+ it("reads km a year as km a day", function()
110
+ local speed = MetricUnit.kilometers(20) / MetricUnit.years(1)
111
+ expect(speed:In(MetricUnit.kilometers(1) / MetricUnit.days(1))).toBeCloseTo(20 / DAYS_A_YEAR, 12)
112
+ expect(MetricUnit.isKind(speed, "speed")).toBe(true)
113
+ end)
114
+
115
+ it("reads a speed in km/h with In", function()
116
+ local speed = MetricUnit.meters(10) / MetricUnit.seconds(1)
117
+ expect(speed:In(MetricUnit.kilometers(1) / MetricUnit.hours(1))).toBeCloseTo(36, 9)
118
+ end)
119
+
120
+ it("errors when In meets another dimension", function()
121
+ expect(function()
122
+ MetricUnit.kilometers(1):In(MetricUnit.days(1))
123
+ end).toThrow("In cannot read length in time")
124
+ end)
125
+
126
+ it("cancels to dimensionless and reads it with In", function()
127
+ local ratio = MetricUnit.kilometers(3) / MetricUnit.meters(1_500)
128
+ expect(ratio:In(MetricUnit.none(1))).toBeCloseTo(2, 9)
129
+ expect(MetricUnit.sameDimension(ratio, MetricUnit.none(1))).toBe(true)
130
+ end)
131
+
132
+ it("makes a rate from a number over a time", function()
133
+ local rate = 0.06 / MetricUnit.years(1)
134
+ expect(MetricUnit.isKind(rate, "rate")).toBe(true)
135
+ expect(rate:In("perYear")).toBeCloseTo(0.06, 12)
136
+ expect(MetricUnit.fuzzyEq(rate, MetricUnit.perYear(0.06))).toBe(true)
137
+ end)
138
+
139
+ it("multiplies a rate by a time to a dimensionless count", function()
140
+ local count = MetricUnit.perDay(3) * MetricUnit.weeks(1)
141
+ expect(count:In(MetricUnit.none(1))).toBeCloseTo(21, 9)
142
+ end)
143
+ end)
144
+
145
+ describe("MetricUnit operators", function()
146
+ it("scales by a number on either side", function()
147
+ expect((MetricUnit.years(1) / 4):In("day")).toBeCloseTo(DAYS_A_YEAR / 4, 9)
148
+ expect((2 * MetricUnit.kilometers(3)):In("kilometer")).toBe(6)
149
+ expect((MetricUnit.kilometers(3) * 2):In("kilometer")).toBe(6)
150
+ end)
151
+
152
+ it("inverts a unit divided into a number", function()
153
+ local rate = 2 / MetricUnit.days(4)
154
+ expect(rate:In("perDay")).toBeCloseTo(0.5, 9)
155
+ end)
156
+
157
+ it("adds and subtracts one dimension", function()
158
+ expect((MetricUnit.kilometers(1) + MetricUnit.meters(500)):In("meter")).toBeCloseTo(1_500, 9)
159
+ expect((MetricUnit.years(1) - MetricUnit.days(0.25)):In("day")).toBeCloseTo(DAYS_A_YEAR - 0.25, 9)
160
+ end)
161
+
162
+ it("errors adding or subtracting different dimensions, naming both", function()
163
+ expect(function()
164
+ local _ = MetricUnit.kilometers(1) + MetricUnit.days(1)
165
+ end).toThrow("cannot add length and time")
166
+ expect(function()
167
+ local _ = MetricUnit.kilometers(1) - MetricUnit.perDay(1)
168
+ end).toThrow("cannot subtract length and 1/time")
169
+ end)
170
+
171
+ it("errors adding a plain number other than 0 to a dimensioned unit", function()
172
+ expect(function()
173
+ local _ = MetricUnit.kilometers(1) + 2
174
+ end).toThrow("cannot add length and the plain number 2")
175
+ end)
176
+
177
+ it("adds a plain number to a dimensionless unit", function()
178
+ expect((MetricUnit.none(1) + 2):In(MetricUnit.none(1))).toBe(3)
179
+ end)
180
+
181
+ it("takes a literal 0 as zero of the other side", function()
182
+ local function growth(x: boolean): MetricUnit | number
183
+ return if x then MetricUnit.perYear(0.05) else 0
184
+ end
185
+ expect((MetricUnit.perYear(0.01) + growth(false)):In("perYear")).toBeCloseTo(0.01, 12)
186
+ expect((growth(false) + MetricUnit.perYear(0.01)):In("perYear")).toBeCloseTo(0.01, 12)
187
+ expect((MetricUnit.kilometers(2) - 0):In("kilometer")).toBe(2)
188
+ expect((0 - MetricUnit.kilometers(2)):In("kilometer")).toBe(-2)
189
+ end)
190
+
191
+ it("negates", function()
192
+ expect((-MetricUnit.kilometers(2)):In("kilometer")).toBe(-2)
193
+ end)
194
+
195
+ it("compares one dimension", function()
196
+ expect(MetricUnit.kilometers(1) < MetricUnit.meters(1_500)).toBe(true)
197
+ expect(MetricUnit.kilometers(1) <= MetricUnit.meters(1_000)).toBe(true)
198
+ expect(MetricUnit.kilometers(2) > MetricUnit.meters(1_500)).toBe(true)
199
+ expect(MetricUnit.hours(24) == MetricUnit.days(1)).toBe(true)
200
+ expect(MetricUnit.hours(23) == MetricUnit.days(1)).toBe(false)
201
+ end)
202
+
203
+ it("errors comparing different dimensions, naming both", function()
204
+ expect(function()
205
+ local _ = MetricUnit.kilometers(1) < MetricUnit.days(1)
206
+ end).toThrow("cannot compare length and time")
207
+ expect(function()
208
+ local _ = MetricUnit.kilometers(1) <= MetricUnit.kilograms(1)
209
+ end).toThrow("cannot compare length and mass")
210
+ expect(function()
211
+ local _ = MetricUnit.kilometers(1) == MetricUnit.days(1)
212
+ end).toThrow("cannot compare length and time")
213
+ end)
214
+
215
+ it("is frozen, every operation giving a new value", function()
216
+ local a = MetricUnit.kilometers(1)
217
+ local b = a + MetricUnit.kilometers(1)
218
+ expect(table.isfrozen(a :: any)).toBe(true)
219
+ expect(table.isfrozen(b :: any)).toBe(true)
220
+ expect(a:In("kilometer")).toBe(1)
221
+ expect(function()
222
+ (a :: any)._amount = 5
223
+ end).toThrow()
224
+ end)
225
+ end)
226
+
227
+ describe("MetricUnit helpers", function()
228
+ it("takes the min, max and clamp of one dimension, a literal 0 among them", function()
229
+ expect(MetricUnit.min(MetricUnit.kilometers(2), MetricUnit.meters(500)):In("meter")).toBeCloseTo(500, 9)
230
+ expect(
231
+ MetricUnit.max(MetricUnit.kilometers(2), MetricUnit.meters(500), MetricUnit.kilometers(3)):In("kilometer")
232
+ ).toBe(3)
233
+ expect(MetricUnit.max(0, MetricUnit.kilometers(-2)):In("kilometer")).toBe(0)
234
+ expect(MetricUnit.min(0, MetricUnit.kilometers(2)):In("kilometer")).toBe(0)
235
+ expect(MetricUnit.clamp(MetricUnit.kilometers(5), 0, MetricUnit.kilometers(3)):In("kilometer")).toBe(3)
236
+ expect(MetricUnit.clamp(MetricUnit.kilometers(-5), 0, MetricUnit.kilometers(3)):In("kilometer")).toBe(0)
237
+ end)
238
+
239
+ it("errors taking the min of different dimensions", function()
240
+ expect(function()
241
+ MetricUnit.min(MetricUnit.kilometers(1), MetricUnit.days(1))
242
+ end).toThrow("cannot compare")
243
+ end)
244
+
245
+ it("takes the absolute value", function()
246
+ expect(MetricUnit.abs(MetricUnit.kilometers(-2)):In("kilometer")).toBe(2)
247
+ end)
248
+
249
+ it("takes the square root, halving exponents", function()
250
+ local area = MetricUnit.kilometers(3) * MetricUnit.kilometers(3)
251
+ expect(MetricUnit.sqrt(area):In("kilometer")).toBeCloseTo(3, 9)
252
+ local spread = MetricUnit.kilometers(2) * MetricUnit.kilometers(2) / MetricUnit.days(1)
253
+ expect(MetricUnit.sqrt(spread * MetricUnit.days(4)):In("kilometer")).toBeCloseTo(4, 9)
254
+ end)
255
+
256
+ it("errors taking the square root of an odd exponent", function()
257
+ expect(function()
258
+ MetricUnit.sqrt(MetricUnit.kilometers(4))
259
+ end).toThrow("length to the power 0.5 has a fractional exponent")
260
+ end)
261
+
262
+ it("raises to a whole power", function()
263
+ local volume = MetricUnit.pow(MetricUnit.meters(2), 3)
264
+ expect(volume:In(MetricUnit.pow(MetricUnit.meters(1), 3))).toBeCloseTo(8, 9)
265
+ expect(MetricUnit.pow(MetricUnit.days(2), -1):In("perDay")).toBeCloseTo(0.5, 9)
266
+ expect(MetricUnit.pow(MetricUnit.days(2), 0):In(MetricUnit.none(1))).toBe(1)
267
+ expect(function()
268
+ MetricUnit.pow(MetricUnit.days(2), 0.5)
269
+ end).toThrow("whole power")
270
+ end)
271
+
272
+ it("tells units, zeros, signs and dimensions apart", function()
273
+ expect(MetricUnit.isUnit(MetricUnit.kilometers(1))).toBe(true)
274
+ expect(MetricUnit.isUnit(1)).toBe(false)
275
+ expect(MetricUnit.isUnit({})).toBe(false)
276
+ expect(MetricUnit.isZero(MetricUnit.kilometers(0))).toBe(true)
277
+ expect(MetricUnit.isZero(0)).toBe(true)
278
+ expect(MetricUnit.isZero(MetricUnit.kilometers(1))).toBe(false)
279
+ expect(MetricUnit.sign(MetricUnit.kilometers(-3))).toBe(-1)
280
+ expect(MetricUnit.sign(MetricUnit.kilometers(0))).toBe(0)
281
+ expect(MetricUnit.sameDimension(MetricUnit.kilometers(1), MetricUnit.meters(1))).toBe(true)
282
+ expect(MetricUnit.sameDimension(MetricUnit.kilometers(1), MetricUnit.days(1))).toBe(false)
283
+ expect(MetricUnit.sameDimension(MetricUnit.kilometers(1), 0)).toBe(true)
284
+ expect(MetricUnit.sameDimension(MetricUnit.kilometers(1), 2)).toBe(false)
285
+ end)
286
+
287
+ it("compares within a tolerance", function()
288
+ expect(MetricUnit.fuzzyEq(MetricUnit.hours(24), MetricUnit.days(1))).toBe(true)
289
+ expect(MetricUnit.fuzzyEq(MetricUnit.kilometers(1), MetricUnit.meters(1_001))).toBe(false)
290
+ expect(MetricUnit.fuzzyEq(MetricUnit.kilometers(1), MetricUnit.meters(1_001), MetricUnit.meters(2))).toBe(true)
291
+ expect(function()
292
+ MetricUnit.fuzzyEq(MetricUnit.kilometers(1), MetricUnit.days(1))
293
+ end).toThrow("cannot compare length and time")
294
+ end)
295
+
296
+ it("checks dimensions for API boundaries", function()
297
+ expect(MetricUnit.isKind(MetricUnit.days(1), "time")).toBe(true)
298
+ expect(MetricUnit.isKind(MetricUnit.kilometers(1), "time")).toBe(false)
299
+ expect(MetricUnit.isKind(1, "time")).toBe(false)
300
+ expect(MetricUnit.isKind(MetricUnit.kilometers(1), "length")).toBe(true)
301
+ expect(MetricUnit.isKind(MetricUnit.perYear(1), "rate")).toBe(true)
302
+ expect(MetricUnit.isKind(MetricUnit.kilometers(1) / MetricUnit.days(1), "speed")).toBe(true)
303
+ expect(MetricUnit.isKind(MetricUnit.perYear(1), "speed")).toBe(false)
304
+ expect(MetricUnit.isKind(MetricUnit.kilobytes(1), "memory")).toBe(true)
305
+ expect(MetricUnit.isKind(MetricUnit.kilograms(1), "mass")).toBe(true)
306
+ end)
307
+ end)
308
+
309
+ describe("MetricUnit.expect", function()
310
+ it("returns the amount in SI base units", function()
311
+ expect(MetricUnit.expect(MetricUnit.kilometers(0.5), "length", "reach")).toBe(500)
312
+ expect(MetricUnit.expect(MetricUnit.years(1), "time", "every")).toBe(YEAR)
313
+ expect(MetricUnit.expect(MetricUnit.new(60, "perMinute"), "rate", "rate")).toBeCloseTo(1, 9)
314
+ expect(MetricUnit.expect(MetricUnit.kilometers(36) / MetricUnit.hours(1), "speed", "speed")).toBeCloseTo(10, 9)
315
+ expect(MetricUnit.expect(MetricUnit.kilobytes(2), "memory", "memory")).toBe(2_000)
316
+ expect(MetricUnit.expect(MetricUnit.kilometers(1) * MetricUnit.kilometers(1), { length = 2 }, "area")).toBe(1e6)
317
+ end)
318
+
319
+ it("takes a literal 0 as zero of any dimension", function()
320
+ expect(MetricUnit.expect(0, "length", "reach")).toBe(0)
321
+ expect(MetricUnit.expect(0, "rate", "rate")).toBe(0)
322
+ end)
323
+
324
+ it("names the slot, the dimension expected and what was given", function()
325
+ expect(function()
326
+ MetricUnit.expect(MetricUnit.days(1), "length", "reach")
327
+ end).toThrow("MetricUnit.expect: reach expects length, got time")
328
+ expect(function()
329
+ MetricUnit.expect(2, "rate", "rate")
330
+ end).toThrow("MetricUnit.expect: rate expects 1/time, got the plain number 2")
331
+ expect(function()
332
+ MetricUnit.expect("far" :: any, "length", "reach")
333
+ end).toThrow("MetricUnit.expect: reach expects length, got string")
334
+ expect(function()
335
+ MetricUnit.expect(MetricUnit.kilometers(1), "speed", "speed")
336
+ end).toThrow("speed expects length/time, got length")
337
+ end)
338
+
339
+ it("errors on an unknown dimension name", function()
340
+ expect(function()
341
+ MetricUnit.expect(MetricUnit.kilometers(1), "distance" :: any, "reach")
342
+ end).toThrow('no kind named "distance"')
343
+ end)
344
+ end)
345
+
346
+ describe("MetricUnit data", function()
347
+ it("round trips through plain data", function()
348
+ local speed = MetricUnit.kilometers(20) / MetricUnit.years(1)
349
+ local data = MetricUnit.toData(speed)
350
+ expect(data).toEqual({ amount = 20_000 / YEAR, dimensions = { length = 1, time = -1 } })
351
+ expect(table.isfrozen(data)).toBe(false)
352
+ local back = MetricUnit.fromData(data)
353
+ expect(back == speed).toBe(true)
354
+ expect(MetricUnit.isKind(back, "speed")).toBe(true)
355
+ end)
356
+
357
+ it("errors on data that is not a unit", function()
358
+ expect(function()
359
+ MetricUnit.fromData({ amount = 1, dimensions = { length = 0.5 } })
360
+ end).toThrow("MetricUnit.fromData takes")
361
+ end)
362
+ end)
@@ -1,15 +1,47 @@
1
1
  --!strict
2
2
  --[=[
3
- Provides ways to convert studs to metric and back.
3
+ Provides ways to convert studs to metric and back, at 3.57 studs to the metre as these functions have always
4
+ counted them. Each is a plain multiplication, by a factor [MetricUnit] works out once, so they are cheap enough
5
+ to call every frame; for typed values, or Roblox's own 28 cm stud, use [MetricUnit] directly.
4
6
  @class MetricUtils
5
7
  ]=]
6
8
 
9
+ local require = require(script.Parent.loader).load(script)
10
+
11
+ local MetricUnit = require("MetricUnit")
12
+ local MetricUnitSystems = require("MetricUnitSystems")
13
+
7
14
  local MetricUtils = {}
8
15
 
9
- local STUDS_PER_METER = 3.57
10
- local SECONDS_PER_HOUR = 3600
11
- local SPEED_OF_SOUND_IN_METERS_PER_SECOND = 343
12
- local KPH_TO_MPH = 0.621371192
16
+ type Factors = {
17
+ secondsOfSoundPerStud: number,
18
+ kilometersPerStud: number,
19
+ metersPerStud: number,
20
+ kphPerStudPerSecond: number,
21
+ mphPerStudPerSecond: number,
22
+ }
23
+
24
+ local factors: Factors? = nil
25
+
26
+ local function getFactors(): Factors
27
+ local found = factors
28
+ if found then
29
+ return found
30
+ end
31
+ local studs = MetricUnitSystems.roblox({ name = "metricUtils", metersPerStud = 1 / 3.57 })
32
+ local stud = studs:New(1, "stud")
33
+ local studPerSecond = studs:New(1, "studPerSecond")
34
+ local speedOfSoundInDryAirAt20C = MetricUnit.new(343, "meterPerSecond")
35
+ local computed: Factors = table.freeze({
36
+ secondsOfSoundPerStud = (stud / speedOfSoundInDryAirAt20C):InSeconds(),
37
+ kilometersPerStud = stud:In("kilometer"),
38
+ metersPerStud = stud:In("meter"),
39
+ kphPerStudPerSecond = studPerSecond:In("kilometerPerHour"),
40
+ mphPerStudPerSecond = studPerSecond:In("milePerHour"),
41
+ })
42
+ factors = computed
43
+ return computed
44
+ end
13
45
 
14
46
  --[=[
15
47
  Computes sound delay for the given studs
@@ -17,8 +49,7 @@ local KPH_TO_MPH = 0.621371192
17
49
  @return number -- seconds
18
50
  ]=]
19
51
  function MetricUtils.getSoundDelaySeconds(studs: number): number
20
- local meters = MetricUtils.studsToMeters(studs)
21
- return meters / SPEED_OF_SOUND_IN_METERS_PER_SECOND
52
+ return studs * getFactors().secondsOfSoundPerStud
22
53
  end
23
54
 
24
55
  --[=[
@@ -27,7 +58,7 @@ end
27
58
  @return number -- kilometers
28
59
  ]=]
29
60
  function MetricUtils.studsToKilometers(studs: number): number
30
- return studs / STUDS_PER_METER / 1000
61
+ return studs * getFactors().kilometersPerStud
31
62
  end
32
63
 
33
64
  --[=[
@@ -36,7 +67,7 @@ end
36
67
  @return number -- meters
37
68
  ]=]
38
69
  function MetricUtils.studsToMeters(studs: number): number
39
- return studs / STUDS_PER_METER
70
+ return studs * getFactors().metersPerStud
40
71
  end
41
72
 
42
73
  --[=[
@@ -45,17 +76,17 @@ end
45
76
  @return number -- kph
46
77
  ]=]
47
78
  function MetricUtils.studsPerSecondToKph(studsPerSecond: number): number
48
- return studsPerSecond / STUDS_PER_METER / 1000 * SECONDS_PER_HOUR
79
+ return studsPerSecond * getFactors().kphPerStudPerSecond
49
80
  end
50
81
 
51
82
  --[=[
52
83
  Convert from studs per a second to meters per a second
53
84
 
54
85
  @param studsPerSecond number
55
- @return number -- kph
86
+ @return number -- meters per a second
56
87
  ]=]
57
88
  function MetricUtils.studsPerSecondToMetersPerSecond(studsPerSecond: number): number
58
- return studsPerSecond / STUDS_PER_METER
89
+ return studsPerSecond * getFactors().metersPerStud
59
90
  end
60
91
 
61
92
  --[=[
@@ -64,16 +95,16 @@ end
64
95
  @return number -- mph
65
96
  ]=]
66
97
  function MetricUtils.studsPerSecondToMph(studsPerSecond: number): number
67
- return studsPerSecond / STUDS_PER_METER / 1000 * SECONDS_PER_HOUR * KPH_TO_MPH
98
+ return studsPerSecond * getFactors().mphPerStudPerSecond
68
99
  end
69
100
 
70
101
  --[=[
71
- Convert mph to studs per a second
102
+ Convert kph to studs per a second
72
103
  @param kph number
73
104
  @return number -- studs per a second
74
105
  ]=]
75
106
  function MetricUtils.kphToStudsPerSecond(kph: number): number
76
- return kph * STUDS_PER_METER * 1000 / SECONDS_PER_HOUR
107
+ return kph / getFactors().kphPerStudPerSecond
77
108
  end
78
109
 
79
110
  --[=[
@@ -82,7 +113,7 @@ end
82
113
  @return number -- studs per a second
83
114
  ]=]
84
115
  function MetricUtils.mphToStudsPerSecond(mph: number): number
85
- return mph * STUDS_PER_METER * 1000 / SECONDS_PER_HOUR / KPH_TO_MPH
116
+ return mph / getFactors().mphPerStudPerSecond
86
117
  end
87
118
 
88
119
  return MetricUtils
@@ -0,0 +1,76 @@
1
+ --!strict
2
+ --[[
3
+ @class MetricUtils.spec.lua
4
+ ]]
5
+
6
+ local require = require(script.Parent.loader).load(script)
7
+
8
+ local Jest = require("Jest")
9
+ local MetricUtils = require("MetricUtils")
10
+
11
+ local describe = Jest.Globals.describe
12
+ local expect = Jest.Globals.expect
13
+ local it = Jest.Globals.it
14
+
15
+ describe("MetricUtils.getSoundDelaySeconds", function()
16
+ it("takes one second for sound to cross 343 meters", function()
17
+ expect(MetricUtils.getSoundDelaySeconds(343 * 3.57)).toBeCloseTo(1, 9)
18
+ end)
19
+
20
+ it("is zero at zero distance", function()
21
+ expect(MetricUtils.getSoundDelaySeconds(0)).toBe(0)
22
+ end)
23
+ end)
24
+
25
+ describe("MetricUtils.studsToMeters", function()
26
+ it("converts at 3.57 studs per meter", function()
27
+ expect(MetricUtils.studsToMeters(357)).toBeCloseTo(100, 9)
28
+ expect(MetricUtils.studsToMeters(-3.57)).toBeCloseTo(-1, 9)
29
+ expect(MetricUtils.studsToMeters(0)).toBe(0)
30
+ end)
31
+ end)
32
+
33
+ describe("MetricUtils.studsToKilometers", function()
34
+ it("converts studs to kilometers", function()
35
+ expect(MetricUtils.studsToKilometers(3570)).toBeCloseTo(1, 9)
36
+ expect(MetricUtils.studsToKilometers(0)).toBe(0)
37
+ end)
38
+ end)
39
+
40
+ describe("MetricUtils.studsPerSecondToMetersPerSecond", function()
41
+ it("converts a stud velocity to meters per second", function()
42
+ expect(MetricUtils.studsPerSecondToMetersPerSecond(35.7)).toBeCloseTo(10, 9)
43
+ end)
44
+ end)
45
+
46
+ describe("MetricUtils.studsPerSecondToKph", function()
47
+ it("converts a stud velocity to kilometers per hour", function()
48
+ expect(MetricUtils.studsPerSecondToKph(3.57)).toBeCloseTo(3.6, 9)
49
+ end)
50
+ end)
51
+
52
+ describe("MetricUtils.studsPerSecondToMph", function()
53
+ it("converts a stud velocity to miles per hour", function()
54
+ expect(MetricUtils.studsPerSecondToMph(3.57 * 0.44704)).toBeCloseTo(1, 9)
55
+ end)
56
+ end)
57
+
58
+ describe("MetricUtils.kphToStudsPerSecond", function()
59
+ it("converts kilometers per hour to a stud velocity", function()
60
+ expect(MetricUtils.kphToStudsPerSecond(3.6)).toBeCloseTo(3.57, 9)
61
+ end)
62
+
63
+ it("inverts studsPerSecondToKph", function()
64
+ expect(MetricUtils.kphToStudsPerSecond(MetricUtils.studsPerSecondToKph(16))).toBeCloseTo(16, 9)
65
+ end)
66
+ end)
67
+
68
+ describe("MetricUtils.mphToStudsPerSecond", function()
69
+ it("converts miles per hour to a stud velocity", function()
70
+ expect(MetricUtils.mphToStudsPerSecond(1)).toBeCloseTo(3.57 * 0.44704, 9)
71
+ end)
72
+
73
+ it("inverts studsPerSecondToMph", function()
74
+ expect(MetricUtils.mphToStudsPerSecond(MetricUtils.studsPerSecondToMph(16))).toBeCloseTo(16, 9)
75
+ end)
76
+ end)