@spaceteams/weft-layer-dimensional 0.2.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Spaceteams
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,53 @@
1
+ import { LayerEvaluator } from "@spaceteams/weft";
2
+
3
+ //#region src/unit.d.ts
4
+ /**
5
+ * A rational unit representation using sorted base-unit arrays.
6
+ *
7
+ * `num` lists the base units in the numerator, `denom` in the denominator.
8
+ * Both arrays are kept sorted alphabetically so structural equality works.
9
+ *
10
+ * Examples:
11
+ * - metres: `{ num: ["m"], denom: [] }`
12
+ * - metres per second: `{ num: ["m"], denom: ["s"] }`
13
+ * - dimensionless: `{ num: [], denom: [] }`
14
+ */
15
+ type Unit = {
16
+ readonly num: readonly string[];
17
+ readonly denom: readonly string[];
18
+ };
19
+ /** Create a base unit, e.g. `unit("m")` → `{ num: ["m"], denom: [] }`. */
20
+ declare function unit(name: string): Unit;
21
+ /** The dimensionless unit (scalar). */
22
+ declare function dimensionless(): Unit;
23
+ /** Create an arbitrary compound unit from numerator and denominator base units. */
24
+ declare function compoundUnit(num: readonly string[], denom: readonly string[]): Unit;
25
+ /** Multiply two units: `(a·b)`. Combines numerators and denominators, cancelling common factors. */
26
+ declare function multiplyUnits(a: Unit, b: Unit): Unit;
27
+ /** Divide two units: `a / b`. */
28
+ declare function divideUnits(a: Unit, b: Unit): Unit;
29
+ /** Structural equality of two units. */
30
+ declare function unitsEqual(a: Unit, b: Unit): boolean;
31
+ /**
32
+ * Format a unit as a human-readable string.
33
+ *
34
+ * - `"m"`, `"kg"` — simple base unit
35
+ * - `"m/s"` — single numerator / single denominator
36
+ * - `"m²"` — repeated base unit uses superscript exponents
37
+ * - `"kg·m/s²"` — full compound unit
38
+ * - `"1"` — dimensionless
39
+ * - `"1/s"` — pure denominator
40
+ */
41
+ declare function formatUnit(u: Unit): string;
42
+ //#endregion
43
+ //#region src/dimensional-layer.d.ts
44
+ /**
45
+ * SI-style dimensional analysis layer for weft computation models.
46
+ *
47
+ * Propagates {@link Unit} values through the computation graph based on each
48
+ * rule's `op`. When units are incompatible (e.g. adding metres to seconds),
49
+ * the layer returns `undefined` — it never throws.
50
+ */
51
+ declare const dimensionalLayer: LayerEvaluator<Unit>;
52
+ //#endregion
53
+ export { Unit, compoundUnit, dimensionalLayer, dimensionless, divideUnits, formatUnit, multiplyUnits, unit, unitsEqual };
package/dist/index.mjs ADDED
@@ -0,0 +1,180 @@
1
+ //#region src/unit.ts
2
+ /** Create a base unit, e.g. `unit("m")` → `{ num: ["m"], denom: [] }`. */
3
+ function unit(name) {
4
+ return {
5
+ num: [name],
6
+ denom: []
7
+ };
8
+ }
9
+ /** The dimensionless unit (scalar). */
10
+ function dimensionless() {
11
+ return {
12
+ num: [],
13
+ denom: []
14
+ };
15
+ }
16
+ /** Create an arbitrary compound unit from numerator and denominator base units. */
17
+ function compoundUnit(num, denom) {
18
+ const result = cancelCommon([...num], [...denom]);
19
+ return {
20
+ num: result.num.sort(),
21
+ denom: result.denom.sort()
22
+ };
23
+ }
24
+ /** Multiply two units: `(a·b)`. Combines numerators and denominators, cancelling common factors. */
25
+ function multiplyUnits(a, b) {
26
+ return compoundUnit([...a.num, ...b.num], [...a.denom, ...b.denom]);
27
+ }
28
+ /** Divide two units: `a / b`. */
29
+ function divideUnits(a, b) {
30
+ return compoundUnit([...a.num, ...b.denom], [...a.denom, ...b.num]);
31
+ }
32
+ /** Structural equality of two units. */
33
+ function unitsEqual(a, b) {
34
+ return arraysEqual(a.num, b.num) && arraysEqual(a.denom, b.denom);
35
+ }
36
+ /**
37
+ * Format a unit as a human-readable string.
38
+ *
39
+ * - `"m"`, `"kg"` — simple base unit
40
+ * - `"m/s"` — single numerator / single denominator
41
+ * - `"m²"` — repeated base unit uses superscript exponents
42
+ * - `"kg·m/s²"` — full compound unit
43
+ * - `"1"` — dimensionless
44
+ * - `"1/s"` — pure denominator
45
+ */
46
+ function formatUnit(u) {
47
+ const numStr = formatHalf(u.num);
48
+ const denomStr = formatHalf(u.denom);
49
+ if (numStr === "" && denomStr === "") return "1";
50
+ if (denomStr === "") return numStr;
51
+ if (numStr === "") return `1/${denomStr}`;
52
+ return `${numStr}/${denomStr}`;
53
+ }
54
+ const superscripts = {
55
+ 2: "²",
56
+ 3: "³",
57
+ 4: "⁴",
58
+ 5: "⁵",
59
+ 6: "⁶",
60
+ 7: "⁷",
61
+ 8: "⁸",
62
+ 9: "⁹"
63
+ };
64
+ function formatHalf(units) {
65
+ if (units.length === 0) return "";
66
+ const counts = /* @__PURE__ */ new Map();
67
+ for (const u of units) counts.set(u, (counts.get(u) ?? 0) + 1);
68
+ const parts = [];
69
+ for (const [name, count] of [...counts.entries()].sort((a, b) => a[0].localeCompare(b[0]))) if (count === 1) parts.push(name);
70
+ else parts.push(`${name}${superscripts[count] ?? `^${count}`}`);
71
+ return parts.join("·");
72
+ }
73
+ function cancelCommon(num, denom) {
74
+ const remaining = [...denom];
75
+ const canceled = [];
76
+ for (const u of num) {
77
+ const idx = remaining.indexOf(u);
78
+ if (idx >= 0) remaining.splice(idx, 1);
79
+ else canceled.push(u);
80
+ }
81
+ return {
82
+ num: canceled,
83
+ denom: remaining
84
+ };
85
+ }
86
+ function arraysEqual(a, b) {
87
+ if (a.length !== b.length) return false;
88
+ for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
89
+ return true;
90
+ }
91
+ //#endregion
92
+ //#region src/dimensional-layer.ts
93
+ /**
94
+ * SI-style dimensional analysis layer for weft computation models.
95
+ *
96
+ * Propagates {@link Unit} values through the computation graph based on each
97
+ * rule's `op`. When units are incompatible (e.g. adding metres to seconds),
98
+ * the layer returns `undefined` — it never throws.
99
+ */
100
+ const dimensionalLayer = {
101
+ name: "units",
102
+ version: "1",
103
+ eval(op, deps, spec) {
104
+ switch (op) {
105
+ case "sum":
106
+ case "difference":
107
+ case "weighted-sum":
108
+ case "min":
109
+ case "max": return requireSameUnit(deps);
110
+ case "ratio": {
111
+ const numKey = spec.numerator;
112
+ const denomRaw = spec.denominator;
113
+ const denomKey = typeof denomRaw === "string" ? denomRaw : typeof denomRaw === "object" && denomRaw !== null && "id" in denomRaw ? denomRaw.id : void 0;
114
+ const numUnit = numKey ? deps.get(numKey) : void 0;
115
+ const denomUnit = denomKey ? deps.get(denomKey) : void 0;
116
+ if (!numUnit || !denomUnit) return void 0;
117
+ return divideUnits(numUnit, denomUnit);
118
+ }
119
+ case "scale":
120
+ case "product": return multiplyAllUnits(deps);
121
+ case "negate":
122
+ case "abs":
123
+ case "clamp":
124
+ case "round": return firstUnit(deps);
125
+ case "conditional": {
126
+ const condKey = spec.condition;
127
+ for (const [id, u] of deps) if (id !== condKey) return u;
128
+ return;
129
+ }
130
+ case "future-value":
131
+ case "annuity-payment":
132
+ case "present-value": return financialUnit(deps, spec);
133
+ case "match": return firstUnit(deps);
134
+ case "compare":
135
+ case "and":
136
+ case "or":
137
+ case "not":
138
+ case "coerce":
139
+ case "format": return dimensionless();
140
+ case "project":
141
+ case "pluck":
142
+ case "pick":
143
+ case "compose":
144
+ case "spread": return;
145
+ default: return requireSameUnit(deps);
146
+ }
147
+ },
148
+ codec: {
149
+ encode: (u) => ({
150
+ num: [...u.num],
151
+ denom: [...u.denom]
152
+ }),
153
+ decode: (j) => j
154
+ }
155
+ };
156
+ function requireSameUnit(deps) {
157
+ let result;
158
+ for (const u of deps.values()) if (result === void 0) result = u;
159
+ else if (!unitsEqual(result, u)) return;
160
+ return result;
161
+ }
162
+ function multiplyAllUnits(deps) {
163
+ const values = [...deps.values()];
164
+ if (values.length === 0) return void 0;
165
+ let result = values[0];
166
+ for (let i = 1; i < values.length; i++) result = multiplyUnits(result, values[i]);
167
+ return result;
168
+ }
169
+ function firstUnit(deps) {
170
+ for (const u of deps.values()) return u;
171
+ }
172
+ function financialUnit(deps, spec) {
173
+ const monetaryOperand = spec.pv ?? spec.fv;
174
+ if (monetaryOperand === void 0) return void 0;
175
+ const monetaryKey = typeof monetaryOperand === "string" ? monetaryOperand : typeof monetaryOperand === "object" && monetaryOperand !== null && "id" in monetaryOperand ? monetaryOperand.id : void 0;
176
+ if (!monetaryKey) return void 0;
177
+ return deps.get(monetaryKey);
178
+ }
179
+ //#endregion
180
+ export { compoundUnit, dimensionalLayer, dimensionless, divideUnits, formatUnit, multiplyUnits, unit, unitsEqual };
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@spaceteams/weft-layer-dimensional",
3
+ "version": "0.2.0",
4
+ "description": "SI unit propagation and dimensional analysis layer for @spaceteams/weft",
5
+ "type": "module",
6
+ "files": [
7
+ "dist"
8
+ ],
9
+ "exports": {
10
+ ".": "./dist/index.mjs",
11
+ "./package.json": "./package.json"
12
+ },
13
+ "publishConfig": {
14
+ "access": "public"
15
+ },
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "https://github.com/spaceteams/weft.git",
19
+ "directory": "layers/dimensional"
20
+ },
21
+ "keywords": [
22
+ "weft",
23
+ "layer",
24
+ "dimensional",
25
+ "units",
26
+ "SI"
27
+ ],
28
+ "license": "MIT",
29
+ "dependencies": {
30
+ "@spaceteams/weft": "0.5.0"
31
+ },
32
+ "scripts": {
33
+ "build": "tsdown",
34
+ "dev": "tsdown --watch",
35
+ "typecheck": "tsc --noEmit",
36
+ "test": "vitest run",
37
+ "test:watch": "vitest",
38
+ "lint": "biome check .",
39
+ "lint:fix": "biome check --write .",
40
+ "format": "biome format --write ."
41
+ }
42
+ }