btm-expressions
Version:
BTM (bowtie-math) is a math object model to enable parsing of mathematical expressions into a tree structure that can be manipulated, evaluated, and compared.
554 lines (503 loc) • 19.2 kB
JavaScript
/*!
* BTM JavaScript Library v@VERSION
* https://github.com/dbrianwalton/BTM
*
* Copyright D. Brian Walton
* Released under the MIT license (https://opensource.org/licenses/MIT)
*
* Date: @DATE
*/
/* ***************************************************
* Define the Multi-Operand Expression (for sum and product)
* *************************************************** */
import { exprType, opPrec } from "./BTM_root.js"
import { expression } from "./expression.js"
import { create_scalar } from "./scalar_expr.js"
import { rational_number } from "./rational_number.js"
// Do some preliminary testing to reduce the op if redundant inputs.
export function create_multiop(menv, op, inputs) {
var newInputs = [];
for (let i in inputs) {
if (inputs[i].type == exprType.multiop && inputs[i].op == op) {
newInputs.push(...inputs[i].inputs);
} else {
newInputs.push(inputs[i]);
}
}
return new multiop_expr(menv, op, newInputs);
}
export class multiop_expr extends expression {
constructor(menv, op, inputs) {
super(menv);
this.type = exprType.multiop;
this.op = op;
this.inputs = inputs; // an array
for (var i in inputs) {
if (typeof inputs[i] == 'undefined')
inputs[i] = new expression(menv);
inputs[i].parent = this;
}
switch (op) {
case '+':
this.prec = opPrec.addsub;
break;
case '*':
this.prec = opPrec.multdiv;
break;
default:
alert("Unknown multi-operand operator: '"+op+"'.");
break;
}
}
toString() {
var theStr,
opStr,
isError = false,
showOp;
theStr = '';
for (var i in this.inputs) {
showOp = true;
if (typeof this.inputs[i] == 'undefined') {
opStr = '?';
isError = true;
} else {
opStr = this.inputs[i].toString();
if ((this.inputs[i].type >= exprType.unop
&& this.inputs[i].prec <= this.prec)
|| (this.inputs[i].type == exprType.number
&& this.inputs[i].number.q != 1
&& opPrec.multdiv <= this.prec)
) {
opStr = '(' + opStr + ')';
}
}
theStr += ( i>0 ? this.op : '' ) + opStr;
}
return(theStr);
}
// Return an array containing all tested equivalent strings.
allStringEquivs() {
var allInputsArrays = [];
var indexList = [];
for (var i in this.inputs) {
allInputsArrays[i] = this.inputs[i].allStringEquivs();
indexList.push(i);
}
var inputPerms = permutations(indexList);
var retValue = [];
var theOp = this.op;
if (theOp == '|') {
// Don't want "or" to be translated as absolute value
theOp = ' $ ';
}
function buildStringEquivs(indexList, leftStr) {
if (typeof leftStr == "undefined") {
leftStr = "";
} else if (indexList.length > 0) {
leftStr += theOp;
}
if (indexList.length > 0) {
var workInputs = allInputsArrays[indexList[0]];
for (var i in workInputs) {
buildStringEquivs(indexList.slice(1), leftStr + "(" + workInputs[i] + ")");
}
} else {
retValue.push(leftStr);
}
}
for (var i in inputPerms) {
buildStringEquivs(inputPerms[i]);
}
return(retValue);
}
toTeX(showSelect) {
var theStr;
var theOp;
var opStr;
var argStrL, argStrR, opStrL, opStrR;
if (typeof showSelect == 'undefined') {
showSelect = false;
}
theOp = this.op;
if (this.op == '*') {
if (showSelect && this.select) {
theOp = '\\times';
} else {
theOp = ' ';
}
}
if (showSelect && this.select) {
argStrL = '{\\color{blue}';
argStrR = '}';
opStrL = '{\\color{red}';
opStrR = '}';
} else {
argStrL = '';
argStrR = '';
opStrL = '';
opStrR = '';
}
theStr = '';
var minPrec = this.prec;
for (var i in this.inputs) {
if (typeof this.inputs[i] == 'undefined') {
opStr = '?';
theStr += ( i>0 ? opStrL + theOp + opStrR : '' ) + opStr;
} else {
if (this.op == '*'
&& this.inputs[i].type == exprType.unop && this.inputs[i].op == '/'
&& !(showSelect && this.select))
{
opStr = argStrL + this.inputs[i].inputs[0].toTeX(showSelect) + argStrR;
if (this.inputs[i].inputs[0].type >= exprType.unop && this.inputs[i].inputs[0].prec < minPrec) {
opStr = '\\left(' + opStr + '\\right)';
}
if (theStr == '') {
theStr = '1'
}
theStr = '\\frac{' + theStr + '}{' + opStr + '}';
} else if (this.op == '+'
&& this.inputs[i].type == exprType.unop && this.inputs[i].op == '-'
&& !(showSelect && this.select))
{
opStr = argStrL + this.inputs[i].toTeX(showSelect) + argStrR;
theStr += opStr;
} else {
opStr = argStrL + this.inputs[i].toTeX(showSelect) + argStrR;
if ((this.inputs[i].type >= exprType.unop
&& this.inputs[i].prec <= minPrec)
|| (i>0 && this.op == '*' && this.inputs[i].type == exprType.number)) {
opStr = '\\left(' + opStr + '\\right)';
}
theStr += ( i>0 ? opStrL + theOp + opStrR : '' ) + opStr;
}
}
}
return(theStr);
}
toMathML() {
var theStr;
var theOp;
var opStr;
switch (this.op) {
case '+':
theOp = "<plus/>"
break;
case '*':
theOp = "<times/>"
break;
}
theStr = "<apply>" + theOp;
for (var i in this.inputs) {
if (typeof this.inputs[i] == 'undefined') {
opStr = '?';
} else {
opStr = this.inputs[i].toMathML();
}
theStr += opStr;
}
theStr += "</apply>";
return(theStr);
}
operateToTeX() {
var opString = this.op;
switch (opString) {
case '*':
opString = '\\times';
break;
}
return(opString);
}
isCommutative() {
var commutes = false;
if (this.op === '+' || this.op === '*') {
commutes = true;
}
return(commutes);
}
evaluate(bindings) {
var inputVal;
var i;
var retVal;
switch (this.op) {
case '+':
retVal = 0;
for (i in this.inputs) {
inputVal = this.inputs[i].evaluate(bindings);
retVal += inputVal;
}
break;
case '*':
retVal = 1;
for (i in this.inputs) {
inputVal = this.inputs[i].evaluate(bindings);
retVal *= inputVal;
}
break;
default:
console.log("The binary operator '" + this.op + "' is not defined.");
retVal = undefined;
break;
}
return(retVal);
}
// Flatten and also sort terms.
flatten() {
var newInputs = [];
for (var i in this.inputs) {
var nextInput = this.inputs[i].flatten();
if (nextInput.type == exprType.multiop && nextInput.op == this.op) {
for (var j in nextInput.inputs) {
newInputs.push(nextInput.inputs[j]);
}
} else {
newInputs.push(nextInput);
}
}
var retValue;
if (newInputs.length == 0) {
// Adding no elements = 0
// Multiplying no elements = 1
retValue = create_scalar(this.menv, this.op == '+' ? 0 : 1);
} else if (newInputs.length == 1) {
retValue = newInputs[0];
} else {
// Sort the inputs by precedence for products
// Usually very small, so bubble sort is good enough
if (this.op=='*') {
var tmp;
for (var i=0; i<newInputs.length-1; i++) {
for (var j=i+1; j<newInputs.length; j++) {
if (newInputs[i].type > newInputs[j].type) {
tmp = newInputs[i];
newInputs[i] = newInputs[j];
newInputs[j] = tmp;
}
}
}
}
retValue = create_multiop(this.menv, this.op, newInputs);
}
return(retValue);
}
// See if this operator is now redundant.
// Return the resulting expression.
reduce() {
var workExpr = super.reduce();
var newExpr = workExpr;
if (workExpr.type == exprType.multiop && workExpr.inputs.length <= 1) {
if (workExpr.inputs.length == 0) {
// Sum with no elements = 0
// Product with no elements = 1
newExpr = create_scalar(this.menv, workExpr.op == '+' ? 0 : 1);
} else {
// Sum or product with one element *is* that element.
newExpr = workExpr.inputs[0];
}
newExpr.parent = this.parent;
if (this.parent !== null) {
this.parent.inputSubst(this, newExpr);
}
}
return(newExpr);
}
simplifyConstants() {
var constIndex = [],
newInputs = [];
// Simplify all inputs
// Notice which inputs are themselves constant
for (let i in this.inputs) {
this.inputs[i] = this.inputs[i].simplifyConstants();
this.inputs[i].parent = this;
if (this.inputs[i].type == exprType.number ||
(this.inputs[i].type == exprType.unop && this.inputs[i].inputs[0].type == exprType.number)
) {
constIndex.push(i);
} else {
newInputs.push(this.inputs[i]);
}
}
// For all inputs that are constants, group them together and simplify.
var newExpr = this;
if (constIndex.length > 1) {
var newConstant;
switch (this.op) {
case '+':
newConstant = new rational_number(0);
for (let i in constIndex) {
if (this.inputs[constIndex[i]].type == exprType.number) {
newConstant = newConstant.add(this.inputs[constIndex[i]].number);
} else if (this.inputs[constIndex[i]].type == exprType.unop) {
switch (this.inputs[constIndex[i]].op) {
case '+':
case '*':
newConstant = newConstant.add(this.inputs[constIndex[i]].inputs[0].number);
break;
case '-':
newConstant = newConstant.subtract(this.inputs[constIndex[i]].inputs[0].number);
break;
case '/':
newConstant = newConstant.add(this.inputs[constIndex[i]].inputs[0].number.multInverse());
break;
}
}
}
break;
case '*':
newConstant = new rational_number(1);
for (let i in constIndex) {
if (this.inputs[constIndex[i]].type == exprType.number) {
newConstant = newConstant.multiply(this.inputs[constIndex[i]].number);
} else if (this.inputs[constIndex[i]].type == exprType.unop) {
switch (this.inputs[constIndex[i]].op) {
case '+':
case '*':
newConstant = newConstant.multiply(this.inputs[constIndex[i]].inputs[0].number);
break;
case '-':
newConstant = newConstant.multiply(this.inputs[constIndex[i]].inputs[0].number.addInverse());
break;
case '/':
newConstant = newConstant.divide(this.inputs[constIndex[i]].inputs[0].number);
break;
}
}
}
break;
}
// For addition, the constant goes to the end.
// For multiplication, the constant goes to the beginning.
var newInput;
switch (this.op) {
case '+':
newInputs.push(newInput = create_scalar(this.menv, newConstant));
break;
case '*':
newInputs.splice(0, 0, newInput = create_scalar(this.menv, newConstant));
break;
}
if (newInputs.length == 1) {
newExpr = newInputs[0];
} else {
newInput.parent = this;
newExpr = create_multiop(this.menv, this.op, newInputs);
}
}
return(newExpr);
}
// This comparison routine needs to deal with two issues.
// (1) The passed expression has more inputs than this (in which case we group them)
// (2) Possibility of commuting makes the match work.
match(expr, bindings) {
function copyBindings(bindings)
{
var retValue = {};
for (var key in bindings) {
retValue[key] = bindings[key];
}
return(retValue);
}
var retValue = null,
n = this.inputs.length;
if ((expr.type == exprType.multiop || expr.type == exprType.binop)
&& this.op == expr.op && n <= expr.inputs.length) {
// Match on first n-1 and group remainder at end.
var cmpExpr,
cmpInputs = [];
for (var i=0; i<n; i++) {
if (i<(n-1) || expr.inputs.length==n) {
cmpInputs[i] = expr.inputs[i].copy();
} else {
// Create copies of the inputs
var newInputs = [];
for (var j=0; j<=expr.inputs.length-n; j++) {
newInputs[j] = expr.inputs[n+j-1].copy();
}
cmpInputs[i] = create_multiop(this.menv, expr.op, newInputs);
}
}
cmpExpr = create_multiop(this.menv, expr.op, cmpInputs);
// Now make the comparison.
retValue = copyBindings(bindings);
retValue = expression.prototype.match.call(this, cmpExpr, retValue);
// If still fail to match, try the reverse grouping: match on last n-1 and group beginning.
if (retValue == null && n < expr.inputs.length) {
var diff = expr.inputs.length - n;
cmpInputs = [];
for (var i=0; i<n; i++) {
if (i==0) {
// Create copies of the inputs
var newInputs = [];
for (var j=0; j<=diff; j++) {
newInputs[j] = expr.inputs[j].copy();
}
cmpInputs[i] = create_multiop(this.menv, expr.op, newInputs);
} else {
cmpInputs[i] = expr.inputs[diff+i].copy();
}
}
cmpExpr = create_multiop(this.menv, expr.op, cmpInputs);
// Now make the comparison.
retValue = copyBindings(bindings);
retValue = expression.prototype.match.call(this, cmpExpr, retValue);
}
}
return(retValue);
}
copy() {
var newInputs = new Array();
for (var i in this.inputs) {
newInputs.push(this.inputs[i].copy());
}
return (create_multiop(this.menv, this.op, newInputs));
}
compose(bindings) {
var newInputs = [];
for (var i in this.inputs) {
newInputs.push(this.inputs[i].compose(bindings));
}
var retValue;
if (newInputs.length == 0) {
retValue = create_scalar(this.menv, this.op == '+' ? 0 : 1);
} else if (newInputs.length == 1) {
retValue = newInputs[0];
} else {
retValue = create_multiop(this.menv, this.op, newInputs);
}
return(retValue);
}
derivative(ivar, varList) {
var dTerms = [];
var theDeriv;
var i, dudx;
for (i in this.inputs) {
if (!this.inputs[i].isConstant()) {
dudx = this.inputs[i].derivative(ivar, varList);
switch (this.op) {
case '+':
dTerms.push(dudx);
break;
case '*':
var dProdTerms = [];
for (let j in this.inputs) {
if (i == j) {
dProdTerms.push(dudx);
} else {
dProdTerms.push(this.inputs[j].compose({}));
}
}
dTerms.push(create_multiop(this.menv, '*', dProdTerms));
break;
}
}
}
if (dTerms.length == 0) {
theDeriv = create_scalar(this.menv, 0);
} else if (dTerms.length == 1) {
theDeriv = dTerms[0];
} else {
theDeriv = create_multiop(this.menv, '+', dTerms);
}
return(theDeriv);
}
}