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('...')ellerFunction('...')som er dårlig - arbeider bare med
+,- - feiler på operatorprioritet (f.eks.
1+2*3skal 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.
