2013-08-28 3 views
5

У меня есть пользовательский ввод для уравнения - этот ввод генерирует код LaTeX с помощью отдельного API, который я не кодировал (а именно, Mathquill, не это важно).Преобразование LaTeX в динамическую функцию Javascript

Моя проблема лучше всего проиллюстрировать на примере: предположим, что код LaTeX генерируется из пользовательского ввода был такой:

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

Как бы преобразовать это (на лету, конечно) в функцию JavaScript в которой, жестко закодировано, будет выглядеть следующим образом:

function(x) { 
    return Math.pow(x, 2) + 3 * x - 10 * Math.sin(2 * x); 
} 

есть ли API, или я, глядя на что-то писать, который будет интерпретировать символы LaTeX и сделать функцию, так или иначе? Или что?

+0

Существует вполне определенное подмножество математики выражения, которые вы планируете поддерживать? Экспоненты и триггерные функции могут хорошо переводиться, но такие вещи, как интегралы и производные, не имеют простой аналогии в JavaScript. Вам может быть интересно взаимодействовать с поддержкой TeX Wolfram | Alpha, если вам действительно нужна оценка произвольных математических выражений: http://blog.wolframalpha.com/2010/09/30/talk-to-wolframalpha-in-tex/ –

+0

I «Не стоит сомневаться в исчислении, хотя было бы неплохо, это ни в коем случае не обязательно. Проект, над которым я работаю, представляет собой биссекту с быстрым интервалом, чтобы аппроксимировать корни функции (иррациональные). – felamaslen

ответ

3

Я написал (ни в коем случае общего назначения) решения, в значительной степени основана на коде Джорджа.

Здесь:

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; 
}; 

Пример использования:

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 
+0

Я понимаю, что список функций совершенно отсутствует, например. нет обратного триггера, но это должно быть довольно просто добавить больше. – felamaslen

+0

Это хорошо! Только то, что мне нужно. Хотя я собираюсь изменить его так, чтобы он мог принимать именованные переменные. На данный момент это просто заменяет единственный аргумент для любой переменной в латексном выражении. –

+0

Я думаю, что это сломается, если у вас есть отрицательное число, потому что он считает свою операцию вычитания –

3

Ну, вам нужно будет решить, какие именно операции вы поддерживаете в какой-то момент. После этого не должно быть сложно реализовать оценщика с использованием анализатора, такого как Shunting-yard algorithm, чтобы получить представление уравнения, которое легче оценить (т. Е. Абстрактное синтаксическое дерево).

У меня есть простой пример такого оценщика, написанный на JavaScript по адресу: http://gjp.cc/projects/logic_tables.html Вместо LaTeX нужны логические выражения, такие как !(p ^^ q) & ~(p || q), но это может быть полезным для вас примером.

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; 
}; 
+0

Спасибо, я рассмотрю алгоритм шунтирования. – felamaslen

+0

Ну, благодаря вам мне удалось выполнить задачу под рукой, сильно изменив код и добавив некоторые из моих собственных дополнений (разные операторы и т. Д.). Cheers: D – felamaslen

2

Может быть, вы могли бы попробовать LatexJS. LatexJS - это служба API, которую я собрал для преобразования нотации латексной математики в функции Javascript. Таким образом, вы будете вводить латексные выражения и динамически возвращать функции Javascript. Например:

ввода

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

Выходного

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

Оценка

> func = (x)=>{return Math.pow(x,2)+3*x-10*Math.sin(2*x)}; 
> func(2) 
< 17.56802495307928 
+0

Это действительно приятно, но желаю, чтобы это было бесплатно –

Смежные вопросы