I have a user input for an equation - this input generates LaTeX code using a separate API which I did not code (namely, Mathquill, not that it matters).
My problem is best illustrated by an example: suppose the LaTeX code generated from the user input was this:
x^2+3x-10sin\left(2x\right)
How would I convert this (on the fly of course) into a JavaScript function which, hard-coded, would look like this:
function(x) {
return Math.pow(x, 2) + 3 * x - 10 * Math.sin(2 * x);
}
Are there any APIs or am I looking at writing something which will interpret the LaTeX symbols and make a function, somehow? Or what?
I have written a (by no means general purpose) solution, heavily based on George's code.
Here it is:
var CALC_CONST = {
// define your constants
e: Math.E,
pi: Math.PI
};
var CALC_NUMARGS = [
[/^(\^|\*|\/|\+|\-)$/, 2],
[/^(floor|ceil|(sin|cos|tan|sec|csc|cot)h?)$/, 1]
];
var Calc = function(expr, infix) {
this.valid = true;
this.expr = expr;
if (!infix) {
// by default treat expr as raw latex
this.expr = this.latexToInfix(expr);
}
var OpPrecedence = function(op) {
if (typeof op == "undefined") return 0;
return op.match(/^(floor|ceil|(sin|cos|tan|sec|csc|cot)h?)$/) ? 10
: (op === "^") ? 9
: (op === "*" || op === "/") ? 8
: (op === "+" || op === "-") ? 7
: 0;
}
var OpAssociativity = function(op) {
return op.match(/^(floor|ceil|(sin|cos|tan|sec|csc|cot)h?)$/) ? "R" : "L";
}
var numArgs = function(op) {
for (var i = 0; i < CALC_NUMARGS.length; i++) {
if (CALC_NUMARGS[i][0].test(op)) return CALC_NUMARGS[i][1];
}
return false;
}
this.rpn_expr = [];
var rpn_expr = this.rpn_expr;
this.expr = this.expr.replace(/\s+/g, "");
// This nice long regex matches any valid token in a user
// supplied expression (e.g. an operator, a constant or
// a variable)
var in_tokens = this.expr.match(/(\^|\*|\/|\+|\-|\(|\)|[a-zA-Z0-9\.]+)/gi);
var op_stack = [];
in_tokens.forEach(function(token) {
if (/^[a-zA-Z]$/.test(token)) {
if (CALC_CONST.hasOwnProperty(token)) {
// Constant. Pushes a value onto the stack.
rpn_expr.push(["num", CALC_CONST[token]]);
}
else {
// Variables (i.e. x as in f(x))
rpn_expr.push(["var", token]);
}
}
else {
var numVal = parseFloat(token);
if (!isNaN(numVal)) {
// Number - push onto the stack
rpn_expr.push(["num", numVal]);
}
else if (token === ")") {
// Pop tokens off the op_stack onto the rpn_expr until we reach the matching (
while (op_stack[op_stack.length - 1] !== "(") {
rpn_expr.push([numArgs(op_stack[op_stack.length - 1]), op_stack.pop()]);
if (op_stack.length === 0) {
this.valid = false;
return;
}
}
// remove the (
op_stack.pop();
}
else if (token === "(") {
op_stack.push(token);
}
else {
// Operator
var tokPrec = OpPrecedence(token),
headPrec = OpPrecedence(op_stack[op_stack.length - 1]);
while ((OpAssociativity(token) === "L" && tokPrec <= headPrec) ||
(OpAssociativity(token) === "R" && tokPrec < headPrec)) {
rpn_expr.push([numArgs(op_stack[op_stack.length - 1]), op_stack.pop()]);
if (op_stack.length === 0) break;
headPrec = OpPrecedence(op_stack[op_stack.length - 1]);
}
op_stack.push(token);
}
}
});
// Push all remaining operators onto the final expression
while (op_stack.length > 0) {
var popped = op_stack.pop();
if (popped === ")") {
this.valid = false;
break;
}
rpn_expr.push([numArgs(popped), popped]);
}
}
/**
* returns the result of evaluating the current expression
*/
Calc.prototype.eval = function(x) {
var stack = [], rpn_expr = this.rpn_expr;
rpn_expr.forEach(function(token) {
if (typeof token[0] == "string") {
switch (token[0]) {
case "var":
// Variable, i.e. x as in f(x); push value onto stack
//if (token[1] != "x") return false;
stack.push(x);
break;
case "num":
// Number; push value onto stack
stack.push(token[1]);
break;
}
}
else {
// Operator
var numArgs = token[0];
var args = [];
do {
args.unshift(stack.pop());
} while (args.length < numArgs);
switch (token[1]) {
/* BASIC ARITHMETIC OPERATORS */
case "*":
stack.push(args[0] * args[1]);
break;
case "/":
stack.push(args[0] / args[1]);
break;
case "+":
stack.push(args[0] + args[1]);
break;
case "-":
stack.push(args[0] - args[1]);
break;
// exponents
case "^":
stack.push(Math.pow(args[0], args[1]));
break;
/* TRIG FUNCTIONS */
case "sin":
stack.push(Math.sin(args[0]));
break;
case "cos":
stack.push(Math.cos(args[0]));
break;
case "tan":
stack.push(Math.tan(args[0]));
break;
case "sec":
stack.push(1 / Math.cos(args[0]));
break;
case "csc":
stack.push(1 / Math.sin(args[0]));
break;
case "cot":
stack.push(1 / Math.tan(args[0]));
break;
case "sinh":
stack.push(.5 * (Math.pow(Math.E, args[0]) - Math.pow(Math.E, -args[0])));
break;
case "cosh":
stack.push(.5 * (Math.pow(Math.E, args[0]) + Math.pow(Math.E, -args[0])));
break;
case "tanh":
stack.push((Math.pow(Math.E, 2*args[0]) - 1) / (Math.pow(Math.E, 2*args[0]) + 1));
break;
case "sech":
stack.push(2 / (Math.pow(Math.E, args[0]) + Math.pow(Math.E, -args[0])));
break;
case "csch":
stack.push(2 / (Math.pow(Math.E, args[0]) - Math.pow(Math.E, -args[0])));
break;
case "coth":
stack.push((Math.pow(Math.E, 2*args[0]) + 1) / (Math.pow(Math.E, 2*args[0]) - 1));
break;
case "floor":
stack.push(Math.floor(args[0]));
break;
case "ceil":
stack.push(Math.ceil(args[0]));
break;
default:
// unknown operator; error out
return false;
}
}
});
return stack.pop();
};
Calc.prototype.latexToInfix = function(latex) {
/**
* function: converts latex notation to infix notation (human-readable, to be converted
* again to prefix in order to be processed
*
* Supported functions / operators / notation:
* parentheses, exponents, adding, subtracting, multipling, dividing, fractions
* trigonometric (including hyperbolic) functions, floor, ceil
*/
var infix = latex;
infix = infix
.replace(/\\frac{([^}]+)}{([^}]+)}/g, "($1)/($2)") // fractions
.replace(/\\left\(/g, "(") // open parenthesis
.replace(/\\right\)/g, ")") // close parenthesis
.replace(/[^\(](floor|ceil|(sin|cos|tan|sec|csc|cot)h?)\(([^\(\)]+)\)[^\)]/g, "($&)") // functions
.replace(/([^(floor|ceil|(sin|cos|tan|sec|csc|cot)h?|\+|\-|\*|\/)])\(/g, "$1*(")
.replace(/\)([\w])/g, ")*$1")
.replace(/([0-9])([A-Za-z])/g, "$1*$2")
;
return infix;
};
Example of usage:
var latex = "e^x+\\frac{2}{3}x-4sin\\left(x\\right)";
var calc = new Calc(latex);
var test = calc.eval(3.5); // 36.85191820278412
Well, you're going to have to decide on exactly which operations you support at some point. After that it shouldn't be hard to implement an evaluator using a parser like the Shunting-yard algorithm to yield a representation of the equation that is more easy to evaluate (that is, an abstract syntax tree).
I have a simple example of this kind of evaluator written in JavaScript at: http://gjp.cc/projects/logic_tables.html It takes logical expressions like !(p ^^ q) & ~(p || q)
instead of LaTeX, but it might still be a useful example for you.
The JavaScript (http://gpittarelli.com/projects/logic_tables.js):
var CALCULATOR_CONSTANTS = {
/* True values. */
't': true,
'true': true,
/* False values. */
'c': false,
'false': false
};
// The Calculator constructor takes an expression and parses
// it into an AST (refered to as rpn_expr)
var Calculator = function(expr) {
this.valid = true;
var OpPrecedence = function(op) {
return (op === "!" || op === "~")? 9
: (op === "&" || op === "&&")? 7
: (op === "|" || op === "||" )? 7
: (op === "^" || op === "^^")? 7
: (op === "->")? 5
: (op === "<-")? 5
: 0;
}
var OpAssociativity = function(op) {
return (op === "!" || op === "~")? "R":"L";
}
this.rpn_expr = [];
this.variables = [];
var rpn_expr = this.rpn_expr;
var variables = this.variables;
expr = expr.replace(/\s+/g, "");
// This nice long regex matches any valid token in a user
// supplied expression (e.g. an operator, a constant or
// a variable)
var in_tokens = expr.match(/(\!|\~|\|+|&+|\(|\)|\^+|(->)|(<-)|[a-zA-Z0-9]+)/gi);
var op_stack = [];
in_tokens.forEach(function(token) {
if (/[a-zA-Z0-9]+/.test(token)) {
if (CALCULATOR_CONSTANTS.hasOwnProperty(token)) {
// Constant. Pushes a boolean value onto the stack.
rpn_expr.push(CALCULATOR_CONSTANTS[token]);
} else {
// Variables
rpn_expr.push(token);
variables.push(token);
}
}
else if (token === ")") {
// Pop tokens off the op_stack onto the rpn_expr until we
// reach the matching (
while (op_stack[op_stack.length-1] !== "(") {
rpn_expr.push(op_stack.pop());
if (op_stack.length === 0) {
this.valid = false;
return;
}
}
// Remove the (
op_stack.pop();
}
else if (token === "(") {
op_stack.push(token);
}
else {
// Operator
var tokPrec = OpPrecedence( token ),
headPrec = OpPrecedence( op_stack[op_stack.length-1] );
while ((OpAssociativity(token) === "L" && tokPrec <= headPrec)
|| (OpAssociativity(token) === "R" && tokPrec < headPrec) ) {
rpn_expr.push(op_stack.pop());
if (op_stack.length === 0)
break;
headPrec = OpPrecedence( op_stack[op_stack.length-1] );
}
op_stack.push(token);
}
});
// Push all remaining operators onto the final expression
while (op_stack.length > 0) {
var popped = op_stack.pop();
if (popped === ")") {
this.valid = false;
break;
}
rpn_expr.push(popped);
}
this.optimize();
}
/** Returns the variables used in the currently loaded expression. */
Calculator.prototype.getVariables = function() { return this.variables; }
Calculator.prototype.optimize = function() {
// Single-pass optimization, mainly just to show the concept.
// Looks for statements that can be pre computed, eg:
// p | true
// q & false
// r ^ r
// etc...
// We do this by reading through the RPN expression as if we were
// evaluating it, except instead rebuild it as we go.
var stack = [], rpn_expr = this.rpn_expr;
rpn_expr.forEach(function(token) {
if (typeof token === "boolean") {
// Constant.
stack.push(token);
} else if (/[a-zA-Z0-9]+/.test(token)) {
// Identifier - push onto the stack
stack.push(token);
} else {
// Operator - The actual optimization takes place here.
// TODO: Add optimizations for more operators.
if (token === "^" || token === "^^") {
var a = stack.pop(), b = stack.pop();
if (a === b) { // p ^ p == false
stack.push(false);
} else {
stack.push(b);
stack.push(a);
stack.push(token);
}
} else if (token === "|" || token === "||") {
var a = stack.pop(), b = stack.pop();
if (a === true || b === true) {
// If either of the operands is a tautology, OR is
// also a tautology.
stack.push(true);
} else if (a === b) { // p | p == p
stack.push(a);
} else {
stack.push(b);
stack.push(a);
stack.push(token);
}
} else if (token === "!" || token === "~") {
var p = stack.pop();
if (typeof p === "boolean") {
// NOT of a constant value can always
// be precalculated.
stack.push(!p);
} else {
stack.push(p);
stack.push(token);
}
} else {
stack.push(token);
}
}
});
this.rpn_expr = stack;
}
/**
* returns the result of evaluating the current expressions
* with the passed in <code>variables</code> object. <i>variables</i>
* should be an object who properties map from key => value
*/
Calculator.prototype.eval = function(variables) {
var stack = [], rpn_expr = this.rpn_expr;
rpn_expr.forEach(function(token) {
if (typeof token === "boolean") {
// Constant.
stack.push(token);
} else if (/[a-zA-Z0-9]+/.test(token)) {
// Identifier - push its boolean value onto the stack
stack.push(!!variables[token]);
} else {
// Operator
var q = stack.pop(), p = stack.pop();
if (token === "^" || token === "^^") {
stack.push((p? 1:0) ^ (q? 1:0));
} else if (token === "|" || token === "||") {
stack.push(p || q);
} else if (token === "&" || token === "&&") {
stack.push(p && q);
} else if (token === "!" || token === "~") {
stack.push(p);
stack.push(!q);
} else if (token === "->") {
stack.push((!p) || q);
} else if (token === "<-") {
stack.push((!q) || p);
}
}
});
return stack.pop()? 1:0;
};
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With