exprify 1.0.1 → 1.0.2

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.
@@ -398,6 +398,60 @@ function tokenize(expr, context = {}) {
398
398
  return final;
399
399
  }
400
400
 
401
+ const isDenseMatrixWrapper = (value) =>
402
+ value &&
403
+ typeof value === "object" &&
404
+ value.exprify === "DenseMatrix" &&
405
+ "data" in value &&
406
+ "size" in value;
407
+
408
+ const cloneMatrixData = (value) => {
409
+ if (Array.isArray(value)) {
410
+ return value.map(cloneMatrixData);
411
+ }
412
+
413
+ return value;
414
+ };
415
+
416
+ const getMatrixSize = (data) => {
417
+ if (Array.isArray(data) && data.every(Array.isArray)) {
418
+ return [data.length, data[0]?.length || 0];
419
+ }
420
+
421
+ if (Array.isArray(data)) {
422
+ return [data.length];
423
+ }
424
+
425
+ throw new Error("Matrix data must be an array");
426
+ };
427
+
428
+ const wrapDenseMatrix = (data) => ({
429
+ exprify: "DenseMatrix",
430
+ data: cloneMatrixData(data),
431
+ size: getMatrixSize(data)
432
+ });
433
+
434
+ const unwrapDenseMatrix = (value) =>
435
+ isDenseMatrixWrapper(value) ? cloneMatrixData(value.data) : value;
436
+
437
+ const serializeExprifyValue = (value) => {
438
+ if (isDenseMatrixWrapper(value)) {
439
+ return JSON.stringify(value);
440
+ }
441
+
442
+ if (Array.isArray(value) || (value && typeof value === "object")) {
443
+ return JSON.stringify(value, (_, current) => {
444
+ if (isDenseMatrixWrapper(current)) {
445
+ return current;
446
+ }
447
+
448
+ return current;
449
+ });
450
+ }
451
+
452
+ return value;
453
+ };
454
+
401
455
  function evaluateAST(node, context = {}) {
402
456
 
403
457
  const vars = context.variables;
@@ -418,6 +472,7 @@ function evaluateAST(node, context = {}) {
418
472
  Array.isArray(v) && v.length > 0 && v.every(Array.isArray);
419
473
 
420
474
  const normalizeMatrix = (value) => {
475
+ value = unwrapDenseMatrix(value);
421
476
  if (isMatrix(value)) return value.map((row) => [...row]);
422
477
  if (Array.isArray(value)) return [value];
423
478
  throw new Error("Expected matrix-compatible value");
@@ -590,6 +645,16 @@ function evaluateAST(node, context = {}) {
590
645
  const simplifyComplex = (value) =>
591
646
  value.im === 0 ? value.re : value;
592
647
 
648
+ const createFunctionScope = (params, args) => {
649
+ const scopedValues = {};
650
+
651
+ params.forEach((param, index) => {
652
+ scopedValues[param] = args[index];
653
+ });
654
+
655
+ return scopedValues;
656
+ };
657
+
593
658
  const evalComplexBinary = (operator, left, right) => {
594
659
  const a = toComplex(left);
595
660
  const b = toComplex(right);
@@ -645,6 +710,9 @@ function evaluateAST(node, context = {}) {
645
710
 
646
711
  if (node.left.type === "Identifier") {
647
712
  vars.set(node.left.name, value);
713
+ if (node.right.type === "ArrayExpression") {
714
+ return wrapDenseMatrix(unwrapDenseMatrix(value));
715
+ }
648
716
  return value;
649
717
  }
650
718
 
@@ -658,6 +726,20 @@ function evaluateAST(node, context = {}) {
658
726
  throw new Error("Invalid assignment target");
659
727
  }
660
728
 
729
+ case "FunctionAssignmentExpression": {
730
+ if (node.operator !== "=") {
731
+ throw new Error(`Operator ${node.operator} is not supported for function definitions`);
732
+ }
733
+
734
+ const fn = (...args) => {
735
+ const scopedContext = context.withScope(createFunctionScope(node.params, args));
736
+ return evaluateAST(node.right, scopedContext);
737
+ };
738
+
739
+ fns.register(node.left.name, fn);
740
+ return fn;
741
+ }
742
+
661
743
  /* ===== UNARY ===== */
662
744
  case "UnaryExpression": {
663
745
  const val = evaluateAST(node.argument, context);
@@ -678,7 +760,7 @@ function evaluateAST(node, context = {}) {
678
760
  let left = evaluateAST(node.left, context);
679
761
  let right = evaluateAST(node.right, context);
680
762
 
681
- // 🔥 UNIT handling
763
+ // UNIT handling
682
764
  if (isUnitObj(left) || isUnitObj(right)) {
683
765
 
684
766
  if (!units) throw new Error("Unit system not available");
@@ -1428,6 +1510,7 @@ function createFunctionRegistry(initial = {}) {
1428
1510
  }
1429
1511
 
1430
1512
  function validateSquareMatrix(matrix) {
1513
+ matrix = unwrapDenseMatrix(matrix);
1431
1514
  if (!Array.isArray(matrix) || matrix.length === 0) {
1432
1515
  throw new Error("det() expects a non-empty matrix");
1433
1516
  }
@@ -1451,6 +1534,7 @@ function validateSquareMatrix(matrix) {
1451
1534
  }
1452
1535
 
1453
1536
  function determinant(matrix) {
1537
+ matrix = unwrapDenseMatrix(matrix);
1454
1538
  validateSquareMatrix(matrix);
1455
1539
 
1456
1540
  if (matrix.length === 1) {
@@ -1470,6 +1554,334 @@ function determinant(matrix) {
1470
1554
  }, 0);
1471
1555
  }
1472
1556
 
1557
+ function asMatrixData(value) {
1558
+ const data = unwrapDenseMatrix(value);
1559
+ if (!Array.isArray(data)) {
1560
+ throw new Error("Expected matrix data");
1561
+ }
1562
+ return data;
1563
+ }
1564
+
1565
+ function solveLinearSystem(coefficients, constants) {
1566
+ const n = coefficients.length;
1567
+ const augmented = coefficients.map((row, rowIndex) => [...row, constants[rowIndex]]);
1568
+
1569
+ for (let pivot = 0; pivot < n; pivot++) {
1570
+ let maxRow = pivot;
1571
+ let maxValue = Math.abs(augmented[pivot][pivot]);
1572
+
1573
+ for (let row = pivot + 1; row < n; row++) {
1574
+ const current = Math.abs(augmented[row][pivot]);
1575
+ if (current > maxValue) {
1576
+ maxValue = current;
1577
+ maxRow = row;
1578
+ }
1579
+ }
1580
+
1581
+ if (maxValue === 0) {
1582
+ throw new Error("Linear system is singular");
1583
+ }
1584
+
1585
+ if (maxRow !== pivot) {
1586
+ [augmented[pivot], augmented[maxRow]] = [augmented[maxRow], augmented[pivot]];
1587
+ }
1588
+
1589
+ const pivotValue = augmented[pivot][pivot];
1590
+ for (let col = pivot; col <= n; col++) {
1591
+ augmented[pivot][col] /= pivotValue;
1592
+ }
1593
+
1594
+ for (let row = 0; row < n; row++) {
1595
+ if (row === pivot) continue;
1596
+ const factor = augmented[row][pivot];
1597
+ for (let col = pivot; col <= n; col++) {
1598
+ augmented[row][col] -= factor * augmented[pivot][col];
1599
+ }
1600
+ }
1601
+ }
1602
+
1603
+ return augmented.map((row) => row[n]);
1604
+ }
1605
+
1606
+ function lupDecomposition(input) {
1607
+ const matrix = asMatrixData(input).map((row) => [...row]);
1608
+ validateSquareMatrix(matrix);
1609
+
1610
+ const n = matrix.length;
1611
+ const permutation = Array.from({ length: n }, (_, index) => index);
1612
+
1613
+ for (let pivot = 0; pivot < n; pivot++) {
1614
+ let maxRow = pivot;
1615
+ let maxValue = Math.abs(matrix[pivot][pivot]);
1616
+
1617
+ for (let row = pivot + 1; row < n; row++) {
1618
+ const current = Math.abs(matrix[row][pivot]);
1619
+ if (current > maxValue) {
1620
+ maxValue = current;
1621
+ maxRow = row;
1622
+ }
1623
+ }
1624
+
1625
+ if (maxValue === 0) {
1626
+ throw new Error("Matrix is singular");
1627
+ }
1628
+
1629
+ if (maxRow !== pivot) {
1630
+ [matrix[pivot], matrix[maxRow]] = [matrix[maxRow], matrix[pivot]];
1631
+ [permutation[pivot], permutation[maxRow]] = [permutation[maxRow], permutation[pivot]];
1632
+ }
1633
+
1634
+ for (let row = pivot + 1; row < n; row++) {
1635
+ matrix[row][pivot] /= matrix[pivot][pivot];
1636
+ for (let col = pivot + 1; col < n; col++) {
1637
+ matrix[row][col] -= matrix[row][pivot] * matrix[pivot][col];
1638
+ }
1639
+ }
1640
+ }
1641
+
1642
+ const L = matrix.map((row, rowIndex) =>
1643
+ row.map((value, colIndex) => {
1644
+ if (rowIndex === colIndex) return 1;
1645
+ if (rowIndex > colIndex) return value;
1646
+ return 0;
1647
+ })
1648
+ );
1649
+
1650
+ const U = matrix.map((row, rowIndex) =>
1651
+ row.map((value, colIndex) => (rowIndex <= colIndex ? value : 0))
1652
+ );
1653
+
1654
+ return {
1655
+ L: wrapDenseMatrix(L),
1656
+ U: wrapDenseMatrix(U),
1657
+ p: permutation
1658
+ };
1659
+ }
1660
+
1661
+ function linearSolve(aInput, bInput) {
1662
+ const { L, U, p } = lupDecomposition(aInput);
1663
+ const a = asMatrixData(aInput);
1664
+ const bData = asMatrixData(bInput);
1665
+ const bVector = Array.isArray(bData[0]) ? bData.map((row) => row[0]) : bData;
1666
+
1667
+ if (a.length !== bVector.length) {
1668
+ throw new Error("Right-hand side dimension mismatch");
1669
+ }
1670
+
1671
+ const permutedB = p.map((index) => bVector[index]);
1672
+ const y = new Array(a.length).fill(0);
1673
+
1674
+ for (let row = 0; row < a.length; row++) {
1675
+ y[row] = permutedB[row];
1676
+ for (let col = 0; col < row; col++) {
1677
+ y[row] -= L.data[row][col] * y[col];
1678
+ }
1679
+ }
1680
+
1681
+ const x = new Array(a.length).fill(0);
1682
+ for (let row = a.length - 1; row >= 0; row--) {
1683
+ x[row] = y[row];
1684
+ for (let col = row + 1; col < a.length; col++) {
1685
+ x[row] -= U.data[row][col] * x[col];
1686
+ }
1687
+ x[row] /= U.data[row][row];
1688
+ }
1689
+
1690
+ return wrapDenseMatrix(x.map((value) => [value]));
1691
+ }
1692
+
1693
+ function solveLyapunov(aInput, qInput) {
1694
+ const A = asMatrixData(aInput).map((row) => [...row]);
1695
+ const Q = asMatrixData(qInput).map((row) => [...row]);
1696
+ validateSquareMatrix(A);
1697
+ validateSquareMatrix(Q);
1698
+
1699
+ const n = A.length;
1700
+ if (Q.length !== n) {
1701
+ throw new Error("A and Q must have the same dimensions");
1702
+ }
1703
+
1704
+ const coefficients = [];
1705
+ const constants = [];
1706
+
1707
+ for (let row = 0; row < n; row++) {
1708
+ for (let col = 0; col < n; col++) {
1709
+ const equation = new Array(n * n).fill(0);
1710
+
1711
+ for (let k = 0; k < n; k++) {
1712
+ equation[k * n + col] += A[row][k];
1713
+ equation[row * n + k] += A[col][k];
1714
+ }
1715
+
1716
+ coefficients.push(equation);
1717
+ constants.push(-Q[row][col]);
1718
+ }
1719
+ }
1720
+
1721
+ const solution = solveLinearSystem(coefficients, constants);
1722
+ const X = [];
1723
+
1724
+ for (let row = 0; row < n; row++) {
1725
+ X.push(solution.slice(row * n, (row + 1) * n));
1726
+ }
1727
+
1728
+ return wrapDenseMatrix(X);
1729
+ }
1730
+
1731
+ function evaluatePolynomial(coefficients, x) {
1732
+ return coefficients.reduce((sum, coefficient, index) => sum + (coefficient * (x ** index)), 0);
1733
+ }
1734
+
1735
+ function syntheticDivide(coefficients, root) {
1736
+ const descending = [...coefficients].reverse();
1737
+ const quotient = [descending[0]];
1738
+
1739
+ for (let index = 1; index < descending.length - 1; index++) {
1740
+ quotient.push(descending[index] + (quotient[index - 1] * root));
1741
+ }
1742
+
1743
+ const remainder = descending[descending.length - 1] + (quotient[quotient.length - 1] * root);
1744
+ return {
1745
+ quotient: quotient.reverse(),
1746
+ remainder
1747
+ };
1748
+ }
1749
+
1750
+ function solveQuadratic(coefficients) {
1751
+ const [c, b, a] = coefficients;
1752
+ const discriminant = (b ** 2) - (4 * a * c);
1753
+ if (discriminant < 0) {
1754
+ throw new Error("Only real roots are supported");
1755
+ }
1756
+
1757
+ const sqrtDisc = Math.sqrt(discriminant);
1758
+ return [
1759
+ (-b + sqrtDisc) / (2 * a),
1760
+ (-b - sqrtDisc) / (2 * a)
1761
+ ];
1762
+ }
1763
+
1764
+ function polynomialRoots(...coefficients) {
1765
+ while (coefficients.length > 1 && coefficients[coefficients.length - 1] === 0) {
1766
+ coefficients.pop();
1767
+ }
1768
+
1769
+ const degree = coefficients.length - 1;
1770
+ if (degree < 1) {
1771
+ throw new Error("polynomialRoot() expects at least a linear polynomial");
1772
+ }
1773
+
1774
+ if (degree === 1) {
1775
+ const [b, a] = coefficients;
1776
+ return [-b / a];
1777
+ }
1778
+
1779
+ if (degree === 2) {
1780
+ return solveQuadratic(coefficients);
1781
+ }
1782
+
1783
+ if (degree === 3) {
1784
+ const constant = coefficients[0];
1785
+ coefficients[3];
1786
+ const candidates = [];
1787
+ const limit = Math.abs(constant);
1788
+
1789
+ for (let divisor = 1; divisor <= Math.max(1, limit); divisor++) {
1790
+ if (limit % divisor === 0) {
1791
+ candidates.push(divisor, -divisor);
1792
+ }
1793
+ }
1794
+
1795
+ for (const candidate of candidates) {
1796
+ if (evaluatePolynomial(coefficients, candidate) === 0) {
1797
+ const reduced = syntheticDivide(coefficients, candidate);
1798
+ const remainingRoots = solveQuadratic(reduced.quotient);
1799
+ return [candidate, ...remainingRoots];
1800
+ }
1801
+ }
1802
+ }
1803
+
1804
+ throw new Error("polynomialRoot() currently supports degree up to 3");
1805
+ }
1806
+
1807
+ function dotProduct(a, b) {
1808
+ return a.reduce((sum, value, index) => sum + (value * b[index]), 0);
1809
+ }
1810
+
1811
+ function vectorNorm(vector) {
1812
+ return Math.sqrt(dotProduct(vector, vector));
1813
+ }
1814
+
1815
+ function scaleVector(vector, scalar) {
1816
+ return vector.map((value) => value * scalar);
1817
+ }
1818
+
1819
+ function subtractVectors(a, b) {
1820
+ return a.map((value, index) => value - b[index]);
1821
+ }
1822
+
1823
+ function transpose(matrix) {
1824
+ return matrix[0].map((_, colIndex) => matrix.map((row) => row[colIndex]));
1825
+ }
1826
+
1827
+ function qrDecomposition(input) {
1828
+ const A = asMatrixData(input).map((row) => [...row]);
1829
+ if (!A.length || !A.every((row) => row.length === A[0].length)) {
1830
+ throw new Error("qr() expects a rectangular matrix");
1831
+ }
1832
+
1833
+ const rowCount = A.length;
1834
+ const colCount = A[0].length;
1835
+ const columns = transpose(A);
1836
+ const qColumns = [];
1837
+
1838
+ for (let col = 0; col < colCount; col++) {
1839
+ let vector = [...columns[col]];
1840
+
1841
+ for (let existing = 0; existing < qColumns.length; existing++) {
1842
+ const projection = dotProduct(qColumns[existing], columns[col]);
1843
+ vector = subtractVectors(vector, scaleVector(qColumns[existing], projection));
1844
+ }
1845
+
1846
+ const norm = vectorNorm(vector);
1847
+ if (norm === 0) {
1848
+ throw new Error("qr() requires linearly independent columns");
1849
+ }
1850
+
1851
+ qColumns.push(scaleVector(vector, 1 / norm));
1852
+ }
1853
+
1854
+ for (let basisIndex = 0; qColumns.length < rowCount && basisIndex < rowCount; basisIndex++) {
1855
+ let candidate = Array.from({ length: rowCount }, (_, index) => (index === basisIndex ? 1 : 0));
1856
+
1857
+ for (const column of qColumns) {
1858
+ const projection = dotProduct(column, candidate);
1859
+ candidate = subtractVectors(candidate, scaleVector(column, projection));
1860
+ }
1861
+
1862
+ const norm = vectorNorm(candidate);
1863
+ if (norm > 1e-10) {
1864
+ qColumns.push(scaleVector(candidate, 1 / norm));
1865
+ }
1866
+ }
1867
+
1868
+ const Q = Array.from({ length: rowCount }, (_, rowIndex) =>
1869
+ qColumns.map((column) => column[rowIndex])
1870
+ );
1871
+
1872
+ const fullR = Array.from({ length: rowCount }, () => Array(colCount).fill(0));
1873
+ for (let row = 0; row < rowCount; row++) {
1874
+ for (let col = 0; col < colCount; col++) {
1875
+ fullR[row][col] = dotProduct(qColumns[row], columns[col]);
1876
+ }
1877
+ }
1878
+
1879
+ return {
1880
+ Q: wrapDenseMatrix(Q),
1881
+ R: wrapDenseMatrix(fullR)
1882
+ };
1883
+ }
1884
+
1473
1885
  function splitTerms(expression) {
1474
1886
  const normalized = expression.replace(/\s+/g, "");
1475
1887
  if (!normalized) {
@@ -1606,6 +2018,11 @@ const internalFunctions = {
1606
2018
 
1607
2019
  pow: (a, b) => a ** b,
1608
2020
  det: (matrix) => determinant(matrix),
2021
+ polynomialRoot: (...coefficients) => polynomialRoots(...coefficients),
2022
+ lsolve: (a, b) => linearSolve(a, b),
2023
+ lup: (matrix) => lupDecomposition(matrix),
2024
+ lyap: (a, q) => solveLyapunov(a, q),
2025
+ qr: (matrix) => qrDecomposition(matrix),
1609
2026
  simplify: (expression) => {
1610
2027
  if (typeof expression !== "string") {
1611
2028
  throw new Error("simplify() expects an expression string");
@@ -1765,7 +2182,7 @@ function buildAST(tokens) {
1765
2182
  case "Identifier":
1766
2183
  return { type: "Identifier", name: token.name };
1767
2184
 
1768
- case "Function": // 🔥 ADD THIS
2185
+ case "Function":
1769
2186
  return {
1770
2187
  type: "Identifier",
1771
2188
  name: token.name
@@ -2170,6 +2587,29 @@ function buildAST(tokens) {
2170
2587
  ) {
2171
2588
  const operator = tokens[current - 1].value;
2172
2589
 
2590
+ if (left.type === "CallExpression") {
2591
+ const isFunctionTarget =
2592
+ left.callee?.type === "Identifier" &&
2593
+ left.arguments.every((arg) => arg.type === "Identifier");
2594
+
2595
+ if (!isFunctionTarget) {
2596
+ throw new Error("Invalid function definition");
2597
+ }
2598
+
2599
+ const right = parseAssignment();
2600
+
2601
+ return {
2602
+ type: "FunctionAssignmentExpression",
2603
+ operator,
2604
+ left: {
2605
+ type: "Identifier",
2606
+ name: left.callee.name
2607
+ },
2608
+ params: left.arguments.map((arg) => arg.name),
2609
+ right
2610
+ };
2611
+ }
2612
+
2173
2613
  if (
2174
2614
  left.type !== "Identifier" &&
2175
2615
  left.type !== "MemberExpression" &&
@@ -2235,6 +2675,18 @@ const formatComplex = (value) => {
2235
2675
  return `${real} ${sign} ${imagPart}`;
2236
2676
  };
2237
2677
 
2678
+ const formatScalar = (value) => {
2679
+ if (typeof value !== "number") {
2680
+ return String(value);
2681
+ }
2682
+
2683
+ if (Number.isInteger(value)) {
2684
+ return String(value);
2685
+ }
2686
+
2687
+ return Number(value.toFixed(14)).toString();
2688
+ };
2689
+
2238
2690
  const formatResult = (value) => {
2239
2691
  if (isComplex(value)) {
2240
2692
  return formatComplex(value);
@@ -2244,12 +2696,20 @@ const formatResult = (value) => {
2244
2696
  return `${value.value} ${value.unit}`;
2245
2697
  }
2246
2698
 
2699
+ if (isDenseMatrixWrapper(value)) {
2700
+ return serializeExprifyValue(value);
2701
+ }
2702
+
2247
2703
  if (isMatrix(value)) {
2248
- return value.map((row) => row.join("\t")).join("\n");
2704
+ return value.map((row) => row.map(formatScalar).join("\t")).join("\n");
2249
2705
  }
2250
2706
 
2251
2707
  if (Array.isArray(value)) {
2252
- return value.join("\n");
2708
+ return JSON.stringify(value);
2709
+ }
2710
+
2711
+ if (value && typeof value === "object") {
2712
+ return serializeExprifyValue(value);
2253
2713
  }
2254
2714
 
2255
2715
  return value;
@@ -2263,6 +2723,213 @@ class exprify {
2263
2723
  this.functions = createFunctionRegistry(internalFunctions);
2264
2724
  this.variables = createVarStore();
2265
2725
  this._cache = new Map();
2726
+ this.variables.set("pi", Math.PI);
2727
+ this.variables.set("e", Math.E);
2728
+ this.addFunction("parse", (expression) => {
2729
+ if (typeof expression !== "string") {
2730
+ throw new Error("parse() expects an expression string");
2731
+ }
2732
+ return expression;
2733
+ });
2734
+ this.addFunction("leafCount", (value) => {
2735
+ const countLeafTokens = (expression) => {
2736
+ const strippedKeys = expression.replace(/(^|[{,]\s*)[a-zA-Z_][a-zA-Z0-9_]*\s*:/g, "$1");
2737
+ const matches = strippedKeys.match(/\d+(\.\d+)?(e[+-]?\d+)?n?|[a-zA-Z_][a-zA-Z0-9_]*/gi);
2738
+ return matches ? matches.length : 0;
2739
+ };
2740
+
2741
+ let ast = value;
2742
+ if (typeof value === "string") {
2743
+ try {
2744
+ ast = this.parse(value).ast;
2745
+ } catch {
2746
+ return countLeafTokens(value);
2747
+ }
2748
+ }
2749
+
2750
+ const countLeaves = (node) => {
2751
+ if (!node || typeof node !== "object") return 0;
2752
+
2753
+ switch (node.type) {
2754
+ case "Literal":
2755
+ case "ImaginaryLiteral":
2756
+ case "UnitLiteral":
2757
+ case "Identifier":
2758
+ return 1;
2759
+ default:
2760
+ return Object.values(node).reduce((sum, child) => {
2761
+ if (Array.isArray(child)) {
2762
+ return sum + child.reduce((inner, item) => inner + countLeaves(item), 0);
2763
+ }
2764
+
2765
+ return sum + countLeaves(child);
2766
+ }, 0);
2767
+ }
2768
+ };
2769
+
2770
+ return countLeaves(ast);
2771
+ });
2772
+ this.addFunction("matrix", (value) => wrapDenseMatrix(value));
2773
+ this.addFunction("sparse", (value) => wrapDenseMatrix(value));
2774
+ this.addFunction("rationalize", (expression, withDetails = false) => {
2775
+ if (typeof expression !== "string") {
2776
+ throw new Error("rationalize() expects an expression string");
2777
+ }
2778
+
2779
+ const normalizedExpression = expression
2780
+ .replace(/\s+/g, "")
2781
+ .replace(/(\d)([a-zA-Z(])/g, "$1*$2")
2782
+ .replace(/([a-zA-Z)])(\d)/g, "$1*$2");
2783
+
2784
+ const polyKey = (powers) => JSON.stringify(Object.entries(powers).sort(([a], [b]) => a.localeCompare(b)));
2785
+ const keyToPowers = (key) => Object.fromEntries(JSON.parse(key));
2786
+ const constPoly = (value) => new Map([[polyKey({}), value]]);
2787
+ const varPoly = (name) => new Map([[polyKey({ [name]: 1 }), 1]]);
2788
+ const cleanPoly = (poly) => new Map([...poly.entries()].filter(([, coeff]) => coeff !== 0));
2789
+ const addPoly = (a, b, sign = 1) => {
2790
+ const result = new Map(a);
2791
+ for (const [key, coeff] of b.entries()) {
2792
+ result.set(key, (result.get(key) || 0) + (sign * coeff));
2793
+ }
2794
+ return cleanPoly(result);
2795
+ };
2796
+ const multiplyPoly = (a, b) => {
2797
+ const result = new Map();
2798
+ for (const [keyA, coeffA] of a.entries()) {
2799
+ const powersA = keyToPowers(keyA);
2800
+ for (const [keyB, coeffB] of b.entries()) {
2801
+ const powersB = keyToPowers(keyB);
2802
+ const merged = { ...powersA };
2803
+ for (const [name, power] of Object.entries(powersB)) {
2804
+ merged[name] = (merged[name] || 0) + power;
2805
+ }
2806
+ const key = polyKey(merged);
2807
+ result.set(key, (result.get(key) || 0) + (coeffA * coeffB));
2808
+ }
2809
+ }
2810
+ return cleanPoly(result);
2811
+ };
2812
+ const powPoly = (poly, exponent) => {
2813
+ let result = constPoly(1);
2814
+ for (let index = 0; index < exponent; index++) {
2815
+ result = multiplyPoly(result, poly);
2816
+ }
2817
+ return result;
2818
+ };
2819
+ const rational = (num, den = constPoly(1)) => ({ num, den });
2820
+ const addRat = (a, b, sign = 1) => rational(
2821
+ addPoly(
2822
+ multiplyPoly(a.num, b.den),
2823
+ multiplyPoly(b.num, a.den),
2824
+ sign
2825
+ ),
2826
+ multiplyPoly(a.den, b.den)
2827
+ );
2828
+ const mulRat = (a, b) => rational(multiplyPoly(a.num, b.num), multiplyPoly(a.den, b.den));
2829
+ const divRat = (a, b) => rational(multiplyPoly(a.num, b.den), multiplyPoly(a.den, b.num));
2830
+ const negRat = (value) => rational(addPoly(new Map(), value.num, -1), value.den);
2831
+ const astToRat = (node) => {
2832
+ switch (node.type) {
2833
+ case "Literal":
2834
+ return rational(constPoly(node.value));
2835
+ case "Identifier":
2836
+ return rational(varPoly(node.name));
2837
+ case "UnaryExpression":
2838
+ if (node.operator === "-") return negRat(astToRat(node.argument));
2839
+ throw new Error("Unsupported unary operator");
2840
+ case "BinaryExpression": {
2841
+ const left = astToRat(node.left);
2842
+ const right = astToRat(node.right);
2843
+ switch (node.operator) {
2844
+ case "+": return addRat(left, right);
2845
+ case "-": return addRat(left, right, -1);
2846
+ case "*": return mulRat(left, right);
2847
+ case "/": return divRat(left, right);
2848
+ case "^": {
2849
+ if (node.right.type !== "Literal" || !Number.isInteger(node.right.value) || node.right.value < 0) {
2850
+ throw new Error("Unsupported exponent");
2851
+ }
2852
+ return rational(
2853
+ powPoly(left.num, node.right.value),
2854
+ powPoly(left.den, node.right.value)
2855
+ );
2856
+ }
2857
+ default:
2858
+ throw new Error("Unsupported operator in rationalize()");
2859
+ }
2860
+ }
2861
+ default:
2862
+ throw new Error("Unsupported expression in rationalize()");
2863
+ }
2864
+ };
2865
+ const formatPoly = (poly) => {
2866
+ const entries = [...poly.entries()]
2867
+ .filter(([, coeff]) => coeff !== 0)
2868
+ .sort(([keyA], [keyB]) => {
2869
+ const powersA = keyToPowers(keyA);
2870
+ const powersB = keyToPowers(keyB);
2871
+ const firstVarA = Object.keys(powersA).sort()[0] || "";
2872
+ const firstVarB = Object.keys(powersB).sort()[0] || "";
2873
+
2874
+ if (firstVarA !== firstVarB) {
2875
+ return firstVarA.localeCompare(firstVarB);
2876
+ }
2877
+
2878
+ const degreeA = Object.values(powersA).reduce((sum, value) => sum + value, 0);
2879
+ const degreeB = Object.values(powersB).reduce((sum, value) => sum + value, 0);
2880
+ return degreeB - degreeA;
2881
+ });
2882
+
2883
+ if (!entries.length) return "0";
2884
+
2885
+ return entries.map(([key, coeff], index) => {
2886
+ const powers = keyToPowers(key);
2887
+ const absCoeff = Math.abs(coeff);
2888
+ const variablePart = Object.entries(powers)
2889
+ .map(([name, power]) => power === 1 ? name : `${name} ^ ${power}`)
2890
+ .join(" * ");
2891
+ let body = variablePart;
2892
+
2893
+ if (!body) {
2894
+ body = `${absCoeff}`;
2895
+ } else if (absCoeff !== 1) {
2896
+ body = `${absCoeff} * ${body}`;
2897
+ }
2898
+
2899
+ if (index === 0) {
2900
+ return coeff < 0 ? `- ${body}`.replace("- ", "-") : body;
2901
+ }
2902
+
2903
+ return coeff < 0 ? `- ${body}` : `+ ${body}`;
2904
+ }).join(" ");
2905
+ };
2906
+
2907
+ const ast = this.parse(normalizedExpression).ast;
2908
+ const result = astToRat(ast);
2909
+ const numerator = formatPoly(result.num);
2910
+ const denominator = formatPoly(result.den);
2911
+ const variableSet = new Set();
2912
+
2913
+ for (const poly of [result.num, result.den]) {
2914
+ for (const key of poly.keys()) {
2915
+ for (const name of Object.keys(keyToPowers(key))) {
2916
+ variableSet.add(name);
2917
+ }
2918
+ }
2919
+ }
2920
+
2921
+ if (!withDetails) {
2922
+ return `(${numerator}) / (${denominator})`;
2923
+ }
2924
+
2925
+ return {
2926
+ numerator,
2927
+ denominator,
2928
+ coefficients: [],
2929
+ variables: [...variableSet].sort(),
2930
+ expression: `(${numerator}) / (${denominator})`
2931
+ };
2932
+ });
2266
2933
  }
2267
2934
 
2268
2935
  setVariable(name, value) {