exprify 1.0.4 → 1.0.7

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.
@@ -0,0 +1,880 @@
1
+ import { tokenize } from '../parser/tokenizer.js';
2
+ import { evaluateAST } from '../parser/evaluator.js';
3
+ import { createContext } from './context.js';
4
+ import { mathOperations } from '../math/operations.js';
5
+ import { createUnitsStore } from '../utils/store.js';
6
+ import { globalUnits } from '../utils/globalUnits.js';
7
+ import { createVarStore } from '../variables/store.js';
8
+ import { createFunctionRegistry } from '../function/registry.js';
9
+ import { internalFunctions } from '../function/internal.js';
10
+ import { isDenseMatrixWrapper, serializeExprifyValue, wrapDenseMatrix } from '../utils/matrix.js';
11
+ import { buildAST } from '../parser/astBuild.js';
12
+ import { isFraction, formatFraction } from '../math/fraction.js';
13
+ import { isBigNumber, formatBigNumber } from '../math/bignumber.js';
14
+
15
+ const isComplex = (/** @type {any} */ value) =>
16
+ value && typeof value === 'object' && 're' in value && 'im' in value;
17
+
18
+ const isUnitValue = (/** @type {any} */ value) =>
19
+ value && typeof value === 'object' && 'value' in value && 'unit' in value;
20
+
21
+ const isMatrix = (/** @type {any[]} */ value) =>
22
+ Array.isArray(value) && value.length > 0 && value.every(Array.isArray);
23
+
24
+ const formatComplex = (/** @type {{ re: any; im: number; }} */ value) => {
25
+ if (!isComplex(value)) {
26
+ return value;
27
+ }
28
+
29
+ const real = value.re;
30
+ const imaginary = Math.abs(value.im);
31
+ const sign = value.im < 0 ? '-' : '+';
32
+
33
+ if (real === 0) {
34
+ if (value.im === 1) {
35
+ return 'i';
36
+ }
37
+ if (value.im === -1) {
38
+ return '-i';
39
+ }
40
+ return `${value.im}i`;
41
+ }
42
+
43
+ const imagPart = imaginary === 1 ? 'i' : `${imaginary}i`;
44
+ return `${real} ${sign} ${imagPart}`;
45
+ };
46
+
47
+ const formatScalar = (/** @type {unknown} */ value) => {
48
+ if (isBigNumber(value)) {
49
+ return formatBigNumber(value);
50
+ }
51
+ if (typeof value !== 'number') {
52
+ return String(value);
53
+ }
54
+
55
+ if (Number.isInteger(value)) {
56
+ return String(value);
57
+ }
58
+
59
+ return Number(value.toFixed(14)).toString();
60
+ };
61
+
62
+ const formatResult = (/** @type {any} */ value) => {
63
+ if (isFraction(value)) {
64
+ return formatFraction(value);
65
+ }
66
+
67
+ if (isBigNumber(value)) {
68
+ return formatBigNumber(value);
69
+ }
70
+
71
+ if (isComplex(value)) {
72
+ return formatComplex(value);
73
+ }
74
+
75
+ if (isUnitValue(value)) {
76
+ return `${value.value} ${value.unit}`;
77
+ }
78
+
79
+ if (isDenseMatrixWrapper(value)) {
80
+ return serializeExprifyValue(value);
81
+ }
82
+
83
+ if (isMatrix(value)) {
84
+ return value.map((/** @type {unknown[]} */ row) => row.map(formatScalar).join('\t')).join('\n');
85
+ }
86
+
87
+ if (Array.isArray(value)) {
88
+ return JSON.stringify(value);
89
+ }
90
+
91
+ if (value && typeof value === 'object') {
92
+ return serializeExprifyValue(value);
93
+ }
94
+
95
+ return value;
96
+ };
97
+
98
+ class exprify {
99
+ constructor() {
100
+ this.math = mathOperations;
101
+ this.units = createUnitsStore(globalUnits);
102
+ this.functions = createFunctionRegistry(internalFunctions);
103
+ this.variables = createVarStore();
104
+ this._cache = new Map();
105
+ this.variables.set('pi', Math.PI);
106
+ this.variables.set('e', Math.E);
107
+ this.variables.set('PHI', (1 + Math.sqrt(5)) / 2);
108
+ this.variables.set('TAU', 2 * Math.PI);
109
+ this.variables.set('INFINITY', Infinity);
110
+ this.variables.set('NaN', NaN);
111
+ this.addFunction('parse', (/** @type {any} */ expression) => {
112
+ if (typeof expression !== 'string') {
113
+ throw new Error('parse() expects an expression string');
114
+ }
115
+ return expression;
116
+ });
117
+ this.addFunction('leafCount', (/** @type {string} */ value) => {
118
+ const countLeafTokens = (/** @type {string} */ expression) => {
119
+ const strippedKeys = expression.replace(/(^|[{,]\s*)[a-zA-Z_][a-zA-Z0-9_]*\s*:/g, '$1');
120
+ const matches = strippedKeys.match(/\d+(\.\d+)?(e[+-]?\d+)?n?|[a-zA-Z_][a-zA-Z0-9_]*/gi);
121
+ return matches ? matches.length : 0;
122
+ };
123
+
124
+ let ast = value;
125
+ if (typeof value === 'string') {
126
+ try {
127
+ ast = this.parse(value).ast;
128
+ } catch {
129
+ return countLeafTokens(value);
130
+ }
131
+ }
132
+
133
+ const countLeaves = (/** @type {any} */ node) => {
134
+ if (!node || typeof node !== 'object') {
135
+ return 0;
136
+ }
137
+
138
+ switch (node.type) {
139
+ case 'Literal':
140
+ case 'ImaginaryLiteral':
141
+ case 'UnitLiteral':
142
+ case 'Identifier':
143
+ return 1;
144
+ default:
145
+ return Object.values(node).reduce((sum, child) => {
146
+ if (Array.isArray(child)) {
147
+ return sum + child.reduce((inner, item) => inner + countLeaves(item), 0);
148
+ }
149
+
150
+ return sum + countLeaves(child);
151
+ }, 0);
152
+ }
153
+ };
154
+
155
+ return countLeaves(ast);
156
+ });
157
+ this.addFunction('matrix', (/** @type {any} */ value) => wrapDenseMatrix(value));
158
+ this.addFunction('sparse', (/** @type {any} */ value) => wrapDenseMatrix(value));
159
+
160
+ // --- rationalize(): polynomial/rational arithmetic using Map<JSON-power-tuple, coefficient> ---
161
+ this.addFunction('rationalize', (/** @type {string} */ expression, withDetails = false) => {
162
+ if (typeof expression !== 'string') {
163
+ throw new Error('rationalize() expects an expression string');
164
+ }
165
+
166
+ const normalizedExpression = expression
167
+ .replace(/\s+/g, '')
168
+ .replace(/(\d)([a-zA-Z(])/g, '$1*$2')
169
+ .replace(/([a-zA-Z)])(\d)/g, '$1*$2');
170
+
171
+ const polyKey = (powers) =>
172
+ JSON.stringify(Object.entries(powers).sort(([a], [b]) => a.localeCompare(b)));
173
+ const keyToPowers = (/** @type {string} */ key) => Object.fromEntries(JSON.parse(key));
174
+ const constPoly = (/** @type {number} */ value) => new Map([[polyKey({}), value]]);
175
+ const varPoly = (/** @type {any} */ name) => new Map([[polyKey({ [name]: 1 }), 1]]);
176
+ const cleanPoly = (/** @type {any[] | Map<any, any>} */ poly) =>
177
+ new Map([...poly.entries()].filter(([, coeff]) => coeff !== 0));
178
+ const addPoly = (
179
+ /** @type {Iterable<readonly [any, any]> | null | undefined} */ a,
180
+ /** @type {any[] | Map<any, any>} */ b,
181
+ sign = 1
182
+ ) => {
183
+ const result = new Map(a);
184
+ for (const [key, coeff] of b.entries()) {
185
+ result.set(key, (result.get(key) || 0) + sign * coeff);
186
+ }
187
+ return cleanPoly(result);
188
+ };
189
+ const multiplyPoly = (/** @type {any} */ a, /** @type {any} */ b) => {
190
+ const result = new Map();
191
+ for (const [keyA, coeffA] of a.entries()) {
192
+ const powersA = keyToPowers(keyA);
193
+ for (const [keyB, coeffB] of b.entries()) {
194
+ const powersB = keyToPowers(keyB);
195
+ const merged = { ...powersA };
196
+ for (const [name, power] of Object.entries(powersB)) {
197
+ merged[name] = (merged[name] || 0) + power;
198
+ }
199
+ const key = polyKey(merged);
200
+ result.set(key, (result.get(key) || 0) + coeffA * coeffB);
201
+ }
202
+ }
203
+ return cleanPoly(result);
204
+ };
205
+ const powPoly = (/** @type {any} */ poly, /** @type {number} */ exponent) => {
206
+ let result = constPoly(1);
207
+ for (let index = 0; index < exponent; index++) {
208
+ result = multiplyPoly(result, poly);
209
+ }
210
+ return result;
211
+ };
212
+ const rational = (/** @type {Map<any, any>} */ num, den = constPoly(1)) => ({ num, den });
213
+ const addRat = (
214
+ /** @type {{ num: any; den: any; }} */ a,
215
+ /** @type {{ den: any; num: any; }} */ b,
216
+ sign = 1
217
+ ) =>
218
+ rational(
219
+ addPoly(multiplyPoly(a.num, b.den), multiplyPoly(b.num, a.den), sign),
220
+ multiplyPoly(a.den, b.den)
221
+ );
222
+ const mulRat = (
223
+ /** @type {{ num: any; den: any; }} */ a,
224
+ /** @type {{ num: any; den: any; }} */ b
225
+ ) => rational(multiplyPoly(a.num, b.num), multiplyPoly(a.den, b.den));
226
+ const divRat = (
227
+ /** @type {{ num: any; den: any; }} */ a,
228
+ /** @type {{ den: any; num: any; }} */ b
229
+ ) => rational(multiplyPoly(a.num, b.den), multiplyPoly(a.den, b.num));
230
+ const negRat = (
231
+ /** @type {{ num: any[] | Map<any, any>; den: Map<string, number> | undefined; }} */ value
232
+ ) => rational(addPoly(new Map(), value.num, -1), value.den);
233
+ const astToRat = (/** @type {any} */ node) => {
234
+ switch (node.type) {
235
+ case 'Literal':
236
+ return rational(constPoly(node.value));
237
+ case 'Identifier':
238
+ return rational(varPoly(node.name));
239
+ case 'UnaryExpression':
240
+ if (node.operator === '-') {
241
+ return negRat(astToRat(node.argument));
242
+ }
243
+ throw new Error('Unsupported unary operator');
244
+ case 'BinaryExpression': {
245
+ const left = astToRat(node.left);
246
+ const right = astToRat(node.right);
247
+ switch (node.operator) {
248
+ case '+':
249
+ return addRat(left, right);
250
+ case '-':
251
+ return addRat(left, right, -1);
252
+ case '*':
253
+ return mulRat(left, right);
254
+ case '/':
255
+ return divRat(left, right);
256
+ case '^': {
257
+ if (
258
+ node.right.type !== 'Literal' ||
259
+ !Number.isInteger(node.right.value) ||
260
+ node.right.value < 0
261
+ ) {
262
+ throw new Error('Unsupported exponent');
263
+ }
264
+ return rational(
265
+ powPoly(left.num, node.right.value),
266
+ powPoly(left.den, node.right.value)
267
+ );
268
+ }
269
+ default:
270
+ throw new Error('Unsupported operator in rationalize()');
271
+ }
272
+ }
273
+ default:
274
+ throw new Error('Unsupported expression in rationalize()');
275
+ }
276
+ };
277
+ const formatPoly = (/** @type {any} */ poly) => {
278
+ const entries = [...poly.entries()]
279
+ .filter(([, coeff]) => coeff !== 0)
280
+ .sort(([keyA], [keyB]) => {
281
+ const powersA = keyToPowers(keyA);
282
+ const powersB = keyToPowers(keyB);
283
+ const firstVarA = Object.keys(powersA).sort()[0] || '';
284
+ const firstVarB = Object.keys(powersB).sort()[0] || '';
285
+
286
+ if (firstVarA !== firstVarB) {
287
+ return firstVarA.localeCompare(firstVarB);
288
+ }
289
+
290
+ const degreeA = Object.values(powersA).reduce((sum, value) => sum + value, 0);
291
+ const degreeB = Object.values(powersB).reduce((sum, value) => sum + value, 0);
292
+ return degreeB - degreeA;
293
+ });
294
+
295
+ if (!entries.length) {
296
+ return '0';
297
+ }
298
+
299
+ return entries
300
+ .map(([key, coeff], index) => {
301
+ const powers = keyToPowers(key);
302
+ const absCoeff = Math.abs(coeff);
303
+ const variablePart = Object.entries(powers)
304
+ .map(([name, power]) => (power === 1 ? name : `${name} ^ ${power}`))
305
+ .join(' * ');
306
+ let body = variablePart;
307
+
308
+ if (!body) {
309
+ body = `${absCoeff}`;
310
+ } else if (absCoeff !== 1) {
311
+ body = `${absCoeff} * ${body}`;
312
+ }
313
+
314
+ if (index === 0) {
315
+ return coeff < 0 ? `- ${body}`.replace('- ', '-') : body;
316
+ }
317
+
318
+ return coeff < 0 ? `- ${body}` : `+ ${body}`;
319
+ })
320
+ .join(' ');
321
+ };
322
+
323
+ const ast = this.parse(normalizedExpression).ast;
324
+ const result = astToRat(ast);
325
+ const numerator = formatPoly(result.num);
326
+ const denominator = formatPoly(result.den);
327
+ const variableSet = new Set();
328
+
329
+ for (const poly of [result.num, result.den]) {
330
+ for (const key of poly.keys()) {
331
+ for (const name of Object.keys(keyToPowers(key))) {
332
+ variableSet.add(name);
333
+ }
334
+ }
335
+ }
336
+
337
+ if (!withDetails) {
338
+ return `(${numerator}) / (${denominator})`;
339
+ }
340
+
341
+ return {
342
+ numerator,
343
+ denominator,
344
+ coefficients: [],
345
+ variables: [...variableSet].sort(),
346
+ expression: `(${numerator}) / (${denominator})`,
347
+ };
348
+ });
349
+
350
+ this.addFunction('map', (/** @type {any[]} */ arr, /** @type {any} */ fnOrName) => {
351
+ if (!Array.isArray(arr)) {
352
+ throw new Error('map() expects an array');
353
+ }
354
+ const fn = typeof fnOrName === 'string' ? this.functions.get(fnOrName) : fnOrName;
355
+ if (typeof fn !== 'function') {
356
+ throw new Error('map() requires a function or function name');
357
+ }
358
+ return arr.map((x) => fn(x));
359
+ });
360
+
361
+ this.addFunction('filter', (/** @type {any[]} */ arr, /** @type {any} */ fnOrName) => {
362
+ if (!Array.isArray(arr)) {
363
+ throw new Error('filter() expects an array');
364
+ }
365
+ const fn = typeof fnOrName === 'string' ? this.functions.get(fnOrName) : fnOrName;
366
+ if (typeof fn !== 'function') {
367
+ throw new Error('filter() requires a function or function name');
368
+ }
369
+ return arr.filter((x) => fn(x));
370
+ });
371
+
372
+ // Numeric integration via Simpson's 1/3 rule with 100 subintervals
373
+ this.addFunction(
374
+ 'integral',
375
+ (/** @type {any} */ expr, /** @type {number} */ a, /** @type {number} */ b) => {
376
+ if (typeof expr !== 'string') {
377
+ throw new Error('integral() expects an expression string');
378
+ }
379
+ const compiled = this.compile(expr);
380
+ const n = 100;
381
+ const h = (b - a) / n;
382
+ let sum = compiled({ x: a }) + compiled({ x: b });
383
+ for (let i = 1; i < n; i++) {
384
+ const x = a + i * h;
385
+ const f = compiled({ x });
386
+ sum += i % 2 === 0 ? 2 * f : 4 * f;
387
+ }
388
+ return (h / 3) * sum;
389
+ }
390
+ );
391
+
392
+ // Summation: evaluate expr for variable = start..end
393
+ this.addFunction(
394
+ 'sigma',
395
+ (
396
+ /** @type {any} */ variable,
397
+ /** @type {any} */ start,
398
+ /** @type {number} */ end,
399
+ /** @type {any} */ expr
400
+ ) => {
401
+ if (typeof expr !== 'string') {
402
+ throw new Error('sigma() expects an expression string');
403
+ }
404
+ const compiled = this.compile(expr);
405
+ let total = 0;
406
+ for (let i = start; i <= end; i++) {
407
+ total += compiled({ [variable]: i });
408
+ }
409
+ return total;
410
+ }
411
+ );
412
+
413
+ // Product: multiply expr for variable = start..end
414
+ this.addFunction(
415
+ 'pi',
416
+ (
417
+ /** @type {any} */ variable,
418
+ /** @type {any} */ start,
419
+ /** @type {number} */ end,
420
+ /** @type {any} */ expr
421
+ ) => {
422
+ if (typeof expr !== 'string') {
423
+ throw new Error('pi() expects an expression string');
424
+ }
425
+ const compiled = this.compile(expr);
426
+ let total = 1;
427
+ for (let i = start; i <= end; i++) {
428
+ total *= compiled({ [variable]: i });
429
+ }
430
+ return total;
431
+ }
432
+ );
433
+
434
+ this.addFunction(
435
+ 'substitute',
436
+ (/** @type {any} */ expr, /** @type {any} */ variable, /** @type {any} */ value) => {
437
+ if (typeof expr !== 'string') {
438
+ throw new Error('substitute() expects an expression string');
439
+ }
440
+ const compiled = this.compile(expr);
441
+ return compiled({ [variable]: value });
442
+ }
443
+ );
444
+
445
+ // Numeric limit: evaluate at progressively smaller epsilon until convergence
446
+ this.addFunction(
447
+ 'limit',
448
+ (
449
+ /** @type {any} */ expr,
450
+ /** @type {any} */ variable,
451
+ /** @type {number} */ approach,
452
+ /** @type {string} */ direction
453
+ ) => {
454
+ if (typeof expr !== 'string') {
455
+ throw new Error('limit() expects an expression string');
456
+ }
457
+ const compiled = this.compile(expr);
458
+ const epsilons = [1e-1, 1e-2, 1e-3, 1e-4, 1e-5, 1e-6, 1e-7, 1e-8, 1e-9, 1e-10];
459
+ let lastVal = NaN;
460
+ for (const eps of epsilons) {
461
+ let x;
462
+ if (direction === 'right') {
463
+ x = approach + eps;
464
+ } else if (direction === 'left') {
465
+ x = approach - eps;
466
+ } else {
467
+ x = approach + eps;
468
+ }
469
+ const val = compiled({ [variable]: x });
470
+ if (isFinite(val)) {
471
+ lastVal = val;
472
+ }
473
+ }
474
+ return lastVal;
475
+ }
476
+ );
477
+
478
+ // --- expand(): detect polynomial degree via forward differences, solve Vandermonde system for coefficients ---
479
+ this.addFunction('expand', (/** @type {string} */ expr) => {
480
+ if (typeof expr !== 'string') {
481
+ throw new Error('expand() expects an expression string');
482
+ }
483
+ const variableMatch = expr.match(/[a-zA-Z_][a-zA-Z0-9_]*/);
484
+ if (!variableMatch) {
485
+ throw new Error('expand() could not identify variable');
486
+ }
487
+ const v = variableMatch[0];
488
+ const cleaned = expr.replace(/\s+/g, '').replace(/\\/g, '\\\\').replace(/"/g, '\\"');
489
+ const addStar = (/** @type {string} */ s) => s.replace(/(\d)([a-zA-Z_])/g, '$1*$2');
490
+ const evalAt = (/** @type {number} */ x) =>
491
+ this.evaluate(`substitute("${addStar(cleaned)}", "${v}", ${x})`);
492
+
493
+ const maxDegree = 10;
494
+ const vals = [];
495
+ for (let i = 0; i <= maxDegree; i++) {
496
+ vals.push(evalAt(i));
497
+ }
498
+
499
+ let degree = 0;
500
+ let diffs = [...vals];
501
+ for (let d = 0; d <= maxDegree; d++) {
502
+ if (Math.abs(diffs[0]) > 1e-10) {
503
+ degree = d;
504
+ }
505
+ const next = [];
506
+ for (let i = 0; i < diffs.length - 1; i++) {
507
+ next.push(diffs[i + 1] - diffs[i]);
508
+ }
509
+ diffs = next;
510
+ if (diffs.every((x) => Math.abs(x) < 1e-10)) {
511
+ break;
512
+ }
513
+ }
514
+
515
+ const n = degree + 1;
516
+ const m = Array.from({ length: n }, (_, i) => {
517
+ const row = Array.from({ length: n }, (_, j) => i ** j);
518
+ row.push(vals[i]);
519
+ return row;
520
+ });
521
+ for (let col = 0; col < n; col++) {
522
+ let pivot = col;
523
+ while (pivot < n && Math.abs(m[pivot][col]) < 1e-12) {
524
+ pivot++;
525
+ }
526
+ if (pivot === n) {
527
+ continue;
528
+ }
529
+ [m[col], m[pivot]] = [m[pivot], m[col]];
530
+ const pv = m[col][col];
531
+ for (let j = col; j <= n; j++) {
532
+ m[col][j] /= pv;
533
+ }
534
+ for (let row = 0; row < n; row++) {
535
+ if (row !== col) {
536
+ const f = m[row][col];
537
+ for (let j = col; j <= n; j++) {
538
+ m[row][j] -= f * m[col][j];
539
+ }
540
+ }
541
+ }
542
+ }
543
+ const coeffs = m.map((row) => (Math.abs(row[n]) < 1e-10 ? 0 : row[n]));
544
+ const terms = [];
545
+ for (let i = degree; i >= 0; i--) {
546
+ const c = coeffs[i];
547
+ if (Math.abs(c) < 1e-10) {
548
+ continue;
549
+ }
550
+ const sign = terms.length === 0 ? (c < 0 ? '-' : '') : c < 0 ? ' - ' : ' + ';
551
+ const absC = Math.abs(c);
552
+ const cStr = i === 0 ? `${absC}` : absC === 1 ? '' : `${absC}`;
553
+ const pStr = i === 0 ? '' : i === 1 ? v : `${v}^${i}`;
554
+ terms.push(`${sign}${cStr}${pStr}`);
555
+ }
556
+ return terms.join('') || '0';
557
+ });
558
+
559
+ // --- factor(): detect degree, solve coefficients, apply rational root theorem + synthetic division ---
560
+ this.addFunction('factor', (/** @type {string} */ poly) => {
561
+ if (typeof poly !== 'string') {
562
+ throw new Error('factor() expects an expression string');
563
+ }
564
+ const cleaned = poly.replace(/\s+/g, '');
565
+ const variableMatch = cleaned.match(/[a-zA-Z_][a-zA-Z0-9_]*/);
566
+ if (!variableMatch) {
567
+ throw new Error('factor() could not identify variable');
568
+ }
569
+ const variable = variableMatch[0];
570
+ const addStar = (/** @type {string} */ s) =>
571
+ s.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/(\d)([a-zA-Z_])/g, '$1*$2');
572
+ const cleanedExpr = addStar(cleaned);
573
+ const maxPower = 6;
574
+ const vals = [];
575
+ for (let power = 0; power <= maxPower; power++) {
576
+ vals.push(this.evaluate(`substitute("${cleanedExpr}", "${variable}", ${power})`));
577
+ }
578
+ let diff = vals.slice();
579
+ let degree = 0;
580
+ for (let d = 0; d <= maxPower; d++) {
581
+ if (diff.every((x) => Math.abs(x) < 1e-10)) {
582
+ degree = Math.max(0, d - 1);
583
+ break;
584
+ }
585
+ if (d < maxPower) {
586
+ const next = [];
587
+ for (let i = 0; i < diff.length - 1; i++) {
588
+ next.push(diff[i + 1] - diff[i]);
589
+ }
590
+ diff = next;
591
+ }
592
+ }
593
+ if (degree === 0) {
594
+ return `(${poly})`;
595
+ }
596
+ const n = degree + 1;
597
+ const m = Array.from({ length: n }, (_, i) => {
598
+ const row = Array.from({ length: n }, (_, j) => i ** j);
599
+ row.push(vals[i]);
600
+ return row;
601
+ });
602
+ for (let col = 0; col < n; col++) {
603
+ let pivot = col;
604
+ while (pivot < n && Math.abs(m[pivot][col]) < 1e-12) {
605
+ pivot++;
606
+ }
607
+ if (pivot === n) {
608
+ continue;
609
+ }
610
+ [m[col], m[pivot]] = [m[pivot], m[col]];
611
+ const pv = m[col][col];
612
+ for (let j = col; j <= n; j++) {
613
+ m[col][j] /= pv;
614
+ }
615
+ for (let row = 0; row < n; row++) {
616
+ if (row !== col) {
617
+ const f = m[row][col];
618
+ for (let j = col; j <= n; j++) {
619
+ m[row][j] -= f * m[col][j];
620
+ }
621
+ }
622
+ }
623
+ }
624
+ const coeffs = m.map((r) => (Math.abs(r[n]) < 1e-10 ? 0 : r[n]));
625
+ if (degree >= 1 && degree <= 3) {
626
+ const polyRootFn = this.functions.get('polynomialRoot');
627
+ const rootArr = polyRootFn(...coeffs);
628
+ const rootArrFlat = Array.isArray(rootArr) ? rootArr : [rootArr];
629
+ const unique = [
630
+ ...new Set(
631
+ rootArrFlat.map((r) => (Number.isInteger(r) ? r : Math.round(r * 1e10) / 1e10))
632
+ ),
633
+ ].sort((a, b) => a - b);
634
+ if (unique.length === degree) {
635
+ const lead = coeffs[degree];
636
+ const leadStr =
637
+ Math.abs(lead - 1) > 1e-10 ? (Math.abs(lead + 1) < 1e-10 ? '-' : `${lead}`) : '';
638
+ const factors = unique.map((r) => {
639
+ if (Math.abs(r) < 1e-10) {
640
+ return variable;
641
+ }
642
+ return r > 0 ? `(${variable} - ${r})` : `(${variable} + ${Math.abs(r)})`;
643
+ });
644
+ return `${leadStr}${factors.join('')}`;
645
+ }
646
+ }
647
+ return `(${poly})`;
648
+ });
649
+
650
+ // --- solve(): split on '=', form f(x)=0, detect polynomial degree, find roots ---
651
+ this.addFunction('solve', (/** @type {string} */ eqn, /** @type {string} */ variable) => {
652
+ if (typeof eqn !== 'string') {
653
+ throw new Error('solve() expects an equation string');
654
+ }
655
+ const parts = eqn.split('=');
656
+ if (parts.length !== 2) {
657
+ throw new Error('solve() expects an equation with =');
658
+ }
659
+ const lhs = parts[0].trim();
660
+ const rhs = parts[1].trim();
661
+ const expr = `(${lhs}) - (${rhs})`;
662
+ const cleaned = expr.replace(/\s+/g, '');
663
+ const variableMatch = cleaned.match(/[a-zA-Z_][a-zA-Z0-9_]*/);
664
+ const v = variable || (variableMatch ? variableMatch[0] : 'x');
665
+ const addStar = (/** @type {string} */ s) =>
666
+ s.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/(\d)([a-zA-Z_])/g, '$1*$2');
667
+ const cleanedExpr = addStar(cleaned);
668
+ const maxPower = 6;
669
+ const vals = [];
670
+ for (let power = 0; power <= maxPower; power++) {
671
+ vals.push(this.evaluate(`substitute("${cleanedExpr}", "${v}", ${power})`));
672
+ }
673
+ let diff = vals.slice();
674
+ let degree = 0;
675
+ for (let d = 0; d <= maxPower; d++) {
676
+ if (diff.every((x) => Math.abs(x) < 1e-10)) {
677
+ degree = Math.max(0, d - 1);
678
+ break;
679
+ }
680
+ if (d < maxPower) {
681
+ const next = [];
682
+ for (let i = 0; i < diff.length - 1; i++) {
683
+ next.push(diff[i + 1] - diff[i]);
684
+ }
685
+ diff = next;
686
+ }
687
+ }
688
+ if (degree === 0) {
689
+ throw new Error('No solution found');
690
+ }
691
+ const n = degree + 1;
692
+ const m = Array.from({ length: n }, (_, i) => {
693
+ const row = Array.from({ length: n }, (_, j) => i ** j);
694
+ row.push(vals[i]);
695
+ return row;
696
+ });
697
+ for (let col = 0; col < n; col++) {
698
+ let pivot = col;
699
+ while (pivot < n && Math.abs(m[pivot][col]) < 1e-12) {
700
+ pivot++;
701
+ }
702
+ if (pivot === n) {
703
+ continue;
704
+ }
705
+ [m[col], m[pivot]] = [m[pivot], m[col]];
706
+ const pv = m[col][col];
707
+ for (let j = col; j <= n; j++) {
708
+ m[col][j] /= pv;
709
+ }
710
+ for (let row = 0; row < n; row++) {
711
+ if (row !== col) {
712
+ const f = m[row][col];
713
+ for (let j = col; j <= n; j++) {
714
+ m[row][j] -= f * m[col][j];
715
+ }
716
+ }
717
+ }
718
+ }
719
+ const coeffs = m.map((r) => (Math.abs(r[n]) < 1e-10 ? 0 : r[n]));
720
+ if (degree >= 1 && degree <= 3) {
721
+ const polyRootFn = this.functions.get('polynomialRoot');
722
+ const rootArr = polyRootFn(...coeffs);
723
+ const rootArrFlat = Array.isArray(rootArr) ? rootArr : [rootArr];
724
+ return rootArrFlat.sort((a, b) => a - b);
725
+ }
726
+ throw new Error('solve() currently supports degree up to 3');
727
+ });
728
+ }
729
+
730
+ /**
731
+ * @param {any} name
732
+ * @param {any} value
733
+ */
734
+ setVariable(name, value) {
735
+ this.variables.set(name, value);
736
+ }
737
+
738
+ /**
739
+ * @param {any} name
740
+ */
741
+ getVariable(name) {
742
+ return this.variables.get(name);
743
+ }
744
+
745
+ /**
746
+ * @param {string} name
747
+ * @param {any} fn
748
+ */
749
+ addFunction(name, fn) {
750
+ this.functions.register(name, fn);
751
+ }
752
+
753
+ _createContext() {
754
+ return createContext({
755
+ functions: this.functions,
756
+ variables: this.variables,
757
+ units: this.units,
758
+ evaluate: this.evaluate.bind(this),
759
+ });
760
+ }
761
+
762
+ /**
763
+ * @param {any} expr
764
+ */
765
+ tokenize(expr) {
766
+ if (typeof expr !== 'string') {
767
+ throw new Error('Expression must be a string');
768
+ }
769
+ return tokenize(expr, this._createContext());
770
+ }
771
+
772
+ /**
773
+ * @param {string} expr
774
+ */
775
+ parse(expr) {
776
+ const tokens = this.tokenize(expr);
777
+ const ast = buildAST(tokens);
778
+ return { tokens, ast };
779
+ }
780
+
781
+ /**
782
+ * @param {string} expr
783
+ * @param {object} [scope]
784
+ */
785
+ evaluate(expr, scope = {}) {
786
+ return formatResult(this._evaluateRaw(expr, scope));
787
+ }
788
+
789
+ /**
790
+ * @param {string} expr
791
+ * @param {object} [scope]
792
+ */
793
+ _evaluateRaw(expr, scope = {}) {
794
+ const { ast } = this.parse(expr);
795
+ const ctx = this._createContext();
796
+ const mergedCtx = Object.keys(scope).length > 0 ? ctx.withScope(scope) : ctx;
797
+ return evaluateAST(ast, mergedCtx);
798
+ }
799
+
800
+ /**
801
+ * @param {string} expr
802
+ */
803
+ compile(expr) {
804
+ if (this._cache.has(expr)) {
805
+ return this._cache.get(expr);
806
+ }
807
+
808
+ const { ast } = this.parse(expr);
809
+
810
+ const compiledFn = (scope = {}) => {
811
+ const baseContext = this._createContext();
812
+ const scopedContext = baseContext.withScope(scope);
813
+ return formatResult(evaluateAST(ast, scopedContext));
814
+ };
815
+
816
+ this._cache.set(expr, compiledFn);
817
+ return compiledFn;
818
+ }
819
+
820
+ clearCache() {
821
+ this._cache.clear();
822
+ }
823
+
824
+ exportState() {
825
+ return {
826
+ variables: this.variables.all(),
827
+ functions: this.functions.getAllFunctionsName(),
828
+ units: this.units.getUnits(),
829
+ };
830
+ }
831
+
832
+ importState(state) {
833
+ if (state.variables) {
834
+ this.variables.merge(state.variables);
835
+ }
836
+ if (state.units) {
837
+ this.units.setUnits(state.units);
838
+ }
839
+ if (state.functions) {
840
+ for (const name of state.functions) {
841
+ if (!this.functions.has(name)) {
842
+ // warn: function could not be restored (built-in only)
843
+ }
844
+ }
845
+ }
846
+ return this;
847
+ }
848
+
849
+ chain() {
850
+ return new Chain(this);
851
+ }
852
+ }
853
+
854
+ class Chain {
855
+ /** @param {exprify} exprifyInstance */
856
+ constructor(exprifyInstance) {
857
+ this._expr = exprifyInstance;
858
+ this._rawResult = undefined;
859
+ }
860
+
861
+ evaluate(expr, scope = {}) {
862
+ this._rawResult = this._expr._evaluateRaw(expr, { ...scope, ans: this._rawResult });
863
+ return this;
864
+ }
865
+
866
+ setVariable(name, value) {
867
+ this._expr.setVariable(name, value);
868
+ return this;
869
+ }
870
+
871
+ compile(expr) {
872
+ return this._expr.compile(expr);
873
+ }
874
+
875
+ done() {
876
+ return formatResult(this._rawResult);
877
+ }
878
+ }
879
+
880
+ export default exprify;