Files
paradigms/javascript/objectExpression.js
2026-04-08 21:25:17 +03:00

553 lines
14 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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,
};
}