@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,153 @@
1
+ --!strict
2
+ --[[
3
+ @class MetricUnit.Display.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 describe = Jest.Globals.describe
12
+ local expect = Jest.Globals.expect
13
+ local it = Jest.Globals.it
14
+
15
+ local function preferenceOf(value: MetricUnit.MetricUnit): string?
16
+ local system = value:GetDisplaySystem()
17
+ local unit = value:GetDisplayUnit()
18
+ return if unit then unit.id elseif system then system:GetName() else nil
19
+ end
20
+
21
+ -- Formatting puts a no-break space between a number and its unit; these compare it as a plain space
22
+ local function plain(text: string): string
23
+ return (string.gsub(text, "\u{00A0}", " "))
24
+ end
25
+
26
+ local function format(value: any, options: MetricUnit.FormatOptions?): string
27
+ return plain(MetricUnit.format(value, options))
28
+ end
29
+
30
+ local function mismatches(cases: { { any } }): { string }
31
+ local found = {}
32
+ for _, case in cases do
33
+ local text = string.gsub(tostring(case[1]), "\u{00A0}", " ")
34
+ if text ~= case[2] then
35
+ table.insert(found, string.format("expected %q, got %q", case[2], text))
36
+ end
37
+ end
38
+ return found
39
+ end
40
+
41
+ describe("MetricUnit display preference", function()
42
+ it("prefers imperial units or a pinned unit only when asked", function()
43
+ local distance = MetricUnit.kilometers(5)
44
+ expect(format(distance)).toBe("5 km")
45
+ expect(format(distance:ToUSCustomary())).toBe("3.107 mi")
46
+ expect(format(distance:As("mile"))).toBe("3.107 mi")
47
+ expect(format(distance:As("meter"))).toBe("5,000 m")
48
+ expect(format(MetricUnit.meters(28):As("stud"))).toBe("100 studs")
49
+ expect(format(distance:ToUSCustomary():ToMetric())).toBe("5 km")
50
+ expect(preferenceOf(distance:ToUSCustomary())).toBe("usCustomary")
51
+ expect(preferenceOf(distance)).toBeNil()
52
+ end)
53
+
54
+ it("never takes a measurement system from the locale", function()
55
+ local distance = MetricUnit.kilometers(5)
56
+ expect(format(distance, { locale = "en-us" })).toBe("5 km")
57
+ expect(format(distance, { locale = "en-us", unitDisplay = "long" })).toBe("5 kilometers")
58
+ expect(format(MetricUnit.celsius(20), { locale = "en-us" })).toBe("20 °C")
59
+ end)
60
+
61
+ it("lets an explicit unit option win over the preference", function()
62
+ expect(format(MetricUnit.kilometers(5):ToUSCustomary(), { unit = "meter" })).toBe("5,000 m")
63
+ end)
64
+
65
+ it("errors pinning a unit of another dimension", function()
66
+ expect(function()
67
+ MetricUnit.kilometers(5):As("kilogram")
68
+ end).toThrow('MetricUnit:As("kilogram") is mass, not length')
69
+ end)
70
+
71
+ it("never changes the amount, comparison, reading or saved data", function()
72
+ local distance = MetricUnit.kilometers(5)
73
+ local imperial = distance:ToUSCustomary()
74
+ expect(imperial == distance).toBe(true)
75
+ expect(imperial:In("kilometer")).toBe(5)
76
+ expect(MetricUnit.expect(imperial, "length", "reach")).toBe(5000)
77
+ expect(MetricUnit.toData(imperial)).toEqual({ amount = 5000, dimensions = { length = 1 } })
78
+ expect(preferenceOf(MetricUnit.fromData(MetricUnit.toData(imperial)))).toBeNil()
79
+ end)
80
+
81
+ it("carries through arithmetic of one dimension when the sides agree", function()
82
+ local imperial = MetricUnit.kilometers(5):ToUSCustomary()
83
+ expect(preferenceOf((imperial + MetricUnit.kilometers(1)))).toBe("usCustomary")
84
+ expect(preferenceOf((MetricUnit.kilometers(1) + imperial))).toBe("usCustomary")
85
+ expect(preferenceOf((imperial + MetricUnit.kilometers(1):ToMetric()))).toBeNil()
86
+ expect(preferenceOf(MetricUnit.max(imperial, MetricUnit.kilometers(1)))).toBe("usCustomary")
87
+ expect(preferenceOf(MetricUnit.sum({ imperial, MetricUnit.kilometers(1) }))).toBe("usCustomary")
88
+ expect(preferenceOf(-imperial)).toBe("usCustomary")
89
+ end)
90
+
91
+ it("keeps a pinned unit through scaling, and its system through a change of dimension", function()
92
+ local pinned = MetricUnit.kilometers(5):As("mile")
93
+ expect(preferenceOf((pinned * 2))).toBe("mile")
94
+ expect(preferenceOf((2 * pinned))).toBe("mile")
95
+ expect(preferenceOf((pinned / 2))).toBe("mile")
96
+ expect(preferenceOf((pinned + MetricUnit.kilometers(1)))).toBe("mile")
97
+ expect(preferenceOf(pinned / MetricUnit.hours(1))).toBe("usCustomaryAndImperial")
98
+ expect(format(MetricUnit.kilometers(5):ToUSCustomary() / MetricUnit.hours(1))).toBe("3.107 mph")
99
+ -- A stud's system shows studs per second for the speed it makes
100
+ expect(preferenceOf(MetricUnit.meters(1):As("stud") / MetricUnit.seconds(1))).toBe("roblox")
101
+ end)
102
+ end)
103
+
104
+ describe("MetricUnit tostring", function()
105
+ it("is the English short format with four significant digits", function()
106
+ expect(mismatches({
107
+ { MetricUnit.days(3.5), "3 days, 12 hours" },
108
+ { MetricUnit.years(2), "2 years" },
109
+ { MetricUnit.minutes(90), "1 hour, 30 minutes" },
110
+ { MetricUnit.seconds(30), "30 seconds" },
111
+ { MetricUnit.kilometers(2), "2 km" },
112
+ { MetricUnit.meters(500), "500 m" },
113
+ { MetricUnit.millimeters(4), "4 mm" },
114
+ { MetricUnit.kilobytes(1_600), "1.6 MB" },
115
+ { MetricUnit.bytes(12), "12 B" },
116
+ { MetricUnit.tonnes(3), "3 t" },
117
+ { MetricUnit.grams(20), "20 g" },
118
+ { MetricUnit.kilometers(0), "0 m" },
119
+ { MetricUnit.days(0), "0 seconds" },
120
+ { MetricUnit.perYear(0.03), "0.03/yr" },
121
+ { MetricUnit.perDay(5), "5/d" },
122
+ { MetricUnit.perSecond(2), "2/s" },
123
+ { MetricUnit.new(60, "hertz"), "60/s" },
124
+ { MetricUnit.kilometers(18) / MetricUnit.hours(1), "5 m/s" },
125
+ { MetricUnit.kilometers(20) / MetricUnit.years(1), "2.282 m/h" },
126
+ { MetricUnit.new(1_600, "kilobytePerSecond"), "1.6 MB/s" },
127
+ { MetricUnit.bytes(20) / MetricUnit.days(1), "20 B/d" },
128
+ { MetricUnit.milliseconds(250), "250 milliseconds" },
129
+ { MetricUnit.new(40, "microsecond"), "40 microseconds" },
130
+ { MetricUnit.kilometers(3) * MetricUnit.kilometers(2), "6 km²" },
131
+ { MetricUnit.new(50, "squareMeter"), "50 m²" },
132
+ { MetricUnit.new(2.5, "liter"), "2.5 L" },
133
+ { MetricUnit.new(3, "cubicMeter"), "3 m³" },
134
+ { MetricUnit.new(9.81, "meterPerSecondSquared"), "9.81 m/s²" },
135
+ { MetricUnit.new(1_500, "newton"), "1.5 kN" },
136
+ { MetricUnit.new(1, "kilowattHour"), "3.6 MJ" },
137
+ { MetricUnit.new(2_500, "watt"), "2.5 kW" },
138
+ { MetricUnit.new(1, "bar"), "100 kPa" },
139
+ { MetricUnit.new(1_000, "kilogramPerCubicMeter"), "1,000 kg/m³" },
140
+ { MetricUnit.new(2, "gigabyte"), "2 GB" },
141
+ { MetricUnit.new(5, "milligram"), "5 mg" },
142
+ { MetricUnit.kilograms(2) / MetricUnit.kilometers(1), "0.002 kg/m" },
143
+ { MetricUnit.kilograms(2) * MetricUnit.new(3, "meterPerSecond"), "6 m·kg/s" },
144
+ { MetricUnit.none(0.25), "0.25" },
145
+ { MetricUnit.years(10_000), "10,000 years" },
146
+ { MetricUnit.celsius(5), "5 °C" },
147
+ { MetricUnit.celsius(5) - MetricUnit.celsius(2), "3 K" },
148
+ { MetricUnit.meters(1 / 3), "33.33 cm" },
149
+ { MetricUnit.kilometers(5):ToUSCustomary(), "3.107 mi" },
150
+ { MetricUnit.instant(90), "epoch + 1 minute, 30 seconds" },
151
+ })).toEqual({})
152
+ end)
153
+ end)
@@ -0,0 +1,114 @@
1
+ --!strict
2
+ --[[
3
+ @class MetricUnit.Electrical.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 describe = Jest.Globals.describe
12
+ local expect = Jest.Globals.expect
13
+ local it = Jest.Globals.it
14
+
15
+ local function format(value: any): string
16
+ return (string.gsub(MetricUnit.format(value), "\u{00A0}", " "))
17
+ end
18
+
19
+ local function parse(text: string, kind: any?): MetricUnit.MetricUnit
20
+ local value, err = MetricUnit.parse(text, kind)
21
+ if not value then
22
+ error(string.format("could not parse %q: %s", text, tostring(err)))
23
+ end
24
+ return value
25
+ end
26
+
27
+ describe("MetricUnit circuits", function()
28
+ it("follows Ohm's law and the power law", function()
29
+ local voltage = MetricUnit.new(12, "volt")
30
+ local resistance = MetricUnit.new(4.7, "kilohm")
31
+ local current = voltage / resistance
32
+ expect(current:GetKind()).toBe("current")
33
+ expect(current:In("milliampere")).toBeCloseTo(2.5532, 4)
34
+ expect((current * resistance):In("volt")).toBeCloseTo(12, 9)
35
+ expect((voltage * current):GetKind()).toBe("power")
36
+ expect((voltage * current):In("watt")).toBeCloseTo(0.030638, 6)
37
+ end)
38
+
39
+ it("makes seconds from RC and L/R time constants", function()
40
+ local tau = MetricUnit.new(10, "kilohm") * MetricUnit.new(100, "nanofarad")
41
+ expect(tau:GetKind()).toBe("time")
42
+ expect(tau:In("millisecond")).toBeCloseTo(1, 9)
43
+ local inductive = MetricUnit.new(10, "millihenry") / MetricUnit.new(1, "ohm")
44
+ expect(inductive:In("millisecond")).toBeCloseTo(10, 9)
45
+ end)
46
+
47
+ it("relates charge, capacitance and stored energy", function()
48
+ local capacitance = MetricUnit.new(470, "microfarad")
49
+ local voltage = MetricUnit.new(5, "volt")
50
+ local charge = capacitance * voltage
51
+ expect(charge:GetKind()).toBe("charge")
52
+ expect(charge:In("coulomb")).toBeCloseTo(0.00235, 9)
53
+ expect((charge / MetricUnit.seconds(1)):In("milliampere")).toBeCloseTo(2.35, 9)
54
+ local energy = 0.5 * capacitance * MetricUnit.pow(voltage, 2)
55
+ expect(energy:In("joule")).toBeCloseTo(0.005875, 9)
56
+ end)
57
+
58
+ it("relates flux, flux density, inductance and conductance", function()
59
+ local flux = MetricUnit.new(2, "volt") * MetricUnit.seconds(3)
60
+ expect(flux:GetKind()).toBe("magneticFlux")
61
+ expect(flux:In("weber")).toBe(6)
62
+ expect((flux / MetricUnit.new(2, "squareMeter")):In("tesla")).toBe(3)
63
+ expect((flux / MetricUnit.new(2, "ampere")):In("henry")).toBe(3)
64
+ expect((1 / MetricUnit.new(50, "ohm")):In("siemens")).toBeCloseTo(0.02, 9)
65
+ end)
66
+
67
+ it("names every electrical kind", function()
68
+ expect(MetricUnit.new(1, "ampere"):GetKind()).toBe("current")
69
+ expect(MetricUnit.new(1, "coulomb"):GetKind()).toBe("charge")
70
+ expect(MetricUnit.new(1, "volt"):GetKind()).toBe("voltage")
71
+ expect(MetricUnit.new(1, "ohm"):GetKind()).toBe("resistance")
72
+ expect(MetricUnit.new(1, "siemens"):GetKind()).toBe("conductance")
73
+ expect(MetricUnit.new(1, "farad"):GetKind()).toBe("capacitance")
74
+ expect(MetricUnit.new(1, "henry"):GetKind()).toBe("inductance")
75
+ expect(MetricUnit.new(1, "weber"):GetKind()).toBe("magneticFlux")
76
+ expect(MetricUnit.new(1, "tesla"):GetKind()).toBe("magneticFluxDensity")
77
+ end)
78
+ end)
79
+
80
+ describe("MetricUnit electrical formatting", function()
81
+ it("picks the prefix a component is labelled with", function()
82
+ expect(format(MetricUnit.new(4700, "ohm"))).toBe("4.7 kΩ")
83
+ expect(format(MetricUnit.new(0.1, "microfarad"))).toBe("100 nF")
84
+ expect(format(MetricUnit.new(0.0015, "ampere"))).toBe("1.5 mA")
85
+ expect(format(MetricUnit.new(3.3, "volt"))).toBe("3.3 V")
86
+ expect(format(MetricUnit.new(2200, "microhenry"))).toBe("2.2 mH")
87
+ expect(format(MetricUnit.new(0.05, "tesla"))).toBe("50 mT")
88
+ expect(format(MetricUnit.new(2, "volt") / MetricUnit.seconds(1))).toBe("2 V/s")
89
+ end)
90
+ end)
91
+
92
+ describe("MetricUnit electrical parsing", function()
93
+ it("reads component values as they are written", function()
94
+ expect(parse("4.7kΩ"):In("ohm")).toBeCloseTo(4700, 9)
95
+ expect(parse("4.7 kilohms"):In("ohm")).toBeCloseTo(4700, 9)
96
+ expect(parse("100nF"):In("nanofarad")).toBeCloseTo(100, 9)
97
+ expect(parse("10 uF"):In("microfarad")).toBeCloseTo(10, 9)
98
+ expect(parse("10 µF"):In("microfarad")).toBeCloseTo(10, 9)
99
+ expect(parse("12 V"):In("volt")).toBe(12)
100
+ expect(parse("2 amps"):In("ampere")).toBe(2)
101
+ end)
102
+
103
+ it("reads the Greek mu and the ohm sign as the micro sign and omega", function()
104
+ expect(parse("10 \u{03BC}F"):In("microfarad")).toBeCloseTo(10, 9)
105
+ expect(parse("5 \u{2126}"):In("ohm")).toBe(5)
106
+ end)
107
+
108
+ it("keeps C and F as temperatures unless a charge or capacitance is expected", function()
109
+ expect(parse("20 C"):GetKind()).toBe("temperature")
110
+ expect(parse("20 F"):GetKind()).toBe("temperature")
111
+ expect(parse("20 C", "charge"):In("coulomb")).toBe(20)
112
+ expect(parse("20 F", "capacitance"):In("farad")).toBe(20)
113
+ end)
114
+ end)
@@ -0,0 +1,112 @@
1
+ --!strict
2
+ --[[
3
+ @class MetricUnit.Math.spec.lua
4
+ ]]
5
+
6
+ local require = require(script.Parent.loader).load(script)
7
+
8
+ local Jest = require("Jest")
9
+ local MetricUnit = require("MetricUnit")
10
+ local MetricUnitSystems = require("MetricUnitSystems")
11
+
12
+ local describe = Jest.Globals.describe
13
+ local expect = Jest.Globals.expect
14
+ local it = Jest.Globals.it
15
+
16
+ describe("MetricUnit.lerp", function()
17
+ it("interpolates differences and absolutes alike", function()
18
+ expect(MetricUnit.lerp(MetricUnit.meters(0), MetricUnit.meters(10), 0.25):In("meter")).toBe(2.5)
19
+ local temperature = MetricUnit.lerp(MetricUnit.celsius(10), MetricUnit.celsius(20), 0.5)
20
+ expect(temperature:IsAbsolute()).toBe(true)
21
+ expect(temperature:In("celsius")).toBeCloseTo(15, 9)
22
+ local instant = MetricUnit.lerp(MetricUnit.instant(0), MetricUnit.instant(100), 0.5)
23
+ expect(instant:GetKind()).toBe("instant")
24
+ expect(instant:InSeconds()).toBe(50)
25
+ end)
26
+
27
+ it("errors between different dimensions", function()
28
+ expect(function()
29
+ MetricUnit.lerp(MetricUnit.meters(1), MetricUnit.seconds(1), 0.5)
30
+ end).toThrow("cannot subtract")
31
+ end)
32
+ end)
33
+
34
+ describe("MetricUnit rounding", function()
35
+ it("rounds, floors and ceils to a unit", function()
36
+ expect(MetricUnit.round(MetricUnit.seconds(95), "minute"):In("minute")).toBe(2)
37
+ expect(MetricUnit.floor(MetricUnit.seconds(95), "minute"):In("minute")).toBe(1)
38
+ expect(MetricUnit.ceil(MetricUnit.seconds(61), "minute"):In("minute")).toBe(2)
39
+ expect(MetricUnit.round(MetricUnit.meters(1234), "kilometer"):In("kilometer")).toBe(1)
40
+ end)
41
+
42
+ it("snaps to a multiple of a step", function()
43
+ expect(MetricUnit.round(MetricUnit.meters(12.4), MetricUnit.meters(5)):In("meter")).toBe(10)
44
+ expect(MetricUnit.round(MetricUnit.meters(12.6), MetricUnit.meters(5)):In("meter")).toBe(15)
45
+ expect(function()
46
+ MetricUnit.round(MetricUnit.meters(1), MetricUnit.meters(-5))
47
+ end).toThrow("takes a positive step of length")
48
+ expect(function()
49
+ MetricUnit.round(MetricUnit.meters(1), MetricUnit.seconds(1))
50
+ end).toThrow("takes a positive step of length, not time")
51
+ end)
52
+
53
+ it("rounds a temperature on its scale and an instant from its epoch", function()
54
+ local warm = MetricUnit.round(MetricUnit.celsius(21.6), "celsius")
55
+ expect(warm:IsAbsolute()).toBe(true)
56
+ expect(warm:In("celsius")).toBeCloseTo(22, 9)
57
+ local instant = MetricUnit.round(MetricUnit.instant(95), "minute")
58
+ expect(instant:GetKind()).toBe("instant")
59
+ expect(instant:InSeconds()).toBe(120)
60
+ end)
61
+
62
+ it("keeps the display preference and errors on an unknown unit", function()
63
+ expect(MetricUnit.round(MetricUnit.kilometers(1.4):ToUSCustomary(), "mile"):GetDisplaySystem()).toBe(
64
+ MetricUnitSystems.US_CUSTOMARY
65
+ )
66
+ expect(function()
67
+ MetricUnit.round(MetricUnit.meters(1), "klicks" :: any)
68
+ end).toThrow('MetricUnit.round knows no unit "klicks"')
69
+ end)
70
+ end)
71
+
72
+ describe("MetricUnit aggregation", function()
73
+ it("sums values of one dimension", function()
74
+ expect(MetricUnit.sum({ MetricUnit.meters(1), MetricUnit.kilometers(1) }):In("meter")).toBe(1001)
75
+ expect(MetricUnit.sum({ 1, 2 }):GetBaseAmount()).toBe(3)
76
+ expect(function()
77
+ MetricUnit.sum({ MetricUnit.celsius(1), MetricUnit.celsius(2) })
78
+ end).toThrow("cannot add two absolute temperatures")
79
+ expect(function()
80
+ MetricUnit.sum({})
81
+ end).toThrow("takes a list of at least one value")
82
+ end)
83
+
84
+ it("averages values, absolutes included", function()
85
+ expect(MetricUnit.mean({ MetricUnit.meters(1), MetricUnit.meters(3) }):In("meter")).toBe(2)
86
+ local temperature = MetricUnit.mean({ MetricUnit.celsius(10), MetricUnit.celsius(20) })
87
+ expect(temperature:IsAbsolute()).toBe(true)
88
+ expect(temperature:In("celsius")).toBeCloseTo(15, 9)
89
+ expect(MetricUnit.mean({ MetricUnit.instant(10), MetricUnit.instant(20) }):InSeconds()).toBe(15)
90
+ end)
91
+
92
+ it("takes the smallest and largest of a list through min and max", function()
93
+ local list = { MetricUnit.meters(1), MetricUnit.meters(5), MetricUnit.meters(3) }
94
+ expect(MetricUnit.max(table.unpack(list)):In("meter")).toBe(5)
95
+ expect(MetricUnit.min(table.unpack(list)):In("meter")).toBe(1)
96
+ end)
97
+ end)
98
+
99
+ describe("MetricUnit rounding an absolute value by a step", function()
100
+ it("rounds a temperature on the scale it is shown in", function()
101
+ local step = MetricUnit.difference(5, "celsius")
102
+ expect(MetricUnit.round(MetricUnit.celsius(21.6), step):In("celsius")).toBeCloseTo(20, 9)
103
+ expect(MetricUnit.round(MetricUnit.celsius(23), step):In("celsius")).toBeCloseTo(25, 9)
104
+ local fahrenheitStep = MetricUnit.difference(10, "fahrenheit")
105
+ expect(MetricUnit.round(MetricUnit.fahrenheit(68), fahrenheitStep):In("fahrenheit")).toBeCloseTo(70, 9)
106
+ end)
107
+
108
+ it("rounds an instant from its epoch", function()
109
+ local rounded = MetricUnit.round(MetricUnit.instant(95), MetricUnit.minutes(1))
110
+ expect(rounded:In("second")).toBeCloseTo(120, 9)
111
+ end)
112
+ end)
@@ -0,0 +1,34 @@
1
+ --!strict
2
+ --[[
3
+ @class MetricUnit.Roblox.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 describe = Jest.Globals.describe
12
+ local expect = Jest.Globals.expect
13
+ local it = Jest.Globals.it
14
+
15
+ describe("MetricUnit and the Roblox engine", function()
16
+ it("converts Roblox mass units and stud accelerations through the documented stud and RMU", function()
17
+ -- A Roblox mass unit is a cubic stud of water
18
+ expect(MetricUnit.new(1, "robloxMassUnit"):In("kilogram")).toBeCloseTo(21.952, 9)
19
+ expect(MetricUnit.new(196.2, "studPerSecondSquared"):In("meterPerSecondSquared")).toBeCloseTo(196.2 * 0.28, 9)
20
+ -- An engine force is a mass times an acceleration, so it needs no unit of its own
21
+ local force = MetricUnit.new(1, "robloxMassUnit") * MetricUnit.new(1, "studPerSecondSquared")
22
+ expect(force:GetKind()).toBe("force")
23
+ expect(force:In("newton")).toBeCloseTo(21.952 * 0.28, 9)
24
+ end)
25
+
26
+ it("reads a velocity's speed in studs per second", function()
27
+ if typeof(Vector3) ~= "table" and typeof(Vector3) ~= "userdata" then
28
+ return -- Vector3 exists in Roblox only
29
+ end
30
+ local speed = MetricUnit.fromStudVelocity(Vector3.new(3, 4, 0))
31
+ expect(speed:In("studPerSecond")).toBeCloseTo(5, 9)
32
+ expect(speed:In("meterPerSecond")).toBeCloseTo(5 * 0.28, 9)
33
+ end)
34
+ end)
@@ -0,0 +1,127 @@
1
+ --!strict
2
+ --[[
3
+ @class MetricUnit.SI.spec.lua
4
+ ]]
5
+
6
+ local require = require(script.Parent.loader).load(script)
7
+
8
+ local Jest = require("Jest")
9
+ local MetricUnit = require("MetricUnit")
10
+ local MetricUnitDefinitions = require("MetricUnitDefinitions")
11
+
12
+ local describe = Jest.Globals.describe
13
+ local expect = Jest.Globals.expect
14
+ local it = Jest.Globals.it
15
+
16
+ local function format(value: any, options: any?): string
17
+ return (string.gsub(MetricUnit.format(value, options), "\u{00A0}", " "))
18
+ end
19
+
20
+ local function parse(text: string, kind: any?): MetricUnit.MetricUnit
21
+ local value, err = MetricUnit.parse(text, kind)
22
+ if not value then
23
+ error(string.format("could not parse %q: %s", text, tostring(err)))
24
+ end
25
+ return value
26
+ end
27
+
28
+ describe("MetricUnit SI prefixes", function()
29
+ it("makes every SI prefix of a prefixable unit", function()
30
+ expect(#MetricUnitDefinitions.getPrefixes()).toBe(32)
31
+ for _, prefix in MetricUnitDefinitions.getPrefixes() do
32
+ local unit = MetricUnitDefinitions.getUnit(prefix.name .. "volt")
33
+ local isBinary = string.find(prefix.symbol, "i", 1, true) ~= nil
34
+ if isBinary then
35
+ expect(unit).toBeNil()
36
+ else
37
+ assert(unit, prefix.name .. "volt")
38
+ expect(unit.symbol).toBe(prefix.symbol .. "V")
39
+ expect(unit.factor).toBe(prefix.factor)
40
+ end
41
+ end
42
+ expect(MetricUnit.new(3, "nanometer" :: any):In("meter")).toBeCloseTo(3e-9, 20)
43
+ expect(MetricUnit.new(1, "quettameter" :: any):In("meter")).toBe(1e30)
44
+ end)
45
+
46
+ it("prefixes the gram, so the kilogram is one SI base unit", function()
47
+ local kilogram = assert(MetricUnitDefinitions.getUnit("kilogram"))
48
+ expect(kilogram.factor).toBe(1)
49
+ expect(kilogram.prefix).toEqual({ name = "kilo", base = "gram" })
50
+ expect(MetricUnitDefinitions.getUnit("kilokilogram" :: any)).toBeNil()
51
+ expect(MetricUnit.new(5, "microgram"):In("milligram")).toBeCloseTo(0.005, 12)
52
+ end)
53
+
54
+ it("gives bytes binary prefixes and only multiples", function()
55
+ expect(MetricUnit.new(1, "mebibyte" :: any):In("byte")).toBe(1024 ^ 2)
56
+ expect(MetricUnit.new(1, "terabyte"):In("gigabyte")).toBe(1000)
57
+ expect(MetricUnitDefinitions.getUnit("millibyte" :: any)).toBeNil()
58
+ expect(MetricUnitDefinitions.getUnit("kibibit" :: any)).toBeNil()
59
+ end)
60
+
61
+ it("names prefixed units in English, eliding the vowel before ohm", function()
62
+ expect(format(MetricUnit.new(3, "nanometer" :: any), { unit = "nanometer", unitDisplay = "long" })).toBe(
63
+ "3 nanometers"
64
+ )
65
+ expect(format(MetricUnit.new(1, "kilohm"), { unit = "kilohm", unitDisplay = "long" })).toBe("1 kilohm")
66
+ expect(format(MetricUnit.new(2, "megohm"), { unit = "megohm", unitDisplay = "long" })).toBe("2 megohms")
67
+ expect(format(MetricUnit.new(2, "millihenry"), { unit = "millihenry", unitDisplay = "long" })).toBe(
68
+ "2 millihenries"
69
+ )
70
+ expect(format(MetricUnit.new(2, "kilometer"), { unit = "kilometer", unitDisplay = "long", locale = "en-gb" })).toBe(
71
+ "2 kilometres"
72
+ )
73
+ end)
74
+
75
+ it("parses any prefix by symbol, name, or u for micro", function()
76
+ expect(parse("5 GHz"):In("hertz")).toBe(5e9)
77
+ expect(parse("3 nm"):In("nanometer" :: any)).toBeCloseTo(3, 9)
78
+ expect(parse("2 megawatts"):In("watt")).toBe(2e6)
79
+ expect(parse("10 us"):In("microsecond")).toBeCloseTo(10, 9)
80
+ expect(parse("4 uA"):In("microampere")).toBeCloseTo(4, 9)
81
+ expect(parse("250 ml"):In("liter")).toBeCloseTo(0.25, 9)
82
+ expect(parse("2 dL"):In("deciliter")).toBeCloseTo(2, 9)
83
+ expect(parse("3 milliamps"):In("ampere")).toBeCloseTo(0.003, 9)
84
+ expect(parse("5 M"):GetKind()).toBe("time")
85
+ end)
86
+ end)
87
+
88
+ describe("MetricUnit amount of substance and light", function()
89
+ it("converts mass, moles and concentration", function()
90
+ local waterMolarMass = MetricUnit.new(18.015, "gramPerMole")
91
+ local moles = MetricUnit.grams(36.03) / waterMolarMass
92
+ expect(moles:GetKind()).toBe("amount")
93
+ expect(moles:In("mole")).toBeCloseTo(2, 9)
94
+ local concentration = moles / MetricUnit.new(4, "liter")
95
+ expect(concentration:GetKind()).toBe("molarConcentration")
96
+ expect(concentration:In("millimolePerLiter")).toBeCloseTo(500, 6)
97
+ expect((moles / MetricUnit.seconds(2)):In("katal")).toBeCloseTo(1, 9)
98
+ end)
99
+
100
+ it("reads liquid volume from mass through a density", function()
101
+ local water = MetricUnit.new(1000, "kilogramPerCubicMeter")
102
+ local volume = MetricUnit.kilograms(2) / water
103
+ expect(volume:In("liter")).toBeCloseTo(2, 9)
104
+ expect(format(volume)).toBe("2 L")
105
+ expect((MetricUnit.new(330, "milliliter") * water):In("gram")).toBeCloseTo(330, 9)
106
+ expect(MetricUnit.new(1, "usGallon"):In("centiliter")).toBeCloseTo(378.5411784, 6)
107
+ end)
108
+
109
+ it("derives lumens and lux from the candela and the steradian", function()
110
+ local flux = MetricUnit.new(100, "candela") * MetricUnit.new(2, "steradian")
111
+ expect(flux:GetKind()).toBe("luminousFlux")
112
+ expect(flux:In("lumen")).toBe(200)
113
+ local illuminance = flux / MetricUnit.new(4, "squareMeter")
114
+ expect(illuminance:GetKind()).toBe("illuminance")
115
+ expect(illuminance:In("lux")).toBe(50)
116
+ expect(MetricUnit.pow(MetricUnit.radians(1), 2):In("steradian")).toBe(1)
117
+ end)
118
+
119
+ it("shares dimensions where SI does, showing the first unit unless asked", function()
120
+ local dose = MetricUnit.new(2, "joule") / MetricUnit.kilograms(1)
121
+ expect(dose:GetKind()).toBe("absorbedDose")
122
+ expect(format(dose)).toBe("2 Gy")
123
+ expect(format(dose:As("sievert"))).toBe("2 Sv")
124
+ expect(MetricUnit.new(5, "becquerel"):In("hertz")).toBe(5)
125
+ expect(format(MetricUnit.new(3, "millisievert"))).toBe("3 mGy")
126
+ end)
127
+ end)
@@ -0,0 +1,94 @@
1
+ --!strict
2
+ --[[
3
+ @class MetricUnit.Spring.spec.lua
4
+ ]]
5
+
6
+ local require = require(script.Parent.loader).load(script)
7
+
8
+ local Jest = require("Jest")
9
+ local LinearValue = require("LinearValue")
10
+ local MetricUnit = require("MetricUnit")
11
+ local Spring = require("Spring")
12
+
13
+ local describe = Jest.Globals.describe
14
+ local expect = Jest.Globals.expect
15
+ local it = Jest.Globals.it
16
+
17
+ local function manualClock(): (() -> number, (number) -> ())
18
+ local now = 0
19
+ return function()
20
+ return now
21
+ end, function(seconds: number)
22
+ now += seconds
23
+ end
24
+ end
25
+
26
+ describe("MetricUnit in a Spring", function()
27
+ it("animates a length to its target", function()
28
+ local clock, advance = manualClock()
29
+ local spring = Spring.new(MetricUnit.meters(0), clock)
30
+ spring.Speed = 20
31
+ spring.Target = MetricUnit.meters(10)
32
+
33
+ advance(0.05)
34
+ local midway = (spring.Position :: any):In("meter")
35
+ expect(midway > 0 and midway < 10).toBe(true)
36
+ -- Spring's clock is a plain number, so its velocity is in the position's units per second
37
+ expect((spring.Velocity :: any):GetKind()).toBe("length")
38
+
39
+ advance(10)
40
+ expect((spring.Position :: any):In("meter")).toBeCloseTo(10, 6)
41
+ expect((spring.Velocity :: any):In("meter")).toBeCloseTo(0, 6)
42
+ end)
43
+
44
+ it("starts from a spring made with no initial value", function()
45
+ local clock, advance = manualClock()
46
+ local spring = Spring.new(nil :: any, clock)
47
+ spring.Target = MetricUnit.new(5, "volt")
48
+ advance(30)
49
+ expect((spring.Position :: any):In("volt")).toBeCloseTo(5, 6)
50
+ end)
51
+
52
+ it("takes an impulse in the position's units, and refuses one of another dimension", function()
53
+ local clock, advance = manualClock()
54
+ local spring = Spring.new(MetricUnit.meters(0), clock)
55
+ spring:Impulse(MetricUnit.meters(4))
56
+ advance(0.01)
57
+ expect((spring.Position :: any):In("meter") > 0).toBe(true)
58
+ expect(function()
59
+ spring:Impulse(MetricUnit.new(4, "meterPerSecond") :: any)
60
+ end).toThrow()
61
+ end)
62
+
63
+ it("skips time and snaps to a target without animating", function()
64
+ local clock = manualClock()
65
+ local spring = Spring.new(MetricUnit.celsius(0) - MetricUnit.celsius(0), clock)
66
+ spring:SetTarget(MetricUnit.difference(20, "celsius"), true)
67
+ expect((spring.Position :: any):In("kelvin")).toBe(20)
68
+ spring.Target = MetricUnit.difference(0, "celsius")
69
+ spring:TimeSkip(60)
70
+ expect((spring.Position :: any):In("kelvin")).toBeCloseTo(0, 6)
71
+ end)
72
+
73
+ it("springs an absolute value through its offset from a reference", function()
74
+ expect(function()
75
+ Spring.new(MetricUnit.celsius(20))
76
+ end).toThrow("cannot multiply an absolute temperature")
77
+
78
+ local clock, advance = manualClock()
79
+ local reference = MetricUnit.celsius(0)
80
+ local spring = Spring.new(MetricUnit.celsius(15) - reference, clock)
81
+ spring.Target = MetricUnit.celsius(25) - reference
82
+ advance(60)
83
+ expect((reference + spring.Position :: any):In("celsius")).toBeCloseTo(25, 6)
84
+ end)
85
+ end)
86
+
87
+ describe("MetricUnit and LinearValue", function()
88
+ it("passes through LinearValue untouched, since it is linear already", function()
89
+ local value = MetricUnit.meters(3)
90
+ expect(LinearValue.isLinear(value)).toBe(false)
91
+ expect(LinearValue.toLinearIfNeeded(value)).toBe(value)
92
+ expect(LinearValue.fromLinearIfNeeded(value)).toBe(value)
93
+ end)
94
+ end)