@quenty/metricutils 2.4.4 → 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 +6 -0
  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 +46 -15
  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,55 @@
1
+ --!strict
2
+ --[[
3
+ @class MetricUnitDefinitions.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 MetricUnitDimensionUtils = require("MetricUnitDimensionUtils")
12
+
13
+ local describe = Jest.Globals.describe
14
+ local expect = Jest.Globals.expect
15
+ local it = Jest.Globals.it
16
+
17
+ -- A unit is written by name unless @quenty/time's duration words or a per phrase write it
18
+ local function isWrittenByName(unit: MetricUnitDefinitions.UnitDefinition): boolean
19
+ return unit.per == nil and (not unit.duration or unit.id == "microsecond")
20
+ end
21
+
22
+ describe("MetricUnitDefinitions", function()
23
+ it("declares each unit once, and every unit makes and reads back through MetricUnit.new", function()
24
+ local seen: { [string]: boolean } = {}
25
+ for _, unit in MetricUnitDefinitions.getUnits() do
26
+ expect(seen[unit.id]).toBeNil()
27
+ seen[unit.id] = true
28
+ expect(MetricUnitDefinitions.getUnit(unit.id)).toBe(unit)
29
+ expect(MetricUnit.new(1, unit.id):In(unit.id)).toBeCloseTo(1, 9)
30
+ expect(MetricUnitDimensionUtils.isDimensions(unit.dimensions)).toBe(true)
31
+ expect(unit.factor > 0).toBe(true)
32
+ end
33
+ end)
34
+
35
+ it("gives English names to every unit written by name, and to no other", function()
36
+ for _, unit in MetricUnitDefinitions.getUnits() do
37
+ if isWrittenByName(unit) then
38
+ local names = unit.names
39
+ assert(names, string.format("%s has no English names", unit.id))
40
+ expect(#names.one > 0 and #names.other > 0).toBe(true)
41
+ else
42
+ expect(unit.names).toBeNil()
43
+ end
44
+ end
45
+ end)
46
+
47
+ it("resolves @quenty/time's names to time units", function()
48
+ for alias, id in MetricUnitDefinitions.getTimeAliases() do
49
+ local unit = MetricUnitDefinitions.getUnit(alias)
50
+ assert(unit, alias)
51
+ expect(unit.id).toBe(id)
52
+ expect(unit.duration).toBe(true)
53
+ end
54
+ end)
55
+ end)
@@ -0,0 +1,164 @@
1
+ --!strict
2
+ --[=[
3
+ Functions over dimensions: frozen tables of integer exponents by name, zero exponents left out, so a speed is
4
+ `{ length = 1, time = -1 }` and a plain number is `{}`. Any name is a dimension; `time`, `length`, `memory`,
5
+ `mass`, `temperature`, `angle`, `current`, `amount` and `luminousIntensity` are the base dimensions [MetricUnit] has units for, and derived ones such as force
6
+ and energy are products of those. The named combinations, such as `"speed"`, are in [MetricUnitKinds].
7
+
8
+ ```lua
9
+ MetricUnitDimensionUtils.combine({ length = 1 }, { time = 1 }, -1) -- { length = 1, time = -1 }
10
+ MetricUnitDimensionUtils.describe({ length = 1, time = -1 }) -- "length/time"
11
+ ```
12
+
13
+ @class MetricUnitDimensionUtils
14
+ ]=]
15
+
16
+ local MetricUnitDimensionUtils = {}
17
+
18
+ --[=[
19
+ Integer exponents keyed by dimension name, with zero exponents left out.
20
+
21
+ @type Dimensions { [string]: number }
22
+ @within MetricUnitDimensionUtils
23
+ ]=]
24
+ export type Dimensions = { [string]: number }
25
+
26
+ --[=[
27
+ No dimensions: a plain number's, and what `combine` returns when every exponent cancels.
28
+
29
+ @prop NONE Dimensions
30
+ @within MetricUnitDimensionUtils
31
+ ]=]
32
+ MetricUnitDimensionUtils.NONE = table.freeze({}) :: Dimensions
33
+
34
+ --[=[
35
+ The exponents of `a` plus `sign` times those of `b`: a product when `sign` is 1, a quotient when it is -1.
36
+
37
+ @param a Dimensions
38
+ @param b Dimensions
39
+ @param sign number -- 1 or -1
40
+ @return Dimensions
41
+ ]=]
42
+ function MetricUnitDimensionUtils.combine(a: Dimensions, b: Dimensions, sign: number): Dimensions
43
+ local result: Dimensions = table.clone(a)
44
+ for name, exponent in b do
45
+ local total = (result[name] or 0) + sign * exponent
46
+ if total == 0 then
47
+ result[name] = nil
48
+ else
49
+ result[name] = total
50
+ end
51
+ end
52
+ return table.freeze(result)
53
+ end
54
+
55
+ --[=[
56
+ The exponents of `a` times `factor`, erroring if any is not a whole number.
57
+
58
+ @param a Dimensions
59
+ @param factor number
60
+ @return Dimensions
61
+ ]=]
62
+ function MetricUnitDimensionUtils.scale(a: Dimensions, factor: number): Dimensions
63
+ local result: Dimensions = {}
64
+ for name, exponent in a do
65
+ local scaled = exponent * factor
66
+ if scaled ~= math.floor(scaled) then
67
+ error(
68
+ string.format(
69
+ "MetricUnit: %s to the power %s has a fractional exponent",
70
+ MetricUnitDimensionUtils.describe(a),
71
+ tostring(factor)
72
+ ),
73
+ 3
74
+ )
75
+ end
76
+ if scaled ~= 0 then
77
+ result[name] = scaled
78
+ end
79
+ end
80
+ return table.freeze(result)
81
+ end
82
+
83
+ --[=[
84
+ Whether `a` and `b` have the same exponents.
85
+
86
+ @param a Dimensions
87
+ @param b Dimensions
88
+ @return boolean
89
+ ]=]
90
+ function MetricUnitDimensionUtils.equal(a: Dimensions, b: Dimensions): boolean
91
+ if a == b then
92
+ return true
93
+ end
94
+ for name, exponent in a do
95
+ if b[name] ~= exponent then
96
+ return false
97
+ end
98
+ end
99
+ for name in b do
100
+ if a[name] == nil then
101
+ return false
102
+ end
103
+ end
104
+ return true
105
+ end
106
+
107
+ --[=[
108
+ Whether `value` is a table of whole-number exponents keyed by name, none of them zero.
109
+
110
+ @param value any
111
+ @return boolean
112
+ ]=]
113
+ function MetricUnitDimensionUtils.isDimensions(value: any): boolean
114
+ if type(value) ~= "table" then
115
+ return false
116
+ end
117
+ for name, exponent in value do
118
+ if
119
+ type(name) ~= "string"
120
+ or type(exponent) ~= "number"
121
+ or exponent == 0
122
+ or exponent ~= math.floor(exponent)
123
+ then
124
+ return false
125
+ end
126
+ end
127
+ return true
128
+ end
129
+
130
+ --[=[
131
+ A readable formula for `dimensions`: "time", "length/time", "1/time", "length^2" or "dimensionless".
132
+
133
+ @param dimensions Dimensions
134
+ @return string
135
+ ]=]
136
+ function MetricUnitDimensionUtils.describe(dimensions: Dimensions): string
137
+ local over: { string } = {}
138
+ local under: { string } = {}
139
+ for _, name in MetricUnitDimensionUtils._sortedNames(dimensions) do
140
+ local exponent = dimensions[name]
141
+ local power = math.abs(exponent)
142
+ local part = if power == 1 then name else name .. "^" .. tostring(power)
143
+ table.insert(if exponent > 0 then over else under, part)
144
+ end
145
+ if #over == 0 and #under == 0 then
146
+ return "dimensionless"
147
+ end
148
+ local text = if #over == 0 then "1" else table.concat(over, "·")
149
+ for _, part in under do
150
+ text ..= "/" .. part
151
+ end
152
+ return text
153
+ end
154
+
155
+ function MetricUnitDimensionUtils._sortedNames(dimensions: Dimensions): { string }
156
+ local names: { string } = {}
157
+ for name in dimensions do
158
+ table.insert(names, name)
159
+ end
160
+ table.sort(names)
161
+ return names
162
+ end
163
+
164
+ return table.freeze(MetricUnitDimensionUtils)
@@ -0,0 +1,49 @@
1
+ --!strict
2
+ --[[
3
+ @class MetricUnitDimensionUtils.spec.lua
4
+ ]]
5
+
6
+ local require = require(script.Parent.loader).load(script)
7
+
8
+ local Jest = require("Jest")
9
+ local MetricUnitDimensionUtils = require("MetricUnitDimensionUtils")
10
+
11
+ local describe = Jest.Globals.describe
12
+ local expect = Jest.Globals.expect
13
+ local it = Jest.Globals.it
14
+
15
+ describe("MetricUnitDimensionUtils", function()
16
+ it("combines exponents, dropping zeros, frozen", function()
17
+ local speed = MetricUnitDimensionUtils.combine({ length = 1 }, { time = 1 }, -1)
18
+ expect(speed).toEqual({ length = 1, time = -1 })
19
+ expect(table.isfrozen(speed)).toBe(true)
20
+ expect(MetricUnitDimensionUtils.combine(speed, { time = 1 }, 1)).toEqual({ length = 1 })
21
+ expect(MetricUnitDimensionUtils.combine(speed, speed, -1)).toEqual(MetricUnitDimensionUtils.NONE)
22
+ end)
23
+
24
+ it("takes any name as a dimension", function()
25
+ local people = MetricUnitDimensionUtils.combine({ people = 1 }, { length = 1 }, -2)
26
+ expect(MetricUnitDimensionUtils.describe(people)).toBe("people/length^2")
27
+ end)
28
+
29
+ it("scales exponents, erroring on a fractional one", function()
30
+ expect(MetricUnitDimensionUtils.scale({ length = 2 }, 0.5)).toEqual({ length = 1 })
31
+ expect(function()
32
+ MetricUnitDimensionUtils.scale({ length = 1 }, 0.5)
33
+ end).toThrow("length to the power 0.5 has a fractional exponent")
34
+ end)
35
+
36
+ it("describes dimensions as a formula", function()
37
+ expect(MetricUnitDimensionUtils.describe(MetricUnitDimensionUtils.NONE)).toBe("dimensionless")
38
+ expect(MetricUnitDimensionUtils.describe({ time = -1 })).toBe("1/time")
39
+ expect(MetricUnitDimensionUtils.describe({ length = 2, mass = 1 })).toBe("length^2·mass")
40
+ end)
41
+
42
+ it("tells equal dimensions apart, and valid ones from others", function()
43
+ expect(MetricUnitDimensionUtils.equal({ length = 1, time = -1 }, { time = -1, length = 1 })).toBe(true)
44
+ expect(MetricUnitDimensionUtils.equal({ length = 1 }, { length = 1, time = -1 })).toBe(false)
45
+ expect(MetricUnitDimensionUtils.isDimensions({ length = 2 })).toBe(true)
46
+ expect(MetricUnitDimensionUtils.isDimensions({ length = 0.5 })).toBe(false)
47
+ expect(MetricUnitDimensionUtils.isDimensions({ length = 0 })).toBe(false)
48
+ end)
49
+ end)
@@ -0,0 +1,237 @@
1
+ --!strict
2
+ --[=[
3
+ The named kinds of measurement, such as `"length"` and `"speed"`: each a set of dimensions, absolute or a
4
+ difference. [MetricUnit.GetKind], [MetricUnit.isKind] and [MetricUnit.expect] take these names. The dimension
5
+ functions they are built from are in [MetricUnitDimensionUtils].
6
+
7
+ ```lua
8
+ MetricUnitKinds.SPEED -- { length = 1, time = -1 }
9
+ MetricUnitKinds.fromName("speed") -- { length = 1, time = -1 }, false
10
+ MetricUnitKinds.getKind({ temperature = 1 }, true) -- "temperature"
11
+ ```
12
+
13
+ @class MetricUnitKinds
14
+ ]=]
15
+
16
+ local require = require(script.Parent.loader).load(script)
17
+
18
+ local MetricUnitDimensionUtils = require("MetricUnitDimensionUtils")
19
+
20
+ local MetricUnitKinds = {}
21
+
22
+ type Dimensions = MetricUnitDimensionUtils.Dimensions
23
+
24
+ --[=[
25
+ A kind's name. `"temperature"` is an absolute temperature and `"temperatureDifference"` a difference between two.
26
+ `"time"` is a span of time and `"instant"` a point in time, measured from an epoch the caller chooses. `"none"` is
27
+ a plain, dimensionless number.
28
+
29
+ @type KindName "none" | "time" | "length" | "area" | "volume" | "memory" | "mass" | "rate" | "speed" | "acceleration" | "force" | "energy" | "power" | "pressure" | "density" | "dataRate" | "temperature" | "temperatureDifference" | "instant" | "angle" | "angularVelocity" | "angularAcceleration" | "current" | "charge" | "voltage" | "resistance" | "conductance" | "capacitance" | "inductance" | "magneticFlux" | "magneticFluxDensity" | "amount" | "luminousIntensity" | "solidAngle" | "luminousFlux" | "illuminance" | "absorbedDose" | "catalyticActivity" | "molarMass" | "molarConcentration" | "fuelEconomy"
30
+ @within MetricUnitKinds
31
+ ]=]
32
+ export type KindName =
33
+ "none"
34
+ | "time"
35
+ | "length"
36
+ | "area"
37
+ | "volume"
38
+ | "memory"
39
+ | "mass"
40
+ | "rate"
41
+ | "speed"
42
+ | "acceleration"
43
+ | "force"
44
+ | "energy"
45
+ | "power"
46
+ | "pressure"
47
+ | "density"
48
+ | "dataRate"
49
+ | "temperature"
50
+ | "temperatureDifference"
51
+ | "instant"
52
+ | "angle"
53
+ | "angularVelocity"
54
+ | "angularAcceleration"
55
+ | "current"
56
+ | "charge"
57
+ | "voltage"
58
+ | "resistance"
59
+ | "conductance"
60
+ | "capacitance"
61
+ | "inductance"
62
+ | "magneticFlux"
63
+ | "magneticFluxDensity"
64
+ | "amount"
65
+ | "luminousIntensity"
66
+ | "solidAngle"
67
+ | "luminousFlux"
68
+ | "illuminance"
69
+ | "absorbedDose"
70
+ | "catalyticActivity"
71
+ | "molarMass"
72
+ | "molarConcentration"
73
+ | "fuelEconomy"
74
+
75
+ MetricUnitKinds.TIME = table.freeze({ time = 1 }) :: Dimensions
76
+ MetricUnitKinds.LENGTH = table.freeze({ length = 1 }) :: Dimensions
77
+ MetricUnitKinds.MEMORY = table.freeze({ memory = 1 }) :: Dimensions
78
+ MetricUnitKinds.MASS = table.freeze({ mass = 1 }) :: Dimensions
79
+ MetricUnitKinds.TEMPERATURE = table.freeze({ temperature = 1 }) :: Dimensions
80
+ MetricUnitKinds.ANGLE = table.freeze({ angle = 1 }) :: Dimensions
81
+ MetricUnitKinds.CURRENT = table.freeze({ current = 1 }) :: Dimensions
82
+ MetricUnitKinds.AMOUNT = table.freeze({ amount = 1 }) :: Dimensions
83
+ MetricUnitKinds.LUMINOUS_INTENSITY = table.freeze({ luminousIntensity = 1 }) :: Dimensions
84
+
85
+ local function times(a: Dimensions, b: Dimensions): Dimensions
86
+ return MetricUnitDimensionUtils.combine(a, b, 1)
87
+ end
88
+
89
+ local function per(a: Dimensions, b: Dimensions): Dimensions
90
+ return MetricUnitDimensionUtils.combine(a, b, -1)
91
+ end
92
+
93
+ MetricUnitKinds.AREA = times(MetricUnitKinds.LENGTH, MetricUnitKinds.LENGTH)
94
+ MetricUnitKinds.VOLUME = times(MetricUnitKinds.AREA, MetricUnitKinds.LENGTH)
95
+ MetricUnitKinds.RATE = per(MetricUnitDimensionUtils.NONE, MetricUnitKinds.TIME)
96
+ MetricUnitKinds.SPEED = per(MetricUnitKinds.LENGTH, MetricUnitKinds.TIME)
97
+ MetricUnitKinds.ACCELERATION = per(MetricUnitKinds.SPEED, MetricUnitKinds.TIME)
98
+ MetricUnitKinds.FORCE = times(MetricUnitKinds.MASS, MetricUnitKinds.ACCELERATION)
99
+ MetricUnitKinds.ENERGY = times(MetricUnitKinds.FORCE, MetricUnitKinds.LENGTH)
100
+ MetricUnitKinds.POWER = per(MetricUnitKinds.ENERGY, MetricUnitKinds.TIME)
101
+ MetricUnitKinds.PRESSURE = per(MetricUnitKinds.FORCE, MetricUnitKinds.AREA)
102
+ MetricUnitKinds.DENSITY = per(MetricUnitKinds.MASS, MetricUnitKinds.VOLUME)
103
+ MetricUnitKinds.DATA_RATE = per(MetricUnitKinds.MEMORY, MetricUnitKinds.TIME)
104
+ MetricUnitKinds.ANGULAR_VELOCITY = per(MetricUnitKinds.ANGLE, MetricUnitKinds.TIME)
105
+ MetricUnitKinds.ANGULAR_ACCELERATION = per(MetricUnitKinds.ANGULAR_VELOCITY, MetricUnitKinds.TIME)
106
+ MetricUnitKinds.CHARGE = times(MetricUnitKinds.CURRENT, MetricUnitKinds.TIME)
107
+ MetricUnitKinds.VOLTAGE = per(MetricUnitKinds.POWER, MetricUnitKinds.CURRENT)
108
+ MetricUnitKinds.RESISTANCE = per(MetricUnitKinds.VOLTAGE, MetricUnitKinds.CURRENT)
109
+ MetricUnitKinds.CONDUCTANCE = per(MetricUnitKinds.CURRENT, MetricUnitKinds.VOLTAGE)
110
+ MetricUnitKinds.CAPACITANCE = per(MetricUnitKinds.CHARGE, MetricUnitKinds.VOLTAGE)
111
+ MetricUnitKinds.MAGNETIC_FLUX = times(MetricUnitKinds.VOLTAGE, MetricUnitKinds.TIME)
112
+ MetricUnitKinds.INDUCTANCE = per(MetricUnitKinds.MAGNETIC_FLUX, MetricUnitKinds.CURRENT)
113
+ MetricUnitKinds.MAGNETIC_FLUX_DENSITY = per(MetricUnitKinds.MAGNETIC_FLUX, MetricUnitKinds.AREA)
114
+ MetricUnitKinds.SOLID_ANGLE = times(MetricUnitKinds.ANGLE, MetricUnitKinds.ANGLE)
115
+ MetricUnitKinds.LUMINOUS_FLUX = times(MetricUnitKinds.LUMINOUS_INTENSITY, MetricUnitKinds.SOLID_ANGLE)
116
+ MetricUnitKinds.ILLUMINANCE = per(MetricUnitKinds.LUMINOUS_FLUX, MetricUnitKinds.AREA)
117
+ MetricUnitKinds.ABSORBED_DOSE = per(MetricUnitKinds.ENERGY, MetricUnitKinds.MASS)
118
+ MetricUnitKinds.CATALYTIC_ACTIVITY = per(MetricUnitKinds.AMOUNT, MetricUnitKinds.TIME)
119
+ MetricUnitKinds.MOLAR_MASS = per(MetricUnitKinds.MASS, MetricUnitKinds.AMOUNT)
120
+ MetricUnitKinds.MOLAR_CONCENTRATION = per(MetricUnitKinds.AMOUNT, MetricUnitKinds.VOLUME)
121
+ MetricUnitKinds.FUEL_ECONOMY = per(MetricUnitKinds.LENGTH, MetricUnitKinds.VOLUME)
122
+
123
+ type NamedKind = { name: KindName, dimensions: Dimensions, absolute: boolean }
124
+
125
+ local NAMED_KINDS_IN_LOOKUP_ORDER: { NamedKind } = {
126
+ { name = "none", dimensions = MetricUnitDimensionUtils.NONE, absolute = false },
127
+ { name = "time", dimensions = MetricUnitKinds.TIME, absolute = false },
128
+ { name = "length", dimensions = MetricUnitKinds.LENGTH, absolute = false },
129
+ { name = "area", dimensions = MetricUnitKinds.AREA, absolute = false },
130
+ { name = "volume", dimensions = MetricUnitKinds.VOLUME, absolute = false },
131
+ { name = "memory", dimensions = MetricUnitKinds.MEMORY, absolute = false },
132
+ { name = "mass", dimensions = MetricUnitKinds.MASS, absolute = false },
133
+ { name = "rate", dimensions = MetricUnitKinds.RATE, absolute = false },
134
+ { name = "speed", dimensions = MetricUnitKinds.SPEED, absolute = false },
135
+ { name = "acceleration", dimensions = MetricUnitKinds.ACCELERATION, absolute = false },
136
+ { name = "force", dimensions = MetricUnitKinds.FORCE, absolute = false },
137
+ { name = "energy", dimensions = MetricUnitKinds.ENERGY, absolute = false },
138
+ { name = "power", dimensions = MetricUnitKinds.POWER, absolute = false },
139
+ { name = "pressure", dimensions = MetricUnitKinds.PRESSURE, absolute = false },
140
+ { name = "density", dimensions = MetricUnitKinds.DENSITY, absolute = false },
141
+ { name = "dataRate", dimensions = MetricUnitKinds.DATA_RATE, absolute = false },
142
+ { name = "temperature", dimensions = MetricUnitKinds.TEMPERATURE, absolute = true },
143
+ { name = "temperatureDifference", dimensions = MetricUnitKinds.TEMPERATURE, absolute = false },
144
+ { name = "instant", dimensions = MetricUnitKinds.TIME, absolute = true },
145
+ { name = "angle", dimensions = MetricUnitKinds.ANGLE, absolute = false },
146
+ { name = "angularVelocity", dimensions = MetricUnitKinds.ANGULAR_VELOCITY, absolute = false },
147
+ { name = "angularAcceleration", dimensions = MetricUnitKinds.ANGULAR_ACCELERATION, absolute = false },
148
+ { name = "current", dimensions = MetricUnitKinds.CURRENT, absolute = false },
149
+ { name = "charge", dimensions = MetricUnitKinds.CHARGE, absolute = false },
150
+ { name = "voltage", dimensions = MetricUnitKinds.VOLTAGE, absolute = false },
151
+ { name = "resistance", dimensions = MetricUnitKinds.RESISTANCE, absolute = false },
152
+ { name = "conductance", dimensions = MetricUnitKinds.CONDUCTANCE, absolute = false },
153
+ { name = "capacitance", dimensions = MetricUnitKinds.CAPACITANCE, absolute = false },
154
+ { name = "inductance", dimensions = MetricUnitKinds.INDUCTANCE, absolute = false },
155
+ { name = "magneticFlux", dimensions = MetricUnitKinds.MAGNETIC_FLUX, absolute = false },
156
+ { name = "magneticFluxDensity", dimensions = MetricUnitKinds.MAGNETIC_FLUX_DENSITY, absolute = false },
157
+ { name = "amount", dimensions = MetricUnitKinds.AMOUNT, absolute = false },
158
+ { name = "luminousIntensity", dimensions = MetricUnitKinds.LUMINOUS_INTENSITY, absolute = false },
159
+ { name = "solidAngle", dimensions = MetricUnitKinds.SOLID_ANGLE, absolute = false },
160
+ { name = "luminousFlux", dimensions = MetricUnitKinds.LUMINOUS_FLUX, absolute = false },
161
+ { name = "illuminance", dimensions = MetricUnitKinds.ILLUMINANCE, absolute = false },
162
+ { name = "absorbedDose", dimensions = MetricUnitKinds.ABSORBED_DOSE, absolute = false },
163
+ { name = "catalyticActivity", dimensions = MetricUnitKinds.CATALYTIC_ACTIVITY, absolute = false },
164
+ { name = "molarMass", dimensions = MetricUnitKinds.MOLAR_MASS, absolute = false },
165
+ { name = "molarConcentration", dimensions = MetricUnitKinds.MOLAR_CONCENTRATION, absolute = false },
166
+ { name = "fuelEconomy", dimensions = MetricUnitKinds.FUEL_ECONOMY, absolute = false },
167
+ }
168
+
169
+ local KIND_BY_NAME: { [string]: NamedKind } = {}
170
+ for _, kind in NAMED_KINDS_IN_LOOKUP_ORDER do
171
+ KIND_BY_NAME[kind.name] = table.freeze(kind)
172
+ end
173
+ table.freeze(NAMED_KINDS_IN_LOOKUP_ORDER)
174
+ table.freeze(KIND_BY_NAME)
175
+
176
+ --[=[
177
+ The dimensions a [KindName] stands for, or the table given, and whether it names an absolute value. Errors on an
178
+ unknown name.
179
+
180
+ @param kind KindName | Dimensions
181
+ @return Dimensions
182
+ @return boolean -- whether the name is of an absolute value
183
+ ]=]
184
+ function MetricUnitKinds.fromName(kind: KindName | Dimensions): (Dimensions, boolean)
185
+ if type(kind) == "table" then
186
+ return kind, false
187
+ end
188
+ local named = KIND_BY_NAME[kind]
189
+ if not named then
190
+ error(string.format("MetricUnit: no kind named %q", tostring(kind)), 3)
191
+ end
192
+ return named.dimensions, named.absolute
193
+ end
194
+
195
+ --[=[
196
+ The [KindName] of `dimensions`, absolute or a difference, or nil for a compound without a name.
197
+
198
+ ```lua
199
+ MetricUnitKinds.getKind({ length = 1, time = -1 }, false) -- "speed"
200
+ MetricUnitKinds.getKind({ temperature = 1 }, true) -- "temperature"
201
+ MetricUnitKinds.getKind({ length = 4 }, false) -- nil
202
+ ```
203
+
204
+ @param dimensions Dimensions
205
+ @param absolute boolean
206
+ @return KindName?
207
+ ]=]
208
+ function MetricUnitKinds.getKind(dimensions: Dimensions, absolute: boolean): KindName?
209
+ for _, kind in NAMED_KINDS_IN_LOOKUP_ORDER do
210
+ if kind.absolute == absolute and MetricUnitDimensionUtils.equal(kind.dimensions, dimensions) then
211
+ return kind.name
212
+ end
213
+ end
214
+ return nil
215
+ end
216
+
217
+ --[=[
218
+ What a value of `dimensions` is, absolute or a difference: "absolute temperature", "temperature difference",
219
+ "instant", "length/time".
220
+
221
+ @param dimensions Dimensions
222
+ @param absolute boolean
223
+ @return string
224
+ ]=]
225
+ function MetricUnitKinds.describeKind(dimensions: Dimensions, absolute: boolean): string
226
+ local formula = MetricUnitDimensionUtils.describe(dimensions)
227
+ if absolute and MetricUnitDimensionUtils.equal(dimensions, MetricUnitKinds.TIME) then
228
+ return "instant"
229
+ elseif absolute then
230
+ return "absolute " .. formula
231
+ elseif MetricUnitDimensionUtils.equal(dimensions, MetricUnitKinds.TEMPERATURE) then
232
+ return formula .. " difference"
233
+ end
234
+ return formula
235
+ end
236
+
237
+ return table.freeze(MetricUnitKinds)
@@ -0,0 +1,51 @@
1
+ --!strict
2
+ --[[
3
+ @class MetricUnitKinds.spec.lua
4
+ ]]
5
+
6
+ local require = require(script.Parent.loader).load(script)
7
+
8
+ local Jest = require("Jest")
9
+ local MetricUnitDimensionUtils = require("MetricUnitDimensionUtils")
10
+ local MetricUnitKinds = require("MetricUnitKinds")
11
+
12
+ local describe = Jest.Globals.describe
13
+ local expect = Jest.Globals.expect
14
+ local it = Jest.Globals.it
15
+
16
+ describe("MetricUnitKinds", function()
17
+ it("derives every kind from the base dimensions", function()
18
+ expect(MetricUnitKinds.DENSITY).toEqual({ mass = 1, length = -3 })
19
+ expect(MetricUnitKinds.POWER).toEqual({ mass = 1, length = 2, time = -3 })
20
+ expect(MetricUnitKinds.VOLTAGE).toEqual({ mass = 1, length = 2, time = -3, current = -1 })
21
+ expect(MetricUnitKinds.RESISTANCE).toEqual({ mass = 1, length = 2, time = -3, current = -2 })
22
+ expect(MetricUnitKinds.CAPACITANCE).toEqual({ mass = -1, length = -2, time = 4, current = 2 })
23
+ expect(MetricUnitKinds.MAGNETIC_FLUX_DENSITY).toEqual({ mass = 1, time = -2, current = -1 })
24
+ end)
25
+
26
+ it("reads a kind's dimensions and absoluteness from its name", function()
27
+ expect((MetricUnitKinds.fromName("dataRate"))).toEqual({ memory = 1, time = -1 })
28
+ expect((MetricUnitKinds.fromName("none"))).toBe(MetricUnitDimensionUtils.NONE)
29
+ expect(select(2, MetricUnitKinds.fromName("temperature"))).toBe(true)
30
+ expect(select(2, MetricUnitKinds.fromName("temperatureDifference"))).toBe(false)
31
+ expect((MetricUnitKinds.fromName({ length = 4 }))).toEqual({ length = 4 })
32
+ expect(function()
33
+ MetricUnitKinds.fromName("velocity" :: any)
34
+ end).toThrow('no kind named "velocity"')
35
+ end)
36
+
37
+ it("names the kind of a set of dimensions", function()
38
+ expect(MetricUnitKinds.getKind(MetricUnitKinds.SPEED, false)).toBe("speed")
39
+ expect(MetricUnitKinds.getKind(MetricUnitKinds.TEMPERATURE, true)).toBe("temperature")
40
+ expect(MetricUnitKinds.getKind(MetricUnitKinds.TIME, true)).toBe("instant")
41
+ expect(MetricUnitKinds.getKind(MetricUnitDimensionUtils.NONE, false)).toBe("none")
42
+ expect(MetricUnitKinds.getKind({ length = 4 }, false)).toBeNil()
43
+ end)
44
+
45
+ it("describes a kind, absolute or a difference", function()
46
+ expect(MetricUnitKinds.describeKind(MetricUnitKinds.TEMPERATURE, true)).toBe("absolute temperature")
47
+ expect(MetricUnitKinds.describeKind(MetricUnitKinds.TEMPERATURE, false)).toBe("temperature difference")
48
+ expect(MetricUnitKinds.describeKind(MetricUnitKinds.TIME, true)).toBe("instant")
49
+ expect(MetricUnitKinds.describeKind(MetricUnitKinds.SPEED, false)).toBe("length/time")
50
+ end)
51
+ end)
@@ -0,0 +1,129 @@
1
+ --!strict
2
+ --[=[
3
+ Which unit to display a value in when the caller does not name one: for each dimension, the units to choose from
4
+ by size (`2500 m` displays as `2.5 km`), the time unit a rate is displayed per, and the SI symbols for an unnamed
5
+ compound. Used by [MetricUnitFormatUtils].
6
+
7
+ @class MetricUnitDisplayUnitUtils
8
+ ]=]
9
+
10
+ local require = require(script.Parent.loader).load(script)
11
+
12
+ local MetricUnitDefinitions = require("MetricUnitDefinitions")
13
+ local MetricUnitDimensionUtils = require("MetricUnitDimensionUtils")
14
+ local MetricUnitSystem = require("MetricUnitSystem")
15
+ local MetricUnitSystems = require("MetricUnitSystems")
16
+
17
+ local MetricUnitDisplayUnitUtils = {}
18
+
19
+ type UnitDefinition = MetricUnitDefinitions.UnitDefinition
20
+
21
+ local PER_UNIT_IDS_SHORTEST_FIRST: { string } = table.freeze({ "second", "minute", "hour", "day", "year" })
22
+ local perUnitsShortestFirst: { UnitDefinition }? = nil
23
+
24
+ local SI_BASE_UNIT_SYMBOLS: { [string]: string } = table.freeze({
25
+ time = "s",
26
+ length = "m",
27
+ memory = "B",
28
+ mass = "kg",
29
+ temperature = "K",
30
+ angle = "rad",
31
+ current = "A",
32
+ amount = "mol",
33
+ luminousIntensity = "cd",
34
+ }) :: any
35
+
36
+ --[=[
37
+ The units a value of `dimensions` is shown in, largest first, from `system` and its fallbacks ([MetricUnitSystems.METRIC]
38
+ by default), or nil when none lists the dimension. An absolute value is shown on a scale people read, °C rather
39
+ than kelvin, where the system has one.
40
+
41
+ @param dimensions Dimensions
42
+ @param system MetricUnitSystem?
43
+ @param absolute boolean?
44
+ @return { UnitDefinition }?
45
+ ]=]
46
+ function MetricUnitDisplayUnitUtils.getDisplayUnits(
47
+ dimensions: MetricUnitDimensionUtils.Dimensions,
48
+ system: MetricUnitSystem.MetricUnitSystem?,
49
+ absolute: boolean?
50
+ ): { UnitDefinition }?
51
+ return MetricUnitSystem.GetDisplayUnits(system or MetricUnitSystems.METRIC, dimensions, absolute)
52
+ end
53
+
54
+ --[=[
55
+ The largest unit on `units` that `amount` base units fills at least once, the smallest when it fills none, or
56
+ [MetricUnitDisplayUnitUtils.getBaseUnit] when `amount` is zero.
57
+
58
+ @param amount number
59
+ @param units { UnitDefinition }
60
+ @return UnitDefinition
61
+ ]=]
62
+ function MetricUnitDisplayUnitUtils.pickDisplayUnit(amount: number, units: { UnitDefinition }): UnitDefinition
63
+ if amount == 0 then
64
+ return MetricUnitDisplayUnitUtils.getBaseUnit(units)
65
+ end
66
+ for _, unit in units do
67
+ if math.abs(amount / unit.factor) >= 1 then
68
+ return unit
69
+ end
70
+ end
71
+ return units[#units]
72
+ end
73
+
74
+ --[=[
75
+ The unit on `units` that is one SI base unit, or the smallest when none is.
76
+
77
+ @param units { UnitDefinition }
78
+ @return UnitDefinition
79
+ ]=]
80
+ function MetricUnitDisplayUnitUtils.getBaseUnit(units: { UnitDefinition }): UnitDefinition
81
+ for _, unit in units do
82
+ if unit.factor == 1 then
83
+ return unit
84
+ end
85
+ end
86
+ return units[#units]
87
+ end
88
+
89
+ --[=[
90
+ The shortest time unit that a rate of `amount` per second reaches at least once over, or a year when none does.
91
+
92
+ @param amount number -- per second
93
+ @return UnitDefinition
94
+ ]=]
95
+ function MetricUnitDisplayUnitUtils.pickDisplayPerUnit(amount: number): UnitDefinition
96
+ local units = MetricUnitDisplayUnitUtils._perUnitsShortestFirst()
97
+ for _, unit in units do
98
+ if math.abs(amount * unit.factor) >= 1 then
99
+ return unit
100
+ end
101
+ end
102
+ return units[#units]
103
+ end
104
+
105
+ function MetricUnitDisplayUnitUtils._perUnitsShortestFirst(): { UnitDefinition }
106
+ local found = perUnitsShortestFirst
107
+ if found then
108
+ return found
109
+ end
110
+ local units: { UnitDefinition } = {}
111
+ for _, id in PER_UNIT_IDS_SHORTEST_FIRST do
112
+ table.insert(units, (assert(MetricUnitDefinitions.getUnit(id), id)))
113
+ end
114
+ local frozen = table.freeze(units)
115
+ perUnitsShortestFirst = frozen
116
+ return frozen
117
+ end
118
+
119
+ --[=[
120
+ The SI base unit symbol for a dimension name, such as `"m"` for `"length"`, or the name itself when it has none.
121
+
122
+ @param name string
123
+ @return string
124
+ ]=]
125
+ function MetricUnitDisplayUnitUtils.getBaseSymbol(name: string): string
126
+ return SI_BASE_UNIT_SYMBOLS[name] or name
127
+ end
128
+
129
+ return table.freeze(MetricUnitDisplayUnitUtils)