@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,212 @@
1
+ --!strict
2
+ --[[
3
+ @class MetricUnit.Temperature.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 YEAR = MetricUnit.years(1):InSeconds()
13
+
14
+ local describe = Jest.Globals.describe
15
+ local expect = Jest.Globals.expect
16
+ local it = Jest.Globals.it
17
+
18
+ local function format(value: any): string
19
+ return (string.gsub(MetricUnit.format(value), "\u{00A0}", " "))
20
+ end
21
+
22
+ describe("MetricUnit absolute temperatures", function()
23
+ it("reads each scale back from its own zero", function()
24
+ expect(MetricUnit.celsius(5):In("celsius")).toBeCloseTo(5, 9)
25
+ expect(MetricUnit.celsius(5):In("kelvin")).toBeCloseTo(278.15, 9)
26
+ expect(MetricUnit.kelvin(0):In("celsius")).toBeCloseTo(-273.15, 9)
27
+ expect(MetricUnit.fahrenheit(212):In("celsius")).toBeCloseTo(100, 9)
28
+ expect(MetricUnit.celsius(-40):In("fahrenheit")).toBeCloseTo(-40, 9)
29
+ expect(MetricUnit.fahrenheit(32):In("fahrenheit")).toBeCloseTo(32, 9)
30
+ expect(MetricUnit.isKind(MetricUnit.kelvin(300), "temperature")).toBe(true)
31
+ expect((MetricUnit.celsius(5)):IsAbsolute()).toBe(true)
32
+ end)
33
+
34
+ it("subtracts two absolutes to a difference", function()
35
+ local difference = MetricUnit.celsius(5) - MetricUnit.celsius(2)
36
+ expect(MetricUnit.isKind(difference, "temperatureDifference")).toBe(true)
37
+ expect((difference):IsAbsolute()).toBe(false)
38
+ expect(difference:In("kelvin")).toBeCloseTo(3, 9)
39
+ expect(difference:In("celsius")).toBeCloseTo(3, 9)
40
+ expect(difference:In("fahrenheit")).toBeCloseTo(5.4, 9)
41
+ end)
42
+
43
+ it("adds a difference to an absolute, on either side, to an absolute", function()
44
+ local swing = MetricUnit.difference(12, "celsius")
45
+ expect((MetricUnit.celsius(15) + swing):In("celsius")).toBeCloseTo(27, 9)
46
+ expect((swing + MetricUnit.celsius(15)):In("celsius")).toBeCloseTo(27, 9)
47
+ expect((MetricUnit.celsius(15) - swing):In("celsius")).toBeCloseTo(3, 9)
48
+ expect(MetricUnit.isKind(MetricUnit.celsius(15) - swing, "temperature")).toBe(true)
49
+ expect((MetricUnit.celsius(15) + 0):In("celsius")).toBeCloseTo(15, 9)
50
+ end)
51
+
52
+ it("errors adding two absolutes", function()
53
+ expect(function()
54
+ local _ = MetricUnit.celsius(5) + MetricUnit.celsius(2)
55
+ end).toThrow(
56
+ "cannot add two absolute temperatures; subtract them for a difference, or add a difference to one"
57
+ )
58
+ end)
59
+
60
+ it("errors subtracting an absolute from a difference", function()
61
+ expect(function()
62
+ local _ = MetricUnit.difference(5, "celsius") - MetricUnit.celsius(2)
63
+ end).toThrow("cannot subtract an absolute temperature from a temperature difference")
64
+ expect(function()
65
+ local _ = 0 - MetricUnit.celsius(2)
66
+ end).toThrow("cannot subtract an absolute temperature from a temperature difference")
67
+ end)
68
+
69
+ it("errors scaling, negating or taking powers of an absolute, kelvin included", function()
70
+ expect(function()
71
+ local _ = MetricUnit.celsius(5) * 2
72
+ end).toThrow("cannot multiply an absolute temperature")
73
+ expect(function()
74
+ local _ = 2 * MetricUnit.kelvin(300)
75
+ end).toThrow("cannot multiply an absolute temperature")
76
+ expect(function()
77
+ local _ = MetricUnit.kelvin(300) / MetricUnit.seconds(1)
78
+ end).toThrow("cannot divide an absolute temperature")
79
+ expect(function()
80
+ local _ = -MetricUnit.celsius(5)
81
+ end).toThrow("cannot negate an absolute temperature")
82
+ expect(function()
83
+ MetricUnit.abs(MetricUnit.celsius(5))
84
+ end).toThrow("cannot take the size of an absolute temperature")
85
+ expect(function()
86
+ MetricUnit.pow(MetricUnit.celsius(5), 2)
87
+ end).toThrow("cannot exponentiate an absolute temperature")
88
+ end)
89
+
90
+ it("errors reading an absolute with In or a non-temperature reader", function()
91
+ expect(function()
92
+ MetricUnit.celsius(5):In(MetricUnit.difference(1, "kelvin"))
93
+ end).toThrow("cannot take the ratio of an absolute temperature")
94
+ expect(function()
95
+ MetricUnit.celsius(5):In("day")
96
+ end).toThrow('In("day") reads time, not absolute temperature')
97
+ expect(function()
98
+ MetricUnit.kilometers(5):In("celsius")
99
+ end).toThrow('In("celsius") reads temperature, not length')
100
+ end)
101
+ end)
102
+
103
+ describe("MetricUnit temperature differences", function()
104
+ it("makes differences on each scale", function()
105
+ expect(MetricUnit.difference(3, "kelvin"):In("kelvin")).toBe(3)
106
+ expect(MetricUnit.difference(3, "celsius"):In("kelvin")).toBe(3)
107
+ expect(MetricUnit.difference(9, "fahrenheit"):In("celsius")).toBeCloseTo(5, 9)
108
+ expect(MetricUnit.isKind(MetricUnit.difference(1, "celsius"), "temperatureDifference")).toBe(true)
109
+ expect(MetricUnit.isKind(MetricUnit.difference(1, "celsius"), "temperature")).toBe(false)
110
+ end)
111
+
112
+ it("scales, negates and divides freely", function()
113
+ local swing = MetricUnit.difference(12, "celsius")
114
+ expect((swing * 0.5):In("celsius")).toBeCloseTo(6, 9)
115
+ expect((-swing):In("celsius")).toBeCloseTo(-12, 9)
116
+ expect((swing / MetricUnit.years(1)):In(MetricUnit.difference(1, "kelvin") / MetricUnit.days(1))).toBeCloseTo(
117
+ 12 * DAY / YEAR,
118
+ 12
119
+ )
120
+ end)
121
+ end)
122
+
123
+ describe("MetricUnit temperature comparisons", function()
124
+ it("compares two absolutes or two differences", function()
125
+ expect(MetricUnit.celsius(5) < MetricUnit.celsius(6)).toBe(true)
126
+ expect(MetricUnit.celsius(5) == MetricUnit.kelvin(278.15)).toBe(true)
127
+ expect(MetricUnit.fuzzyEq(MetricUnit.fahrenheit(32), MetricUnit.celsius(0))).toBe(true)
128
+ expect(MetricUnit.difference(1, "celsius") <= MetricUnit.difference(2, "fahrenheit")).toBe(true)
129
+ expect(MetricUnit.max(MetricUnit.celsius(5), MetricUnit.celsius(9)):In("celsius")).toBeCloseTo(9, 9)
130
+ expect(MetricUnit.isKind(MetricUnit.max(MetricUnit.celsius(5), MetricUnit.celsius(9)), "temperature")).toBe(
131
+ true
132
+ )
133
+ end)
134
+
135
+ it("errors comparing an absolute with a difference", function()
136
+ expect(function()
137
+ local _ = MetricUnit.celsius(5) < MetricUnit.difference(6, "celsius")
138
+ end).toThrow("cannot compare an absolute temperature and a temperature difference")
139
+ expect(function()
140
+ local _ = MetricUnit.celsius(5) == MetricUnit.difference(5, "kelvin")
141
+ end).toThrow("cannot compare an absolute temperature and a temperature difference")
142
+ expect(function()
143
+ MetricUnit.max(MetricUnit.celsius(5), 0)
144
+ end).toThrow("cannot compare absolute temperature and the plain number 0")
145
+ end)
146
+ end)
147
+
148
+ describe("MetricUnit temperature boundaries and data", function()
149
+ it("expects an absolute temperature or a difference by name", function()
150
+ expect(MetricUnit.expect(MetricUnit.celsius(5), "temperature", "air")).toBeCloseTo(278.15, 9)
151
+ expect(MetricUnit.expect(MetricUnit.difference(12, "celsius"), "temperatureDifference", "swing")).toBe(12)
152
+ expect(MetricUnit.expect(0, "temperatureDifference", "swing")).toBe(0)
153
+ expect(function()
154
+ MetricUnit.expect(MetricUnit.difference(12, "celsius"), "temperature", "air")
155
+ end).toThrow("MetricUnit.expect: air expects absolute temperature, got temperature difference")
156
+ expect(function()
157
+ MetricUnit.expect(MetricUnit.celsius(12), "temperatureDifference", "swing")
158
+ end).toThrow("MetricUnit.expect: swing expects temperature difference, got absolute temperature")
159
+ expect(function()
160
+ MetricUnit.expect(0, "temperature", "air")
161
+ end).toThrow("MetricUnit.expect: air expects absolute temperature, got the plain number 0")
162
+ end)
163
+
164
+ it("round trips an absolute through plain data", function()
165
+ local data = MetricUnit.toData(MetricUnit.kelvin(300))
166
+ expect(data).toEqual({ amount = 300, dimensions = { temperature = 1 }, absolute = true })
167
+ local back = MetricUnit.fromData(data)
168
+ expect(MetricUnit.isKind(back, "temperature")).toBe(true)
169
+ expect(back:In("kelvin")).toBe(300)
170
+ expect(MetricUnit.toData(MetricUnit.difference(3, "kelvin"))).toEqual({
171
+ amount = 3,
172
+ dimensions = { temperature = 1 },
173
+ })
174
+ end)
175
+ end)
176
+
177
+ describe("MetricUnit.point and MetricUnit.difference", function()
178
+ it("makes a point from a unit's zero, in any unit", function()
179
+ expect(MetricUnit.point(20, "celsius") == MetricUnit.celsius(20)).toBe(true)
180
+ expect(MetricUnit.point(90, "second") == MetricUnit.instant(90)).toBe(true)
181
+ expect(MetricUnit.point(1.5, "minute"):In("second")).toBe(90)
182
+ expect(MetricUnit.point(5, "meter"):IsAbsolute()).toBe(true)
183
+ end)
184
+
185
+ it("makes a difference, in any unit", function()
186
+ expect(MetricUnit.difference(9, "fahrenheit"):In("kelvin")).toBeCloseTo(5, 9)
187
+ expect(MetricUnit.difference(12, "celsius"):IsAbsolute()).toBe(false)
188
+ expect(MetricUnit.difference(5, "meter") == MetricUnit.meters(5)).toBe(true)
189
+ end)
190
+
191
+ it("follows the point rules for every dimension", function()
192
+ local gap = MetricUnit.point(7, "meter") - MetricUnit.point(2, "meter")
193
+ expect(gap == MetricUnit.meters(5)).toBe(true)
194
+ expect(function()
195
+ local _ = MetricUnit.point(7, "meter") + MetricUnit.point(2, "meter")
196
+ end).toThrow()
197
+ end)
198
+
199
+ it("formats a point on the scale people read and a difference in kelvin", function()
200
+ expect(format(MetricUnit.point(20, "celsius"))).toBe("20 °C")
201
+ expect(format(MetricUnit.difference(20, "celsius"))).toBe("20 K")
202
+ end)
203
+
204
+ it("errors on an unknown unit or a non-number", function()
205
+ expect(function()
206
+ MetricUnit.point(1, "klicks" :: any)
207
+ end).toThrow('MetricUnit.point knows no unit "klicks"')
208
+ expect(function()
209
+ MetricUnit.difference("1" :: any, "meter")
210
+ end).toThrow("MetricUnit.difference takes a number")
211
+ end)
212
+ end)
@@ -0,0 +1,202 @@
1
+ --!strict
2
+ --[[
3
+ @class MetricUnit.Time.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 TimeDurationUtils = require("TimeDurationUtils")
11
+ local TimeLocalizationUtils = require("TimeLocalizationUtils")
12
+
13
+ local MONTH = MetricUnit.months(1):InSeconds()
14
+ local YEAR = MetricUnit.years(1):InSeconds()
15
+
16
+ local describe = Jest.Globals.describe
17
+ local expect = Jest.Globals.expect
18
+ local it = Jest.Globals.it
19
+
20
+ -- Formatting puts a no-break space between a number and its unit; these compare it as a plain space
21
+ local function plain(text: string): string
22
+ return (string.gsub(text, "\u{00A0}", " "))
23
+ end
24
+
25
+ local function format(value: any, options: MetricUnit.FormatOptions?): string
26
+ return plain(MetricUnit.format(value, options))
27
+ end
28
+
29
+ describe("MetricUnit.format durations", function()
30
+ it("writes averaged years and months whole", function()
31
+ expect(format(MetricUnit.years(1))).toBe("1 year")
32
+ expect(format(MetricUnit.months(1))).toBe("1 month")
33
+ expect(format(MetricUnit.years(1) / 12)).toBe("1 month")
34
+ expect(format(MetricUnit.months(12))).toBe("1 year")
35
+ expect(format(MetricUnit.years(100))).toBe("100 years")
36
+ expect(format(MetricUnit.years(1) + MetricUnit.days(1))).toBe("1 year, 1 day")
37
+ end)
38
+
39
+ it("breaks a duration into its two largest units", function()
40
+ expect(format(MetricUnit.minutes(90))).toBe("1 hour, 30 minutes")
41
+ expect(format(MetricUnit.days(1.5))).toBe("1 day, 12 hours")
42
+ expect(format(MetricUnit.hours(36.6))).toBe("1 day, 13 hours")
43
+ expect(format(MetricUnit.seconds(45))).toBe("45 seconds")
44
+ expect(format(MetricUnit.years(1234))).toBe("1,234 years")
45
+ end)
46
+
47
+ it("carries a rounded unit into the next larger one", function()
48
+ expect(format(MetricUnit.days(1) + MetricUnit.hours(23) + MetricUnit.minutes(59.7))).toBe("2 days")
49
+ expect(format(MetricUnit.minutes(59.9), { maxParts = 1 })).toBe("1 hour")
50
+ end)
51
+
52
+ it("takes how many units to show and the separator", function()
53
+ expect(format(MetricUnit.minutes(90), { maxParts = 1 })).toBe("2 hours")
54
+ expect(format(MetricUnit.days(1) + MetricUnit.hours(2) + MetricUnit.minutes(3), { maxParts = 3 })).toBe(
55
+ "1 day, 2 hours, 3 minutes"
56
+ )
57
+ expect(format(MetricUnit.minutes(90), { separator = " " })).toBe("1 hour 30 minutes")
58
+ end)
59
+
60
+ it("writes zero, milliseconds and less than a millisecond", function()
61
+ expect(format(MetricUnit.seconds(0))).toBe("0 seconds")
62
+ expect(format(MetricUnit.milliseconds(250))).toBe("250 milliseconds")
63
+ expect(format(MetricUnit.new(40, "microsecond"))).toBe("40 microseconds")
64
+ end)
65
+
66
+ it("signs a negative duration once", function()
67
+ expect(format(-MetricUnit.minutes(90))).toBe("-1 hour, 30 minutes")
68
+ end)
69
+
70
+ it("writes one fixed time unit with a fractional amount", function()
71
+ expect(format(MetricUnit.minutes(90), { unit = "hour" })).toBe("1.5 hours")
72
+ expect(format(MetricUnit.hours(1), { unit = "hour" })).toBe("1 hour")
73
+ expect(format(MetricUnit.hours(36), { unit = "day", locale = "es-es" })).toBe("1,5 días")
74
+ end)
75
+
76
+ it("uses @quenty/time's localized duration words", function()
77
+ expect(format(MetricUnit.minutes(90), { locale = "es-es" })).toBe("1 hora, 30 minutos")
78
+ expect(format(MetricUnit.days(2), { locale = "fr-fr" })).toBe("2 jours")
79
+ expect(format(MetricUnit.hours(2), { locale = "ru-ru" })).toBe("2 часа")
80
+ local hours: TimeLocalizationUtils.DurationPhrase = { one = "%d hr", other = "%d hrs" }
81
+ expect(format(MetricUnit.minutes(90), { durationStrings = { hours = hours } })).toBe("1 hr, 30 minutes")
82
+ end)
83
+ end)
84
+
85
+ describe("MetricUnit.format clock durations", function()
86
+ it("writes a clock, minutes and seconds under an hour", function()
87
+ expect(format(MetricUnit.minutes(90), { durationStyle = "clock" })).toBe("1:30:00")
88
+ expect(format(MetricUnit.seconds(45), { durationStyle = "clock" })).toBe("0:45")
89
+ expect(format(MetricUnit.seconds(3725), { durationStyle = "clock" })).toBe("1:02:05")
90
+ expect(format(MetricUnit.hours(30), { durationStyle = "clock" })).toBe("30:00:00")
91
+ end)
92
+
93
+ it("passes a template to TimeDurationUtils, refusing years and months", function()
94
+ expect(format(MetricUnit.seconds(90), { durationTemplate = "mm:ss" })).toBe("01:30")
95
+ expect(format(MetricUnit.days(2), { durationTemplate = "d __" })).toBe("2 days")
96
+ expect(format(MetricUnit.seconds(90), { durationTemplate = "[at] m:ss" })).toBe("at 1:30")
97
+ expect(function()
98
+ format(MetricUnit.years(1), { durationTemplate = "y __" })
99
+ end).toThrow("counts years or months")
100
+ end)
101
+ end)
102
+
103
+ describe("MetricUnit and @quenty/time", function()
104
+ it("hands exact seconds to TimeDurationUtils", function()
105
+ expect(MetricUnit.minutes(2):InSeconds()).toBe(120)
106
+ expect(TimeDurationUtils.format(MetricUnit.hours(36):InSeconds(), "h:mm")).toBe("36:00")
107
+ expect(MetricUnit.years(1):InSeconds()).toBe(YEAR)
108
+ end)
109
+
110
+ it("measures the exact time between two DateTimes", function()
111
+ local start = MetricUnit.fromDateTime(DateTime.fromUnixTimestamp(0))
112
+ local later = MetricUnit.fromDateTime(DateTime.fromUnixTimestamp(90))
113
+ expect((later - start):InSeconds()).toBe(90)
114
+ expect((start - later):InSeconds()).toBe(-90)
115
+ expect(MetricUnit.isKind(later - start, "time")).toBe(true)
116
+
117
+ -- A calendar year with a leap day is 366 days, longer than the averaged year
118
+ local leapYear = MetricUnit.fromDateTime(DateTime.fromUniversalTime(2025, 1, 1))
119
+ - MetricUnit.fromDateTime(DateTime.fromUniversalTime(2024, 1, 1))
120
+ expect(leapYear:In("day")).toBe(366)
121
+ expect(leapYear > MetricUnit.years(1)).toBe(true)
122
+ end)
123
+
124
+ it("moves a DateTime by an exact span", function()
125
+ local start = MetricUnit.fromDateTime(DateTime.fromUnixTimestamp(0))
126
+ expect((start + MetricUnit.hours(1)):ToDateTime().UnixTimestamp).toBe(3600)
127
+ expect((start + MetricUnit.months(1)):ToDateTime().UnixTimestampMillis).toBe(MONTH * 1000)
128
+ end)
129
+ end)
130
+
131
+ describe("MetricUnit instants", function()
132
+ it("subtracts two instants to a span, and adds a span to an instant", function()
133
+ local worldStart = MetricUnit.instant(0)
134
+ local now = worldStart + MetricUnit.years(250)
135
+ expect(MetricUnit.isKind(now, "instant")).toBe(true)
136
+ expect(now:IsAbsolute()).toBe(true)
137
+ expect(now:GetKind()).toBe("instant")
138
+ expect((now - worldStart):GetKind()).toBe("time")
139
+ expect((now - worldStart):In("year")).toBeCloseTo(250, 9)
140
+ expect((MetricUnit.years(1) + now - now):In("year")).toBeCloseTo(1, 9)
141
+ expect(MetricUnit.isKind(now - MetricUnit.days(1), "instant")).toBe(true)
142
+ expect(MetricUnit.isKind(now, "time")).toBe(false)
143
+ end)
144
+
145
+ it("reads an instant as the time since its epoch", function()
146
+ local instant = MetricUnit.instant(5400)
147
+ expect(instant:InSeconds()).toBe(5400)
148
+ expect(instant:In("hour")).toBe(1.5)
149
+ expect(function()
150
+ instant:In("meter")
151
+ end).toThrow('In("meter") reads length, not instant')
152
+ end)
153
+
154
+ it("rejects adding, scaling and negating instants", function()
155
+ local instant = MetricUnit.instant(10)
156
+ expect(function()
157
+ local _ = instant + instant
158
+ end).toThrow("cannot add two instants; subtract them for a difference, or add a difference to one")
159
+ expect(function()
160
+ local _ = MetricUnit.seconds(1) - instant
161
+ end).toThrow("cannot subtract an instant from a time")
162
+ expect(function()
163
+ local _ = instant * 2
164
+ end).toThrow("cannot multiply an instant")
165
+ expect(function()
166
+ local _ = -instant
167
+ end).toThrow("cannot negate an instant")
168
+ expect(function()
169
+ MetricUnit.abs(instant)
170
+ end).toThrow("cannot take the size of an instant")
171
+ expect(function()
172
+ local _ = instant < MetricUnit.seconds(20)
173
+ end).toThrow("cannot compare an instant and a time")
174
+ end)
175
+
176
+ it("expects instants and spans apart", function()
177
+ expect(MetricUnit.expect(MetricUnit.instant(7), "instant", "at")).toBe(7)
178
+ expect(function()
179
+ MetricUnit.expect(MetricUnit.seconds(7), "instant", "at")
180
+ end).toThrow("MetricUnit.expect: at expects instant, got time")
181
+ expect(function()
182
+ MetricUnit.expect(MetricUnit.instant(7), "time", "every")
183
+ end).toThrow("MetricUnit.expect: every expects time, got instant")
184
+ end)
185
+
186
+ it("round trips through DateTime and plain data", function()
187
+ local dateTime = DateTime.fromUnixTimestampMillis(1_700_000_000_123)
188
+ expect(MetricUnit.fromDateTime(dateTime):ToDateTime().UnixTimestampMillis).toBe(1_700_000_000_123)
189
+ expect(function()
190
+ MetricUnit.seconds(5):ToDateTime()
191
+ end).toThrow("ToDateTime takes an instant, not time")
192
+
193
+ local data = MetricUnit.toData(MetricUnit.instant(42))
194
+ expect(data).toEqual({ amount = 42, dimensions = { time = 1 }, absolute = true })
195
+ expect(MetricUnit.isKind(MetricUnit.fromData(data), "instant")).toBe(true)
196
+ end)
197
+
198
+ it("writes an instant apart from a span", function()
199
+ expect(tostring(MetricUnit.instant(90))).toBe("epoch + " .. tostring(MetricUnit.seconds(90)))
200
+ expect(format(MetricUnit.instant(5400))).toBe("1 hour, 30 minutes")
201
+ end)
202
+ end)
@@ -0,0 +1,210 @@
1
+ --!strict
2
+ --[[
3
+ @class MetricUnit.Units.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
+ local MetricUtils = require("MetricUtils")
12
+
13
+ local describe = Jest.Globals.describe
14
+ local expect = Jest.Globals.expect
15
+ local it = Jest.Globals.it
16
+
17
+ -- Formatting puts a no-break space between a number and its unit; these compare it as a plain space
18
+ local function plain(text: string): string
19
+ return (string.gsub(text, "\u{00A0}", " "))
20
+ end
21
+
22
+ local function format(value: any, options: MetricUnit.FormatOptions?): string
23
+ return plain(MetricUnit.format(value, options))
24
+ end
25
+
26
+ describe("MetricUnit:In", function()
27
+ it("reads by unit id, by @quenty/time's names for time, and by unit value", function()
28
+ expect(MetricUnit.kilometers(2):In("meter")).toBe(2000)
29
+ expect(MetricUnit.minutes(90):In("hour")).toBe(1.5)
30
+ expect(MetricUnit.minutes(90):In("hours")).toBe(1.5)
31
+ expect(MetricUnit.minutes(90):In("h")).toBe(1.5)
32
+ expect(MetricUnit.minutes(90):In(MetricUnit.hours(1))).toBe(1.5)
33
+ expect(MetricUnit.perDay(48):In("perHour")).toBeCloseTo(2, 9)
34
+ expect(MetricUnit.years(1):In("quarter")).toBeCloseTo(4, 9)
35
+ end)
36
+
37
+ it("reads a temperature scale from its zero when absolute", function()
38
+ expect(MetricUnit.celsius(100):In("fahrenheit")).toBeCloseTo(212, 9)
39
+ expect(MetricUnit.difference(5, "celsius"):In("fahrenheit")).toBeCloseTo(9, 9)
40
+ end)
41
+
42
+ it("errors on an unknown id, and on another dimension naming both", function()
43
+ expect(function()
44
+ MetricUnit.meters(1):In("klicks" :: any)
45
+ end).toThrow('MetricUnit:In knows no unit "klicks"')
46
+ expect(function()
47
+ MetricUnit.meters(1):In("second")
48
+ end).toThrow('In("second") reads time, not length')
49
+ expect(function()
50
+ MetricUnit.seconds(1):InSeconds()
51
+ end).never.toThrow()
52
+ end)
53
+ end)
54
+
55
+ describe("MetricUnit.new", function()
56
+ it("makes a value from an amount and a unit id", function()
57
+ expect(MetricUnit.new(5, "kilometer") == MetricUnit.kilometers(5)).toBe(true)
58
+ expect(MetricUnit.new(90, "minutes") == MetricUnit.hours(1.5)).toBe(true)
59
+ expect(MetricUnit.new(20, "celsius"):IsAbsolute()).toBe(true)
60
+ expect(function()
61
+ MetricUnit.new(1, "klicks" :: any)
62
+ end).toThrow('MetricUnit.new knows no unit "klicks"')
63
+ end)
64
+ end)
65
+
66
+ describe("MetricUnit dimension queries", function()
67
+ it("names what a value measures", function()
68
+ expect(MetricUnit.kilometers(1):GetKind()).toBe("length")
69
+ expect((MetricUnit.kilometers(1) / MetricUnit.hours(1)):GetKind()).toBe("speed")
70
+ expect(MetricUnit.celsius(5):GetKind()).toBe("temperature")
71
+ expect(MetricUnit.difference(5, "celsius"):GetKind()).toBe("temperatureDifference")
72
+ expect(MetricUnit.none(1):GetKind()).toBe("none")
73
+ expect(MetricUnit.pow(MetricUnit.meters(1), 4):GetKind()).toBeNil()
74
+ end)
75
+
76
+ it("checks kinds by name or exponents, and errors on an unknown name", function()
77
+ local speed = MetricUnit.meters(1) / MetricUnit.seconds(1)
78
+ expect(speed:IsKind("speed")).toBe(true)
79
+ expect(speed:IsKind({ length = 1, time = -1 })).toBe(true)
80
+ expect(MetricUnit.isKind(speed, "length")).toBe(false)
81
+ expect(MetricUnit.isKind(5, "length")).toBe(false)
82
+ expect(function()
83
+ MetricUnit.isKind(speed, "velocity" :: any)
84
+ end).toThrow('no kind named "velocity"')
85
+ end)
86
+
87
+ it("gives the base amount, frozen dimensions and absoluteness", function()
88
+ local length = MetricUnit.kilometers(2)
89
+ expect(length:GetBaseAmount()).toBe(2000)
90
+ expect(length:GetDimensions()).toEqual({ length = 1 })
91
+ expect(table.isfrozen(length:GetDimensions())).toBe(true)
92
+ expect(length:IsAbsolute()).toBe(false)
93
+ end)
94
+
95
+ it("lists the unit ids a value can be read in, metric first", function()
96
+ local ids = MetricUnit.meters(1):GetUnitIds()
97
+ expect(ids[1]).toBe("meter")
98
+ expect(table.find(ids, "nanometer")).never.toBeNil()
99
+ expect(table.find(ids, "mile")).never.toBeNil()
100
+ expect(table.find(ids, "stud")).never.toBeNil()
101
+ expect(table.find(ids, "kilogram")).toBeNil()
102
+ expect(table.find(MetricUnit.celsius(5):GetUnitIds(), "fahrenheit")).never.toBeNil()
103
+ end)
104
+ end)
105
+
106
+ describe("MetricUnit studs", function()
107
+ it("counts a stud as 28 cm, as Roblox documents it", function()
108
+ expect(MetricUnitDefinitions.ROBLOX_METERS_PER_STUD).toBe(0.28)
109
+ expect(MetricUnit.studs(100):In("meter")).toBeCloseTo(28, 9)
110
+ expect(MetricUnit.meters(1):In("stud")).toBeCloseTo(1 / 0.28, 9)
111
+ expect(MetricUnit.new(100, "studPerSecond"):In("meterPerSecond")).toBeCloseTo(28, 9)
112
+ end)
113
+
114
+ it("keeps MetricUtils at the 3.57 studs to the metre it has always used", function()
115
+ expect(MetricUtils.studsToMeters(357)).toBeCloseTo(100, 9)
116
+ expect(MetricUtils.studsPerSecondToMetersPerSecond(35.7)).toBeCloseTo(10, 9)
117
+ end)
118
+
119
+ it("shows studs as studs, until converted", function()
120
+ expect(format(MetricUnit.studs(250))).toBe("250 studs")
121
+ expect(format(MetricUnit.studs(250):ToMetric())).toBe("70 m")
122
+ expect(format(MetricUnit.studs(250), { unit = "stud" })).toBe("250 studs")
123
+ expect(format(MetricUnit.studs(1), { unit = "stud" })).toBe("1 stud")
124
+ expect(format(MetricUnit.new(16, "studPerSecond"), { unit = "studPerSecond" })).toBe("16 studs/s")
125
+ expect(format(MetricUnit.studs(250):ToUSCustomary())).toBe("229.659 ft")
126
+ end)
127
+ end)
128
+
129
+ describe("MetricUnit US customary units", function()
130
+ it("converts with exact definitions", function()
131
+ expect(MetricUnit.new(1, "inch"):In("millimeter")).toBeCloseTo(25.4, 9)
132
+ expect(MetricUnit.fuzzyEq(MetricUnit.new(3, "foot"), MetricUnit.new(1, "yard"))).toBe(true)
133
+ expect(MetricUnit.new(1, "mile"):In("foot")).toBeCloseTo(5280, 9)
134
+ expect(MetricUnit.new(1, "pound"):In("kilogram")).toBe(0.45359237)
135
+ expect(MetricUnit.new(1, "pound"):In("ounce")).toBeCloseTo(16, 9)
136
+ expect(MetricUnit.new(1, "stone"):In("pound")).toBeCloseTo(14, 9)
137
+ expect(MetricUnit.new(1, "acre"):In("squareFoot")).toBeCloseTo(43560, 6)
138
+ expect(MetricUnit.new(1, "usGallon"):In("usFluidOunce")).toBeCloseTo(128, 9)
139
+ expect(MetricUnit.new(1, "imperialGallon"):In("liter")).toBeCloseTo(4.54609, 9)
140
+ expect(MetricUnit.new(60, "milePerHour"):In("kilometerPerHour")).toBeCloseTo(96.56064, 9)
141
+ expect(MetricUnit.new(1, "knot"):In("kilometerPerHour")).toBeCloseTo(1.852, 9)
142
+ expect(MetricUnit.new(1, "poundForce"):In("newton")).toBeCloseTo(4.4482216152605, 12)
143
+ expect(MetricUnit.new(1, "poundForcePerSquareInch"):In("pascal")).toBeCloseTo(6894.757293168, 6)
144
+ expect(
145
+ (MetricUnit.new(1, "poundForce") / MetricUnit.pow(MetricUnit.new(1, "inch"), 2)):In(
146
+ "poundForcePerSquareInch"
147
+ )
148
+ ).toBeCloseTo(1, 12)
149
+ end)
150
+
151
+ it("shows a value made in a customary unit in customary units, until converted", function()
152
+ expect(format(MetricUnit.new(1, "mile"))).toBe("1 mi")
153
+ expect(format(MetricUnit.new(1, "mile"):ToMetric())).toBe("1.609 km")
154
+ expect(format(MetricUnit.kilometers(1.609))).toBe("1.609 km")
155
+ expect(format(MetricUnit.new(1, "mile"), { unit = "mile" })).toBe("1 mi")
156
+ expect(format(MetricUnit.new(2, "foot"), { unit = "foot", unitDisplay = "long" })).toBe("2 feet")
157
+ expect(format(MetricUnit.new(1, "foot"), { unit = "foot", unitDisplay = "long" })).toBe("1 foot")
158
+ end)
159
+
160
+ it("picks customary units for a value that prefers them", function()
161
+ local function usCustomary(value: MetricUnit.MetricUnit): string
162
+ return format(value:ToUSCustomary())
163
+ end
164
+ expect(usCustomary(MetricUnit.kilometers(5))).toBe("3.107 mi")
165
+ expect(usCustomary(MetricUnit.meters(1))).toBe("3.281 ft")
166
+ expect(usCustomary(MetricUnit.new(10, "centimeter"))).toBe("3.937 in")
167
+ expect(usCustomary(MetricUnit.kilograms(1))).toBe("2.205 lb")
168
+ expect(usCustomary(MetricUnit.new(100, "kilometerPerHour"))).toBe("62.137 mph")
169
+ expect(usCustomary(MetricUnit.celsius(100))).toBe("212 °F")
170
+ expect(usCustomary(MetricUnit.difference(5, "celsius"))).toBe("9 °F")
171
+ expect(usCustomary(MetricUnit.new(1, "bar"))).toBe("14.504 psi")
172
+ expect(usCustomary(MetricUnit.new(1500, "joule"))).toBe("1.422 Btu")
173
+ -- Dimensions without US customary display units stay metric
174
+ expect(usCustomary(MetricUnit.new(5, "volt"))).toBe("5 V")
175
+ expect(format(MetricUnit.kilometers(5):ToUSCustomary(), { autoUnit = false })).toBe("5,000 m")
176
+ end)
177
+ end)
178
+
179
+ describe("MetricUnit bits", function()
180
+ it("shares the memory dimension with bytes, eight to a byte", function()
181
+ expect(MetricUnit.new(8, "bit") == MetricUnit.bytes(1)).toBe(true)
182
+ expect(MetricUnit.new(8, "megabit"):In("kilobyte")).toBeCloseTo(1000, 9)
183
+ expect(MetricUnit.new(8, "megabitPerSecond"):In("kilobytePerSecond")).toBeCloseTo(1000, 9)
184
+ expect(MetricUnit.new(1, "gigabitPerSecond"):GetKind()).toBe("dataRate")
185
+ end)
186
+
187
+ it("never switches between bits and bytes on its own", function()
188
+ expect(format(MetricUnit.new(8, "bit"))).toBe("1 B")
189
+ expect(format(MetricUnit.new(8, "kilobit"), { unit = "kilobit" })).toBe("8 kbit")
190
+ expect(format(MetricUnit.new(100, "megabitPerSecond"), { unit = "megabitPerSecond" })).toBe("100 Mbit/s")
191
+ end)
192
+ end)
193
+
194
+ describe("MetricUnit percentages", function()
195
+ it("reads and makes dimensionless proportions", function()
196
+ expect(MetricUnit.percent(45):In("percent")).toBeCloseTo(45, 9)
197
+ expect(MetricUnit.percent(45):GetBaseAmount()).toBeCloseTo(0.45, 12)
198
+ expect(MetricUnit.new(5, "permille"):In("percent")).toBeCloseTo(0.5, 12)
199
+ expect((MetricUnit.percent(45) * MetricUnit.meters(10)):In("meter")).toBeCloseTo(4.5, 9)
200
+ end)
201
+
202
+ it("formats a proportion as a percent only on request", function()
203
+ local used = MetricUnit.kilobytes(450) / MetricUnit.megabytes(1)
204
+ expect(format(used)).toBe("0.45")
205
+ expect(format(used, { unit = "percent" })).toBe("45%")
206
+ expect(format(used, { unit = "percent", unitDisplay = "long" })).toBe("45 percent")
207
+ expect(format(MetricUnit.new(5, "permille"), { unit = "permille" })).toBe("5‰")
208
+ expect(format(MetricUnit.percent(45):As("percent"))).toBe("45%")
209
+ end)
210
+ end)