2

How can I evaluate arithmetic expressions in Typescript? An example is '(3+5*(-3+-1))'.

Eval(...) is forbidden.

A suggested solution is not accepted at runtime:

let input = '(3+5)';
let resultNumber = (new Function( 'return (' + input + ')'))();

The error is:

SyntaxError: Invalid or unexpected token at new Function ()

Math.js with 136kb footprint (compressed) is too large in my for evaluating simple expressions. It could be customized by limiting the functionality.

So, do you have a small typescript file / service that can evaluate arithmetic expressions? Of course the unary minus/plus should work properly.

halfer
  • 19,824
  • 17
  • 99
  • 186
tm1701
  • 7,307
  • 17
  • 79
  • 168
  • [This post](https://stackoverflow.com/questions/3422673/how-to-evaluate-a-math-expression-given-in-string-form) while asked for Java might contain some info which can help you out. Maybe you can rewrite some of the code to Javascript/Typescript? – Mathyn Mar 22 '19 at 14:55
  • Make no mistake, if you think `eval()` is bad, then `new Function('');` is just as bad for the exact same reasons. – Evert Mar 22 '19 at 15:01
  • 1
    @Mathyn - GREAT - via your post I found a very compact evaluation of arithmetic expressions. Can you add this link to an Answer, and I will +1 it! https://stackoverflow.com/a/26227947/3143823 – tm1701 Mar 22 '19 at 15:22
  • 1
    @tjm1706 I'd love to but I'd have to rewrite the Java code to Typescript myself for it to be a proper answer. – Mathyn Mar 22 '19 at 15:31
  • I am already rewriting the code. You pointed me the way for a solution. So you can post that answer. – tm1701 Mar 22 '19 at 15:44

2 Answers2

4

Thanks to @Mathyn I found a piece of good Java code created by @Boann. I migrated it to Typescript, there you have it.

Below you also find the Karma test code, so you can see what is possible: +, -, (unary), *, ^, /, braces, (value), sin, cos, tan, sqrt, etc.

How to use it? In Angular, you can access it via dependency injection. Otherwise you can create an object. You can specify via a boolean (here 'true') to get the result as an integer.

arithmeticExpressionEvaluator.evaluate('10 + 2 * 6')   // shows 22.0
arithmeticExpressionEvaluator.evaluate('10 + 2 * 6', true)   // shows 22 (integer only)

The complete Typescript source code is:

export class ArithmeticExpressionEvaluator {
    static INVALID_NUMBER = -1234567.654;
    str: string;
    pos = -1;
    ch: string;

    evaluate(expression: string): number {
        return this.evaluateAll(expression, false);
    }

    evaluateAll(expression: string, resultIsInteger: boolean): number {
        this.str = expression;
        pos = -1;
        const outcome = this.parse();
        if (resultIsInteger) {
            return Math.round(outcome);
        }
        return outcome;
    }

    nextChar() {
        this.ch = (++this.pos < this.str.length) ? this.str.charAt(this.pos) : null;
    }

    eat(charToEat: string): boolean {
        while (this.ch === ' ') {
            this.nextChar();
        }
        if (this.ch === charToEat) {
            this.nextChar();
            return true;
        }
        return false;
    }

    parse(): number {
        this.nextChar();
        const x = this.parseExpression();
        if (this.pos < this.str.length) {
            return ArithmeticExpressionEvaluator.INVALID_NUMBER;
        }
        return x;
    }

    parseExpression(): number {
        let x = this.parseTerm();
        for (; ; ) {
            if (this.eat('+')) {  // addition
                x += this.parseTerm();
            } else if (this.eat('-')) {  // subtraction
                x -= this.parseTerm();
            } else {
                return x;
            }
        }
    }

    parseTerm(): number {
        let x = this.parseFactor();
        for (; ;) {
            if (this.eat('*')) {  // multiplication
                x *= this.parseFactor();
            } else if (this.eat('/')) {  // division
                x /= this.parseFactor();
            } else {
                return x;
            }
        }
    }

    parseFactor(): number {
        if (this.eat('+')) {  // unary plus
            return this.parseFactor();
        }
        if (this.eat('-')) { // unary minus
            return -this.parseFactor();
        }
        let x;
        const startPos = this.pos;
        if (this.eat('(')) { // parentheses
            x = this.parseExpression();
            this.eat(')');
        } else if ((this.ch >= '0' && this.ch <= '9') || this.ch === '.') { // numbers
            while ((this.ch >= '0' && this.ch <= '9') || this.ch === '.') {
                this.nextChar();
            }
            x = parseFloat(this.str.substring(startPos, this.pos));
        } else if (this.ch >= 'a' && this.ch <= 'z') { // functions
            while (this.ch >= 'a' && this.ch <= 'z') {
                this.nextChar();
            }
            const func = this.str.substring(startPos, this.pos);
            x = this.parseFactor();
            if (func === 'sqrt') {
                x = Math.sqrt(x);
            } else if (func === 'sin') {
                x = Math.sin(this.degreesToRadians(x));
            } else if (func === 'cos') {
                x = Math.cos(this.degreesToRadians(x));
            } else if (func === 'tan') {
                x = Math.tan(this.degreesToRadians(x));
            } else {
                return ArithmeticExpressionEvaluator.INVALID_NUMBER;
            }
        } else {
            return ArithmeticExpressionEvaluator.INVALID_NUMBER;
        }
        if (this.eat('^')) {  // exponentiation
            x = Math.pow(x, this.parseFactor());
        }
        return x;
    }

    degreesToRadians(degrees: number): number {
        const pi = Math.PI;
        return degrees * (pi / 180);
    }
}

The Karma test code is:

import {ArithmeticExpressionEvaluator} from './arithmetic-expression-evaluator.service';

describe('Arithmetic Expression Evaluation', () => {
    let arithmeticExpressionEvaluator: ArithmeticExpressionEvaluator;
    beforeEach(() => {
        arithmeticExpressionEvaluator = new ArithmeticExpressionEvaluator();
    });
    it('Arithmetic Expression Evaluation - double result', () => {
        expect(arithmeticExpressionEvaluator.evaluate('10 + 2 * 6')).toBe(22.0);
        expect(arithmeticExpressionEvaluator.evaluate('100 * 2 + 12')).toBe(212.0);
        expect(arithmeticExpressionEvaluator.evaluate('100 * 2 + -12')).toBe(188.0);
        expect(arithmeticExpressionEvaluator.evaluate('100 * (2) + -12')).toBe(188.0);
        expect(arithmeticExpressionEvaluator.evaluate('-100 * 2 + 12')).toBe(-188.0);
        expect(arithmeticExpressionEvaluator.evaluate('100 * 2 ^ 12')).toBe(409600.0);
        expect(arithmeticExpressionEvaluator.evaluate('100 * ( 2 + 12 )')).toBe(1400.0);
        expect(arithmeticExpressionEvaluator.evaluate('(100) * (( 2 ) + (12) )')).toBe(1400.0);
        expect(arithmeticExpressionEvaluator.evaluate('100 * ( 2 + 12 ) / 14')).toBe(100.0);
    });
    it('Arithmetic Expression Evaluation - integer result', () => {
        expect(arithmeticExpressionEvaluator.evaluateAll('10 + 2 * 6',  true)).toBe(22);
        expect(arithmeticExpressionEvaluator.evaluateAll('100 * 2 + 12' , true)).toBe(212);
        expect(arithmeticExpressionEvaluator.evaluateAll('100 * 2 + -12', true)).toBe(188);
        expect(arithmeticExpressionEvaluator.evaluateAll('100 * (2) + -12', true)).toBe(188);
        expect(arithmeticExpressionEvaluator.evaluateAll('-100 * 2 + 12' , true)).toBe(-188);
        expect(arithmeticExpressionEvaluator.evaluateAll('100 * 2 ^ 12', true)).toBe(409600);
        expect(arithmeticExpressionEvaluator.evaluateAll('100 * ( 2 + 12 )', true)).toBe(1400);
        expect(arithmeticExpressionEvaluator.evaluateAll('(100) * (( 2 ) + (12) )', true)).toBe(1400);
        expect(arithmeticExpressionEvaluator.evaluateAll('100 * ( 2 + 12 ) / 14', true)).toBe(100);
    });
});
tm1701
  • 7,307
  • 17
  • 79
  • 168
0

If eval() is off the table, you need to write a custom DSL and write your own parser. To some extend you will build what eval() already does for you, but presumably a more restricted version without all the javascript features.

Alternatively find an existing NPM package.

Evert
  • 93,428
  • 18
  • 118
  • 189