125 votes

Évaluation d'une chaîne de caractères en tant qu'expression mathématique en JavaScript

Comment analyser et évaluer une expression mathématique dans une chaîne de caractères (par ex. '1+1' ) sans invoquer eval(string) pour obtenir sa valeur numérique ?

Avec cet exemple, je veux que la fonction accepte '1+1' et retourner 2 .

1voto

jozsefmorrissey Points 11

L'évaluation était bien trop lente pour moi. J'ai donc développé un StringMathEvaluator(SME), qui suit l'ordre des opérations et fonctionne pour toutes les équations arithmétiques contenant ce qui suit :

  • Entiers
  • Décimales
  • Opérateurs mathématiques : +-*/
  • Perenthesis préférentielle : $operator ($expression) $operator
  • Variables : Si et seulement si vous définissez une portée globale et/ou locale.
    • Format : [a-zA-Z][a-zA-Z0-9]*
    • Opérateur de variable d'emboîtement : $var1.$var2
    • Parenthèse de fonction : $functionId(...$commaSepArgs)
    • Crochets de tableau : $arrayId[index]
  • (Ignore les espaces)

Résultats du test de vitesse : (Exécuté dans le navigateur chromium)

                                      ~(80 - 99)% plus rapide avec une complexité d'expression raisonnable.

                     500000 iterations (SME/eval)

Integer Test '4'
(0.346/35.646)Sec - SME 99.03% faster

Simple Equation Test '4+-3'
(0.385/35.09)Sec - SME 98.9% faster

Complex Equation Test '(16 / 44 * 2) + ((4 + (4+3)-(12- 6)) / (2 * 8))'
(2.798/38.116)Sec - SME 92.66% faster

Variable Evaluation Test '2 + 5.5 + Math.round(Math.sqrt(Math.PI)) + values.one + values.two + values.four.nested'
(6.113/38.177)Sec - SME 83.99% faster

Exemple d'utilisation :

Initialiser :

Sans variables :

const math = new StringMathEvaluator();
const twentyOne = math.eval('11 + 10');
console.log('BlackJack' + twentyOne);
// BlackJack21

Avec des variables

const globalScope = {Math};
const math = new StringMathEvaluator(globalScope);

const localScope = {a: [[1, () => ({func: () => [17,13]})],[11,64,2]]};
const str = '((a[0][1]().func()[0] + a[0][1]().func()[1]) * a[1][2] - Math.sqrt(a[1][1]) - a[1][0]) / a[0][0]';
const fortyOne = math.eval(str, localScope);
console.log('Sum' + fortyOne);
// Sum41

PME :

class StringMathEvaluator {
  constructor(globalScope) {
    globalScope = globalScope || {};
    const instance = this;
    let splitter = '.';

    function resolve (path, currObj, globalCheck) {
      if (path === '') return currObj;
      try {
        if ((typeof path) === 'string') path = path.split(splitter);
        for (let index = 0; index < path.length; index += 1) {
          currObj = currObj[path[index]];
        }
        if (currObj === undefined && !globalCheck) throw Error('try global');
        return currObj;
      }  catch (e) {
        return resolve(path, globalScope, true);
      }
    }

    function multiplyOrDivide (values, operands) {
      const op = operands[operands.length - 1];
      if (op === StringMathEvaluator.multi || op === StringMathEvaluator.div) {
        const len = values.length;
        values[len - 2] = op(values[len - 2], values[len - 1])
        values.pop();
        operands.pop();
      }
    }

    const resolveArguments = (initialChar, func) => {
      return function (expr, index, values, operands, scope, path) {
        if (expr[index] === initialChar) {
          const args = [];
          let endIndex = index += 1;
          const terminationChar = expr[index - 1] === '(' ? ')' : ']';
          let terminate = false;
          let openParenCount = 0;
          while(!terminate && endIndex < expr.length) {
            const currChar = expr[endIndex++];
            if (currChar === '(') openParenCount++;
            else if (openParenCount > 0 && currChar === ')') openParenCount--;
            else if (openParenCount === 0) {
              if (currChar === ',') {
                args.push(expr.substr(index, endIndex - index - 1));
                index = endIndex;
              } else if (openParenCount === 0 && currChar === terminationChar) {
                args.push(expr.substr(index, endIndex++ - index - 1));
                terminate = true;
              }
            }
          }

          for (let index = 0; index < args.length; index += 1) {
            args[index] = instance.eval(args[index], scope);
          }
          const state = func(expr, path, scope, args, endIndex);
          if (state) {
            values.push(state.value);
            return state.endIndex;
          }
        }
      }
    };

    function chainedExpressions(expr, value, endIndex, path) {
      if (expr.length === endIndex) return {value, endIndex};
      let values = [];
      let offsetIndex;
      let valueIndex = 0;
      let chained = false;
      do {
        const subStr = expr.substr(endIndex);
        const offsetIndex = isolateArray(subStr, 0, values, [], value, path) ||
                            isolateFunction(subStr, 0, values, [], value, path) ||
                            (subStr[0] === '.' &&
                              isolateVar(subStr, 1, values, [], value));
        if (Number.isInteger(offsetIndex)) {
          value = values[valueIndex];
          endIndex += offsetIndex - 1;
          chained = true;
        }
      } while (offsetIndex !== undefined);
      return {value, endIndex};
    }

    const isolateArray = resolveArguments('[',
      (expr, path, scope, args, endIndex) => {
        endIndex = endIndex - 1;
        let value = resolve(path, scope)[args[args.length - 1]];
        return chainedExpressions(expr, value, endIndex, '');
      });

    const isolateFunction = resolveArguments('(',
      (expr, path, scope, args, endIndex) =>
          chainedExpressions(expr, resolve(path, scope).apply(null, args), endIndex - 1, ''));

    function isolateParenthesis(expr, index, values, operands, scope) {
      const char = expr[index];
      if (char === '(') {
        let openParenCount = 1;
        let endIndex = index + 1;
        while(openParenCount > 0 && endIndex < expr.length) {
          const currChar = expr[endIndex++];
          if (currChar === '(') openParenCount++;
          if (currChar === ')') openParenCount--;
        }
        const len = endIndex - index - 2;
        values.push(instance.eval(expr.substr(index + 1, len), scope));
        multiplyOrDivide(values, operands);
        return endIndex;
      }
    };

    function isolateOperand (char, operands) {
      switch (char) {
        case '*':
        operands.push(StringMathEvaluator.multi);
        return true;
        break;
        case '/':
        operands.push(StringMathEvaluator.div);
        return true;
        break;
        case '+':
        operands.push(StringMathEvaluator.add);
        return true;
        break;
        case '-':
        operands.push(StringMathEvaluator.sub);
        return true;
        break;
      }
      return false;
    }

    function isolateValueReg(reg, resolver, splitter) {
      return function (expr, index, values, operands, scope) {
        const match = expr.substr(index).match(reg);
        let args;
        if (match) {
          let endIndex = index + match[0].length;
          let value = resolver(match[0], scope);
          if (!Number.isFinite(value)) {
            const state = chainedExpressions(expr, scope, endIndex, match[0]);
            if (state !== undefined) {
              value = state.value;
              endIndex = state.endIndex;
            }
          }
          values.push(value);
          multiplyOrDivide(values, operands);
          return endIndex;
        }
      }
    }
    const isolateNumber = isolateValueReg(StringMathEvaluator.numReg, Number.parseFloat);
    const isolateVar = isolateValueReg(StringMathEvaluator.varReg, resolve);

    this.eval = function (expr, scope) {
      scope = scope || globalScope;
      const allowVars = (typeof scope) === 'object';
      let operands = [];
      let values = [];
      let prevWasOpperand = true;
      for (let index = 0; index < expr.length; index += 1) {
        const char = expr[index];
        if (prevWasOpperand) {
          let newIndex = isolateParenthesis(expr, index, values, operands, scope) ||
                        isolateNumber(expr, index, values, operands, scope) ||
                        (allowVars && isolateVar(expr, index, values, operands, scope));
          if (Number.isInteger(newIndex)) {
            index = newIndex - 1;
            prevWasOpperand = false;
          }
        } else {
          prevWasOpperand = isolateOperand(char, operands);
        }
      }
      let value = values[0];
      for (let index = 0; index < values.length - 1; index += 1) {
        value = operands[index](values[index], values[index + 1]);
        values[index + 1] = value;
      }
      return value;
    }
  }
}

StringMathEvaluator.numReg = /^(-|)[0-9\.]{1,}/;
StringMathEvaluator.varReg = /^((\.|)([a-zA-Z][a-zA-Z0-9\.]*))/;
StringMathEvaluator.multi = (n1, n2) => n1 * n2;
StringMathEvaluator.div = (n1, n2) => n1 / n2;
StringMathEvaluator.add = (n1, n2) => n1 + n2;
StringMathEvaluator.sub = (n1, n2) => n1 - n2;

1voto

Salim Hamidi Points 1316

Le meilleur moyen et le plus simple est d'utiliser math.js bibliothèque. Voici quelques exemples de code démontrant comment utiliser la bibliothèque. Cliquez sur aquí pour bricoler.

// functions and constants
math.round(math.e, 3)                // 2.718
math.atan2(3, -3) / math.pi          // 0.75
math.log(10000, 10)                  // 4
math.sqrt(-4)                        // 2i
math.derivative('x^2 + x', 'x')      // 2*x+1
math.pow([[-1, 2], [3, 1]], 2)
     // [[7, 0], [0, 7]]

// expressions
math.evaluate('1.2 * (2 + 4.5)')     // 7.8
math.evaluate('12.7 cm to inch')     // 5 inch
math.evaluate('sin(45 deg) ^ 2')     // 0.5
math.evaluate('9 / 3 + 2i')          // 3 + 2i
math.evaluate('det([-1, 2; 3, 1])')  // -7

// chaining
math.chain(3)
    .add(4)
    .multiply(2)
    .done() // 14

1voto

Yagi91 Points 25

J'ai créé une petite fonction pour analyser une expression mathématique contenant +,/,-,*. J'ai utilisé if les déclarations que je pense switch cases sera meilleur. Tout d'abord, j'ai séparé la chaîne de caractères dans l'opérateur et ses nombres, puis j'ai converti la chaîne de caractères en flottant, puis j'ai itéré en effectuant l'opération.

 const evaluate=(mathExpStr) => {
    mathExpStr.replace(/[+-\/*]$/, "");
    let regExp = /\d+/g;
    let valueArr = (mathExpStr.match(regExp) || []).map((val) =>
      Number.parseFloat(val)
    );
    let operatorArr = mathExpStr.match(/[+-\/*]/g) || [];
    return converter(valueArr, operatorArr)
  }

const converter = (arr,operators)=>{
  let arr2=[...arr]
  for(let i=0;i<arr.length;i++){
    let o;
    if(arr2.length<2){return arr2[0]}
    if(operators[i]=="+"){
      o=arr2[0]+arr2[1]
      arr2.splice(0, 2, o)
      console.log(o,arr2, operators[i])
    }
    if(operators[i]=="-"){
      o=arr2[0]-arr2[1]
      arr2.splice(0,2, o)
      console.log(o,arr2, operators[i])
    }
    if(operators[i]=="*"){
      o=arr2[0]*arr2[1]
      arr2.splice(0,2,o)
      console.log(o,arr2, operators[i])
    }
    if(operators[i]=="/"){
      o=arr2[0]/arr2[1]
      arr2.splice(0,2, o)
      console.log(o,arr2, operators[i])
    }
  }
}
// console.log(converter(valueArr, operatorArr))
console.log(evaluate("1+3+5+6-4*2/4"))

0voto

Ross Points 48

Voici une solution algorithmique similaire à celle de jMichael qui boucle sur l'expression caractère par caractère et suit progressivement gauche/opérateur/droite. La fonction accumule le résultat après chaque tour où elle trouve un caractère opérateur. Cette version ne supporte que les opérateurs '+' et '-' mais elle est écrite pour être étendue à d'autres opérateurs. Note : nous définissons 'currOp' à '+' avant de boucler car nous supposons que l'expression commence par un flottant positif. En fait, dans l'ensemble, je suppose que l'entrée est similaire à ce qui proviendrait d'une calculatrice.

function calculate(exp) {
  const opMap = {
    '+': (a, b) => { return parseFloat(a) + parseFloat(b) },
    '-': (a, b) => { return parseFloat(a) - parseFloat(b) },
  };
  const opList = Object.keys(opMap);

  let acc = 0;
  let next = '';
  let currOp = '+';

  for (let char of exp) {
    if (opList.includes(char)) {
      acc = opMap[currOp](acc, next);
      currOp = char;
      next = '';
    } else {
      next += char;
    } 
  }

  return currOp === '+' ? acc + parseFloat(next) : acc - parseFloat(next);
}

0voto

Arne Jenssen Points 310

Basé sur l'ouvrage d'Aniket Kudale parse

Pour ajouter des variables contextuelles à l'expression

function parseExpr(str: string, params: any) {
  const names = Object.keys(params);
  const vals = Object.values(params);
  return Function(...names, `'use strict'; return (${str})`)(...vals);
}

exemple

> parseExpr('age > 50? x : x/2', {x: 40, age: 46})
20

> parseExpr('age > 50? x : x/2', {x: 40, age: 60})
40

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X