first commit
This commit is contained in:
552
javascript/objectExpression.js
Normal file
552
javascript/objectExpression.js
Normal file
@@ -0,0 +1,552 @@
|
||||
"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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user