exprify 1.0.3 → 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,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;