2013-08-28 5 views
5

Ho un input utente per un'equazione - questo input genera codice LaTeX utilizzando un'API separata che non ho codificato (vale a dire, Mathquill, non che importi).Converti LaTeX in funzione Javascript dinamica

Il mio problema è meglio illustrato con un esempio: supponiamo che il codice LaTeX generato dall'input utente è stato questo:

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

Come potrei convertire questo (al volo, naturalmente) in una funzione JavaScript che, hard-coded, sarebbe simile a questa:

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

ci sono delle API o sto guardando scrivere qualcosa che interpretare i simboli LaTeX e fare una funzione, in qualche modo? O cosa?

+0

C'è un sottoinsieme ben definito di matematica espressioni che intendi supportare? Gli esponenti e le funzioni trigonometriche possono tradurre bene, ma cose come integrali e derivati ​​non hanno contropartite facili in JavaScript. Potresti essere interessato all'interfacciamento con il supporto TeX di Wolfram | Alpha se hai davvero bisogno di valutare espressioni matematiche arbitrarie: http://blog.wolframalpha.com/2010/09/30/talk-to-wolframalpha-in-tex/ –

+0

I Non sono preoccupato per il calcolo, anche se sarebbe bello, non è affatto essenziale. Il progetto su cui sto lavorando è una rapida bisettrice per approssimare le radici (irrazionali) di una funzione. – felamaslen

risposta

3

Ho scritto un (affatto general purpose) soluzione, fortemente basata sul codice di George.

Eccolo:

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

Esempio di utilizzo:

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

Mi rendo conto che la lista delle funzioni è piuttosto carente, ad es. nessun trigonometro inverso, ma dovrebbe essere abbastanza semplice aggiungerne altri. – felamaslen

+0

Questo è buono! Proprio quello che stavo cercando. Anche se ho intenzione di modificarlo in modo che possa accettare variabili nominate. Al momento sostituisce semplicemente il singolo argomento per qualsiasi variabile nell'espressione di lattice. –

+0

Penso che questo si interrompe se si dispone di un numero negativo perché pensa che sia un'operazione di sottrazione –

3

Bene, dovrai decidere esattamente quali operazioni supportare a un certo punto. Dopodiché non dovrebbe essere difficile implementare un valutatore usando un parser come lo Shunting-yard algorithm per produrre una rappresentazione dell'equazione che è più facile da valutare (cioè un albero sintattico astratto).

Ho un semplice esempio di questo tipo di valutatore scritto in JavaScript a: http://gjp.cc/projects/logic_tables.html Ci vuole espressioni logiche come !(p ^^ q) & ~(p || q) invece di LaTeX, ma potrebbe ancora essere un esempio utile per voi.

La 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

Grazie, esaminerò l'algoritmo del jumper. – felamaslen

+0

Beh, grazie a te sono riuscito a completare l'operazione, modificando pesantemente il codice e aggiungendo alcune delle mie aggiunte (operatori diversi e così via). Saluti: D – felamaslen

2

Forse si potrebbe provare LatexJS. LatexJS è un servizio API che ho messo insieme per convertire la notazione matematica del lattice in funzioni Javascript. Quindi inseriresti le espressioni al lattice e ripristinerai le funzioni Javascript in modo dinamico. Per esempio:

ingresso

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

uscita

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

valutazione

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

Questo è davvero bello, ma vorrei che fosse gratis –