Evaluering av en streng som et matematisk uttrykk i JavaScript


Hvordan analyserer og evaluerer jeg et matematisk uttrykk i en streng (f.eks. '1+1') uten å bruke eval(string) for å få dens numeriske verdi?

Med det eksemplet vil jeg at funksjonen skal akseptere '1+1' og returnere 2.

Svar #1

Du kan bruke JavaScript Expression Evaluator-biblioteket, som lar deg gjøre ting som:

Parser.evaluate("2 ^ x", { x: 3 });

Eller mathjs, som tillater ting som:

math.eval('sin(45 deg) ^ 2');

Eller matteuttrykkvurderer som ser ut til å være mer lettvekts.

mexp.eval('sin(45) ^ 2');

Jeg endte opp med å velge mathjs for ett av prosjektene mine, men det var for flere år siden. I dag ville jeg sannsynligvis brukt koden i dette svaret av @tanguy_k.

Svar #2

Ved å bruke Function(), som ligner på eval (kjør aldri dette på ubetrodd input):

function parse(str) {
  return Function(`'use strict'; return (${str})`)()
}

document.write( "1+2+3", '=' , parse("1+2+3"), '<br>');
document.write( "1 + 2 * 3", '=' , parse("1 + 2 * 3"), '<br>');

Forsiktighet: Skal ikke brukes i produksjon

Svar #3

Du kan enkelt gjøre + eller -:

function addbits(s) {
  var total = 0,
      s = s.match(/[+\-]*(\.\d+|\d+(\.\d+)?)/g) || [];
      
  while (s.length) {
    total += parseFloat(s.shift());
  }
  return total;
}

var string = '1+23+4+5-30';
console.log(
  addbits(string)
)

Mer komplisert matematikk gjør eval mer attraktivt - og absolutt enklere å skrive.

Svar #4

Noen må analysere den strengen. Hvis det ikke er tolken (via eval), da må det være deg, som skriver en analysefunksjon for å trekke ut tall, operatorer og alt annet du vil støtte i et matematisk uttrykk.

Så nei, det finnes ingen (enkel) måte uten eval. Hvis du er bekymret for sikkerheten (fordi inputen du analyserer ikke kommer fra en kilde du kontrollerer), kan du kanskje sjekke inputens format (via et hviteliste-regexfilter) før du sender det til eval?

Svar #5

Et alternativ til det utmerkede svaret fra @kennebec, ved å bruke et kortere regulært uttrykk og tillate mellomrom mellom operatorer

function addbits(s) {
    var total = 0;
    s = s.replace(/\s/g, '').match(/[+\-]?([0-9\.\s]+)/g) || [];
    while(s.length) total += parseFloat(s.shift());
    return total;
}

Bruk det som

addbits('5 + 30 - 25.1 + 11');

Oppdatering

Her er en mer optimalisert versjon

function addbits(s) {
    return (s.replace(/\s/g, '').match(/[+\-]?([0-9\.]+)/g) || [])
        .reduce(function(sum, value) {
            return parseFloat(sum) + parseFloat(value);
        });
}

Svar #6

De nåværende svarene er enten:

  • foreslår enorme eksterne biblioteker
  • å bruke eval('...') eller Function('...') som er dårlig
  • arbeider bare med +, -
  • feiler på operatorprioritet (f.eks. 1+2*3 skal returnere 7, ikke 9) når de implementerer * og/eller /

Her er en implementering (270 linjer med kode) som erstatter eval.

Det støtter +, -, *, /, %, ^, parenteser og funksjoner (min, max, sin, cos, tan, log). Du kan også enkelt legge til støtte for flere funksjoner som sqrt, asin, acos...)

Den bruker TypeScript, er dokumentert og testet.

Den bruker internt shunting yard-algoritmen og omvendt polsk notasjon.

// WTF!
// parseFloat('-0') => -0 vs parseFloat(-0) => 0
// -0 === 0 => true vs Object.is(-0, 0) => false
const minus0Hack = (value: number) => (Object.is(value, -0) ? '-0' : value);

export const operators: {
  [operator: string]:
    | {
        func: (...args: string[]) => string;
        precedence: number;
        associativity: 'left' | 'right';
        arity: number; // Needed by evalReversePolishNotation()
      }
    | undefined;
} = {
  '+': {
    func: (x, y) => `${minus0Hack(Number(x) + Number(y))}`,
    precedence: 1,
    associativity: 'left',
    arity: 2
  },
  '-': {
    func: (x, y) => `${minus0Hack(Number(x) - Number(y))}`,
    precedence: 1,
    associativity: 'left',
    arity: 2
  },
  '*': {
    func: (x, y) => `${minus0Hack(Number(x) * Number(y))}`,
    precedence: 2,
    associativity: 'left',
    arity: 2
  },
  '/': {
    func: (x, y) => `${minus0Hack(Number(x) / Number(y))}`,
    precedence: 2,
    associativity: 'left',
    arity: 2
  },
  '%': {
    func: (x, y) => `${minus0Hack(Number(x) % Number(y))}`,
    precedence: 2,
    associativity: 'left',
    arity: 2
  },
  '^': {
    // Why Math.pow() instead of **?
    // -2 ** 2 => "SyntaxError: Unary operator used immediately before exponentiation expression..."
    // Math.pow(-2, 2) => -4
    // eslint-disable-next-line prefer-exponentiation-operator, no-restricted-properties
    func: (x, y) => `${minus0Hack(Math.pow(Number(x), Number(y)))}`,
    precedence: 3,
    associativity: 'right',
    arity: 2
  }
};
export const operatorsKeys = Object.keys(operators);

export const functions: {
  [operator: string]:
    | {
        func: (...args: string[]) => string;
        // Needed by evalReversePolishNotation()
        arity: number;
      }
    | undefined;
} = {
  min: { func: (x, y) => `${minus0Hack(Math.min(Number(x), Number(y)))}`, arity: 2 },
  max: { func: (x, y) => `${minus0Hack(Math.max(Number(x), Number(y)))}`, arity: 2 },
  sin: { func: x => `${minus0Hack(Math.sin(Number(x)))}`, arity: 1 },
  cos: { func: x => `${minus0Hack(Math.cos(Number(x)))}`, arity: 1 },
  tan: { func: x => `${minus0Hack(Math.tan(Number(x)))}`, arity: 1 },
  log: { func: x => `${Math.log(Number(x))}`, arity: 1 } // No need for -0 hack
};
export const functionsKeys = Object.keys(functions);

const top = (stack: string[]): string | undefined => stack[stack.length - 1];

/**
 * Shunting yard algorithm: converts infix expression to postfix expression (reverse Polish notation)
 *
 * Example: ['1', '+', '2'] => ['1', '2', '+']
 *
 * https://en.wikipedia.org/wiki/Shunting_yard_algorithm
 * https://github.com/poteat/shunting-yard-typescript
 * https://blog.kallisti.net.nz/2008/02/extension-to-the-shunting-yard-algorithm-to-allow-variable-numbers-of-arguments-to-functions/
 */
export function shuntingYard(tokens: string[]) {
  const output = new Array<string>();
  const operatorStack = new Array<string>();

  for (const token of tokens) {
    if (functions[token] !== undefined) {
      operatorStack.push(token);
    } else if (token === ',') {
      while (operatorStack.length > 0 && top(operatorStack) !== '(') {
        output.push(operatorStack.pop()!);
      }
      if (operatorStack.length === 0) {
        throw new Error("Misplaced ','");
      }
    } else if (operators[token] !== undefined) {
      const o1 = token;
      while (
        operatorStack.length > 0 &&
        top(operatorStack) !== undefined &&
        top(operatorStack) !== '(' &&
        (operators[top(operatorStack)!]!.precedence > operators[o1]!.precedence ||
          (operators[o1]!.precedence === operators[top(operatorStack)!]!.precedence &&
            operators[o1]!.associativity === 'left'))
      ) {
        output.push(operatorStack.pop()!); // o2
      }
      operatorStack.push(o1);
    } else if (token === '(') {
      operatorStack.push(token);
    } else if (token === ')') {
      while (operatorStack.length > 0 && top(operatorStack) !== '(') {
        output.push(operatorStack.pop()!);
      }
      if (operatorStack.length > 0 && top(operatorStack) === '(') {
        operatorStack.pop();
      } else {
        throw new Error('Parentheses mismatch');
      }
      if (functions[top(operatorStack)!] !== undefined) {
        output.push(operatorStack.pop()!);
      }
    } else {
      output.push(token);
    }
  }

  // Remaining items
  while (operatorStack.length > 0) {
    const operator = top(operatorStack);
    if (operator === '(') {
      throw new Error('Parentheses mismatch');
    } else {
      output.push(operatorStack.pop()!);
    }
  }

  return output;
}

/**
 * Evaluates reverse Polish notation (RPN) (postfix expression).
 *
 * Example: ['1', '2', '+'] => 3
 *
 * https://en.wikipedia.org/wiki/Reverse_Polish_notation
 * https://github.com/poteat/shunting-yard-typescript
 */
export function evalReversePolishNotation(tokens: string[]) {
  const stack = new Array<string>();

  const ops = { ...operators, ...functions };

  for (const token of tokens) {
    const op = ops[token];

    // eslint-disable-next-line unicorn/no-negated-condition
    if (op !== undefined) {
      const parameters = [];
      for (let i = 0; i < op.arity; i++) {
        parameters.push(stack.pop()!);
      }
      stack.push(op.func(...parameters.reverse()));
    } else {
      stack.push(token);
    }
  }

  if (stack.length > 1) {
    throw new Error('Insufficient operators');
  }

  return Number(stack[0]);
}

/**
 * Breaks a mathematical expression into tokens.
 *
 * Example: "1 + 2" => [1, '+', 2]
 *
 * https://gist.github.com/tchayen/44c28e8d4230b3b05e9f
 */
export function tokenize(expression: string) {
  // "1  +" => "1 +"
  const expr = expression.replace(/\s+/g, ' ');

  const tokens = [];

  let acc = '';
  let currentNumber = '';

  for (let i = 0; i < expr.length; i++) {
    const c = expr.charAt(i);
    const prev_c = expr.charAt(i - 1); // '' if index out of range
    const next_c = expr.charAt(i + 1); // '' if index out of range

    const lastToken = top(tokens);

    const numberParsingStarted = currentNumber !== '';

    if (
      // 1
      /\d/.test(c) ||
      // Unary operator: +1 or -1
      ((c === '+' || c === '-') &&
        !numberParsingStarted &&
        (lastToken === undefined ||
          lastToken === ',' ||
          lastToken === '(' ||
          operatorsKeys.includes(lastToken)) &&
        /\d/.test(next_c))
    ) {
      currentNumber += c;
    } else if (c === '.') {
      if (numberParsingStarted && currentNumber.includes('.')) {
        throw new Error(`Double '.' in number: '${currentNumber}${c}'`);
      } else {
        currentNumber += c;
      }
    } else if (c === ' ') {
      if (/\d/.test(prev_c) && /\d/.test(next_c)) {
        throw new Error(`Space in number: '${currentNumber}${c}${next_c}'`);
      }
    } else if (functionsKeys.includes(acc + c)) {
      acc += c;
      if (!functionsKeys.includes(acc + next_c)) {
        tokens.push(acc);
        acc = '';
      }
    } else if (operatorsKeys.includes(c) || c === '(' || c === ')' || c === ',') {
      if (
        operatorsKeys.includes(c) &&
        !numberParsingStarted &&
        operatorsKeys.includes(lastToken!)
      ) {
        throw new Error(`Consecutive operators: '${lastToken!}${c}'`);
      }
      if (numberParsingStarted) {
        tokens.push(currentNumber);
      }
      tokens.push(c);
      currentNumber = '';
    } else {
      acc += c;
    }
  }

  if (acc !== '') {
    throw new Error(`Invalid characters: '${acc}'`);
  }

  // Add last number to the tokens
  if (currentNumber !== '') {
    tokens.push(currentNumber);
  }

  // ['+', '1'] => ['0', '+', '1']
  // ['-', '1'] => ['0', '-', '1']
  if (tokens[0] === '+' || tokens[0] === '-') {
    tokens.unshift('0');
  }

  return tokens;
}

export function calculate(expression: string) {
  const tokens = tokenize(expression);
  const rpn = shuntingYard(tokens);
  return evalReversePolishNotation(rpn);
}

Den viktigste delen, enhetstestene:

/* eslint-disable no-eval, unicorn/prefer-number-properties */

import { calculate, evalReversePolishNotation, shuntingYard, tokenize } from './calculator';
import { convertMathExpressionToEval, getRandomMathExpression } from './getRandomMathExpression';
import { getRandomInt } from './getRandomNumber';

test('shuntingYard()', () => {
  {
    // https://en.wikipedia.org/wiki/Shunting_yard_algorithm#Detailed_examples
    const rpn = shuntingYard('3 + 4 * 2 / ( 1 - 5 ) ^ 2 ^ 3'.split(' '));
    expect(rpn).toEqual(['3', '4', '2', '*', '1', '5', '-', '2', '3', '^', '^', '/', '+']);
  }

  {
    // https://en.wikipedia.org/wiki/Shunting_yard_algorithm#Detailed_examples
    const rpn = shuntingYard('sin ( max ( 2 3 ) / 3 * 3.14 )'.split(' '));
    expect(rpn).toEqual(['2', '3', 'max', '3', '/', '3.14', '*', 'sin']);
  }

  // Parentheses mismatch
  expect(() => shuntingYard(['('])).toThrow('Parentheses mismatch');
  expect(() => shuntingYard([')'])).toThrow('Parentheses mismatch');
  expect(() => shuntingYard('1 - ( 2 * 3 ) )'.split(' '))).toThrow('Parentheses mismatch');
  expect(() => shuntingYard('1 - ( 2 * 3 ) ) + 4'.split(' '))).toThrow('Parentheses mismatch');

  // Ignore ','
  expect(shuntingYard('max ( 1 , 2 )'.split(' '))).toEqual(['1', '2', 'max']);

  // if the token is: ',':
  //   while the operator at the top of the operator stack is not a left parenthesis:
  //     pop the operator from the operator stack into the output queue
  expect(shuntingYard('max ( 0 + 1 , 2 )'.split(' '))).toEqual(['0', '1', '+', '2', 'max']);

  // Misplaced ','
  expect(() => shuntingYard('1 , 2'.split(' '))).toThrow("Misplaced ','");
  expect(() => shuntingYard(', 1 / 2'.split(' '))).toThrow("Misplaced ','");
  expect(() => shuntingYard('1 , / 2'.split(' '))).toThrow("Misplaced ','");
  expect(() => shuntingYard('1 / , 2'.split(' '))).toThrow("Misplaced ','");
  expect(() => shuntingYard('1 / 2 ,'.split(' '))).toThrow("Misplaced ','");
  expect(() =>
    shuntingYard('sin ( , max , ( , 2 , 3 , ) , / , 3 , * , 3.14 , )'.split(' '))
  ).not.toThrow();

  // Edge cases
  expect(shuntingYard([''])).toEqual(['']);
  expect(shuntingYard([' '])).toEqual([' ']);
  expect(shuntingYard(['1'])).toEqual(['1']);
  expect(shuntingYard(['a'])).toEqual(['a']);
  expect(shuntingYard(['1a'])).toEqual(['1a']);
  expect(shuntingYard(['*'])).toEqual(['*']);
  expect(shuntingYard(['/'])).toEqual(['/']);

  // All together expression
  expect(
    shuntingYard(
      '( ( 3.1 + cos ( -4 ) / 2 ) * max ( -6 , 6 ) ^ sin ( 6 ) * 9 ) / tan ( log ( 8.8 + -2 ) % 7 ) + ( 6 * -1 - min ( 6 , -4.2 ) )'.split(
        ' '
      )
    )
  ).toEqual(
    '3.1 -4 cos 2 / + -6 6 max 6 sin ^ * 9 * 8.8 -2 + log 7 % tan / 6 -1 * 6 -4.2 min - +'.split(
      ' '
    )
  );
});

test('reversePolishNotation()', () => {
  // https://rosettacode.org/wiki/Parsing/RPN_calculator_algorithm#JavaScript
  expect(
    evalReversePolishNotation(['3', '4', '2', '*', '1', '5', '-', '2', '3', '^', '^', '/', '+'])
  ).toEqual(3 + (4 * 2) / (1 - 5) ** (2 ** 3));
  expect(
    evalReversePolishNotation(['3', '4', '2', '*', '1', '5', '-', '2', '3', '^', '^', '/', '+'])
  ).toEqual(3.000_122_070_312_5);

  // https://en.wikipedia.org/wiki/Shunting_yard_algorithm#Detailed_examples
  expect(evalReversePolishNotation(['2', '3', 'max', '3', '/', '3.14', '*', 'sin'])).toEqual(
    Math.sin((Math.max(2, 3) / 3) * 3.14)
  );
  expect(evalReversePolishNotation(['2', '3', 'max', '3', '/', '3.14', '*', 'sin'])).toEqual(
    0.001_592_652_916_486_828_2
  );

  // Edge cases
  expect(evalReversePolishNotation([''])).toEqual(0); // :-(
  expect(evalReversePolishNotation([' '])).toEqual(0); // :-(
  expect(evalReversePolishNotation(['1'])).toEqual(1);
  expect(evalReversePolishNotation(['a'])).toBeNaN();
  expect(evalReversePolishNotation(['1a'])).toBeNaN();
  expect(evalReversePolishNotation(['*'])).toBeNaN();
  expect(evalReversePolishNotation(['/'])).toBeNaN();
  expect(() => evalReversePolishNotation(['1', '2'])).toThrow('Insufficient operators');

  // All together expression
  expect(
    evalReversePolishNotation(
      '3.1 -4 cos 2 / + -6 6 max 6 sin ^ * 9 * 8.8 -2 + log 7 % tan / 6 -1 * 6 -4.2 min - +'.split(
        ' '
      )
    )
  ).toEqual(
    eval(
      '((3.1 + Math.cos(-4) / 2) * Math.max(-6, 6) ** Math.sin(6) * 9) / Math.tan(Math.log(8.8 + -2) % 7) + (6 * -1 - Math.min(6, -4.2))'
    )
  );
});

test('tokenize()', () => {
  // https://en.wikipedia.org/wiki/Shunting_yard_algorithm#Detailed_examples
  expect(tokenize('3 + 4 * 2 / (1 - 5) ^ 2 ^ 3')).toEqual(
    '3 + 4 * 2 / ( 1 - 5 ) ^ 2 ^ 3'.split(' ')
  );

  // https://en.wikipedia.org/wiki/Shunting_yard_algorithm#Detailed_examples
  expect(tokenize('sin(max(2, 3) / 3 * 3.14)')).toEqual(
    'sin ( max ( 2 , 3 ) / 3 * 3.14 )'.split(' ')
  );

  expect(tokenize('1+2')).toEqual(['1', '+', '2']);
  expect(tokenize('min(1,2)')).toEqual(['min', '(', '1', ',', '2', ')']);
  expect(tokenize('1.1+2.2')).toEqual(['1.1', '+', '2.2']);
  expect(tokenize('min(1.1,2.2)')).toEqual(['min', '(', '1.1', ',', '2.2', ')']);

  // Decimals
  expect(tokenize('1.1 + 2.2 - 3.3 * 4.4 / 5.5 % 6.6 ^ 7.7')).toEqual(
    '1.1 + 2.2 - 3.3 * 4.4 / 5.5 % 6.6 ^ 7.7'.split(' ')
  );

  // White spaces
  expect(tokenize('')).toEqual([]);
  expect(tokenize(' ')).toEqual([]);
  expect(tokenize(' 1  +  2 ')).toEqual(['1', '+', '2']);
  expect(tokenize('1 \n + \n 2')).toEqual(['1', '+', '2']);
  expect(tokenize('1 \t + \t 2')).toEqual(['1', '+', '2']);

  // Single number
  expect(tokenize('0')).toEqual(['0']);
  expect(tokenize('1')).toEqual(['1']);
  expect(tokenize('-0')).toEqual(['-0']);
  expect(tokenize('-1')).toEqual(['-1']);
  expect(tokenize('(1)')).toEqual(['(', '1', ')']);
  expect(tokenize('(-1)')).toEqual(['(', '-1', ')']);
  expect(tokenize('-(1)')).toEqual(['0', '-', '(', '1', ')']);

  // Starting with +/-
  expect(tokenize('+0')).toEqual(['+0']);
  expect(tokenize('+ 0')).toEqual(['0', '+', '0']);
  expect(tokenize('-0')).toEqual(['-0']);
  expect(tokenize('- 0')).toEqual(['0', '-', '0']);
  expect(tokenize('+1')).toEqual(['+1']);
  expect(tokenize('+ 1')).toEqual(['0', '+', '1']);
  expect(tokenize('-1')).toEqual(['-1']);
  expect(tokenize('- 1')).toEqual(['0', '-', '1']);
  expect(tokenize('+1 + 1')).toEqual(['+1', '+', '1']);
  expect(tokenize('+ 1 + 1')).toEqual(['0', '+', '1', '+', '1']);
  expect(tokenize('-1 + 1')).toEqual(['-1', '+', '1']);
  expect(tokenize('- 1 + 1')).toEqual(['0', '-', '1', '+', '1']);
  expect(tokenize('+')).toEqual(['0', '+']);
  expect(tokenize('-')).toEqual(['0', '-']);

  // Do not confuse '+1' / '-1' with 'x + 1' / 'x - 1' depending on the context
  expect(tokenize('(1+2)+1')).toEqual(['(', '1', '+', '2', ')', '+', '1']);
  expect(tokenize('(1+2)-1')).toEqual(['(', '1', '+', '2', ')', '-', '1']);
  expect(tokenize('1 + -2')).toEqual(['1', '+', '-2']);
  expect(tokenize('1+-2')).toEqual(['1', '+', '-2']);

  // Space in number
  expect(() => tokenize('1 2')).toThrow("Space in number: '1 2'");
  expect(() => tokenize('1  2')).toThrow("Space in number: '1 2'");
  expect(() => tokenize('0 + 1 / (2 3) * 4')).toThrow("Space in number: '2 3'");
  expect(() => tokenize('min(1 2)')).toThrow("Space in number: '1 2'");

  // Double '.' in number
  expect(() => tokenize('1+2.3.4')).toThrow("Double '.' in number: '2.3.'");
  expect(() => tokenize('1+2.3.4.5')).toThrow("Double '.' in number: '2.3.'");
  expect(() => tokenize('0 + 1 / 2.3.4 * 5')).toThrow("Double '.' in number: '2.3.'");
  expect(() => tokenize('min(1, 2.3.4)')).toThrow("Double '.' in number: '2.3.'");

  // Consecutive operators
  expect(tokenize('1++2')).toEqual(['1', '+', '+2']);
  expect(tokenize('1-+2')).toEqual(['1', '-', '+2']);
  expect(tokenize('1--2')).toEqual(['1', '-', '-2']);
  expect(() => tokenize('1++')).toThrow("Consecutive operators: '++'");
  expect(() => tokenize('1-+')).toThrow("Consecutive operators: '-+'");
  expect(() => tokenize('1--')).toThrow("Consecutive operators: '--'");
  expect(() => tokenize('1-*2')).toThrow("Consecutive operators: '-*'");
  expect(() => tokenize('0 + 1 / (2-*3) * 4')).toThrow("Consecutive operators: '-*'");
  expect(() => tokenize('min(1-*2, 3)')).toThrow("Consecutive operators: '-*'");

  // Other edge cases
  expect(tokenize('1,2')).toEqual(['1', ',', '2']);
  expect(tokenize('1+2+')).toEqual(['1', '+', '2', '+']); // :-(
  expect(() => tokenize('1+2a')).toThrow("Invalid characters: 'a'");
  expect(() => tokenize('10 Hello')).toThrow("Invalid characters: 'Hello'");
  expect(tokenize('1-.')).toEqual(['1', '-', '.']); // :-(
  expect(tokenize('*')).toEqual(['*']);
  expect(tokenize('/')).toEqual(['/']);

  // All together expression
  expect(
    tokenize(
      '((3.1 + cos(-4) / 2) * max(-6, 6) ^ sin(6) * 9) / tan(log(8.8 + -2) % 7) + (6 * -1 - min(6, -4.2))'
    )
  ).toEqual(
    '( ( 3.1 + cos ( -4 ) / 2 ) * max ( -6 , 6 ) ^ sin ( 6 ) * 9 ) / tan ( log ( 8.8 + -2 ) % 7 ) + ( 6 * -1 - min ( 6 , -4.2 ) )'.split(
      ' '
    )
  );
  expect(
    tokenize('((3.1+cos(-4)/2)*max(-6,6)^sin(6)*9)/tan(log(8.8+-2)%7)+(6*-1-min(6,-4.2))')
  ).toEqual(
    '( ( 3.1 + cos ( -4 ) / 2 ) * max ( -6 , 6 ) ^ sin ( 6 ) * 9 ) / tan ( log ( 8.8 + -2 ) % 7 ) + ( 6 * -1 - min ( 6 , -4.2 ) )'.split(
      ' '
    )
  );
});

test('calculate()', () => {
  // https://en.wikipedia.org/wiki/Shunting_yard_algorithm#Detailed_examples
  expect(calculate('3 + 4 * 2 / (1 - 5) ^ 2 ^ 3')).toEqual(3.000_122_070_312_5);

  // https://en.wikipedia.org/wiki/Shunting_yard_algorithm#Detailed_examples
  expect(calculate('sin(max(2, 3) / 3 * 3.14)')).toEqual(0.001_592_652_916_486_828_2);

  expect(calculate('1+2')).toEqual(3);
  expect(calculate('min(1,2)')).toEqual(1);
  expect(calculate('1.1+2.2')).toEqual(3.300_000_000_000_000_3);
  expect(calculate('min(1.1,2.2)')).toEqual(1.1);

  // if the token is: ',':
  //   while the operator at the top of the operator stack is not a left parenthesis:
  //     pop the operator from the operator stack into the output queue
  expect(calculate('max(0 + 1, 2)')).toEqual(2);

  // Decimals
  expect(calculate('1.1 + 2.2 - 3.3 * 4.4 / 5.5 % 6.6 ^ 7.7')).toEqual(
    eval('1.1 + 2.2 - 3.3 * 4.4 / 5.5 % 6.6 ** 7.7')
  );

  // White spaces
  expect(calculate('')).toBeNaN();
  expect(calculate(' ')).toBeNaN();
  expect(calculate(' 1  +  2 ')).toEqual(3);
  expect(calculate('1 \n + \n 2')).toEqual(3);
  expect(calculate('1 \t + \t 2')).toEqual(3);

  // -0 hack
  expect(calculate('-0 + -0')).toEqual(-0);
  expect(calculate('-0 - 0')).toEqual(-0);
  expect(calculate('0 * -1')).toEqual(-0);
  expect(calculate('0 / -1')).toEqual(-0);
  expect(calculate('-1 % 1')).toEqual(-0);
  expect(calculate('-0 ^ 1')).toEqual(-0);
  expect(calculate('min(-0, 1)')).toEqual(-0);
  expect(calculate('max(-0, -1)')).toEqual(-0);
  expect(calculate('sin(-0)')).toEqual(-0);
  //expect(Math.cos(Math.PI / 2)).toEqual(0);
  expect(calculate('tan(-0)')).toEqual(-0);
  expect(calculate('log(1)')).toEqual(0); // No need for -0 hack

  // Math.pow() vs **
  expect(calculate('-2 ^ 2')).toEqual((-2) ** 2);
  expect(eval('Math.pow(-2, 2)')).toEqual(4);
  expect(() => eval('-2 ** 2')).toThrow(
    'Unary operator used immediately before exponentiation expression.'
  );

  // Infinity/-Infinity
  expect(calculate('1 / 0')).toEqual(Infinity);
  expect(calculate('1 / -0')).toEqual(-Infinity);
  expect(calculate('-1 / 0')).toEqual(-Infinity);
  expect(calculate('1 + 1 / 0')).toEqual(Infinity);
  expect(calculate('1 - 1 / 0')).toEqual(-Infinity);
  expect(calculate('10 ^ 1000')).toEqual(Infinity);
  expect(calculate('0 - 10 ^ 1000')).toEqual(-Infinity);
  expect(calculate('0 ^ -1')).toEqual(Infinity);
  expect(calculate('-0 ^ -1')).toEqual(-Infinity);
  expect(calculate('log(0)')).toEqual(-Infinity);

  // NaN
  expect(calculate('log(-1)')).toBeNaN();
  expect(calculate('-1 ^ 0.1')).toBeNaN();
  expect(calculate('1 % 0')).toBeNaN();
  expect(calculate('1 / 0 * 0')).toBeNaN();

  // Single number
  expect(calculate('0')).toEqual(0);
  expect(calculate('1')).toEqual(1);
  expect(calculate('-0')).toEqual(-0);
  expect(calculate('-1')).toEqual(-1);
  expect(calculate('(1)')).toEqual(1);
  expect(calculate('(-1)')).toEqual(-1);
  expect(calculate('-(1)')).toEqual(-1);

  // Starting with +/-
  expect(calculate('+0')).toEqual(0);
  expect(calculate('+ 0')).toEqual(0);
  expect(calculate('-0')).toEqual(-0);
  expect(calculate('- 0')).toEqual(0);
  expect(calculate('+1')).toEqual(1);
  expect(calculate('+ 1')).toEqual(+1);
  expect(calculate('-1')).toEqual(-1);
  expect(calculate('- 1')).toEqual(-1);
  expect(calculate('+1 + 1')).toEqual(2);
  expect(calculate('+ 1 + 1')).toEqual(2);
  expect(calculate('-1 + 1')).toEqual(0);
  expect(calculate('- 1 + 1')).toEqual(0);
  expect(calculate('+')).toBeNaN();
  expect(calculate('-')).toBeNaN();

  // Do not confuse '+1' / '-1' with 'x + 1' / 'x - 1' depending on the context
  expect(calculate('(1+2)+1')).toEqual(4);
  expect(calculate('(1+2)-1')).toEqual(2);
  expect(calculate('1 + -2')).toEqual(-1);
  expect(calculate('1+-2')).toEqual(-1);

  // Space in number
  expect(() => calculate('1 2')).toThrow("Space in number: '1 2'");
  expect(() => calculate('1  2')).toThrow("Space in number: '1 2'");
  expect(() => calculate('0 + 1 / (2 3) * 4')).toThrow("Space in number: '2 3'");
  expect(() => calculate('min(1 2)')).toThrow("Space in number: '1 2'");

  // Double '.' in number
  expect(() => calculate('1+2.3.4')).toThrow("Double '.' in number: '2.3.'");
  expect(() => calculate('1+2.3.4.5')).toThrow("Double '.' in number: '2.3.'");
  expect(() => calculate('0 + 1 / 2.3.4 * 5')).toThrow("Double '.' in number: '2.3.'");
  expect(() => calculate('min(1, 2.3.4)')).toThrow("Double '.' in number: '2.3.'");

  // Consecutive operators
  expect(calculate('1++2')).toEqual(3);
  expect(calculate('1-+2')).toEqual(-1);
  expect(calculate('1--2')).toEqual(3);
  expect(() => calculate('1++')).toThrow("Consecutive operators: '++'");
  expect(() => calculate('1-+')).toThrow("Consecutive operators: '-+'");
  expect(() => calculate('1--')).toThrow("Consecutive operators: '--'");
  expect(() => calculate('1-*2')).toThrow("Consecutive operators: '-*'");
  expect(() => calculate('0 + 1 / (2-*3) * 4')).toThrow("Consecutive operators: '-*'");
  expect(() => calculate('min(1-*2, 3)')).toThrow("Consecutive operators: '-*'");

  // Misplaced ','
  expect(() => calculate('1,2')).toThrow("Misplaced ','");
  expect(() => calculate(',1/2')).toThrow("Misplaced ','");
  expect(() => calculate('1,/2')).toThrow("Misplaced ','");
  expect(() => calculate('1/,2')).toThrow("Misplaced ','");
  expect(() => calculate('1/2,')).toThrow("Misplaced ','");
  expect(() => calculate('sin(,max,(,2,3,),/,3,*,3.14,)')).toThrow('Insufficient operators');
  expect(calculate('sin(,max(,2,3,),/3,*3.14,)')).toEqual(0.001_592_652_916_486_828_2);

  // Other edge cases
  expect(calculate('1+2+')).toBeNaN();
  expect(() => calculate('1+2a')).toThrow("Invalid characters: 'a'");
  expect(() => calculate('10 Hello')).toThrow("Invalid characters: 'Hello'");
  expect(calculate('1-.')).toBeNaN();
  expect(calculate('*')).toBeNaN();
  expect(calculate('/')).toBeNaN();

  // All together expression
  expect(
    calculate(
      '((3.1 + cos(-4) / 2) * max(-6, 6) ^ sin(6) * 9) / tan(log(8.8 + -2) % 7) + (6 * -1 - min(6, -4.2))'
    )
  ).toEqual(
    eval(
      '((3.1 + Math.cos(-4) / 2) * Math.max(-6, 6) ** Math.sin(6) * 9) / Math.tan(Math.log(8.8 + -2) % 7) + (6 * -1 - Math.min(6, -4.2))'
    )
  );
  expect(
    calculate('((3.1+cos(-4)/2)*max(-6,6)^sin(6)*9)/tan(log(8.8+-2)%7)+(6*-1-min(6,-4.2))')
  ).toEqual(
    eval(
      '((3.1+Math.cos(-4)/2)*Math.max(-6,6)**Math.sin(6)*9)/Math.tan(Math.log(8.8+-2)%7)+(6*-1-Math.min(6,-4.2))'
    )
  );
});

test('calculate() with getRandomMathExpression()', () => {
  for (let i = 0; i < 1000; i++) {
    const expr = getRandomMathExpression(getRandomInt(1, 100));
    expect(calculate(expr)).toEqual(eval(convertMathExpressionToEval(expr)));
  }
});

Mer her: https://gist.github.com/tkrotoff/b0b1d39da340f5fc6c5e2a79a8b6cec0


En annen løsning som kun støtter +, -, *, / uten parenteser: https://code.tutsplus.com/tutorials/what-they-didnt-tell-you-about-es5s-array-extras--net-28263 (av Felix Bohm)

function calculate(expression: string) {
  const parts = parse(expression); // Tokenize function to be implemented

  // Build an array with all operations reduced to additions
  const processed = new Array<number>();

  for (let i = 0; i < parts.length; i++) {
    const part = parts[i];
    switch (part) {
      case '+': {
        // Ignore
        break;
      }
      case '-': {
        const rightValue = parts[++i];
        if (typeof rightValue === 'number') {
          processed.push(-1 * rightValue);
        } else {
          processed.push(Number.NaN);
        }
        break;
      }
      case '*': {
        const leftValue = processed.pop();
        const rightValue = parts[++i];
        if (typeof leftValue === 'number' && typeof rightValue === 'number') {
          processed.push(leftValue * rightValue);
        } else {
          processed.push(Number.NaN);
        }
        break;
      }
      case '/': {
        const leftValue = processed.pop();
        const rightValue = parts[++i];
        if (typeof leftValue === 'number' && typeof rightValue === 'number') {
          processed.push(leftValue / rightValue);
        } else {
          processed.push(Number.NaN);
        }
        break;
      }
      default: {
        processed.push(part);
      }
    }
  }

  // Add all numbers and return the result
  return processed.reduce((accumulator, currentValue) => accumulator + currentValue);
}

Svar #7

Jeg laget BigEval for samme formål.
Ved løsning av uttrykk fungerer det nøyaktig som Eval() og støtter operatorer som %, ^, &, ** (potens) og ! (fakultet). Du har også lov til å bruke funksjoner og konstanter (eller si variabler) inne i uttrykket. Uttrykket løses i PEMDAS-rekkefølge, som er vanlig i programmeringsspråk inkludert JavaScript.

var Obj = new BigEval();
var result = Obj.exec("5! + 6.6e3 * (PI + E)"); // 38795.17158152233
var result2 = Obj.exec("sin(45 * deg)**2 + cos(pi / 4)**2"); // 1
var result3 = Obj.exec("0 & -7 ^ -7 - 0%1 + 6%2"); //-7

Den kan også settes til å bruke disse Big Number-bibliotekene for aritmetikk hvis du arbeider med tall med vilkårlig presisjon.

Relaterte artikler