553 lines
14 KiB
JavaScript
553 lines
14 KiB
JavaScript
"use strict";
|
||
|
||
class Expression {
|
||
evaluate() {
|
||
throw new Error("Not implemented");
|
||
}
|
||
toString() {
|
||
throw new Error("Not implemented");
|
||
}
|
||
diff() {
|
||
throw new Error("Not implemented");
|
||
}
|
||
simplify() {
|
||
return this;
|
||
}
|
||
}
|
||
|
||
class Const extends Expression {
|
||
constructor(value) {
|
||
super();
|
||
this._value = value;
|
||
}
|
||
|
||
evaluate(_x, _y, _z) {
|
||
return this._value;
|
||
}
|
||
toString() {
|
||
return String(this._value);
|
||
}
|
||
diff(_varName) {
|
||
return ZERO;
|
||
}
|
||
simplify() {
|
||
return this;
|
||
}
|
||
}
|
||
|
||
const ZERO = new Const(0);
|
||
const ONE = new Const(1);
|
||
|
||
const VAR_INDEX = { x: 0, y: 1, z: 2 };
|
||
|
||
class Variable extends Expression {
|
||
constructor(name) {
|
||
super();
|
||
this._name = name;
|
||
this._index = VAR_INDEX[name];
|
||
}
|
||
|
||
evaluate(...args) {
|
||
return args[this._index];
|
||
}
|
||
toString() {
|
||
return this._name;
|
||
}
|
||
|
||
diff(varName) {
|
||
return varName === this._name ? ONE : ZERO;
|
||
}
|
||
|
||
simplify() {
|
||
return this;
|
||
}
|
||
}
|
||
|
||
function exprEquals(a, b) {
|
||
if (a === b) return true;
|
||
if (a.constructor !== b.constructor) return false;
|
||
if (a instanceof Const) return a._value === b._value;
|
||
if (a instanceof Variable) return a._name === b._name;
|
||
if (a instanceof BinaryOp)
|
||
return exprEquals(a._left, b._left) && exprEquals(a._right, b._right);
|
||
if (a instanceof UnaryOp) return exprEquals(a._operand, b._operand);
|
||
if (a instanceof NaryOp) {
|
||
if (a._args.length !== b._args.length) return false;
|
||
return a._args.every((arg, i) => exprEquals(arg, b._args[i]));
|
||
}
|
||
return false;
|
||
}
|
||
|
||
class BinaryOp extends Expression {
|
||
constructor(left, right) {
|
||
super();
|
||
this._left = left;
|
||
this._right = right;
|
||
}
|
||
|
||
evaluate(x, y, z) {
|
||
return this._op(
|
||
this._left.evaluate(x, y, z),
|
||
this._right.evaluate(x, y, z),
|
||
);
|
||
}
|
||
|
||
toString() {
|
||
return `${this._left.toString()} ${this._right.toString()} ${this._symbol}`;
|
||
}
|
||
|
||
simplify() {
|
||
const l = this._left.simplify();
|
||
const r = this._right.simplify();
|
||
const lConst = l instanceof Const;
|
||
const rConst = r instanceof Const;
|
||
|
||
const special = this._simplifySpecial(l, r, lConst, rConst);
|
||
if (special !== null) return special;
|
||
|
||
if (lConst && rConst) {
|
||
return new Const(this._op(l._value, r._value));
|
||
}
|
||
|
||
return this._rebuild(l, r);
|
||
}
|
||
|
||
_rebuild(l, r) {
|
||
return new this.constructor(l, r);
|
||
}
|
||
|
||
_simplifySpecial(l, r, lConst, rConst) {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
class UnaryOp extends Expression {
|
||
constructor(operand) {
|
||
super();
|
||
this._operand = operand;
|
||
}
|
||
|
||
evaluate(x, y, z) {
|
||
return this._op(this._operand.evaluate(x, y, z));
|
||
}
|
||
|
||
toString() {
|
||
return `${this._operand.toString()} ${this._symbol}`;
|
||
}
|
||
|
||
simplify() {
|
||
const inner = this._operand.simplify();
|
||
if (inner instanceof Const) {
|
||
return new Const(this._op(inner._value));
|
||
}
|
||
return new this.constructor(inner);
|
||
}
|
||
}
|
||
|
||
// Base class for N-ary operations (N = 1..5)
|
||
class NaryOp extends Expression {
|
||
constructor(...args) {
|
||
super();
|
||
this._args = args;
|
||
}
|
||
|
||
evaluate(x, y, z) {
|
||
return this._op(...this._args.map((a) => a.evaluate(x, y, z)));
|
||
}
|
||
|
||
toString() {
|
||
return `${this._args.map((a) => a.toString()).join(" ")} ${this._symbol}`;
|
||
}
|
||
|
||
simplify() {
|
||
const simplified = this._args.map((a) => a.simplify());
|
||
if (simplified.every((a) => a instanceof Const)) {
|
||
return new Const(this._op(...simplified.map((a) => a._value)));
|
||
}
|
||
return new this.constructor(...simplified);
|
||
}
|
||
}
|
||
|
||
class Add extends BinaryOp {
|
||
get _symbol() {
|
||
return "+";
|
||
}
|
||
_op(a, b) {
|
||
return a + b;
|
||
}
|
||
|
||
diff(varName) {
|
||
return new Add(this._left.diff(varName), this._right.diff(varName));
|
||
}
|
||
|
||
_simplifySpecial(l, r, lConst, rConst) {
|
||
if (lConst && l._value === 0) return r;
|
||
if (rConst && r._value === 0) return l;
|
||
return null;
|
||
}
|
||
}
|
||
|
||
class Subtract extends BinaryOp {
|
||
get _symbol() {
|
||
return "-";
|
||
}
|
||
_op(a, b) {
|
||
return a - b;
|
||
}
|
||
|
||
diff(varName) {
|
||
return new Subtract(this._left.diff(varName), this._right.diff(varName));
|
||
}
|
||
|
||
_simplifySpecial(l, r, lConst, rConst) {
|
||
if (rConst && r._value === 0) return l;
|
||
if (lConst && l._value === 0) return new Negate(r).simplify();
|
||
if (exprEquals(l, r)) return ZERO;
|
||
return null;
|
||
}
|
||
}
|
||
|
||
function cancelCommonFactor(num, den) {
|
||
if (exprEquals(num, den)) return [ONE, ONE];
|
||
|
||
if (den instanceof Multiply) {
|
||
const [dA, dB] = [den._left, den._right];
|
||
if (exprEquals(num, dA)) return [ONE, dB];
|
||
if (exprEquals(num, dB)) return [ONE, dA];
|
||
if (num instanceof Multiply) {
|
||
const [nA, nB] = [num._left, num._right];
|
||
if (exprEquals(nA, dA)) return [nB, dB];
|
||
if (exprEquals(nA, dB)) return [nB, dA];
|
||
if (exprEquals(nB, dA)) return [nA, dB];
|
||
if (exprEquals(nB, dB)) return [nA, dA];
|
||
}
|
||
}
|
||
|
||
if (num instanceof Multiply) {
|
||
const [nA, nB] = [num._left, num._right];
|
||
if (exprEquals(den, nA)) return [nB, ONE];
|
||
if (exprEquals(den, nB)) return [nA, ONE];
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
class Multiply extends BinaryOp {
|
||
get _symbol() {
|
||
return "*";
|
||
}
|
||
_op(a, b) {
|
||
return a * b;
|
||
}
|
||
|
||
diff(varName) {
|
||
return new Add(
|
||
new Multiply(this._left.diff(varName), this._right),
|
||
new Multiply(this._left, this._right.diff(varName)),
|
||
);
|
||
}
|
||
|
||
_simplifySpecial(l, r, lConst, rConst) {
|
||
if ((lConst && l._value === 0) || (rConst && r._value === 0)) return ZERO;
|
||
if (lConst && l._value === 1) return r;
|
||
if (rConst && r._value === 1) return l;
|
||
return null;
|
||
}
|
||
}
|
||
|
||
class Divide extends BinaryOp {
|
||
get _symbol() {
|
||
return "/";
|
||
}
|
||
_op(a, b) {
|
||
return a / b;
|
||
}
|
||
|
||
diff(varName) {
|
||
return new Divide(
|
||
new Subtract(
|
||
new Multiply(this._left.diff(varName), this._right),
|
||
new Multiply(this._left, this._right.diff(varName)),
|
||
),
|
||
new Multiply(this._right, this._right),
|
||
);
|
||
}
|
||
|
||
_simplifySpecial(l, r, lConst, rConst) {
|
||
if (lConst && l._value === 0) return ZERO;
|
||
if (rConst && r._value === 1) return l;
|
||
|
||
const cancelled = cancelCommonFactor(l, r);
|
||
if (cancelled !== null) {
|
||
const [newNum, newDen] = cancelled;
|
||
return new Divide(newNum, newDen).simplify();
|
||
}
|
||
|
||
return null;
|
||
}
|
||
}
|
||
|
||
class Negate extends UnaryOp {
|
||
get _symbol() {
|
||
return "negate";
|
||
}
|
||
_op(a) {
|
||
return -a;
|
||
}
|
||
|
||
diff(varName) {
|
||
return new Negate(this._operand.diff(varName));
|
||
}
|
||
|
||
simplify() {
|
||
const inner = this._operand.simplify();
|
||
if (inner instanceof Const) return new Const(-inner._value);
|
||
return new Negate(inner);
|
||
}
|
||
}
|
||
|
||
// ── Power ──────────────────────────────────────────────────────────────────
|
||
// pow(f, g) = f ^ g
|
||
// (f ^ g)' = f ^ g * (g' * ln|f| + g * f' / f)
|
||
class Power extends BinaryOp {
|
||
get _symbol() {
|
||
return "pow";
|
||
}
|
||
_op(a, b) {
|
||
return Math.pow(a, b);
|
||
}
|
||
|
||
diff(varName) {
|
||
const f = this._left;
|
||
const g = this._right;
|
||
|
||
// Simplify f and g first so that e.g. Negate(Const(493)) is treated as Const(-493)
|
||
const gs = g.simplify();
|
||
const fs = f.simplify();
|
||
|
||
// Also compute derivatives and simplify them to detect zero
|
||
const fdiff = f.diff(varName).simplify();
|
||
const gdiff = g.diff(varName).simplify();
|
||
const fdiffZero = fdiff instanceof Const && fdiff._value === 0;
|
||
const gdiffZero = gdiff instanceof Const && gdiff._value === 0;
|
||
|
||
if (gs instanceof Const || gdiffZero) {
|
||
// Power rule: (f^n)' = n * f^(n-1) * f'
|
||
// Works when exponent is constant OR doesn't depend on varName
|
||
const n = gs instanceof Const ? gs._value : g;
|
||
const nExpr = gs instanceof Const ? new Const(gs._value) : g;
|
||
const nMinus1 = gs instanceof Const ? new Const(gs._value - 1) : new Subtract(g, ONE);
|
||
return new Multiply(
|
||
new Multiply(nExpr, new Power(f, nMinus1)),
|
||
fdiff,
|
||
);
|
||
}
|
||
|
||
if (fs instanceof Const || fdiffZero) {
|
||
// Exponential rule: (a^g)' = a^g * ln(|a|) * g'
|
||
// Works when base is constant OR doesn't depend on varName
|
||
return new Multiply(
|
||
new Multiply(new Power(f, g), new Log(new Const(Math.E), f)),
|
||
gdiff,
|
||
);
|
||
}
|
||
|
||
// General case: both base and exponent truly depend on variable
|
||
// (f^g)' = f^g * (g' * ln|f| + g * f'/f)
|
||
return new Multiply(
|
||
new Power(f, g),
|
||
new Add(
|
||
new Multiply(gdiff, new Log(new Const(Math.E), f)),
|
||
new Multiply(g, new Divide(fdiff, f)),
|
||
),
|
||
);
|
||
}
|
||
|
||
_simplifySpecial(l, r, lConst, rConst) {
|
||
if (rConst && r._value === 0) return ONE;
|
||
if (rConst && r._value === 1) return l;
|
||
if (lConst && l._value === 1) return ONE;
|
||
if (lConst && l._value === 0) return ZERO;
|
||
return null;
|
||
}
|
||
}
|
||
|
||
// ── Log ────────────────────────────────────────────────────────────────────
|
||
// log(base, x) = ln|x| / ln|base|
|
||
// Matches the postfix token "log": `base x log`
|
||
//
|
||
// Full quotient-rule derivative (base may depend on the variable):
|
||
// log_b(f) = ln|f| / ln|b|
|
||
// d/dx = (f'/f * ln|b| - b'/b * ln|f|) / (ln|b|)^2
|
||
// = f'/(f * ln|b|) - b' * ln|f| / (b * (ln|b|)^2)
|
||
class Log extends BinaryOp {
|
||
get _symbol() {
|
||
return "log";
|
||
}
|
||
_op(base, x) {
|
||
return Math.log(Math.abs(x)) / Math.log(Math.abs(base));
|
||
}
|
||
|
||
diff(varName) {
|
||
const b = this._left; // base expression
|
||
const f = this._right; // argument expression
|
||
|
||
// Compute derivatives and simplify to detect zeros
|
||
const bdiff = b.diff(varName).simplify();
|
||
const fdiff = f.diff(varName).simplify();
|
||
const bdiffZero = bdiff instanceof Const && bdiff._value === 0;
|
||
const fdiffZero = fdiff instanceof Const && fdiff._value === 0;
|
||
|
||
// log_b(f) = ln|f| / ln|b|
|
||
// Quotient rule: d/dx = (f'/f * ln|b| - b'/b * ln|f|) / (ln|b|)^2
|
||
// = f'/(f * ln|b|) - b'*ln|f| / (b * (ln|b|)^2)
|
||
|
||
// Special case: log_b(b) = 1 always => d/dx = 0
|
||
if (exprEquals(b, f)) return ZERO;
|
||
|
||
const lnB = new Log(new Const(Math.E), b);
|
||
|
||
// Special case: base is constant (b' = 0) => d/dx = f'/(f * ln|b|)
|
||
if (bdiffZero) {
|
||
return new Divide(fdiff, new Multiply(f, lnB));
|
||
}
|
||
|
||
// Special case: argument is constant (f' = 0) => d/dx = -log_b(f) / (b * ln|b|)
|
||
// derived from general formula with fdiff=0: -(b'/b * log_b(f)) / lnB
|
||
// restructured to avoid 1/b factor: -Log(b,f) / (b * lnB)
|
||
if (fdiffZero) {
|
||
return new Negate(new Divide(new Log(b, f), new Multiply(b, lnB)));
|
||
}
|
||
|
||
// General case: use formula (f'/f - b'/b * log_b(f)) / ln|b|
|
||
// This avoids introducing ln|f| separately (which would create structurally
|
||
// different but numerically equal subexpressions that don't cancel).
|
||
const inner = new Subtract(
|
||
new Divide(fdiff, f),
|
||
new Multiply(new Divide(bdiff, b), new Log(b, f)),
|
||
);
|
||
return new Divide(inner, lnB);
|
||
}
|
||
|
||
_simplifySpecial(l, r, lConst, rConst) {
|
||
// log_b(1) = 0
|
||
if (rConst && r._value === 1) return ZERO;
|
||
// log_b(b) = 1 (when base and argument are structurally equal)
|
||
if (exprEquals(l, r)) return ONE;
|
||
return null;
|
||
}
|
||
}
|
||
|
||
// ── SumN ───────────────────────────────────────────────────────────────────
|
||
// sum1..sum5 – sum of N arguments
|
||
// Derivative: sum of derivatives of each argument.
|
||
function makeSumN(n) {
|
||
return class extends NaryOp {
|
||
get _symbol() {
|
||
return `sum${n}`;
|
||
}
|
||
_op(...vals) {
|
||
return vals.reduce((acc, v) => acc + v, 0);
|
||
}
|
||
diff(varName) {
|
||
return this._args
|
||
.map((a) => a.diff(varName))
|
||
.reduce((acc, d) => new Add(acc, d));
|
||
}
|
||
simplify() {
|
||
const simplified = this._args.map((a) => a.simplify());
|
||
// Drop zero summands
|
||
const nonZero = simplified.filter(
|
||
(a) => !(a instanceof Const && a._value === 0),
|
||
);
|
||
if (nonZero.length === 0) return ZERO;
|
||
if (nonZero.length === 1) return nonZero[0];
|
||
if (nonZero.every((a) => a instanceof Const)) {
|
||
return new Const(nonZero.reduce((acc, a) => acc + a._value, 0));
|
||
}
|
||
return new this.constructor(...simplified);
|
||
}
|
||
};
|
||
}
|
||
|
||
const Sum1 = makeSumN(1);
|
||
const Sum2 = makeSumN(2);
|
||
const Sum3 = makeSumN(3);
|
||
const Sum4 = makeSumN(4);
|
||
const Sum5 = makeSumN(5);
|
||
|
||
// ── Operation registry & parser ────────────────────────────────────────────
|
||
const OPERATIONS = {
|
||
"+": Add,
|
||
"-": Subtract,
|
||
"*": Multiply,
|
||
"/": Divide,
|
||
negate: Negate,
|
||
pow: Power,
|
||
log: Log,
|
||
sum1: Sum1,
|
||
sum2: Sum2,
|
||
sum3: Sum3,
|
||
sum4: Sum4,
|
||
sum5: Sum5,
|
||
};
|
||
|
||
// Arity for each token (BinaryOp = 2, UnaryOp = 1, NaryOp = N)
|
||
const ARITY = {
|
||
"+": 2,
|
||
"-": 2,
|
||
"*": 2,
|
||
"/": 2,
|
||
negate: 1,
|
||
pow: 2,
|
||
log: 2,
|
||
sum1: 1,
|
||
sum2: 2,
|
||
sum3: 3,
|
||
sum4: 4,
|
||
sum5: 5,
|
||
};
|
||
|
||
function parse(expr) {
|
||
const tokens = expr
|
||
.trim()
|
||
.split(/\s+/)
|
||
.filter((t) => t.length > 0);
|
||
const stack = [];
|
||
|
||
for (const token of tokens) {
|
||
if (token in OPERATIONS) {
|
||
const Cls = OPERATIONS[token];
|
||
const arity = ARITY[token];
|
||
const operands = stack.splice(stack.length - arity, arity);
|
||
stack.push(new Cls(...operands));
|
||
} else if (token in VAR_INDEX) {
|
||
stack.push(new Variable(token));
|
||
} else {
|
||
stack.push(new Const(Number(token)));
|
||
}
|
||
}
|
||
|
||
return stack.pop();
|
||
}
|
||
|
||
if (typeof module !== "undefined") {
|
||
module.exports = {
|
||
Const,
|
||
Variable,
|
||
Add,
|
||
Subtract,
|
||
Multiply,
|
||
Divide,
|
||
Negate,
|
||
Power,
|
||
Log,
|
||
Sum1,
|
||
Sum2,
|
||
Sum3,
|
||
Sum4,
|
||
Sum5,
|
||
parse,
|
||
};
|
||
}
|