@johnhenry/math 0.0.2 → 0.1.0
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/DualNumber.d.ts +7 -0
- package/dist/DualNumber.d.ts.map +1 -1
- package/dist/DualNumber.js +12 -3
- package/dist/DualNumber.js.map +1 -1
- package/dist/Graph3DUtils.d.ts +2 -0
- package/dist/Graph3DUtils.d.ts.map +1 -1
- package/dist/Graph3DUtils.js +14 -10
- package/dist/Graph3DUtils.js.map +1 -1
- package/dist/GraphUtils.d.ts +4 -1
- package/dist/GraphUtils.d.ts.map +1 -1
- package/dist/GraphUtils.js +15 -7
- package/dist/GraphUtils.js.map +1 -1
- package/dist/Structure.d.ts +20 -3
- package/dist/Structure.d.ts.map +1 -1
- package/dist/Structure.js +80 -5
- package/dist/Structure.js.map +1 -1
- package/dist/Symbolic.d.ts +40 -4
- package/dist/Symbolic.d.ts.map +1 -1
- package/dist/Symbolic.js +624 -104
- package/dist/Symbolic.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/DualNumber.ts +13 -3
- package/src/Graph3DUtils.ts +20 -10
- package/src/GraphUtils.ts +15 -7
- package/src/Structure.ts +77 -5
- package/src/Symbolic.ts +652 -103
- package/src/index.ts +1 -0
package/dist/Symbolic.js
CHANGED
|
@@ -323,13 +323,16 @@ export class InfiniteSolutionsError extends Error {
|
|
|
323
323
|
}
|
|
324
324
|
}
|
|
325
325
|
export class Symbolic {
|
|
326
|
-
/**
|
|
327
|
-
|
|
328
|
-
|
|
326
|
+
/**
|
|
327
|
+
* Parse an expression string such as `"sin(x^2) + 3*x"` into an AST.
|
|
328
|
+
* Pass `{ implicitMultiplication: true }` to also read `2x`, `x y` and `2(x+1)`; see {@link ParseOptions}.
|
|
329
|
+
*/
|
|
330
|
+
static parse(input, options = {}) {
|
|
331
|
+
return new Parser(input, options ?? {}).parse();
|
|
329
332
|
}
|
|
330
333
|
static differentiate(expr, variable = "x") {
|
|
331
334
|
const e = typeof expr === "string" ? Symbolic.parse(expr) : expr;
|
|
332
|
-
return
|
|
335
|
+
return simplifyCore(diff(e, variable));
|
|
333
336
|
}
|
|
334
337
|
/**
|
|
335
338
|
* Like {@link differentiate}, but also returns a bottom-up trace of every
|
|
@@ -342,17 +345,20 @@ export class Symbolic {
|
|
|
342
345
|
const e = typeof expr === "string" ? Symbolic.parse(expr) : expr;
|
|
343
346
|
const steps = [];
|
|
344
347
|
const raw = diffTraced(e, variable, steps);
|
|
345
|
-
return { steps, result:
|
|
348
|
+
return { steps, result: simplifyCore(raw) };
|
|
346
349
|
}
|
|
350
|
+
/**
|
|
351
|
+
* Algebraic simplification: constant folding, like-term collection, and, as a last
|
|
352
|
+
* step, expanding the expression as a rational function and cancelling common
|
|
353
|
+
* factors -- `(x+1)^2 - x^2 - 2*x` gives `1` and `(x^2-1)/(x-1)` gives `x+1`.
|
|
354
|
+
* The expanded/cancelled form is used only when it is strictly smaller than the
|
|
355
|
+
* collected form, so a product such as `(x+1)*2` keeps its shape. Non-polynomial
|
|
356
|
+
* subexpressions (`sin(x)`, `x^y`) are treated as opaque symbols. Like any CAS
|
|
357
|
+
* cancellation, `(x^2-1)/(x-1)` is `x+1` as an expression, ignoring that the
|
|
358
|
+
* original is undefined at `x = 1`.
|
|
359
|
+
*/
|
|
347
360
|
static simplify(expr) {
|
|
348
|
-
|
|
349
|
-
for (let i = 0; i < 30; i++) {
|
|
350
|
-
const next = collectLikeTerms(simplifyOnce(e));
|
|
351
|
-
if (equal(next, e))
|
|
352
|
-
return next;
|
|
353
|
-
e = next;
|
|
354
|
-
}
|
|
355
|
-
return e;
|
|
361
|
+
return normalizeRational(simplifyCore(expr));
|
|
356
362
|
}
|
|
357
363
|
/**
|
|
358
364
|
* Simplifies `expr` with additional domain knowledge about specific
|
|
@@ -371,14 +377,14 @@ export class Symbolic {
|
|
|
371
377
|
* assumptions (e.g. "x+y is positive" isn't expressible).
|
|
372
378
|
*/
|
|
373
379
|
static simplifyAssuming(expr, assumptions) {
|
|
374
|
-
const simplified =
|
|
380
|
+
const simplified = simplifyCore(expr);
|
|
375
381
|
const rewritten = applyAssumptions(simplified, assumptions);
|
|
376
|
-
return
|
|
382
|
+
return simplifyCore(rewritten);
|
|
377
383
|
}
|
|
378
384
|
/** Symbolic anti-derivative (elementary rules; throws {@link NotIntegrableError} otherwise). */
|
|
379
385
|
static integrate(expr, variable = "x") {
|
|
380
386
|
const e = typeof expr === "string" ? Symbolic.parse(expr) : expr;
|
|
381
|
-
return
|
|
387
|
+
return simplifyCore(integ(e, variable));
|
|
382
388
|
}
|
|
383
389
|
/**
|
|
384
390
|
* Definite integral of `expr` over `[lower, upper]` with respect to
|
|
@@ -410,13 +416,13 @@ export class Symbolic {
|
|
|
410
416
|
* @throws {IntegrationSingularityError} if an interior singularity is detected.
|
|
411
417
|
*/
|
|
412
418
|
static integrateDefinite(expr, lower, upper, variable = "x", env = {}) {
|
|
413
|
-
const e =
|
|
419
|
+
const e = simplifyCore(typeof expr === "string" ? Symbolic.parse(expr) : expr);
|
|
414
420
|
const probe = compileExpr(e);
|
|
415
421
|
if (hasInteriorSingularity((val) => probe({ ...env, [variable]: val }), lower, upper)) {
|
|
416
422
|
throw new IntegrationSingularityError();
|
|
417
423
|
}
|
|
418
424
|
try {
|
|
419
|
-
const F =
|
|
425
|
+
const F = simplifyCore(integ(e, variable));
|
|
420
426
|
const at = (val) => evalExpr(F, { ...env, [variable]: val });
|
|
421
427
|
return at(upper) - at(lower);
|
|
422
428
|
}
|
|
@@ -471,7 +477,7 @@ export class Symbolic {
|
|
|
471
477
|
* @throws {NoClosedFormError} if a method matches but its required integral(s) aren't elementary.
|
|
472
478
|
*/
|
|
473
479
|
static solveOdeClosedForm(dyDxExpr, x0, y0, xVar = "x", yVar = "y") {
|
|
474
|
-
const e =
|
|
480
|
+
const e = simplifyCore(typeof dyDxExpr === "string" ? Symbolic.parse(dyDxExpr) : dyDxExpr);
|
|
475
481
|
const sep = matchSeparable(e, xVar, yVar);
|
|
476
482
|
if (sep)
|
|
477
483
|
return solveSeparableOde(sep, xVar, yVar, x0, y0);
|
|
@@ -580,20 +586,20 @@ export class Symbolic {
|
|
|
580
586
|
}
|
|
581
587
|
term = diff(term, variable);
|
|
582
588
|
}
|
|
583
|
-
return
|
|
589
|
+
return simplifyCore(result);
|
|
584
590
|
}
|
|
585
591
|
/** Replace every occurrence of a variable with a sub-expression (e.g. for changing variables or plugging in a solved value). */
|
|
586
592
|
static substitute(expr, variable, replacement) {
|
|
587
593
|
const e = typeof expr === "string" ? Symbolic.parse(expr) : expr;
|
|
588
594
|
const r = typeof replacement === "string" ? Symbolic.parse(replacement) : replacement;
|
|
589
|
-
return
|
|
595
|
+
return simplifyCore(subst(e, variable, r));
|
|
590
596
|
}
|
|
591
597
|
/** Distribute multiplication over addition/subtraction and expand small integer powers of sums, e.g. `(x+1)^2 -> x^2+2*x+1`. */
|
|
592
598
|
static expand(expr) {
|
|
593
599
|
const e = typeof expr === "string" ? Symbolic.parse(expr) : expr;
|
|
594
600
|
let cur = e;
|
|
595
601
|
for (let i = 0; i < 30; i++) {
|
|
596
|
-
const next =
|
|
602
|
+
const next = simplifyCore(expandOnce(cur));
|
|
597
603
|
if (equal(next, cur))
|
|
598
604
|
return next;
|
|
599
605
|
cur = next;
|
|
@@ -694,12 +700,16 @@ export class Symbolic {
|
|
|
694
700
|
* commands (`\sin`, `\arcsin`, `\sinh`, `\operatorname{sech}`, ...), and the
|
|
695
701
|
* two-argument `BinaryFuncName` commands (`\min`, `\max`, `\gcd`,
|
|
696
702
|
* `\operatorname{atan2}`, `\operatorname{hypot}`, `\operatorname{lcm}`).
|
|
697
|
-
* Spacing commands (`\,`, `\;`, `\!`, `\quad`) are ignored.
|
|
703
|
+
* Spacing commands (`\,`, `\;`, `\!`, `\quad`) are ignored. Single-token arguments as TeX
|
|
704
|
+
* reads them (`\frac12`, `\frac1x`, `\sqrt2`) and a power on a function command
|
|
705
|
+
* (`\sin^{2}(x)` is `sin(x)^2`) are accepted. An exponent of `-1` on a function command (`\sin^{-1}`) is
|
|
706
|
+
* refused as ambiguous (inverse function or reciprocal): write `\arcsin(x)` or `(\sin x)^{-1}`. For what people type into a math field
|
|
707
|
+
* (`2x`, `\sin 2x`, `(x+1)(x-1)`, `x^23`) pass `{ implicitMultiplication: true }`; see {@link ParseOptions}.
|
|
698
708
|
*
|
|
699
709
|
* @throws on LaTeX constructs with no `Expr` equivalent, e.g. `\int` or `\sum`.
|
|
700
710
|
*/
|
|
701
|
-
static fromLatex(latex) {
|
|
702
|
-
return Symbolic.parse(latexToInfix(latex));
|
|
711
|
+
static fromLatex(latex, options = {}) {
|
|
712
|
+
return Symbolic.parse(latexToInfix(latex, options), options);
|
|
703
713
|
}
|
|
704
714
|
/**
|
|
705
715
|
* Solve `expr = 0` for `variable`, returning every *real* root @johnhenry/math
|
|
@@ -719,11 +729,11 @@ export class Symbolic {
|
|
|
719
729
|
*/
|
|
720
730
|
static solve(expr, variable = "x") {
|
|
721
731
|
const e = typeof expr === "string" ? Symbolic.parse(expr) : expr;
|
|
722
|
-
const coeffs = polynomialCoeffs(
|
|
732
|
+
const coeffs = polynomialCoeffs(simplifyCore(e), variable, 6);
|
|
723
733
|
if (!coeffs) {
|
|
724
734
|
throw new Error(`solve: expression is not a polynomial in "${variable}" of degree ≤ 6.`);
|
|
725
735
|
}
|
|
726
|
-
return solvePolynomial(coeffs).map((r) =>
|
|
736
|
+
return solvePolynomial(coeffs).map((r) => simplifyCore(r));
|
|
727
737
|
}
|
|
728
738
|
/**
|
|
729
739
|
* Solve a system of linear equations, each written in the same "expr
|
|
@@ -740,7 +750,7 @@ export class Symbolic {
|
|
|
740
750
|
* @throws {SingularSystemError} if the coefficient matrix is singular/rank-deficient.
|
|
741
751
|
*/
|
|
742
752
|
static solveSystem(equations, variables) {
|
|
743
|
-
const exprs = equations.map((eq) =>
|
|
753
|
+
const exprs = equations.map((eq) => simplifyCore(typeof eq === "string" ? Symbolic.parse(eq) : eq));
|
|
744
754
|
if (exprs.length !== variables.length) {
|
|
745
755
|
throw new Error(`solveSystem: expected ${variables.length} equations for ${variables.length} variables, got ${exprs.length}.`);
|
|
746
756
|
}
|
|
@@ -948,7 +958,7 @@ export class Symbolic {
|
|
|
948
958
|
*/
|
|
949
959
|
static factor(expr, variable = "x") {
|
|
950
960
|
const e = typeof expr === "string" ? Symbolic.parse(expr) : expr;
|
|
951
|
-
const simplified =
|
|
961
|
+
const simplified = simplifyCore(e);
|
|
952
962
|
const coeffs = polynomialCoeffs(simplified, variable, 6);
|
|
953
963
|
if (!coeffs)
|
|
954
964
|
return simplified;
|
|
@@ -1132,6 +1142,17 @@ function diff(e, x) {
|
|
|
1132
1142
|
}
|
|
1133
1143
|
}
|
|
1134
1144
|
// -- simplification ---------------------------------------------------------
|
|
1145
|
+
/** Constant folding and like-term collection to a fixed point -- no expansion or cancellation. */
|
|
1146
|
+
function simplifyCore(expr) {
|
|
1147
|
+
let e = typeof expr === "string" ? Symbolic.parse(expr) : expr;
|
|
1148
|
+
for (let i = 0; i < 30; i++) {
|
|
1149
|
+
const next = collectLikeTerms(simplifyOnce(e));
|
|
1150
|
+
if (equal(next, e))
|
|
1151
|
+
return next;
|
|
1152
|
+
e = next;
|
|
1153
|
+
}
|
|
1154
|
+
return e;
|
|
1155
|
+
}
|
|
1135
1156
|
function simplifyOnce(e) {
|
|
1136
1157
|
if (e.type === "const" || e.type === "var")
|
|
1137
1158
|
return e;
|
|
@@ -1876,7 +1897,7 @@ function freshVariableName(e, x) {
|
|
|
1876
1897
|
* legitimately hit a domain error/NaN on either side).
|
|
1877
1898
|
*/
|
|
1878
1899
|
function verifyByDifferentiation(candidate, e, x) {
|
|
1879
|
-
const check =
|
|
1900
|
+
const check = simplifyCore(diff(candidate, x));
|
|
1880
1901
|
let agreements = 0;
|
|
1881
1902
|
for (const p of [0.7, 1.3, -0.9]) {
|
|
1882
1903
|
const expected = evalExpr(e, { [x]: p });
|
|
@@ -1902,10 +1923,10 @@ function verifyByDifferentiation(candidate, e, x) {
|
|
|
1902
1923
|
*/
|
|
1903
1924
|
function tryUSubstitution(e, x) {
|
|
1904
1925
|
for (const g of collectSubstitutionCandidates(e, x)) {
|
|
1905
|
-
const gPrime =
|
|
1926
|
+
const gPrime = simplifyCore(diff(g, x));
|
|
1906
1927
|
if (isConst(gPrime, 0))
|
|
1907
1928
|
continue;
|
|
1908
|
-
const quotient =
|
|
1929
|
+
const quotient = simplifyCore(divideOutFactors(e, gPrime) ?? div(e, gPrime));
|
|
1909
1930
|
if (!containsExpr(quotient, g))
|
|
1910
1931
|
continue;
|
|
1911
1932
|
const uName = freshVariableName(quotient, x);
|
|
@@ -1914,7 +1935,7 @@ function tryUSubstitution(e, x) {
|
|
|
1914
1935
|
continue;
|
|
1915
1936
|
try {
|
|
1916
1937
|
const antiderivativeInU = integRules(substituted, uName);
|
|
1917
|
-
const candidate =
|
|
1938
|
+
const candidate = simplifyCore(substExpr(antiderivativeInU, v(uName), g));
|
|
1918
1939
|
if (verifyByDifferentiation(candidate, e, x))
|
|
1919
1940
|
return candidate;
|
|
1920
1941
|
}
|
|
@@ -2044,13 +2065,13 @@ function matchSeparable(e, xVar, yVar) {
|
|
|
2044
2065
|
function matchLinearOde(e, xVar, yVar) {
|
|
2045
2066
|
if (!containsVar(e, yVar))
|
|
2046
2067
|
return null; // handled by matchSeparable's h(y)=1 case instead
|
|
2047
|
-
const negP =
|
|
2068
|
+
const negP = simplifyCore(diff(e, yVar));
|
|
2048
2069
|
if (containsVar(negP, yVar))
|
|
2049
2070
|
return null; // not affine in y
|
|
2050
|
-
const q =
|
|
2071
|
+
const q = simplifyCore(sub(e, mul(negP, v(yVar))));
|
|
2051
2072
|
if (containsVar(q, yVar))
|
|
2052
2073
|
return null; // sanity check
|
|
2053
|
-
const p =
|
|
2074
|
+
const p = simplifyCore(neg(negP));
|
|
2054
2075
|
// Conservative, matching matchSeparable's philosophy: p/q must depend on
|
|
2055
2076
|
// no free variable other than xVar (an extra symbolic parameter, or a
|
|
2056
2077
|
// stray reference to yVar that survived simplification, both bail out).
|
|
@@ -2105,12 +2126,12 @@ function tryInvertForY(H, yVar, rhs, y0) {
|
|
|
2105
2126
|
return null;
|
|
2106
2127
|
}
|
|
2107
2128
|
function solveSeparableOde(sep, xVar, yVar, x0, y0) {
|
|
2108
|
-
const invH =
|
|
2129
|
+
const invH = simplifyCore(div(num(1), sep.h));
|
|
2109
2130
|
let H;
|
|
2110
2131
|
let G;
|
|
2111
2132
|
try {
|
|
2112
|
-
H =
|
|
2113
|
-
G =
|
|
2133
|
+
H = simplifyCore(integ(invH, yVar));
|
|
2134
|
+
G = simplifyCore(integ(sep.g, xVar));
|
|
2114
2135
|
}
|
|
2115
2136
|
catch (err) {
|
|
2116
2137
|
if (err instanceof NotIntegrableError)
|
|
@@ -2118,20 +2139,20 @@ function solveSeparableOde(sep, xVar, yVar, x0, y0) {
|
|
|
2118
2139
|
throw err;
|
|
2119
2140
|
}
|
|
2120
2141
|
const C = evalExpr(H, { [yVar]: y0 }) - evalExpr(G, { [xVar]: x0 });
|
|
2121
|
-
const rhs =
|
|
2142
|
+
const rhs = simplifyCore(add(G, num(C)));
|
|
2122
2143
|
const explicitY = tryInvertForY(H, yVar, rhs, y0);
|
|
2123
2144
|
if (explicitY)
|
|
2124
|
-
return { explicit: true, y:
|
|
2125
|
-
return { explicit: false, implicitRelation:
|
|
2145
|
+
return { explicit: true, y: simplifyCore(explicitY) };
|
|
2146
|
+
return { explicit: false, implicitRelation: simplifyCore(sub(H, rhs)) };
|
|
2126
2147
|
}
|
|
2127
2148
|
function solveLinearOde(lin, xVar, _yVar, x0, y0) {
|
|
2128
2149
|
let integralP;
|
|
2129
2150
|
let mu;
|
|
2130
2151
|
let muQIntegral;
|
|
2131
2152
|
try {
|
|
2132
|
-
integralP =
|
|
2133
|
-
mu =
|
|
2134
|
-
muQIntegral =
|
|
2153
|
+
integralP = simplifyCore(integ(lin.p, xVar));
|
|
2154
|
+
mu = simplifyCore(fn("exp", integralP));
|
|
2155
|
+
muQIntegral = simplifyCore(integ(simplifyCore(mul(mu, lin.q)), xVar));
|
|
2135
2156
|
}
|
|
2136
2157
|
catch (err) {
|
|
2137
2158
|
if (err instanceof NotIntegrableError)
|
|
@@ -2142,7 +2163,7 @@ function solveLinearOde(lin, xVar, _yVar, x0, y0) {
|
|
|
2142
2163
|
const muAtX0 = evalExpr(mu, { [xVar]: x0 });
|
|
2143
2164
|
const muQIntegralAtX0 = evalExpr(muQIntegral, { [xVar]: x0 });
|
|
2144
2165
|
const C = y0 * muAtX0 - muQIntegralAtX0;
|
|
2145
|
-
const y =
|
|
2166
|
+
const y = simplifyCore(div(add(muQIntegral, num(C)), mu));
|
|
2146
2167
|
return { explicit: true, y };
|
|
2147
2168
|
}
|
|
2148
2169
|
/** A variable name distinct from every name in `avoid` and not free in `e`, for a temporary substitution variable (like {@link freshVariableName}, but avoiding a whole list of names rather than just one). */
|
|
@@ -2198,7 +2219,7 @@ function matchHomogeneous(e, xVar, yVar) {
|
|
|
2198
2219
|
}
|
|
2199
2220
|
if (attempts === 0 || agreements < 4)
|
|
2200
2221
|
return null; // not enough usable probes to trust the match
|
|
2201
|
-
const F =
|
|
2222
|
+
const F = simplifyCore(subst(substituted, xVar, num(1)));
|
|
2202
2223
|
return { vName, F };
|
|
2203
2224
|
}
|
|
2204
2225
|
/**
|
|
@@ -2223,15 +2244,15 @@ function matchHomogeneous(e, xVar, yVar) {
|
|
|
2223
2244
|
function solveHomogeneousOde(hom, xVar, yVar, x0, y0) {
|
|
2224
2245
|
const { vName, F } = hom;
|
|
2225
2246
|
const v0 = y0 / x0;
|
|
2226
|
-
const FMinusV =
|
|
2247
|
+
const FMinusV = simplifyCore(sub(F, v(vName)));
|
|
2227
2248
|
if (FMinusV.type === "const" && FMinusV.value === 0) {
|
|
2228
|
-
return { explicit: true, y:
|
|
2249
|
+
return { explicit: true, y: simplifyCore(mul(num(v0), v(xVar))) };
|
|
2229
2250
|
}
|
|
2230
2251
|
const sepResult = solveSeparableOde({ g: div(num(1), v(xVar)), h: FMinusV }, xVar, vName, x0, v0);
|
|
2231
2252
|
if (sepResult.explicit && sepResult.y) {
|
|
2232
|
-
return { explicit: true, y:
|
|
2253
|
+
return { explicit: true, y: simplifyCore(mul(sepResult.y, v(xVar))) };
|
|
2233
2254
|
}
|
|
2234
|
-
const implicitRelation =
|
|
2255
|
+
const implicitRelation = simplifyCore(subst(sepResult.implicitRelation, vName, div(v(yVar), v(xVar))));
|
|
2235
2256
|
return { explicit: false, implicitRelation };
|
|
2236
2257
|
}
|
|
2237
2258
|
/**
|
|
@@ -2324,8 +2345,8 @@ function matchBernoulli(e, xVar, yVar) {
|
|
|
2324
2345
|
}
|
|
2325
2346
|
if (powMatch.n === 0)
|
|
2326
2347
|
return null; // y^0=1: really just linear, matchLinearOde's job
|
|
2327
|
-
const q = powSign === 1 ? powMatch.coef :
|
|
2328
|
-
const p = linSign === 1 ?
|
|
2348
|
+
const q = powSign === 1 ? powMatch.coef : simplifyCore(neg(powMatch.coef));
|
|
2349
|
+
const p = linSign === 1 ? simplifyCore(neg(linMatch.coef)) : linMatch.coef;
|
|
2329
2350
|
return { p, q, n: powMatch.n };
|
|
2330
2351
|
}
|
|
2331
2352
|
/**
|
|
@@ -2345,10 +2366,10 @@ function matchBernoulli(e, xVar, yVar) {
|
|
|
2345
2366
|
function solveBernoulliOde(bern, xVar, yVar, x0, y0) {
|
|
2346
2367
|
const oneMinusN = 1 - bern.n;
|
|
2347
2368
|
const w0 = y0 ** oneMinusN;
|
|
2348
|
-
const transformedP =
|
|
2349
|
-
const transformedQ =
|
|
2369
|
+
const transformedP = simplifyCore(mul(num(oneMinusN), bern.p));
|
|
2370
|
+
const transformedQ = simplifyCore(mul(num(oneMinusN), bern.q));
|
|
2350
2371
|
const wResult = solveLinearOde({ p: transformedP, q: transformedQ }, xVar, yVar, x0, w0);
|
|
2351
|
-
const y =
|
|
2372
|
+
const y = simplifyCore(pow(wResult.y, num(1 / oneMinusN)));
|
|
2352
2373
|
return { explicit: true, y };
|
|
2353
2374
|
}
|
|
2354
2375
|
/**
|
|
@@ -2371,7 +2392,7 @@ function matchExact(e, xVar, yVar) {
|
|
|
2371
2392
|
if (![...vars].every((n) => n === xVar || n === yVar))
|
|
2372
2393
|
return null;
|
|
2373
2394
|
const M = e.left;
|
|
2374
|
-
const N =
|
|
2395
|
+
const N = simplifyCore(neg(e.right));
|
|
2375
2396
|
const dMdy = diff(M, yVar);
|
|
2376
2397
|
const dNdx = diff(N, xVar);
|
|
2377
2398
|
let agreements = 0;
|
|
@@ -2421,8 +2442,8 @@ function solveExactOde(exact, xVar, yVar, x0, y0) {
|
|
|
2421
2442
|
let g;
|
|
2422
2443
|
let gPrime;
|
|
2423
2444
|
try {
|
|
2424
|
-
F0 =
|
|
2425
|
-
gPrime =
|
|
2445
|
+
F0 = simplifyCore(integ(M, xVar));
|
|
2446
|
+
gPrime = simplifyCore(sub(N, diff(F0, yVar)));
|
|
2426
2447
|
if (containsVar(gPrime, xVar)) {
|
|
2427
2448
|
// Symbolic cancellation fell short -- verify x-independence
|
|
2428
2449
|
// numerically, then pin x to a probe value that evaluates finitely.
|
|
@@ -2435,21 +2456,21 @@ function solveExactOde(exact, xVar, yVar, x0, y0) {
|
|
|
2435
2456
|
if (Math.abs(atProbe - atOther) > 1e-6 * Math.max(1, Math.abs(atProbe))) {
|
|
2436
2457
|
throw new NoClosedFormError();
|
|
2437
2458
|
}
|
|
2438
|
-
pinned =
|
|
2459
|
+
pinned = simplifyCore(subst(gPrime, xVar, num(xp)));
|
|
2439
2460
|
break;
|
|
2440
2461
|
}
|
|
2441
2462
|
if (!pinned)
|
|
2442
2463
|
throw new NoClosedFormError();
|
|
2443
2464
|
gPrime = pinned;
|
|
2444
2465
|
}
|
|
2445
|
-
g =
|
|
2466
|
+
g = simplifyCore(integ(gPrime, yVar));
|
|
2446
2467
|
}
|
|
2447
2468
|
catch (err) {
|
|
2448
2469
|
if (err instanceof NotIntegrableError)
|
|
2449
2470
|
throw new NoClosedFormError();
|
|
2450
2471
|
throw err;
|
|
2451
2472
|
}
|
|
2452
|
-
const F =
|
|
2473
|
+
const F = simplifyCore(add(F0, g));
|
|
2453
2474
|
const C = evalExpr(F, { [xVar]: x0, [yVar]: y0 });
|
|
2454
2475
|
if (!Number.isFinite(C))
|
|
2455
2476
|
throw new NoClosedFormError();
|
|
@@ -2467,13 +2488,13 @@ function solveExactOde(exact, xVar, yVar, x0, y0) {
|
|
|
2467
2488
|
continue;
|
|
2468
2489
|
if (F.type === "sub" && yPart === b)
|
|
2469
2490
|
continue; // F = G(x) - H(y): inverting -H is out of tryInvertForY's shapes
|
|
2470
|
-
const rhs =
|
|
2491
|
+
const rhs = simplifyCore(sub(num(C), xPartSigned));
|
|
2471
2492
|
const explicitY = tryInvertForY(yPart, yVar, rhs, y0);
|
|
2472
2493
|
if (explicitY)
|
|
2473
|
-
return { explicit: true, y:
|
|
2494
|
+
return { explicit: true, y: simplifyCore(explicitY) };
|
|
2474
2495
|
}
|
|
2475
2496
|
}
|
|
2476
|
-
return { explicit: false, implicitRelation:
|
|
2497
|
+
return { explicit: false, implicitRelation: simplifyCore(sub(F, num(C))) };
|
|
2477
2498
|
}
|
|
2478
2499
|
/** If `e` is `a·x + b` (a, b constant), return `{ a, b }`, else `null`. */
|
|
2479
2500
|
function linearCoeffs(e, x) {
|
|
@@ -3345,11 +3366,54 @@ const CMP_LATEX_CMDS = {
|
|
|
3345
3366
|
"\\ne": "!=",
|
|
3346
3367
|
};
|
|
3347
3368
|
/** Convert a LaTeX math string into the plain-infix syntax `Symbolic.parse`'s `Parser` understands. */
|
|
3348
|
-
function latexToInfix(latex) {
|
|
3349
|
-
const cleaned = latex.replace(/\\left|\\right/g, "").replace(
|
|
3350
|
-
return transformLatex(cleaned);
|
|
3369
|
+
function latexToInfix(latex, opts = {}) {
|
|
3370
|
+
const cleaned = latex.replace(/\\left|\\right/g, "").replace(/(?<!\\)\\(?:,|;|!|quad|qquad| )/g, "");
|
|
3371
|
+
return transformLatex(cleaned, opts);
|
|
3372
|
+
}
|
|
3373
|
+
/**
|
|
3374
|
+
* Where the unbracketed argument of a function command ends under implicit multiplication: the longest
|
|
3375
|
+
* product of numbers, letters, `\\pi`, powers, subscripts and bracket groups starting at `start`
|
|
3376
|
+
* (`\\sin 2x` takes `2x`; `\\sin x\\cos x` takes just `x`). Returns `start` when there is none.
|
|
3377
|
+
*/
|
|
3378
|
+
function bareArgumentEnd(s, start) {
|
|
3379
|
+
let j = start;
|
|
3380
|
+
const group = (at) => {
|
|
3381
|
+
const open = s[at];
|
|
3382
|
+
return findGroupEnd(s, at, open, open === "{" ? "}" : ")") + 1;
|
|
3383
|
+
};
|
|
3384
|
+
while (j < s.length) {
|
|
3385
|
+
const c = s[j];
|
|
3386
|
+
if (/[A-Za-z0-9.]/.test(c)) {
|
|
3387
|
+
j++;
|
|
3388
|
+
}
|
|
3389
|
+
else if (c === "(" || c === "{") {
|
|
3390
|
+
j = group(j);
|
|
3391
|
+
}
|
|
3392
|
+
else if (c === "_" && s[j + 1] === "{") {
|
|
3393
|
+
j = group(j + 1);
|
|
3394
|
+
}
|
|
3395
|
+
else if (c === "^") {
|
|
3396
|
+
if (s[j + 1] === "{")
|
|
3397
|
+
j = group(j + 1);
|
|
3398
|
+
else if (s[j + 1] === "\\")
|
|
3399
|
+
j += 1 + (/^\\[a-zA-Z]+/.exec(s.slice(j + 1))?.[0].length ?? 1);
|
|
3400
|
+
else
|
|
3401
|
+
j += 2;
|
|
3402
|
+
}
|
|
3403
|
+
else if (/^\\pi(?![a-zA-Z])/.test(s.slice(j))) {
|
|
3404
|
+
j += 3;
|
|
3405
|
+
}
|
|
3406
|
+
else if (c === " " && /^ +(?:[A-Za-z0-9.({]|\\pi(?![a-zA-Z]))/.test(s.slice(j))) {
|
|
3407
|
+
j += s.slice(j).length - s.slice(j).trimStart().length;
|
|
3408
|
+
}
|
|
3409
|
+
else {
|
|
3410
|
+
break;
|
|
3411
|
+
}
|
|
3412
|
+
}
|
|
3413
|
+
return j;
|
|
3351
3414
|
}
|
|
3352
|
-
function transformLatex(s) {
|
|
3415
|
+
function transformLatex(s, opts = {}) {
|
|
3416
|
+
const implicit = opts.implicitMultiplication === true;
|
|
3353
3417
|
let out = "";
|
|
3354
3418
|
let i = 0;
|
|
3355
3419
|
const readGroup = () => {
|
|
@@ -3360,11 +3424,21 @@ function transformLatex(s) {
|
|
|
3360
3424
|
return content;
|
|
3361
3425
|
};
|
|
3362
3426
|
const readGroupOrParenOrToken = () => {
|
|
3427
|
+
while (s[i] === " ")
|
|
3428
|
+
i++;
|
|
3363
3429
|
if (s[i] === "{" || s[i] === "(") {
|
|
3364
3430
|
const { content, next } = extractGroup(s, i);
|
|
3365
3431
|
i = next;
|
|
3366
3432
|
return content;
|
|
3367
3433
|
}
|
|
3434
|
+
if (implicit) {
|
|
3435
|
+
const end = bareArgumentEnd(s, i);
|
|
3436
|
+
if (end > i) {
|
|
3437
|
+
const run = s.slice(i, end).trim();
|
|
3438
|
+
i = end;
|
|
3439
|
+
return run;
|
|
3440
|
+
}
|
|
3441
|
+
}
|
|
3368
3442
|
const m = /^[a-zA-Z_]\w*|^\d+\.?\d*(?:[eE][+-]?\d+)?/.exec(s.slice(i));
|
|
3369
3443
|
if (m) {
|
|
3370
3444
|
i += m[0].length;
|
|
@@ -3372,6 +3446,32 @@ function transformLatex(s) {
|
|
|
3372
3446
|
}
|
|
3373
3447
|
throw new Error(`Expected an argument at position ${i} in LaTeX source: ${s}`);
|
|
3374
3448
|
};
|
|
3449
|
+
/** A `{...}` group, or the one token TeX takes without braces: a digit, a letter or a `\\command`. */
|
|
3450
|
+
const readGroupOrToken = () => {
|
|
3451
|
+
while (s[i] === " ")
|
|
3452
|
+
i++;
|
|
3453
|
+
if (s[i] === "{")
|
|
3454
|
+
return readGroup();
|
|
3455
|
+
const m = /^(?:\\[a-zA-Z]+|[a-zA-Z0-9])/.exec(s.slice(i));
|
|
3456
|
+
if (!m)
|
|
3457
|
+
throw new Error(`Expected an argument at position ${i} in LaTeX source: ${s}`);
|
|
3458
|
+
i += m[0].length;
|
|
3459
|
+
return m[0];
|
|
3460
|
+
};
|
|
3461
|
+
/** A power written on a function command (`\\sin^{2}`, `\\sin^2`), or null. */
|
|
3462
|
+
const readFunctionPower = () => {
|
|
3463
|
+
if (s[i] !== "^")
|
|
3464
|
+
return null;
|
|
3465
|
+
i++;
|
|
3466
|
+
const power = readGroupOrToken();
|
|
3467
|
+
// `\sin^{-1}` is arcsin in most textbooks and calculators but 1/sin in strict TeX/notation; guessing
|
|
3468
|
+
// wrong would silently change the maths, so it is refused rather than interpreted.
|
|
3469
|
+
if (/^\s*-\s*1\s*$/.test(power)) {
|
|
3470
|
+
throw new Error("Ambiguous LaTeX: a function with exponent -1 (e.g. \\sin^{-1}) may mean the inverse function or the reciprocal; " +
|
|
3471
|
+
"write \\arcsin(x) for the inverse, or (\\sin x)^{-1} / 1/\\sin x for the reciprocal");
|
|
3472
|
+
}
|
|
3473
|
+
return power;
|
|
3474
|
+
};
|
|
3375
3475
|
/** Read a `(...)`/`{...}` group holding exactly 2 comma-separated arguments, for `call2` functions. */
|
|
3376
3476
|
const readArgPair = () => {
|
|
3377
3477
|
if (s[i] !== "(" && s[i] !== "{")
|
|
@@ -3427,10 +3527,11 @@ function transformLatex(s) {
|
|
|
3427
3527
|
if (condPart.trim() === "\\text{otherwise}") {
|
|
3428
3528
|
if (otherwise !== null)
|
|
3429
3529
|
throw new Error("\\begin{cases} may have only one \\text{otherwise} row");
|
|
3430
|
-
otherwise = transformLatex(exprPart);
|
|
3530
|
+
otherwise = transformLatex(exprPart, opts);
|
|
3431
3531
|
}
|
|
3432
3532
|
else {
|
|
3433
|
-
|
|
3533
|
+
const condition = condPart.replace(/^\s*\\text\{\s*(?:if|when)\s*\}/, ""); // "x & \text{if } x<0" as MathLive writes it
|
|
3534
|
+
parts.push(`${transformLatex(condition, opts)}, ${transformLatex(exprPart, opts)}`);
|
|
3434
3535
|
}
|
|
3435
3536
|
}
|
|
3436
3537
|
if (otherwise === null) {
|
|
@@ -3439,29 +3540,29 @@ function transformLatex(s) {
|
|
|
3439
3540
|
out += `piecewise(${parts.concat(otherwise).join(", ")})`;
|
|
3440
3541
|
continue;
|
|
3441
3542
|
}
|
|
3442
|
-
if (cmd === "\\frac") {
|
|
3443
|
-
const numerator =
|
|
3444
|
-
const denominator =
|
|
3445
|
-
out += `((${transformLatex(numerator)})/(${transformLatex(denominator)}))`;
|
|
3543
|
+
if (cmd === "\\frac" || cmd === "\\dfrac" || cmd === "\\tfrac") {
|
|
3544
|
+
const numerator = readGroupOrToken();
|
|
3545
|
+
const denominator = readGroupOrToken();
|
|
3546
|
+
out += `((${transformLatex(numerator, opts)})/(${transformLatex(denominator, opts)}))`;
|
|
3446
3547
|
continue;
|
|
3447
3548
|
}
|
|
3448
3549
|
if (cmd === "\\sqrt") {
|
|
3449
3550
|
if (s[i] === "[") {
|
|
3450
3551
|
const { content: root, next } = extractGroup(s, i);
|
|
3451
3552
|
i = next;
|
|
3452
|
-
const arg =
|
|
3453
|
-
const transformedRoot = transformLatex(root).trim();
|
|
3553
|
+
const arg = readGroupOrToken();
|
|
3554
|
+
const transformedRoot = transformLatex(root, opts).trim();
|
|
3454
3555
|
// \sqrt[3]{...} maps to cbrt(...) rather than a fractional power so
|
|
3455
3556
|
// negative arguments evaluate correctly (Math.cbrt(-8) = -2, but
|
|
3456
3557
|
// (-8)^(1/3) is NaN under JS's `**`).
|
|
3457
3558
|
out +=
|
|
3458
3559
|
transformedRoot === "3"
|
|
3459
|
-
? `cbrt(${transformLatex(arg)})`
|
|
3460
|
-
: `((${transformLatex(arg)})^(1/(${transformedRoot})))`;
|
|
3560
|
+
? `cbrt(${transformLatex(arg, opts)})`
|
|
3561
|
+
: `((${transformLatex(arg, opts)})^(1/(${transformedRoot})))`;
|
|
3461
3562
|
}
|
|
3462
3563
|
else {
|
|
3463
|
-
const arg =
|
|
3464
|
-
out += `sqrt(${transformLatex(arg)})`;
|
|
3564
|
+
const arg = readGroupOrToken();
|
|
3565
|
+
out += `sqrt(${transformLatex(arg, opts)})`;
|
|
3465
3566
|
}
|
|
3466
3567
|
continue;
|
|
3467
3568
|
}
|
|
@@ -3475,8 +3576,10 @@ function transformLatex(s) {
|
|
|
3475
3576
|
if (base !== "10" && base !== "2") {
|
|
3476
3577
|
throw new Error(`Unsupported LaTeX construct: \\log base ${base} (only base 10 and base 2 are supported)`);
|
|
3477
3578
|
}
|
|
3579
|
+
const power = readFunctionPower();
|
|
3478
3580
|
const arg = readGroupOrParenOrToken();
|
|
3479
|
-
|
|
3581
|
+
const call = `${base === "2" ? "log2" : "log10"}(${transformLatex(arg, opts)})`;
|
|
3582
|
+
out += power === null ? call : `${call}^(${transformLatex(power, opts)})`;
|
|
3480
3583
|
continue;
|
|
3481
3584
|
}
|
|
3482
3585
|
if (cmd === "\\lfloor" || cmd === "\\lceil") {
|
|
@@ -3486,7 +3589,7 @@ function transformLatex(s) {
|
|
|
3486
3589
|
throw new Error(`Unmatched '${cmd}' in LaTeX source`);
|
|
3487
3590
|
const inner = s.slice(i, closeIdx);
|
|
3488
3591
|
i = closeIdx + closeCmd.length;
|
|
3489
|
-
out += `${cmd === "\\lfloor" ? "floor" : "ceil"}(${transformLatex(inner)})`;
|
|
3592
|
+
out += `${cmd === "\\lfloor" ? "floor" : "ceil"}(${transformLatex(inner, opts)})`;
|
|
3490
3593
|
continue;
|
|
3491
3594
|
}
|
|
3492
3595
|
if (cmd === "\\operatorname") {
|
|
@@ -3494,26 +3597,30 @@ function transformLatex(s) {
|
|
|
3494
3597
|
const binaryName = OPERATORNAME_BINARY_FUNC_NAMES[opName];
|
|
3495
3598
|
if (binaryName) {
|
|
3496
3599
|
const [left, right] = readArgPair();
|
|
3497
|
-
out += `${binaryName}(${transformLatex(left)}, ${transformLatex(right)})`;
|
|
3600
|
+
out += `${binaryName}(${transformLatex(left, opts)}, ${transformLatex(right, opts)})`;
|
|
3498
3601
|
continue;
|
|
3499
3602
|
}
|
|
3500
3603
|
const funcName = OPERATORNAME_FUNC_NAMES[opName];
|
|
3501
3604
|
if (!funcName)
|
|
3502
3605
|
throw new Error(`Unsupported LaTeX construct: \\operatorname{${opName}}`);
|
|
3606
|
+
const power = readFunctionPower();
|
|
3503
3607
|
const arg = readGroupOrParenOrToken();
|
|
3504
|
-
|
|
3608
|
+
const call = `${funcName}(${transformLatex(arg, opts)})`;
|
|
3609
|
+
out += power === null ? call : `${call}^(${transformLatex(power, opts)})`;
|
|
3505
3610
|
continue;
|
|
3506
3611
|
}
|
|
3507
3612
|
const binaryFuncName = LATEX_BINARY_FUNC_NAMES[cmd];
|
|
3508
3613
|
if (binaryFuncName) {
|
|
3509
3614
|
const [left, right] = readArgPair();
|
|
3510
|
-
out += `${binaryFuncName}(${transformLatex(left)}, ${transformLatex(right)})`;
|
|
3615
|
+
out += `${binaryFuncName}(${transformLatex(left, opts)}, ${transformLatex(right, opts)})`;
|
|
3511
3616
|
continue;
|
|
3512
3617
|
}
|
|
3513
3618
|
const funcName = LATEX_FUNC_NAMES[cmd];
|
|
3514
3619
|
if (funcName) {
|
|
3620
|
+
const power = readFunctionPower();
|
|
3515
3621
|
const arg = readGroupOrParenOrToken();
|
|
3516
|
-
|
|
3622
|
+
const call = `${funcName}(${transformLatex(arg, opts)})`;
|
|
3623
|
+
out += power === null ? call : `${call}^(${transformLatex(power, opts)})`;
|
|
3517
3624
|
continue;
|
|
3518
3625
|
}
|
|
3519
3626
|
throw new Error(`Unsupported LaTeX construct: ${cmd}`);
|
|
@@ -3524,13 +3631,20 @@ function transformLatex(s) {
|
|
|
3524
3631
|
throw new Error("Unmatched '|' in LaTeX source");
|
|
3525
3632
|
const inner = s.slice(i + 1, closeIdx);
|
|
3526
3633
|
i = closeIdx + 1;
|
|
3527
|
-
out += `abs(${transformLatex(inner)})`;
|
|
3634
|
+
out += `abs(${transformLatex(inner, opts)})`;
|
|
3635
|
+
continue;
|
|
3636
|
+
}
|
|
3637
|
+
if (implicit && ch === "^" && /^[a-zA-Z0-9]|^\\[a-zA-Z]/.test(s.slice(i + 1))) {
|
|
3638
|
+
// TeX reads `x^23` as `x^2` times `3`: the exponent is one token
|
|
3639
|
+
const m = /^(?:\\[a-zA-Z]+|[a-zA-Z0-9])/.exec(s.slice(i + 1));
|
|
3640
|
+
i += 1 + m[0].length;
|
|
3641
|
+
out += `^(${transformLatex(m[0], opts)})`;
|
|
3528
3642
|
continue;
|
|
3529
3643
|
}
|
|
3530
3644
|
if (ch === "^" && s[i + 1] === "{") {
|
|
3531
3645
|
const { content, next } = extractGroup(s, i + 1);
|
|
3532
3646
|
i = next;
|
|
3533
|
-
out += `^(${transformLatex(content)})`;
|
|
3647
|
+
out += `^(${transformLatex(content, opts)})`;
|
|
3534
3648
|
continue;
|
|
3535
3649
|
}
|
|
3536
3650
|
if (ch === "_" && s[i + 1] === "{") {
|
|
@@ -3542,7 +3656,7 @@ function transformLatex(s) {
|
|
|
3542
3656
|
if (ch === "{") {
|
|
3543
3657
|
const { content, next } = extractGroup(s, i);
|
|
3544
3658
|
i = next;
|
|
3545
|
-
out += `(${transformLatex(content)})`;
|
|
3659
|
+
out += `(${transformLatex(content, opts)})`;
|
|
3546
3660
|
continue;
|
|
3547
3661
|
}
|
|
3548
3662
|
if (ch === "=") {
|
|
@@ -3558,6 +3672,378 @@ function transformLatex(s) {
|
|
|
3558
3672
|
}
|
|
3559
3673
|
return out;
|
|
3560
3674
|
}
|
|
3675
|
+
const MAX_POLY_TERMS = 400;
|
|
3676
|
+
const MAX_RATIONAL_POWER = 8;
|
|
3677
|
+
class RatBail extends Error {
|
|
3678
|
+
}
|
|
3679
|
+
function atomIndex(ctx, e) {
|
|
3680
|
+
const key = render(e, 0);
|
|
3681
|
+
let i = ctx.keys.indexOf(key);
|
|
3682
|
+
if (i < 0) {
|
|
3683
|
+
i = ctx.keys.length;
|
|
3684
|
+
ctx.keys.push(key);
|
|
3685
|
+
ctx.atoms.push(e);
|
|
3686
|
+
}
|
|
3687
|
+
return i;
|
|
3688
|
+
}
|
|
3689
|
+
function expsCmp(a, b) {
|
|
3690
|
+
const n = Math.max(a.length, b.length);
|
|
3691
|
+
for (let i = 0; i < n; i++) {
|
|
3692
|
+
const d = (a[i] ?? 0) - (b[i] ?? 0);
|
|
3693
|
+
if (d !== 0)
|
|
3694
|
+
return d;
|
|
3695
|
+
}
|
|
3696
|
+
return 0;
|
|
3697
|
+
}
|
|
3698
|
+
function expsKey(a) {
|
|
3699
|
+
let end = a.length;
|
|
3700
|
+
while (end > 0 && !a[end - 1])
|
|
3701
|
+
end--;
|
|
3702
|
+
return a.slice(0, end).join(",");
|
|
3703
|
+
}
|
|
3704
|
+
function polyNormalize(terms) {
|
|
3705
|
+
const byKey = new Map();
|
|
3706
|
+
for (const t of terms) {
|
|
3707
|
+
const k = expsKey(t.exps);
|
|
3708
|
+
const prev = byKey.get(k);
|
|
3709
|
+
if (prev)
|
|
3710
|
+
byKey.set(k, { exps: prev.exps, c: prev.c.add(t.c) });
|
|
3711
|
+
else
|
|
3712
|
+
byKey.set(k, { exps: t.exps, c: t.c });
|
|
3713
|
+
}
|
|
3714
|
+
const out = [...byKey.values()].filter((t) => !t.c.isZero());
|
|
3715
|
+
out.sort((a, b) => expsCmp(b.exps, a.exps));
|
|
3716
|
+
if (out.length > MAX_POLY_TERMS)
|
|
3717
|
+
throw new RatBail();
|
|
3718
|
+
return out;
|
|
3719
|
+
}
|
|
3720
|
+
const polyConst = (c) => (c.isZero() ? [] : [{ exps: [], c }]);
|
|
3721
|
+
const polyAdd = (a, b) => polyNormalize([...a, ...b]);
|
|
3722
|
+
const polyNeg = (a) => a.map((t) => ({ exps: t.exps, c: t.c.negate() }));
|
|
3723
|
+
const polySub = (a, b) => polyAdd(a, polyNeg(b));
|
|
3724
|
+
function polyMul(a, b) {
|
|
3725
|
+
if (a.length * b.length > MAX_POLY_TERMS * 20)
|
|
3726
|
+
throw new RatBail();
|
|
3727
|
+
const out = [];
|
|
3728
|
+
for (const x of a) {
|
|
3729
|
+
for (const y of b) {
|
|
3730
|
+
const n = Math.max(x.exps.length, y.exps.length);
|
|
3731
|
+
const exps = [];
|
|
3732
|
+
for (let i = 0; i < n; i++)
|
|
3733
|
+
exps.push((x.exps[i] ?? 0) + (y.exps[i] ?? 0));
|
|
3734
|
+
out.push({ exps, c: x.c.multiply(y.c) });
|
|
3735
|
+
}
|
|
3736
|
+
}
|
|
3737
|
+
return polyNormalize(out);
|
|
3738
|
+
}
|
|
3739
|
+
function polyEqual(a, b) {
|
|
3740
|
+
return a.length === b.length && a.every((t, i) => expsKey(t.exps) === expsKey(b[i].exps) && t.c.equals(b[i].c));
|
|
3741
|
+
}
|
|
3742
|
+
const polyIsConst = (a) => a.length === 0 || (a.length === 1 && expsKey(a[0].exps) === "");
|
|
3743
|
+
/** Division in lex order: `a = q*b + r` (`b` must be nonzero). */
|
|
3744
|
+
function polyDivMod(a, b) {
|
|
3745
|
+
const lead = b[0];
|
|
3746
|
+
let rem = a;
|
|
3747
|
+
const q = [];
|
|
3748
|
+
const r = [];
|
|
3749
|
+
let guard = 0;
|
|
3750
|
+
while (rem.length > 0) {
|
|
3751
|
+
if (++guard > 20_000)
|
|
3752
|
+
throw new RatBail();
|
|
3753
|
+
const t = rem[0];
|
|
3754
|
+
const n = Math.max(t.exps.length, lead.exps.length);
|
|
3755
|
+
const exps = [];
|
|
3756
|
+
let divisible = true;
|
|
3757
|
+
for (let i = 0; i < n; i++) {
|
|
3758
|
+
const d = (t.exps[i] ?? 0) - (lead.exps[i] ?? 0);
|
|
3759
|
+
if (d < 0)
|
|
3760
|
+
divisible = false;
|
|
3761
|
+
exps.push(d);
|
|
3762
|
+
}
|
|
3763
|
+
if (!divisible) {
|
|
3764
|
+
r.push(t);
|
|
3765
|
+
rem = rem.slice(1);
|
|
3766
|
+
continue;
|
|
3767
|
+
}
|
|
3768
|
+
const qt = { exps, c: t.c.divide(lead.c) };
|
|
3769
|
+
q.push(qt);
|
|
3770
|
+
rem = polySub(rem, polyMul([qt], b));
|
|
3771
|
+
}
|
|
3772
|
+
return { q: polyNormalize(q), r: polyNormalize(r) };
|
|
3773
|
+
}
|
|
3774
|
+
function polyAtoms(...ps) {
|
|
3775
|
+
const s = new Set();
|
|
3776
|
+
for (const p of ps) {
|
|
3777
|
+
for (const t of p) {
|
|
3778
|
+
for (let i = 0; i < t.exps.length; i++)
|
|
3779
|
+
if (t.exps[i] > 0)
|
|
3780
|
+
s.add(i);
|
|
3781
|
+
}
|
|
3782
|
+
}
|
|
3783
|
+
return s;
|
|
3784
|
+
}
|
|
3785
|
+
/** Monic GCD of two polynomials in one atom (Euclid); 1 when several atoms are involved. */
|
|
3786
|
+
function polyGcd(a, b) {
|
|
3787
|
+
if (polyAtoms(a, b).size > 1)
|
|
3788
|
+
return polyConst(Rational.One);
|
|
3789
|
+
let x = a;
|
|
3790
|
+
let y = b;
|
|
3791
|
+
while (y.length > 0) {
|
|
3792
|
+
const { r } = polyDivMod(x, y);
|
|
3793
|
+
x = y;
|
|
3794
|
+
y = r;
|
|
3795
|
+
}
|
|
3796
|
+
if (x.length === 0)
|
|
3797
|
+
return polyConst(Rational.One);
|
|
3798
|
+
const lc = x[0].c;
|
|
3799
|
+
return x.map((t) => ({ exps: t.exps, c: t.c.divide(lc) }));
|
|
3800
|
+
}
|
|
3801
|
+
/** The largest monomial dividing every term (exponent-wise minimum). */
|
|
3802
|
+
function monomialContent(p) {
|
|
3803
|
+
if (p.length === 0)
|
|
3804
|
+
return [];
|
|
3805
|
+
const n = Math.max(...p.map((t) => t.exps.length));
|
|
3806
|
+
const out = [];
|
|
3807
|
+
for (let i = 0; i < n; i++)
|
|
3808
|
+
out.push(Math.min(...p.map((t) => t.exps[i] ?? 0)));
|
|
3809
|
+
return out;
|
|
3810
|
+
}
|
|
3811
|
+
function polyDivMonomial(p, m) {
|
|
3812
|
+
return p.map((t) => ({ exps: t.exps.map((e, i) => e - (m[i] ?? 0)), c: t.c }));
|
|
3813
|
+
}
|
|
3814
|
+
const ratioFromPoly = (n) => ({ n, d: polyConst(Rational.One) });
|
|
3815
|
+
function ratioAdd(a, b, subtract) {
|
|
3816
|
+
const bn = subtract ? polyNeg(b.n) : b.n;
|
|
3817
|
+
if (polyEqual(a.d, b.d))
|
|
3818
|
+
return { n: polyAdd(a.n, bn), d: a.d };
|
|
3819
|
+
return { n: polyAdd(polyMul(a.n, b.d), polyMul(bn, a.d)), d: polyMul(a.d, b.d) };
|
|
3820
|
+
}
|
|
3821
|
+
const ratioMul = (a, b) => ({ n: polyMul(a.n, b.n), d: polyMul(a.d, b.d) });
|
|
3822
|
+
function ratioDiv(a, b) {
|
|
3823
|
+
if (b.n.length === 0)
|
|
3824
|
+
throw new RatBail(); // division by zero: leave it to evaluation
|
|
3825
|
+
return { n: polyMul(a.n, b.d), d: polyMul(a.d, b.n) };
|
|
3826
|
+
}
|
|
3827
|
+
function ratioPow(a, n) {
|
|
3828
|
+
const k = Math.abs(n);
|
|
3829
|
+
if (k > 1000)
|
|
3830
|
+
throw new RatBail();
|
|
3831
|
+
const singleTerm = a.n.length === 1 && a.d.length === 1;
|
|
3832
|
+
if (!singleTerm && k > MAX_RATIONAL_POWER)
|
|
3833
|
+
throw new RatBail();
|
|
3834
|
+
let result;
|
|
3835
|
+
if (singleTerm) {
|
|
3836
|
+
const mono = (t) => ({ exps: t.exps.map((e) => e * k), c: t.c.pow(k) });
|
|
3837
|
+
result = { n: [mono(a.n[0])], d: [mono(a.d[0])] };
|
|
3838
|
+
}
|
|
3839
|
+
else {
|
|
3840
|
+
result = ratioFromPoly(polyConst(Rational.One));
|
|
3841
|
+
for (let i = 0; i < k; i++)
|
|
3842
|
+
result = ratioMul(result, a);
|
|
3843
|
+
}
|
|
3844
|
+
return n < 0 ? ratioDiv(ratioFromPoly(polyConst(Rational.One)), result) : result;
|
|
3845
|
+
}
|
|
3846
|
+
function exactRational(value) {
|
|
3847
|
+
if (!Number.isFinite(value))
|
|
3848
|
+
return null;
|
|
3849
|
+
if (Number.isInteger(value))
|
|
3850
|
+
return new Rational(BigInt(value));
|
|
3851
|
+
const r = Rational.fromNumber(value, 10_000);
|
|
3852
|
+
return r.toNumber() === value ? r : null;
|
|
3853
|
+
}
|
|
3854
|
+
function toRatio(e, ctx) {
|
|
3855
|
+
const atom = (x) => {
|
|
3856
|
+
const i = atomIndex(ctx, x);
|
|
3857
|
+
const exps = new Array(i + 1).fill(0);
|
|
3858
|
+
exps[i] = 1;
|
|
3859
|
+
return ratioFromPoly([{ exps, c: Rational.One }]);
|
|
3860
|
+
};
|
|
3861
|
+
switch (e.type) {
|
|
3862
|
+
case "const": {
|
|
3863
|
+
const r = exactRational(e.value);
|
|
3864
|
+
return r ? ratioFromPoly(polyConst(r)) : atom(e);
|
|
3865
|
+
}
|
|
3866
|
+
case "var":
|
|
3867
|
+
return atom(e);
|
|
3868
|
+
case "neg": {
|
|
3869
|
+
const a = toRatio(e.arg, ctx);
|
|
3870
|
+
return { n: polyNeg(a.n), d: a.d };
|
|
3871
|
+
}
|
|
3872
|
+
case "add":
|
|
3873
|
+
return ratioAdd(toRatio(e.left, ctx), toRatio(e.right, ctx), false);
|
|
3874
|
+
case "sub":
|
|
3875
|
+
return ratioAdd(toRatio(e.left, ctx), toRatio(e.right, ctx), true);
|
|
3876
|
+
case "mul":
|
|
3877
|
+
return ratioMul(toRatio(e.left, ctx), toRatio(e.right, ctx));
|
|
3878
|
+
case "div":
|
|
3879
|
+
return ratioDiv(toRatio(e.left, ctx), toRatio(e.right, ctx));
|
|
3880
|
+
case "pow": {
|
|
3881
|
+
if (e.exp.type === "const" && Number.isInteger(e.exp.value)) {
|
|
3882
|
+
return ratioPow(toRatio(e.base, ctx), e.exp.value);
|
|
3883
|
+
}
|
|
3884
|
+
return atom(pow(normalizeRational(e.base), normalizeRational(e.exp)));
|
|
3885
|
+
}
|
|
3886
|
+
case "func":
|
|
3887
|
+
return atom(fn(e.name, normalizeRational(e.arg)));
|
|
3888
|
+
case "call2":
|
|
3889
|
+
return atom(call2(e.name, normalizeRational(e.left), normalizeRational(e.right)));
|
|
3890
|
+
case "cmp":
|
|
3891
|
+
return atom(cmp(e.op, normalizeRational(e.left), normalizeRational(e.right)));
|
|
3892
|
+
case "piecewise":
|
|
3893
|
+
return atom(piecewise(e.branches.map((br) => ({ cond: normalizeRational(br.cond), expr: normalizeRational(br.expr) })), normalizeRational(e.otherwise)));
|
|
3894
|
+
case "sum":
|
|
3895
|
+
return atom(sumExpr(e.variable, normalizeRational(e.from), normalizeRational(e.to), normalizeRational(e.body)));
|
|
3896
|
+
case "product":
|
|
3897
|
+
return atom(productExpr(e.variable, normalizeRational(e.from), normalizeRational(e.to), normalizeRational(e.body)));
|
|
3898
|
+
}
|
|
3899
|
+
}
|
|
3900
|
+
function bigGcd(a, b) {
|
|
3901
|
+
let x = a < 0n ? -a : a;
|
|
3902
|
+
let y = b < 0n ? -b : b;
|
|
3903
|
+
while (y)
|
|
3904
|
+
[x, y] = [y, x % y];
|
|
3905
|
+
return x;
|
|
3906
|
+
}
|
|
3907
|
+
/** Cancel common factors, then scale to coprime integer coefficients with a positive denominator lead. */
|
|
3908
|
+
function ratioReduce(r) {
|
|
3909
|
+
if (r.n.length === 0)
|
|
3910
|
+
return ratioFromPoly([]);
|
|
3911
|
+
let { n, d } = r;
|
|
3912
|
+
const m = monomialContent([...n, ...d]);
|
|
3913
|
+
if (m.some((e) => e > 0)) {
|
|
3914
|
+
n = polyDivMonomial(n, m);
|
|
3915
|
+
d = polyDivMonomial(d, m);
|
|
3916
|
+
}
|
|
3917
|
+
if (!polyIsConst(d)) {
|
|
3918
|
+
const g = polyGcd(n, d);
|
|
3919
|
+
if (!polyIsConst(g)) {
|
|
3920
|
+
n = polyDivMod(n, g).q;
|
|
3921
|
+
d = polyDivMod(d, g).q;
|
|
3922
|
+
}
|
|
3923
|
+
else {
|
|
3924
|
+
// Several atoms: still cancel when one side divides the other exactly.
|
|
3925
|
+
const dn = polyDivMod(n, d);
|
|
3926
|
+
if (dn.r.length === 0) {
|
|
3927
|
+
n = dn.q;
|
|
3928
|
+
d = polyConst(Rational.One);
|
|
3929
|
+
}
|
|
3930
|
+
else {
|
|
3931
|
+
const nd = polyDivMod(d, n);
|
|
3932
|
+
if (nd.r.length === 0) {
|
|
3933
|
+
d = nd.q;
|
|
3934
|
+
n = polyConst(Rational.One);
|
|
3935
|
+
}
|
|
3936
|
+
}
|
|
3937
|
+
}
|
|
3938
|
+
}
|
|
3939
|
+
if (polyIsConst(d)) {
|
|
3940
|
+
const c = d[0].c;
|
|
3941
|
+
return { n: n.map((t) => ({ exps: t.exps, c: t.c.divide(c) })), d: polyConst(Rational.One) };
|
|
3942
|
+
}
|
|
3943
|
+
// integer coefficients: multiply through by the lcm of the denominators, divide by the content
|
|
3944
|
+
let lcm = 1n;
|
|
3945
|
+
for (const t of [...n, ...d]) {
|
|
3946
|
+
const den = t.c.denominator;
|
|
3947
|
+
lcm = (lcm / bigGcd(lcm, den)) * den;
|
|
3948
|
+
}
|
|
3949
|
+
const scale = new Rational(lcm);
|
|
3950
|
+
n = n.map((t) => ({ exps: t.exps, c: t.c.multiply(scale) }));
|
|
3951
|
+
d = d.map((t) => ({ exps: t.exps, c: t.c.multiply(scale) }));
|
|
3952
|
+
let content = 0n;
|
|
3953
|
+
for (const t of [...n, ...d])
|
|
3954
|
+
content = bigGcd(content, t.c.numerator);
|
|
3955
|
+
if (content > 1n) {
|
|
3956
|
+
const inv = new Rational(1n, content);
|
|
3957
|
+
n = n.map((t) => ({ exps: t.exps, c: t.c.multiply(inv) }));
|
|
3958
|
+
d = d.map((t) => ({ exps: t.exps, c: t.c.multiply(inv) }));
|
|
3959
|
+
}
|
|
3960
|
+
if (d[0].c.sign < 0) {
|
|
3961
|
+
n = polyNeg(n);
|
|
3962
|
+
d = polyNeg(d);
|
|
3963
|
+
}
|
|
3964
|
+
return { n, d };
|
|
3965
|
+
}
|
|
3966
|
+
function monomialExpr(exps, ctx) {
|
|
3967
|
+
let out = null;
|
|
3968
|
+
for (let i = 0; i < exps.length; i++) {
|
|
3969
|
+
const e = exps[i] ?? 0;
|
|
3970
|
+
if (e === 0)
|
|
3971
|
+
continue;
|
|
3972
|
+
const factor = e === 1 ? ctx.atoms[i] : pow(ctx.atoms[i], num(e));
|
|
3973
|
+
out = out ? mul(out, factor) : factor;
|
|
3974
|
+
}
|
|
3975
|
+
return out;
|
|
3976
|
+
}
|
|
3977
|
+
/** A polynomial as a descending-degree sum; fractional coefficients print as `p*m/q`. */
|
|
3978
|
+
function polyToTree(p, ctx) {
|
|
3979
|
+
if (p.length === 0)
|
|
3980
|
+
return num(0);
|
|
3981
|
+
const ordered = [...p].sort((a, b) => {
|
|
3982
|
+
const da = a.exps.reduce((s, x) => s + x, 0);
|
|
3983
|
+
const db = b.exps.reduce((s, x) => s + x, 0);
|
|
3984
|
+
return db - da || expsCmp(b.exps, a.exps);
|
|
3985
|
+
});
|
|
3986
|
+
let out = null;
|
|
3987
|
+
for (const t of ordered) {
|
|
3988
|
+
const negative = t.c.sign < 0;
|
|
3989
|
+
const abs = t.c.abs();
|
|
3990
|
+
const mono = monomialExpr(t.exps, ctx);
|
|
3991
|
+
const numer = abs.numerator;
|
|
3992
|
+
let body;
|
|
3993
|
+
if (!mono)
|
|
3994
|
+
body = num(abs.toNumber());
|
|
3995
|
+
else if (numer === 1n && abs.denominator === 1n)
|
|
3996
|
+
body = mono;
|
|
3997
|
+
else if (abs.denominator === 1n)
|
|
3998
|
+
body = mul(num(Number(numer)), mono);
|
|
3999
|
+
else
|
|
4000
|
+
body = div(numer === 1n ? mono : mul(num(Number(numer)), mono), num(Number(abs.denominator)));
|
|
4001
|
+
if (!out)
|
|
4002
|
+
out = negative ? neg(body) : body;
|
|
4003
|
+
else
|
|
4004
|
+
out = negative ? sub(out, body) : add(out, body);
|
|
4005
|
+
}
|
|
4006
|
+
return out;
|
|
4007
|
+
}
|
|
4008
|
+
function nodeCount(e) {
|
|
4009
|
+
switch (e.type) {
|
|
4010
|
+
case "const":
|
|
4011
|
+
case "var":
|
|
4012
|
+
return 1;
|
|
4013
|
+
case "neg":
|
|
4014
|
+
case "func":
|
|
4015
|
+
return 1 + nodeCount(e.arg);
|
|
4016
|
+
case "pow":
|
|
4017
|
+
return 1 + nodeCount(e.base) + nodeCount(e.exp);
|
|
4018
|
+
case "piecewise":
|
|
4019
|
+
return 1 + nodeCount(e.otherwise) + e.branches.reduce((s, br) => s + nodeCount(br.cond) + nodeCount(br.expr), 0);
|
|
4020
|
+
case "sum":
|
|
4021
|
+
case "product":
|
|
4022
|
+
return 1 + nodeCount(e.from) + nodeCount(e.to) + nodeCount(e.body);
|
|
4023
|
+
default:
|
|
4024
|
+
return 1 + nodeCount(e.left) + nodeCount(e.right);
|
|
4025
|
+
}
|
|
4026
|
+
}
|
|
4027
|
+
/**
|
|
4028
|
+
* Expand `e` as a rational function and cancel common factors, returning the result only
|
|
4029
|
+
* when it has strictly fewer nodes than `e`; otherwise returns `e` itself.
|
|
4030
|
+
*/
|
|
4031
|
+
function normalizeRational(e) {
|
|
4032
|
+
if (e.type === "const" || e.type === "var")
|
|
4033
|
+
return e;
|
|
4034
|
+
try {
|
|
4035
|
+
const ctx = { atoms: [], keys: [] };
|
|
4036
|
+
const reduced = ratioReduce(toRatio(e, ctx));
|
|
4037
|
+
const numer = polyToTree(reduced.n, ctx);
|
|
4038
|
+
const candidate = polyIsConst(reduced.d) ? numer : div(numer, polyToTree(reduced.d, ctx));
|
|
4039
|
+
return nodeCount(candidate) < nodeCount(e) ? candidate : e;
|
|
4040
|
+
}
|
|
4041
|
+
catch (err) {
|
|
4042
|
+
if (err instanceof RatBail)
|
|
4043
|
+
return e;
|
|
4044
|
+
throw err;
|
|
4045
|
+
}
|
|
4046
|
+
}
|
|
3561
4047
|
// -- equation solving & factoring ----------------------------------------------
|
|
3562
4048
|
/** Best-effort exact `Expr` for a numeric value: an integer, a reduced fraction, or (as a last resort) the raw float. */
|
|
3563
4049
|
function toExactExpr(value) {
|
|
@@ -3591,7 +4077,7 @@ function linearCoeffsForSystem(eq, variables) {
|
|
|
3591
4077
|
return null;
|
|
3592
4078
|
const coeffs = [];
|
|
3593
4079
|
for (const name of variables) {
|
|
3594
|
-
const derivative =
|
|
4080
|
+
const derivative = simplifyCore(diff(eq, name));
|
|
3595
4081
|
if (variables.some((other) => containsVar(derivative, other)))
|
|
3596
4082
|
return null;
|
|
3597
4083
|
const coeff = evalExpr(derivative, zeroEnv);
|
|
@@ -3623,7 +4109,7 @@ function polynomialCoeffs(expr, variable, maxDegree) {
|
|
|
3623
4109
|
// derivative of x^0 (`0·x^-1`) evaluates to `0·Infinity = NaN` at x=0
|
|
3624
4110
|
// even though it is symbolically zero — simplifying collapses `0·anything`
|
|
3625
4111
|
// to `0` before that indeterminate form can arise.
|
|
3626
|
-
term =
|
|
4112
|
+
term = simplifyCore(diff(term, variable));
|
|
3627
4113
|
}
|
|
3628
4114
|
while (coeffs.length > 1 && Math.abs(coeffs[coeffs.length - 1]) < 1e-9)
|
|
3629
4115
|
coeffs.pop();
|
|
@@ -3818,7 +4304,7 @@ function factorPolynomial(coeffsIn, variable) {
|
|
|
3818
4304
|
else if (Math.abs(remaining[0] - 1) > 1e-9) {
|
|
3819
4305
|
result = mul(result, toExactExpr(remaining[0]));
|
|
3820
4306
|
}
|
|
3821
|
-
return
|
|
4307
|
+
return simplifyCore(result);
|
|
3822
4308
|
}
|
|
3823
4309
|
// -- limits ---------------------------------------------------------------------
|
|
3824
4310
|
function evalNear(e, x, point, direction) {
|
|
@@ -3842,19 +4328,25 @@ function limitAt(e, x, point, direction, depth) {
|
|
|
3842
4328
|
// operand verbatim into the result) and a limit needing more than a
|
|
3843
4329
|
// couple of rounds (e.g. most limits at infinity) blows up memory
|
|
3844
4330
|
// long before the depth<12 cap is reached.
|
|
3845
|
-
const df =
|
|
3846
|
-
const dg =
|
|
4331
|
+
const df = simplifyCore(diff(e.left, x));
|
|
4332
|
+
const dg = simplifyCore(diff(e.right, x));
|
|
3847
4333
|
return limitAt(div(df, dg), x, point, direction, depth + 1);
|
|
3848
4334
|
}
|
|
3849
4335
|
}
|
|
3850
4336
|
return direct;
|
|
3851
4337
|
}
|
|
3852
4338
|
// -- recursive-descent parser ----------------------------------------------
|
|
4339
|
+
/** Every name the parser treats as a function, longest first, so `asinh(` is not read as `a*sinh(`. */
|
|
4340
|
+
const FUNCTION_NAMES_LONGEST_FIRST = [...FUNCTION_NAMES].sort((a, b) => b.length - a.length);
|
|
3853
4341
|
class Parser {
|
|
3854
4342
|
s;
|
|
4343
|
+
implicit;
|
|
3855
4344
|
pos = 0;
|
|
3856
|
-
constructor(input) {
|
|
3857
|
-
this.
|
|
4345
|
+
constructor(input, options = {}) {
|
|
4346
|
+
this.implicit = options.implicitMultiplication === true;
|
|
4347
|
+
// With implicit multiplication, white space between two numbers is a product (`2 3`), not one number.
|
|
4348
|
+
const spaced = this.implicit ? input.replace(/(\d)\s+(?=[\d.])/g, "$1*") : input;
|
|
4349
|
+
this.s = spaced.replace(/\s+/g, "");
|
|
3858
4350
|
}
|
|
3859
4351
|
parse() {
|
|
3860
4352
|
const e = this.comparison();
|
|
@@ -3973,12 +4465,38 @@ class Parser {
|
|
|
3973
4465
|
}
|
|
3974
4466
|
term() {
|
|
3975
4467
|
let left = this.factor();
|
|
3976
|
-
|
|
3977
|
-
const
|
|
3978
|
-
|
|
3979
|
-
|
|
4468
|
+
for (;;) {
|
|
4469
|
+
const ch = this.peek();
|
|
4470
|
+
if (ch === "*" || ch === "/") {
|
|
4471
|
+
this.pos++;
|
|
4472
|
+
const right = this.factor();
|
|
4473
|
+
left = ch === "*" ? mul(left, right) : div(left, right);
|
|
4474
|
+
}
|
|
4475
|
+
else if (this.implicit && /[a-zA-Z0-9.(]/.test(ch)) {
|
|
4476
|
+
left = mul(left, this.factor()); // juxtaposition: `2x`, `x y`, `2(x+1)`
|
|
4477
|
+
}
|
|
4478
|
+
else {
|
|
4479
|
+
return left;
|
|
4480
|
+
}
|
|
3980
4481
|
}
|
|
3981
|
-
|
|
4482
|
+
}
|
|
4483
|
+
/**
|
|
4484
|
+
* The identifier at `this.pos` under implicit multiplication: a function name followed by `(`, `pi`, or a
|
|
4485
|
+
* single letter with an optional `_subscript`. A letter run like `xy` is therefore read one letter at a time.
|
|
4486
|
+
*/
|
|
4487
|
+
implicitIdentifier() {
|
|
4488
|
+
const rest = this.s.slice(this.pos);
|
|
4489
|
+
const run = /^[a-zA-Z]+/.exec(rest)?.[0] ?? "";
|
|
4490
|
+
const call = FUNCTION_NAMES_LONGEST_FIRST.find((f) => rest.startsWith(f) && rest[f.length] === "(");
|
|
4491
|
+
if (call)
|
|
4492
|
+
return call;
|
|
4493
|
+
const bare = FUNCTION_NAMES_LONGEST_FIRST.find((f) => f.length > 1 && run.startsWith(f));
|
|
4494
|
+
if (bare)
|
|
4495
|
+
throw new Error(`Function ${bare} needs parentheses: ${bare}(...)`);
|
|
4496
|
+
if (rest.startsWith("pi"))
|
|
4497
|
+
return "pi";
|
|
4498
|
+
const sub = /^[a-zA-Z]_(?:\d+|[a-zA-Z])/.exec(rest);
|
|
4499
|
+
return sub ? sub[0] : rest[0];
|
|
3982
4500
|
}
|
|
3983
4501
|
factor() {
|
|
3984
4502
|
if (this.peek() === "-") {
|
|
@@ -4016,9 +4534,11 @@ class Parser {
|
|
|
4016
4534
|
return num(Number(numMatch[0]));
|
|
4017
4535
|
}
|
|
4018
4536
|
// identifier (function or variable or constant)
|
|
4019
|
-
const idMatch =
|
|
4537
|
+
const idMatch = this.implicit
|
|
4538
|
+
? /^[a-zA-Z_]/.exec(this.s.slice(this.pos))
|
|
4539
|
+
: /^[a-zA-Z_]\w*/.exec(this.s.slice(this.pos));
|
|
4020
4540
|
if (idMatch) {
|
|
4021
|
-
const name = idMatch[0];
|
|
4541
|
+
const name = this.implicit ? this.implicitIdentifier() : idMatch[0];
|
|
4022
4542
|
this.pos += name.length;
|
|
4023
4543
|
if ((name === "log" || name === "clamp") && this.peek() === "(") {
|
|
4024
4544
|
this.pos++;
|