exprify 1.0.4 → 1.0.6

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.
@@ -1,8 +1,7 @@
1
1
  export function createUnitsStore(initial = {}) {
2
- let units = { ...initial};
3
-
4
- // ---------- Helpers ----------
2
+ let units = { ...initial };
5
3
 
4
+ // Helpers
6
5
  function getAllUnitsFlat() {
7
6
  const result = new Set();
8
7
 
@@ -19,7 +18,6 @@ export function createUnitsStore(initial = {}) {
19
18
 
20
19
  // Avoid duplicate like "m" vs "meter"
21
20
  if (unitLower !== keyLower) {
22
- // Optional: only single-word units
23
21
  if (unitLower.split(/\s+/).length === 1) {
24
22
  result.add(unitLower);
25
23
  }
@@ -41,6 +39,9 @@ export function createUnitsStore(initial = {}) {
41
39
  return Array.from(result);
42
40
  }
43
41
 
42
+ /**
43
+ * @param {string} input
44
+ */
44
45
  function findUnit(input) {
45
46
  input = input.toLowerCase();
46
47
 
@@ -53,7 +54,7 @@ export function createUnitsStore(initial = {}) {
53
54
  u.unit?.toLowerCase() === input ||
54
55
  u.symbol?.toLowerCase() === input
55
56
  ) {
56
- return { type, key , data: u};
57
+ return { type, key, data: u };
57
58
  }
58
59
  }
59
60
  }
@@ -61,68 +62,89 @@ export function createUnitsStore(initial = {}) {
61
62
  return null;
62
63
  }
63
64
 
64
- // ---------- Core Convert ----------
65
-
65
+ /**
66
+ * @param {number} value
67
+ * @param {any} fromUnit
68
+ * @param {any} toUnit
69
+ */
66
70
  function convert(value, fromUnit, toUnit) {
67
- const from = findUnit(fromUnit);
68
- const to = findUnit(toUnit);
71
+ const from = findUnit(fromUnit);
72
+ const to = findUnit(toUnit);
69
73
 
70
- if (!from) throw new Error(`Unknown unit: ${fromUnit}`);
71
- if (!to) throw new Error(`Unknown unit: ${toUnit}`);
72
-
73
- if (from.type !== to.type) {
74
- throw new Error(`Cannot convert ${fromUnit} to ${toUnit} (${to.data.unit || to.key}). ${from.data.unit || from.key} conversion units like ${Object.keys(units[from.type]).join(", ")}`);
75
- }
76
-
77
- const result = value * (from.data.value / to.data.value);
74
+ if (!from) {
75
+ throw new Error(`Unknown unit: ${fromUnit}`);
76
+ }
77
+ if (!to) {
78
+ throw new Error(`Unknown unit: ${toUnit}`);
79
+ }
78
80
 
79
- return { value: result, unit: to.key };
81
+ if (from.type !== to.type) {
82
+ throw new Error(
83
+ `Cannot convert ${fromUnit} to ${toUnit} (${to.data.unit || to.key}). ${from.data.unit || from.key} conversion units like ${Object.keys(units[from.type]).join(', ')}`
84
+ );
80
85
  }
81
86
 
82
- // ---------- Public API ----------
87
+ const result = value * (from.data.value / to.data.value);
83
88
 
89
+ return { value: result, unit: to.key };
90
+ }
91
+
92
+ // Public API
84
93
  return {
85
94
  // Get all units
86
95
  getUnits: () => units,
87
96
 
88
- // Replace all units
89
- setUnits: (newUnits) => {
97
+ setUnits: (/** @type {{}} */ newUnits) => {
90
98
  units = { ...newUnits };
91
99
  },
92
100
 
93
- // Update single type
94
- updateType: (type, data) => {
101
+ updateType: (/** @type {string | number} */ type, /** @type {any} */ data) => {
95
102
  units[type] = { ...units[type], ...data };
96
103
  },
97
104
 
98
- // Add new unit
99
- addUnit: (type, key, unitObj) => {
100
- if (!units[type]) units[type] = {};
105
+ addUnit: (
106
+ /** @type {string | number} */ type,
107
+ /** @type {string | number} */ key,
108
+ /** @type {any} */ unitObj
109
+ ) => {
110
+ if (!units[type]) {
111
+ units[type] = {};
112
+ }
101
113
  units[type][key] = unitObj;
102
114
  },
115
+ // Unit-aware arithmetic: unify operands to same unit type, then apply operator
116
+ /**
117
+ * @param {string} op
118
+ * @param {{ unit: any; value: any; }} left
119
+ * @param {{ unit: any; value: number; }} right
120
+ */
103
121
  compute(op, left, right) {
122
+ const isUnit = (/** @type {any} */ v) =>
123
+ v && typeof v === 'object' && 'value' in v && 'unit' in v;
104
124
 
105
- const isUnit = (v) =>
106
- v && typeof v === "object" && "value" in v && "unit" in v;
107
-
108
- const apply = (a, b) => {
125
+ const apply = (/** @type {any} */ a, /** @type {any} */ b) => {
109
126
  switch (op) {
110
- case "+": return a + b;
111
- case "-": return a - b;
112
- case "*": return a * b;
113
- case "/": return a / b;
114
- case "%": return a % b;
115
- case "^": return Math.pow(a, b);
127
+ case '+':
128
+ return a + b;
129
+ case '-':
130
+ return a - b;
131
+ case '*':
132
+ return a * b;
133
+ case '/':
134
+ return a / b;
135
+ case '%':
136
+ return a % b;
137
+ case '^':
138
+ return Math.pow(a, b);
116
139
  }
117
140
  };
118
141
 
119
142
  // BOTH UNIT
120
143
  if (isUnit(left) && isUnit(right)) {
121
-
122
144
  const from = this.findUnit(right.unit);
123
145
  const to = this.findUnit(left.unit);
124
146
 
125
- if (from.type !== to.type) {
147
+ if (!from || !to || from.type !== to.type) {
126
148
  throw new Error(`Cannot operate on different unit types`);
127
149
  }
128
150
 
@@ -132,47 +154,47 @@ export function createUnitsStore(initial = {}) {
132
154
  const result = apply(left.value, r);
133
155
 
134
156
  // multiplication/division produce compound units
135
- if (op === "*") {
157
+ if (op === '*') {
136
158
  return { value: result, unit: left.unit };
137
159
  }
138
160
 
139
- if (op === "/") {
161
+ if (op === '/') {
140
162
  return { value: result, unit: left.unit };
141
163
  }
142
164
 
143
- if (op === "^") {
165
+ if (op === '^') {
144
166
  return { value: result, unit: left.unit };
145
167
  }
146
168
 
147
169
  return { value: result, unit: left.unit };
148
170
  }
149
171
 
150
- // ================= LEFT UNIT =================
172
+ // LEFT UNIT
151
173
  if (isUnit(left) && !isUnit(right)) {
152
174
  const result = apply(left.value, right);
153
175
 
154
176
  return { value: result, unit: left.unit };
155
177
  }
156
178
 
157
- // ================= RIGHT UNIT =================
179
+ // RIGHT UNIT
158
180
  if (!isUnit(left) && isUnit(right)) {
159
181
  const result = apply(left, right.value);
160
182
 
161
- if (op === "/") {
183
+ if (op === '/') {
162
184
  return { value: result, unit: right.unit };
163
185
  }
164
186
 
165
187
  return { value: result, unit: right.unit };
166
188
  }
167
189
 
168
- // ================= NORMAL =================
190
+ // NORMAL
169
191
  return apply(left, right);
170
192
  },
171
- // Convert
193
+
172
194
  convert,
173
195
 
174
196
  // Search helpers
175
197
  getAllUnitsFlat,
176
- findUnit
198
+ findUnit,
177
199
  };
178
200
  }
@@ -2,18 +2,19 @@ const validVarName = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
2
2
 
3
3
  export function createVarStore(initial = {}) {
4
4
  let store = Object.create(null);
5
-
6
5
 
7
6
  for (const key in initial) {
8
7
  store[key] = initial[key];
9
8
  }
10
9
 
11
10
  return {
11
+ /**
12
+ * @param {string} name
13
+ * @param {number | undefined} value
14
+ */
12
15
  set(name, value, { override = true } = {}) {
13
-
14
- // Name validation
15
- if (typeof name !== "string" || !name) {
16
- throw new Error("Variable name must be a non-empty string");
16
+ if (typeof name !== 'string' || !name) {
17
+ throw new Error('Variable name must be a non-empty string');
17
18
  }
18
19
 
19
20
  if (!validVarName.test(name)) {
@@ -26,50 +27,52 @@ export function createVarStore(initial = {}) {
26
27
  }
27
28
 
28
29
  // Prevent overwrite (optional)
29
- if (!override && name in variablesDB) {
30
+ if (!override && name in store) {
30
31
  throw new Error(`Variable '${name}' already exists`);
31
32
  }
32
33
 
33
34
  store[name] = value;
34
35
  },
35
36
 
36
- //get variable
37
+ /**
38
+ * @param {string | number} name
39
+ */
37
40
  get(name) {
38
41
  return store[name];
39
42
  },
40
43
 
41
- // check existence
44
+ /**
45
+ * @param {any} name
46
+ */
42
47
  has(name) {
43
48
  return Object.prototype.hasOwnProperty.call(store, name);
44
49
  },
45
50
 
46
- // remove variable
51
+ /**
52
+ * @param {string | number} name
53
+ */
47
54
  remove(name) {
48
55
  delete store[name];
49
56
  },
50
57
 
51
- // get all variables (snapshot)
52
58
  all() {
53
59
  return { ...store };
54
60
  },
55
61
 
56
- // clear all
57
62
  clear() {
58
63
  store = Object.create(null);
59
64
  },
60
65
 
61
- // merge multiple variables
62
66
  merge(obj = {}) {
63
67
  for (const key in obj) {
64
68
  store[key] = obj[key];
65
69
  }
66
70
  },
67
71
 
68
- // clone store (for scoped instances)
69
72
  clone() {
70
73
  return createVarStore(store);
71
- }
74
+ },
72
75
  };
73
76
  }
74
77
 
75
- export default { createVarStore };
78
+ export default { createVarStore };
@@ -1,369 +0,0 @@
1
- import { tokenize } from "../parser/tokenizer.js";
2
- // import { infixToPostfix } from "../parser/infixToPostfix.js";
3
- import { evaluateAST } from "../parser/evaluator.js";
4
- import { createContext } from "./context.js";
5
- import { mathOperations } from "../math/operations.js";
6
-
7
- import { createUnitsStore } from "../utils/store.js";
8
- import { globalUnits } from "../utils/globalUnits.js";
9
-
10
- import { createVarStore } from "../variables/store.js";
11
- import { createFunctionRegistry } from "../function/registry.js";
12
- import { internalFunctions } from "../function/internal.js";
13
- import { isDenseMatrixWrapper, serializeExprifyValue, wrapDenseMatrix } from "../utils/matrix.js";
14
-
15
- import { buildAST } from "../parser/astBuild.js";
16
-
17
-
18
- //
19
-
20
- const isComplex = (value) =>
21
- value && typeof value === "object" && "re" in value && "im" in value;
22
-
23
- const isUnitValue = (value) =>
24
- value && typeof value === "object" && "value" in value && "unit" in value;
25
-
26
- const isMatrix = (value) =>
27
- Array.isArray(value) && value.length > 0 && value.every(Array.isArray);
28
-
29
- const formatComplex = (value) => {
30
- if (!isComplex(value)) return value;
31
-
32
- const real = value.re;
33
- const imaginary = Math.abs(value.im);
34
- const sign = value.im < 0 ? "-" : "+";
35
-
36
- if (real === 0) {
37
- if (value.im === 1) return "i";
38
- if (value.im === -1) return "-i";
39
- return `${value.im}i`;
40
- }
41
-
42
- const imagPart = imaginary === 1 ? "i" : `${imaginary}i`;
43
- return `${real} ${sign} ${imagPart}`;
44
- };
45
-
46
- const formatScalar = (value) => {
47
- if (typeof value !== "number") {
48
- return String(value);
49
- }
50
-
51
- if (Number.isInteger(value)) {
52
- return String(value);
53
- }
54
-
55
- return Number(value.toFixed(14)).toString();
56
- };
57
-
58
- const formatResult = (value) => {
59
- if (isComplex(value)) {
60
- return formatComplex(value);
61
- }
62
-
63
- if (isUnitValue(value)) {
64
- return `${value.value} ${value.unit}`;
65
- }
66
-
67
- if (isDenseMatrixWrapper(value)) {
68
- return serializeExprifyValue(value);
69
- }
70
-
71
- if (isMatrix(value)) {
72
- return value.map((row) => row.map(formatScalar).join("\t")).join("\n");
73
- }
74
-
75
- if (Array.isArray(value)) {
76
- return JSON.stringify(value);
77
- }
78
-
79
- if (value && typeof value === "object") {
80
- return serializeExprifyValue(value);
81
- }
82
-
83
- return value;
84
- };
85
-
86
- class exprify {
87
- constructor() {
88
- // Shared state
89
- this.math = mathOperations;
90
- this.units = createUnitsStore(globalUnits);
91
- this.functions = createFunctionRegistry(internalFunctions);
92
- this.variables = createVarStore();
93
- this._cache = new Map();
94
- this.variables.set("pi", Math.PI);
95
- this.variables.set("e", Math.E);
96
- this.addFunction("parse", (expression) => {
97
- if (typeof expression !== "string") {
98
- throw new Error("parse() expects an expression string");
99
- }
100
- return expression;
101
- });
102
- this.addFunction("leafCount", (value) => {
103
- const countLeafTokens = (expression) => {
104
- const strippedKeys = expression.replace(/(^|[{,]\s*)[a-zA-Z_][a-zA-Z0-9_]*\s*:/g, "$1");
105
- const matches = strippedKeys.match(/\d+(\.\d+)?(e[+-]?\d+)?n?|[a-zA-Z_][a-zA-Z0-9_]*/gi);
106
- return matches ? matches.length : 0;
107
- };
108
-
109
- let ast = value;
110
- if (typeof value === "string") {
111
- try {
112
- ast = this.parse(value).ast;
113
- } catch {
114
- return countLeafTokens(value);
115
- }
116
- }
117
-
118
- const countLeaves = (node) => {
119
- if (!node || typeof node !== "object") return 0;
120
-
121
- switch (node.type) {
122
- case "Literal":
123
- case "ImaginaryLiteral":
124
- case "UnitLiteral":
125
- case "Identifier":
126
- return 1;
127
- default:
128
- return Object.values(node).reduce((sum, child) => {
129
- if (Array.isArray(child)) {
130
- return sum + child.reduce((inner, item) => inner + countLeaves(item), 0);
131
- }
132
-
133
- return sum + countLeaves(child);
134
- }, 0);
135
- }
136
- };
137
-
138
- return countLeaves(ast);
139
- });
140
- this.addFunction("matrix", (value) => wrapDenseMatrix(value));
141
- this.addFunction("sparse", (value) => wrapDenseMatrix(value));
142
- this.addFunction("rationalize", (expression, withDetails = false) => {
143
- if (typeof expression !== "string") {
144
- throw new Error("rationalize() expects an expression string");
145
- }
146
-
147
- const normalizedExpression = expression
148
- .replace(/\s+/g, "")
149
- .replace(/(\d)([a-zA-Z(])/g, "$1*$2")
150
- .replace(/([a-zA-Z)])(\d)/g, "$1*$2");
151
-
152
- const polyKey = (powers) => JSON.stringify(Object.entries(powers).sort(([a], [b]) => a.localeCompare(b)));
153
- const keyToPowers = (key) => Object.fromEntries(JSON.parse(key));
154
- const makePoly = (terms = new Map()) => terms;
155
- const constPoly = (value) => new Map([[polyKey({}), value]]);
156
- const varPoly = (name) => new Map([[polyKey({ [name]: 1 }), 1]]);
157
- const cleanPoly = (poly) => new Map([...poly.entries()].filter(([, coeff]) => coeff !== 0));
158
- const addPoly = (a, b, sign = 1) => {
159
- const result = new Map(a);
160
- for (const [key, coeff] of b.entries()) {
161
- result.set(key, (result.get(key) || 0) + (sign * coeff));
162
- }
163
- return cleanPoly(result);
164
- };
165
- const multiplyPoly = (a, b) => {
166
- const result = new Map();
167
- for (const [keyA, coeffA] of a.entries()) {
168
- const powersA = keyToPowers(keyA);
169
- for (const [keyB, coeffB] of b.entries()) {
170
- const powersB = keyToPowers(keyB);
171
- const merged = { ...powersA };
172
- for (const [name, power] of Object.entries(powersB)) {
173
- merged[name] = (merged[name] || 0) + power;
174
- }
175
- const key = polyKey(merged);
176
- result.set(key, (result.get(key) || 0) + (coeffA * coeffB));
177
- }
178
- }
179
- return cleanPoly(result);
180
- };
181
- const powPoly = (poly, exponent) => {
182
- let result = constPoly(1);
183
- for (let index = 0; index < exponent; index++) {
184
- result = multiplyPoly(result, poly);
185
- }
186
- return result;
187
- };
188
- const rational = (num, den = constPoly(1)) => ({ num, den });
189
- const addRat = (a, b, sign = 1) => rational(
190
- addPoly(
191
- multiplyPoly(a.num, b.den),
192
- multiplyPoly(b.num, a.den),
193
- sign
194
- ),
195
- multiplyPoly(a.den, b.den)
196
- );
197
- const mulRat = (a, b) => rational(multiplyPoly(a.num, b.num), multiplyPoly(a.den, b.den));
198
- const divRat = (a, b) => rational(multiplyPoly(a.num, b.den), multiplyPoly(a.den, b.num));
199
- const negRat = (value) => rational(addPoly(new Map(), value.num, -1), value.den);
200
- const astToRat = (node) => {
201
- switch (node.type) {
202
- case "Literal":
203
- return rational(constPoly(node.value));
204
- case "Identifier":
205
- return rational(varPoly(node.name));
206
- case "UnaryExpression":
207
- if (node.operator === "-") return negRat(astToRat(node.argument));
208
- throw new Error("Unsupported unary operator");
209
- case "BinaryExpression": {
210
- const left = astToRat(node.left);
211
- const right = astToRat(node.right);
212
- switch (node.operator) {
213
- case "+": return addRat(left, right);
214
- case "-": return addRat(left, right, -1);
215
- case "*": return mulRat(left, right);
216
- case "/": return divRat(left, right);
217
- case "^": {
218
- if (node.right.type !== "Literal" || !Number.isInteger(node.right.value) || node.right.value < 0) {
219
- throw new Error("Unsupported exponent");
220
- }
221
- return rational(
222
- powPoly(left.num, node.right.value),
223
- powPoly(left.den, node.right.value)
224
- );
225
- }
226
- default:
227
- throw new Error("Unsupported operator in rationalize()");
228
- }
229
- }
230
- default:
231
- throw new Error("Unsupported expression in rationalize()");
232
- }
233
- };
234
- const formatPoly = (poly) => {
235
- const entries = [...poly.entries()]
236
- .filter(([, coeff]) => coeff !== 0)
237
- .sort(([keyA], [keyB]) => {
238
- const powersA = keyToPowers(keyA);
239
- const powersB = keyToPowers(keyB);
240
- const firstVarA = Object.keys(powersA).sort()[0] || "";
241
- const firstVarB = Object.keys(powersB).sort()[0] || "";
242
-
243
- if (firstVarA !== firstVarB) {
244
- return firstVarA.localeCompare(firstVarB);
245
- }
246
-
247
- const degreeA = Object.values(powersA).reduce((sum, value) => sum + value, 0);
248
- const degreeB = Object.values(powersB).reduce((sum, value) => sum + value, 0);
249
- return degreeB - degreeA;
250
- });
251
-
252
- if (!entries.length) return "0";
253
-
254
- return entries.map(([key, coeff], index) => {
255
- const powers = keyToPowers(key);
256
- const absCoeff = Math.abs(coeff);
257
- const variablePart = Object.entries(powers)
258
- .map(([name, power]) => power === 1 ? name : `${name} ^ ${power}`)
259
- .join(" * ");
260
- let body = variablePart;
261
-
262
- if (!body) {
263
- body = `${absCoeff}`;
264
- } else if (absCoeff !== 1) {
265
- body = `${absCoeff} * ${body}`;
266
- }
267
-
268
- if (index === 0) {
269
- return coeff < 0 ? `- ${body}`.replace("- ", "-") : body;
270
- }
271
-
272
- return coeff < 0 ? `- ${body}` : `+ ${body}`;
273
- }).join(" ");
274
- };
275
-
276
- const ast = this.parse(normalizedExpression).ast;
277
- const result = astToRat(ast);
278
- const numerator = formatPoly(result.num);
279
- const denominator = formatPoly(result.den);
280
- const variableSet = new Set();
281
-
282
- for (const poly of [result.num, result.den]) {
283
- for (const key of poly.keys()) {
284
- for (const name of Object.keys(keyToPowers(key))) {
285
- variableSet.add(name);
286
- }
287
- }
288
- }
289
-
290
- if (!withDetails) {
291
- return `(${numerator}) / (${denominator})`;
292
- }
293
-
294
- return {
295
- numerator,
296
- denominator,
297
- coefficients: [],
298
- variables: [...variableSet].sort(),
299
- expression: `(${numerator}) / (${denominator})`
300
- };
301
- });
302
- }
303
-
304
- setVariable(name, value) {
305
- this.variables.set(name, value);
306
- }
307
-
308
- getVariable(name) {
309
- return this.variables.get(name);
310
- }
311
-
312
- addFunction(name, fn) {
313
- this.functions.register(name, fn);
314
- }
315
-
316
- _createContext() {
317
- return createContext({
318
- functions: this.functions,
319
- variables: this.variables,
320
- units: this.units,
321
- evaluate: this.evaluate.bind(this)
322
- });
323
- }
324
-
325
- tokenize(expr) {
326
- if (typeof expr !== "string") {
327
- throw new Error("Expression must be a string");
328
- }
329
- return tokenize(expr, this._createContext());
330
- }
331
-
332
- parse(expr) {
333
- const tokens = this.tokenize(expr);
334
- const ast = buildAST(tokens);
335
- return { tokens, ast };
336
- }
337
-
338
- evaluate(expr) {
339
- const { ast } = this.parse(expr);
340
- return formatResult(evaluateAST(
341
- ast,
342
- this._createContext()
343
- ));
344
- }
345
-
346
- compile(expr) {
347
- if (this._cache.has(expr)) {
348
- return this._cache.get(expr);
349
- }
350
-
351
- const { ast } = this.parse(expr);
352
-
353
- const compiledFn = (scope = {}) => {
354
- const baseContext = this._createContext();
355
- const scopedContext = baseContext.withScope(scope);
356
- return formatResult(evaluateAST(ast, scopedContext));
357
- };
358
-
359
- this._cache.set(expr, compiledFn);
360
- return compiledFn;
361
- }
362
-
363
- clearCache() {
364
- this._cache.clear();
365
- }
366
-
367
- }
368
-
369
- export default exprify;