@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 +21 -0
- package/dist/index.d.mts +53 -0
- package/dist/index.mjs +180 -0
- package/package.json +42 -0
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.
|
package/dist/index.d.mts
ADDED
|
@@ -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
|
+
}
|