exprify 1.0.1 → 1.0.3

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/dist/exprify.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * exprify v1.0.1
2
+ * exprify v1.0.3
3
3
  * (c) 2026 Nirmal Paul and other contributors
4
4
  *
5
5
  * Released under the GPL-3.0 License
@@ -411,6 +411,60 @@
411
411
  return final;
412
412
  }
413
413
 
414
+ const isDenseMatrixWrapper = (value) =>
415
+ value &&
416
+ typeof value === "object" &&
417
+ value.exprify === "DenseMatrix" &&
418
+ "data" in value &&
419
+ "size" in value;
420
+
421
+ const cloneMatrixData = (value) => {
422
+ if (Array.isArray(value)) {
423
+ return value.map(cloneMatrixData);
424
+ }
425
+
426
+ return value;
427
+ };
428
+
429
+ const getMatrixSize = (data) => {
430
+ if (Array.isArray(data) && data.every(Array.isArray)) {
431
+ return [data.length, data[0]?.length || 0];
432
+ }
433
+
434
+ if (Array.isArray(data)) {
435
+ return [data.length];
436
+ }
437
+
438
+ throw new Error("Matrix data must be an array");
439
+ };
440
+
441
+ const wrapDenseMatrix = (data) => ({
442
+ exprify: "DenseMatrix",
443
+ data: cloneMatrixData(data),
444
+ size: getMatrixSize(data)
445
+ });
446
+
447
+ const unwrapDenseMatrix = (value) =>
448
+ isDenseMatrixWrapper(value) ? cloneMatrixData(value.data) : value;
449
+
450
+ const serializeExprifyValue = (value) => {
451
+ if (isDenseMatrixWrapper(value)) {
452
+ return JSON.stringify(value);
453
+ }
454
+
455
+ if (Array.isArray(value) || (value && typeof value === "object")) {
456
+ return JSON.stringify(value, (_, current) => {
457
+ if (isDenseMatrixWrapper(current)) {
458
+ return current;
459
+ }
460
+
461
+ return current;
462
+ });
463
+ }
464
+
465
+ return value;
466
+ };
467
+
414
468
  function evaluateAST(node, context = {}) {
415
469
 
416
470
  const vars = context.variables;
@@ -431,6 +485,7 @@
431
485
  Array.isArray(v) && v.length > 0 && v.every(Array.isArray);
432
486
 
433
487
  const normalizeMatrix = (value) => {
488
+ value = unwrapDenseMatrix(value);
434
489
  if (isMatrix(value)) return value.map((row) => [...row]);
435
490
  if (Array.isArray(value)) return [value];
436
491
  throw new Error("Expected matrix-compatible value");
@@ -603,6 +658,16 @@
603
658
  const simplifyComplex = (value) =>
604
659
  value.im === 0 ? value.re : value;
605
660
 
661
+ const createFunctionScope = (params, args) => {
662
+ const scopedValues = {};
663
+
664
+ params.forEach((param, index) => {
665
+ scopedValues[param] = args[index];
666
+ });
667
+
668
+ return scopedValues;
669
+ };
670
+
606
671
  const evalComplexBinary = (operator, left, right) => {
607
672
  const a = toComplex(left);
608
673
  const b = toComplex(right);
@@ -658,6 +723,9 @@
658
723
 
659
724
  if (node.left.type === "Identifier") {
660
725
  vars.set(node.left.name, value);
726
+ if (node.right.type === "ArrayExpression") {
727
+ return wrapDenseMatrix(unwrapDenseMatrix(value));
728
+ }
661
729
  return value;
662
730
  }
663
731
 
@@ -671,6 +739,20 @@
671
739
  throw new Error("Invalid assignment target");
672
740
  }
673
741
 
742
+ case "FunctionAssignmentExpression": {
743
+ if (node.operator !== "=") {
744
+ throw new Error(`Operator ${node.operator} is not supported for function definitions`);
745
+ }
746
+
747
+ const fn = (...args) => {
748
+ const scopedContext = context.withScope(createFunctionScope(node.params, args));
749
+ return evaluateAST(node.right, scopedContext);
750
+ };
751
+
752
+ fns.register(node.left.name, fn);
753
+ return fn;
754
+ }
755
+
674
756
  /* ===== UNARY ===== */
675
757
  case "UnaryExpression": {
676
758
  const val = evaluateAST(node.argument, context);
@@ -691,7 +773,7 @@
691
773
  let left = evaluateAST(node.left, context);
692
774
  let right = evaluateAST(node.right, context);
693
775
 
694
- // 🔥 UNIT handling
776
+ // UNIT handling
695
777
  if (isUnitObj(left) || isUnitObj(right)) {
696
778
 
697
779
  if (!units) throw new Error("Unit system not available");
@@ -1441,6 +1523,7 @@
1441
1523
  }
1442
1524
 
1443
1525
  function validateSquareMatrix(matrix) {
1526
+ matrix = unwrapDenseMatrix(matrix);
1444
1527
  if (!Array.isArray(matrix) || matrix.length === 0) {
1445
1528
  throw new Error("det() expects a non-empty matrix");
1446
1529
  }
@@ -1464,6 +1547,7 @@
1464
1547
  }
1465
1548
 
1466
1549
  function determinant(matrix) {
1550
+ matrix = unwrapDenseMatrix(matrix);
1467
1551
  validateSquareMatrix(matrix);
1468
1552
 
1469
1553
  if (matrix.length === 1) {
@@ -1483,6 +1567,334 @@
1483
1567
  }, 0);
1484
1568
  }
1485
1569
 
1570
+ function asMatrixData(value) {
1571
+ const data = unwrapDenseMatrix(value);
1572
+ if (!Array.isArray(data)) {
1573
+ throw new Error("Expected matrix data");
1574
+ }
1575
+ return data;
1576
+ }
1577
+
1578
+ function solveLinearSystem(coefficients, constants) {
1579
+ const n = coefficients.length;
1580
+ const augmented = coefficients.map((row, rowIndex) => [...row, constants[rowIndex]]);
1581
+
1582
+ for (let pivot = 0; pivot < n; pivot++) {
1583
+ let maxRow = pivot;
1584
+ let maxValue = Math.abs(augmented[pivot][pivot]);
1585
+
1586
+ for (let row = pivot + 1; row < n; row++) {
1587
+ const current = Math.abs(augmented[row][pivot]);
1588
+ if (current > maxValue) {
1589
+ maxValue = current;
1590
+ maxRow = row;
1591
+ }
1592
+ }
1593
+
1594
+ if (maxValue === 0) {
1595
+ throw new Error("Linear system is singular");
1596
+ }
1597
+
1598
+ if (maxRow !== pivot) {
1599
+ [augmented[pivot], augmented[maxRow]] = [augmented[maxRow], augmented[pivot]];
1600
+ }
1601
+
1602
+ const pivotValue = augmented[pivot][pivot];
1603
+ for (let col = pivot; col <= n; col++) {
1604
+ augmented[pivot][col] /= pivotValue;
1605
+ }
1606
+
1607
+ for (let row = 0; row < n; row++) {
1608
+ if (row === pivot) continue;
1609
+ const factor = augmented[row][pivot];
1610
+ for (let col = pivot; col <= n; col++) {
1611
+ augmented[row][col] -= factor * augmented[pivot][col];
1612
+ }
1613
+ }
1614
+ }
1615
+
1616
+ return augmented.map((row) => row[n]);
1617
+ }
1618
+
1619
+ function lupDecomposition(input) {
1620
+ const matrix = asMatrixData(input).map((row) => [...row]);
1621
+ validateSquareMatrix(matrix);
1622
+
1623
+ const n = matrix.length;
1624
+ const permutation = Array.from({ length: n }, (_, index) => index);
1625
+
1626
+ for (let pivot = 0; pivot < n; pivot++) {
1627
+ let maxRow = pivot;
1628
+ let maxValue = Math.abs(matrix[pivot][pivot]);
1629
+
1630
+ for (let row = pivot + 1; row < n; row++) {
1631
+ const current = Math.abs(matrix[row][pivot]);
1632
+ if (current > maxValue) {
1633
+ maxValue = current;
1634
+ maxRow = row;
1635
+ }
1636
+ }
1637
+
1638
+ if (maxValue === 0) {
1639
+ throw new Error("Matrix is singular");
1640
+ }
1641
+
1642
+ if (maxRow !== pivot) {
1643
+ [matrix[pivot], matrix[maxRow]] = [matrix[maxRow], matrix[pivot]];
1644
+ [permutation[pivot], permutation[maxRow]] = [permutation[maxRow], permutation[pivot]];
1645
+ }
1646
+
1647
+ for (let row = pivot + 1; row < n; row++) {
1648
+ matrix[row][pivot] /= matrix[pivot][pivot];
1649
+ for (let col = pivot + 1; col < n; col++) {
1650
+ matrix[row][col] -= matrix[row][pivot] * matrix[pivot][col];
1651
+ }
1652
+ }
1653
+ }
1654
+
1655
+ const L = matrix.map((row, rowIndex) =>
1656
+ row.map((value, colIndex) => {
1657
+ if (rowIndex === colIndex) return 1;
1658
+ if (rowIndex > colIndex) return value;
1659
+ return 0;
1660
+ })
1661
+ );
1662
+
1663
+ const U = matrix.map((row, rowIndex) =>
1664
+ row.map((value, colIndex) => (rowIndex <= colIndex ? value : 0))
1665
+ );
1666
+
1667
+ return {
1668
+ L: wrapDenseMatrix(L),
1669
+ U: wrapDenseMatrix(U),
1670
+ p: permutation
1671
+ };
1672
+ }
1673
+
1674
+ function linearSolve(aInput, bInput) {
1675
+ const { L, U, p } = lupDecomposition(aInput);
1676
+ const a = asMatrixData(aInput);
1677
+ const bData = asMatrixData(bInput);
1678
+ const bVector = Array.isArray(bData[0]) ? bData.map((row) => row[0]) : bData;
1679
+
1680
+ if (a.length !== bVector.length) {
1681
+ throw new Error("Right-hand side dimension mismatch");
1682
+ }
1683
+
1684
+ const permutedB = p.map((index) => bVector[index]);
1685
+ const y = new Array(a.length).fill(0);
1686
+
1687
+ for (let row = 0; row < a.length; row++) {
1688
+ y[row] = permutedB[row];
1689
+ for (let col = 0; col < row; col++) {
1690
+ y[row] -= L.data[row][col] * y[col];
1691
+ }
1692
+ }
1693
+
1694
+ const x = new Array(a.length).fill(0);
1695
+ for (let row = a.length - 1; row >= 0; row--) {
1696
+ x[row] = y[row];
1697
+ for (let col = row + 1; col < a.length; col++) {
1698
+ x[row] -= U.data[row][col] * x[col];
1699
+ }
1700
+ x[row] /= U.data[row][row];
1701
+ }
1702
+
1703
+ return wrapDenseMatrix(x.map((value) => [value]));
1704
+ }
1705
+
1706
+ function solveLyapunov(aInput, qInput) {
1707
+ const A = asMatrixData(aInput).map((row) => [...row]);
1708
+ const Q = asMatrixData(qInput).map((row) => [...row]);
1709
+ validateSquareMatrix(A);
1710
+ validateSquareMatrix(Q);
1711
+
1712
+ const n = A.length;
1713
+ if (Q.length !== n) {
1714
+ throw new Error("A and Q must have the same dimensions");
1715
+ }
1716
+
1717
+ const coefficients = [];
1718
+ const constants = [];
1719
+
1720
+ for (let row = 0; row < n; row++) {
1721
+ for (let col = 0; col < n; col++) {
1722
+ const equation = new Array(n * n).fill(0);
1723
+
1724
+ for (let k = 0; k < n; k++) {
1725
+ equation[k * n + col] += A[row][k];
1726
+ equation[row * n + k] += A[col][k];
1727
+ }
1728
+
1729
+ coefficients.push(equation);
1730
+ constants.push(-Q[row][col]);
1731
+ }
1732
+ }
1733
+
1734
+ const solution = solveLinearSystem(coefficients, constants);
1735
+ const X = [];
1736
+
1737
+ for (let row = 0; row < n; row++) {
1738
+ X.push(solution.slice(row * n, (row + 1) * n));
1739
+ }
1740
+
1741
+ return wrapDenseMatrix(X);
1742
+ }
1743
+
1744
+ function evaluatePolynomial(coefficients, x) {
1745
+ return coefficients.reduce((sum, coefficient, index) => sum + (coefficient * (x ** index)), 0);
1746
+ }
1747
+
1748
+ function syntheticDivide(coefficients, root) {
1749
+ const descending = [...coefficients].reverse();
1750
+ const quotient = [descending[0]];
1751
+
1752
+ for (let index = 1; index < descending.length - 1; index++) {
1753
+ quotient.push(descending[index] + (quotient[index - 1] * root));
1754
+ }
1755
+
1756
+ const remainder = descending[descending.length - 1] + (quotient[quotient.length - 1] * root);
1757
+ return {
1758
+ quotient: quotient.reverse(),
1759
+ remainder
1760
+ };
1761
+ }
1762
+
1763
+ function solveQuadratic(coefficients) {
1764
+ const [c, b, a] = coefficients;
1765
+ const discriminant = (b ** 2) - (4 * a * c);
1766
+ if (discriminant < 0) {
1767
+ throw new Error("Only real roots are supported");
1768
+ }
1769
+
1770
+ const sqrtDisc = Math.sqrt(discriminant);
1771
+ return [
1772
+ (-b + sqrtDisc) / (2 * a),
1773
+ (-b - sqrtDisc) / (2 * a)
1774
+ ];
1775
+ }
1776
+
1777
+ function polynomialRoots(...coefficients) {
1778
+ while (coefficients.length > 1 && coefficients[coefficients.length - 1] === 0) {
1779
+ coefficients.pop();
1780
+ }
1781
+
1782
+ const degree = coefficients.length - 1;
1783
+ if (degree < 1) {
1784
+ throw new Error("polynomialRoot() expects at least a linear polynomial");
1785
+ }
1786
+
1787
+ if (degree === 1) {
1788
+ const [b, a] = coefficients;
1789
+ return [-b / a];
1790
+ }
1791
+
1792
+ if (degree === 2) {
1793
+ return solveQuadratic(coefficients);
1794
+ }
1795
+
1796
+ if (degree === 3) {
1797
+ const constant = coefficients[0];
1798
+ coefficients[3];
1799
+ const candidates = [];
1800
+ const limit = Math.abs(constant);
1801
+
1802
+ for (let divisor = 1; divisor <= Math.max(1, limit); divisor++) {
1803
+ if (limit % divisor === 0) {
1804
+ candidates.push(divisor, -divisor);
1805
+ }
1806
+ }
1807
+
1808
+ for (const candidate of candidates) {
1809
+ if (evaluatePolynomial(coefficients, candidate) === 0) {
1810
+ const reduced = syntheticDivide(coefficients, candidate);
1811
+ const remainingRoots = solveQuadratic(reduced.quotient);
1812
+ return [candidate, ...remainingRoots];
1813
+ }
1814
+ }
1815
+ }
1816
+
1817
+ throw new Error("polynomialRoot() currently supports degree up to 3");
1818
+ }
1819
+
1820
+ function dotProduct(a, b) {
1821
+ return a.reduce((sum, value, index) => sum + (value * b[index]), 0);
1822
+ }
1823
+
1824
+ function vectorNorm(vector) {
1825
+ return Math.sqrt(dotProduct(vector, vector));
1826
+ }
1827
+
1828
+ function scaleVector(vector, scalar) {
1829
+ return vector.map((value) => value * scalar);
1830
+ }
1831
+
1832
+ function subtractVectors(a, b) {
1833
+ return a.map((value, index) => value - b[index]);
1834
+ }
1835
+
1836
+ function transpose(matrix) {
1837
+ return matrix[0].map((_, colIndex) => matrix.map((row) => row[colIndex]));
1838
+ }
1839
+
1840
+ function qrDecomposition(input) {
1841
+ const A = asMatrixData(input).map((row) => [...row]);
1842
+ if (!A.length || !A.every((row) => row.length === A[0].length)) {
1843
+ throw new Error("qr() expects a rectangular matrix");
1844
+ }
1845
+
1846
+ const rowCount = A.length;
1847
+ const colCount = A[0].length;
1848
+ const columns = transpose(A);
1849
+ const qColumns = [];
1850
+
1851
+ for (let col = 0; col < colCount; col++) {
1852
+ let vector = [...columns[col]];
1853
+
1854
+ for (let existing = 0; existing < qColumns.length; existing++) {
1855
+ const projection = dotProduct(qColumns[existing], columns[col]);
1856
+ vector = subtractVectors(vector, scaleVector(qColumns[existing], projection));
1857
+ }
1858
+
1859
+ const norm = vectorNorm(vector);
1860
+ if (norm === 0) {
1861
+ throw new Error("qr() requires linearly independent columns");
1862
+ }
1863
+
1864
+ qColumns.push(scaleVector(vector, 1 / norm));
1865
+ }
1866
+
1867
+ for (let basisIndex = 0; qColumns.length < rowCount && basisIndex < rowCount; basisIndex++) {
1868
+ let candidate = Array.from({ length: rowCount }, (_, index) => (index === basisIndex ? 1 : 0));
1869
+
1870
+ for (const column of qColumns) {
1871
+ const projection = dotProduct(column, candidate);
1872
+ candidate = subtractVectors(candidate, scaleVector(column, projection));
1873
+ }
1874
+
1875
+ const norm = vectorNorm(candidate);
1876
+ if (norm > 1e-10) {
1877
+ qColumns.push(scaleVector(candidate, 1 / norm));
1878
+ }
1879
+ }
1880
+
1881
+ const Q = Array.from({ length: rowCount }, (_, rowIndex) =>
1882
+ qColumns.map((column) => column[rowIndex])
1883
+ );
1884
+
1885
+ const fullR = Array.from({ length: rowCount }, () => Array(colCount).fill(0));
1886
+ for (let row = 0; row < rowCount; row++) {
1887
+ for (let col = 0; col < colCount; col++) {
1888
+ fullR[row][col] = dotProduct(qColumns[row], columns[col]);
1889
+ }
1890
+ }
1891
+
1892
+ return {
1893
+ Q: wrapDenseMatrix(Q),
1894
+ R: wrapDenseMatrix(fullR)
1895
+ };
1896
+ }
1897
+
1486
1898
  function splitTerms(expression) {
1487
1899
  const normalized = expression.replace(/\s+/g, "");
1488
1900
  if (!normalized) {
@@ -1619,6 +2031,11 @@
1619
2031
 
1620
2032
  pow: (a, b) => a ** b,
1621
2033
  det: (matrix) => determinant(matrix),
2034
+ polynomialRoot: (...coefficients) => polynomialRoots(...coefficients),
2035
+ lsolve: (a, b) => linearSolve(a, b),
2036
+ lup: (matrix) => lupDecomposition(matrix),
2037
+ lyap: (a, q) => solveLyapunov(a, q),
2038
+ qr: (matrix) => qrDecomposition(matrix),
1622
2039
  simplify: (expression) => {
1623
2040
  if (typeof expression !== "string") {
1624
2041
  throw new Error("simplify() expects an expression string");
@@ -1778,7 +2195,7 @@
1778
2195
  case "Identifier":
1779
2196
  return { type: "Identifier", name: token.name };
1780
2197
 
1781
- case "Function": // 🔥 ADD THIS
2198
+ case "Function":
1782
2199
  return {
1783
2200
  type: "Identifier",
1784
2201
  name: token.name
@@ -2183,6 +2600,29 @@
2183
2600
  ) {
2184
2601
  const operator = tokens[current - 1].value;
2185
2602
 
2603
+ if (left.type === "CallExpression") {
2604
+ const isFunctionTarget =
2605
+ left.callee?.type === "Identifier" &&
2606
+ left.arguments.every((arg) => arg.type === "Identifier");
2607
+
2608
+ if (!isFunctionTarget) {
2609
+ throw new Error("Invalid function definition");
2610
+ }
2611
+
2612
+ const right = parseAssignment();
2613
+
2614
+ return {
2615
+ type: "FunctionAssignmentExpression",
2616
+ operator,
2617
+ left: {
2618
+ type: "Identifier",
2619
+ name: left.callee.name
2620
+ },
2621
+ params: left.arguments.map((arg) => arg.name),
2622
+ right
2623
+ };
2624
+ }
2625
+
2186
2626
  if (
2187
2627
  left.type !== "Identifier" &&
2188
2628
  left.type !== "MemberExpression" &&
@@ -2248,6 +2688,18 @@
2248
2688
  return `${real} ${sign} ${imagPart}`;
2249
2689
  };
2250
2690
 
2691
+ const formatScalar = (value) => {
2692
+ if (typeof value !== "number") {
2693
+ return String(value);
2694
+ }
2695
+
2696
+ if (Number.isInteger(value)) {
2697
+ return String(value);
2698
+ }
2699
+
2700
+ return Number(value.toFixed(14)).toString();
2701
+ };
2702
+
2251
2703
  const formatResult = (value) => {
2252
2704
  if (isComplex(value)) {
2253
2705
  return formatComplex(value);
@@ -2257,12 +2709,20 @@
2257
2709
  return `${value.value} ${value.unit}`;
2258
2710
  }
2259
2711
 
2712
+ if (isDenseMatrixWrapper(value)) {
2713
+ return serializeExprifyValue(value);
2714
+ }
2715
+
2260
2716
  if (isMatrix(value)) {
2261
- return value.map((row) => row.join("\t")).join("\n");
2717
+ return value.map((row) => row.map(formatScalar).join("\t")).join("\n");
2262
2718
  }
2263
2719
 
2264
2720
  if (Array.isArray(value)) {
2265
- return value.join("\n");
2721
+ return JSON.stringify(value);
2722
+ }
2723
+
2724
+ if (value && typeof value === "object") {
2725
+ return serializeExprifyValue(value);
2266
2726
  }
2267
2727
 
2268
2728
  return value;
@@ -2276,6 +2736,213 @@
2276
2736
  this.functions = createFunctionRegistry(internalFunctions);
2277
2737
  this.variables = createVarStore();
2278
2738
  this._cache = new Map();
2739
+ this.variables.set("pi", Math.PI);
2740
+ this.variables.set("e", Math.E);
2741
+ this.addFunction("parse", (expression) => {
2742
+ if (typeof expression !== "string") {
2743
+ throw new Error("parse() expects an expression string");
2744
+ }
2745
+ return expression;
2746
+ });
2747
+ this.addFunction("leafCount", (value) => {
2748
+ const countLeafTokens = (expression) => {
2749
+ const strippedKeys = expression.replace(/(^|[{,]\s*)[a-zA-Z_][a-zA-Z0-9_]*\s*:/g, "$1");
2750
+ const matches = strippedKeys.match(/\d+(\.\d+)?(e[+-]?\d+)?n?|[a-zA-Z_][a-zA-Z0-9_]*/gi);
2751
+ return matches ? matches.length : 0;
2752
+ };
2753
+
2754
+ let ast = value;
2755
+ if (typeof value === "string") {
2756
+ try {
2757
+ ast = this.parse(value).ast;
2758
+ } catch {
2759
+ return countLeafTokens(value);
2760
+ }
2761
+ }
2762
+
2763
+ const countLeaves = (node) => {
2764
+ if (!node || typeof node !== "object") return 0;
2765
+
2766
+ switch (node.type) {
2767
+ case "Literal":
2768
+ case "ImaginaryLiteral":
2769
+ case "UnitLiteral":
2770
+ case "Identifier":
2771
+ return 1;
2772
+ default:
2773
+ return Object.values(node).reduce((sum, child) => {
2774
+ if (Array.isArray(child)) {
2775
+ return sum + child.reduce((inner, item) => inner + countLeaves(item), 0);
2776
+ }
2777
+
2778
+ return sum + countLeaves(child);
2779
+ }, 0);
2780
+ }
2781
+ };
2782
+
2783
+ return countLeaves(ast);
2784
+ });
2785
+ this.addFunction("matrix", (value) => wrapDenseMatrix(value));
2786
+ this.addFunction("sparse", (value) => wrapDenseMatrix(value));
2787
+ this.addFunction("rationalize", (expression, withDetails = false) => {
2788
+ if (typeof expression !== "string") {
2789
+ throw new Error("rationalize() expects an expression string");
2790
+ }
2791
+
2792
+ const normalizedExpression = expression
2793
+ .replace(/\s+/g, "")
2794
+ .replace(/(\d)([a-zA-Z(])/g, "$1*$2")
2795
+ .replace(/([a-zA-Z)])(\d)/g, "$1*$2");
2796
+
2797
+ const polyKey = (powers) => JSON.stringify(Object.entries(powers).sort(([a], [b]) => a.localeCompare(b)));
2798
+ const keyToPowers = (key) => Object.fromEntries(JSON.parse(key));
2799
+ const constPoly = (value) => new Map([[polyKey({}), value]]);
2800
+ const varPoly = (name) => new Map([[polyKey({ [name]: 1 }), 1]]);
2801
+ const cleanPoly = (poly) => new Map([...poly.entries()].filter(([, coeff]) => coeff !== 0));
2802
+ const addPoly = (a, b, sign = 1) => {
2803
+ const result = new Map(a);
2804
+ for (const [key, coeff] of b.entries()) {
2805
+ result.set(key, (result.get(key) || 0) + (sign * coeff));
2806
+ }
2807
+ return cleanPoly(result);
2808
+ };
2809
+ const multiplyPoly = (a, b) => {
2810
+ const result = new Map();
2811
+ for (const [keyA, coeffA] of a.entries()) {
2812
+ const powersA = keyToPowers(keyA);
2813
+ for (const [keyB, coeffB] of b.entries()) {
2814
+ const powersB = keyToPowers(keyB);
2815
+ const merged = { ...powersA };
2816
+ for (const [name, power] of Object.entries(powersB)) {
2817
+ merged[name] = (merged[name] || 0) + power;
2818
+ }
2819
+ const key = polyKey(merged);
2820
+ result.set(key, (result.get(key) || 0) + (coeffA * coeffB));
2821
+ }
2822
+ }
2823
+ return cleanPoly(result);
2824
+ };
2825
+ const powPoly = (poly, exponent) => {
2826
+ let result = constPoly(1);
2827
+ for (let index = 0; index < exponent; index++) {
2828
+ result = multiplyPoly(result, poly);
2829
+ }
2830
+ return result;
2831
+ };
2832
+ const rational = (num, den = constPoly(1)) => ({ num, den });
2833
+ const addRat = (a, b, sign = 1) => rational(
2834
+ addPoly(
2835
+ multiplyPoly(a.num, b.den),
2836
+ multiplyPoly(b.num, a.den),
2837
+ sign
2838
+ ),
2839
+ multiplyPoly(a.den, b.den)
2840
+ );
2841
+ const mulRat = (a, b) => rational(multiplyPoly(a.num, b.num), multiplyPoly(a.den, b.den));
2842
+ const divRat = (a, b) => rational(multiplyPoly(a.num, b.den), multiplyPoly(a.den, b.num));
2843
+ const negRat = (value) => rational(addPoly(new Map(), value.num, -1), value.den);
2844
+ const astToRat = (node) => {
2845
+ switch (node.type) {
2846
+ case "Literal":
2847
+ return rational(constPoly(node.value));
2848
+ case "Identifier":
2849
+ return rational(varPoly(node.name));
2850
+ case "UnaryExpression":
2851
+ if (node.operator === "-") return negRat(astToRat(node.argument));
2852
+ throw new Error("Unsupported unary operator");
2853
+ case "BinaryExpression": {
2854
+ const left = astToRat(node.left);
2855
+ const right = astToRat(node.right);
2856
+ switch (node.operator) {
2857
+ case "+": return addRat(left, right);
2858
+ case "-": return addRat(left, right, -1);
2859
+ case "*": return mulRat(left, right);
2860
+ case "/": return divRat(left, right);
2861
+ case "^": {
2862
+ if (node.right.type !== "Literal" || !Number.isInteger(node.right.value) || node.right.value < 0) {
2863
+ throw new Error("Unsupported exponent");
2864
+ }
2865
+ return rational(
2866
+ powPoly(left.num, node.right.value),
2867
+ powPoly(left.den, node.right.value)
2868
+ );
2869
+ }
2870
+ default:
2871
+ throw new Error("Unsupported operator in rationalize()");
2872
+ }
2873
+ }
2874
+ default:
2875
+ throw new Error("Unsupported expression in rationalize()");
2876
+ }
2877
+ };
2878
+ const formatPoly = (poly) => {
2879
+ const entries = [...poly.entries()]
2880
+ .filter(([, coeff]) => coeff !== 0)
2881
+ .sort(([keyA], [keyB]) => {
2882
+ const powersA = keyToPowers(keyA);
2883
+ const powersB = keyToPowers(keyB);
2884
+ const firstVarA = Object.keys(powersA).sort()[0] || "";
2885
+ const firstVarB = Object.keys(powersB).sort()[0] || "";
2886
+
2887
+ if (firstVarA !== firstVarB) {
2888
+ return firstVarA.localeCompare(firstVarB);
2889
+ }
2890
+
2891
+ const degreeA = Object.values(powersA).reduce((sum, value) => sum + value, 0);
2892
+ const degreeB = Object.values(powersB).reduce((sum, value) => sum + value, 0);
2893
+ return degreeB - degreeA;
2894
+ });
2895
+
2896
+ if (!entries.length) return "0";
2897
+
2898
+ return entries.map(([key, coeff], index) => {
2899
+ const powers = keyToPowers(key);
2900
+ const absCoeff = Math.abs(coeff);
2901
+ const variablePart = Object.entries(powers)
2902
+ .map(([name, power]) => power === 1 ? name : `${name} ^ ${power}`)
2903
+ .join(" * ");
2904
+ let body = variablePart;
2905
+
2906
+ if (!body) {
2907
+ body = `${absCoeff}`;
2908
+ } else if (absCoeff !== 1) {
2909
+ body = `${absCoeff} * ${body}`;
2910
+ }
2911
+
2912
+ if (index === 0) {
2913
+ return coeff < 0 ? `- ${body}`.replace("- ", "-") : body;
2914
+ }
2915
+
2916
+ return coeff < 0 ? `- ${body}` : `+ ${body}`;
2917
+ }).join(" ");
2918
+ };
2919
+
2920
+ const ast = this.parse(normalizedExpression).ast;
2921
+ const result = astToRat(ast);
2922
+ const numerator = formatPoly(result.num);
2923
+ const denominator = formatPoly(result.den);
2924
+ const variableSet = new Set();
2925
+
2926
+ for (const poly of [result.num, result.den]) {
2927
+ for (const key of poly.keys()) {
2928
+ for (const name of Object.keys(keyToPowers(key))) {
2929
+ variableSet.add(name);
2930
+ }
2931
+ }
2932
+ }
2933
+
2934
+ if (!withDetails) {
2935
+ return `(${numerator}) / (${denominator})`;
2936
+ }
2937
+
2938
+ return {
2939
+ numerator,
2940
+ denominator,
2941
+ coefficients: [],
2942
+ variables: [...variableSet].sort(),
2943
+ expression: `(${numerator}) / (${denominator})`
2944
+ };
2945
+ });
2279
2946
  }
2280
2947
 
2281
2948
  setVariable(name, value) {