Convert LaTeX to dynamic Javascript function
Asked Answered
K

3

9

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?

Kat answered 28/8, 2013 at 1:24 Comment(2)
Is there a well defined subset of math expressions you plan to support? Exponents and trig functions may translate well, but things like integrals and derivatives have no easy counterpart in JavaScript. You may be interested in interfacing with Wolfram|Alpha's TeX support if you really need evaluation of arbitrary math expressions: blog.wolframalpha.com/2010/09/30/talk-to-wolframalpha-in-texUnapproachable
I'm not fussed about calculus, although it would be nice, it's by no means essential. The project I'm working on is a quick interval bisector to approximate a function's (irrational) roots.Kat
K
7

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
Kat answered 28/8, 2013 at 22:10 Comment(4)
I realise that the list of functions is quite lacking, e.g. no inverse trig, but it should be fairly simple to add more.Kat
This is good! Just what I was after. Although I am going to modify it so that it can accepted named variables. At the moment is just substitutes the single argument for any variable in the latex expression.Donofrio
I think this breaks if you have a negative number because it thinks its a subtraction operationBeckon
Let's say its \frac{-2}{3}. I fixed it by appending a 0 so its \frac{0-2}{3}Beckon
U
3

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;
};
Unapproachable answered 28/8, 2013 at 1:53 Comment(2)
Thanks, I'll look into the shunting yard algorithm.Kat
Well thanks to you I've managed to complete the task at hand, by heavily modifying your code and adding some of my own additions (different operators and such). Cheers :DKat
B
3

Maybe you could try LatexJS. LatexJS is an API service that I put together in order to convert latex math notation into Javascript functions. So you would input latex expressions and get back Javascript functions dynamically. For example:

Input

x^2+3x-10sin\left(2x\right)

Output

{
    "func": "(x)=>{return Math.pow(x,2)+3*x-10*Math.sin(2*x)};",
    "params": ["x"]
}

Evaluation

> func = (x)=>{return Math.pow(x,2)+3*x-10*Math.sin(2*x)};
> func(2)
< 17.56802495307928
Boycott answered 14/2, 2017 at 17:41 Comment(4)
This is really nice, but wish it was freeBeckon
Same. It is absolutely awesome. However I would totally love it if it was free :(Tight
I am working towards making LatexLambda open source. I still need to publish on npm.Boycott
The API seems to be down. Is the code currently available anywhere?Corgi

© 2022 - 2024 — McMap. All rights reserved.