3
\$\begingroup\$

I'm new to parsing, and there's only one way to learn anything, and that's to do it yourself. So I did. I wrote what is essentially a calculator. But instead you will send it a string and it will be evaluated.

It's syntax is as follows:

5 + 5
2 * sin(45 * 2)
2 * sin 90
2*sin(90)
2*sin90

As you can see, it does not have a strict syntax. The brackets only mean it's an expression inside an expression, so 2 * sin(2 * 45) becomes 2 * sin 90.

There is one problem with it, it does not respect the order of operations (BIDMAS). I'm not sure how to implement that yet. But by using parentheses you can correctly make calculations.

Here is the code:

var Calc = function() {
    'use strict';

    this.vars = {
        pi: Math.PI
    };
};

Calc.prototype.evaluate = function(expression) {
    'use strict';

    // Trigger final instruction to execute at end.
    expression += ' ';

    // Loop Variables

    var length = expression.length,
        iter,
        char,
        type;

    // Flags

    var flag_number = false,
        flag_float = false,
        flag_keyword = false,
        flag_operator = false,
        flag_operator_waiting = false,
        flag_brackets = 0,
        flag_var = false,
        flag_var_name = '',
        flag_math = false,
        flag_math_op = '';

    // Parser Variables

    var buffer = '',
        total = 0,
        cur_operator = null,
        buffer_parsed = 0;

    // Math Functions

    var stdOps = ['log', 'tan'],
        customOps = {
            fact: function(value) {
                var iter,
                    multiplier;

                for(multiplier = value - 1; multiplier > 0; --multiplier) {
                    value *= multiplier;
                }

                return value;
            },

            sin: function(value) {
                return Math.sin(value * Math.PI / 180);
            },

            cos: function(value) {
                return Math.cos(value * Math.PI / 180);
            }
        };


    // Helper Functions

    function isAlpha(char) {
        return 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'.indexOf(char) !== -1;
    }

    function isNumeric(char) {
        return '1234567890.,'.indexOf(char) !== -1;
    }

    function isSpecial(char) {
        return '+-*/^'.indexOf(char) !== -1;
    }

    function applyOperator(left, right, operator) {
        // Addition
        if(operator === '+') {
            return left + right;
        }

        // Subtraction
        if(operator === '-') {
            return left - right;
        }

        // Division
        if(operator === '/') {
            return left / right;
        }

        // Multiplication
        if(operator === '*') {
            return left * right;
        }

        if(operator === '^') {
            return Math.pow(left, right);
        }

        return null;
    }

    for(iter = 0; iter < length; ++iter) {
        char = expression.charAt(iter);

        // Are we parsing a number
        if(flag_number) {
            // The number has finished
            if(!isNumeric(char)) {
                if(isSpecial(buffer) && !isSpecial(char)) {
                    flag_number = false;
                    flag_operator = false;
                }

                // Is it a float?
                if(flag_float) {
                    buffer_parsed = parseFloat(buffer, 10);
                } else {
                    buffer_parsed = parseInt(buffer, 10);
                }

                // Is there an operator waiting?
                if(cur_operator !== '' && cur_operator !== null) {
                    total = applyOperator(total, buffer_parsed, cur_operator);

                    // Reset flags
                    flag_number = false;
                    flag_float = false;
                    flag_operator_waiting = false;

                    // Unkown operator
                    if(total === null) {
                        process.stdout.write('Unrecognised operator ' + cur_operator);
                        break;
                    }

                    // Reset current operator
                    cur_operator = '';
                } else {
                    // Is a mathematical function waiting? (sin, cos, tan etc)
                    if(flag_math) {
                        flag_math = false;
                        buffer = flag_math_op(buffer_parsed).toString();

                        // Check if float
                        if(buffer.indexOf('.') !== -1) {
                            flag_float = true;
                        }

                        iter--;
                        continue;
                    }

                    // Else it's just a number, function call, or variable at the beginning of the expression
                    total = buffer_parsed;
                    flag_number = false;
                }

                buffer = '';
                cur_operator = '';
            } else {
                // Check for float
                if(char === '.') {
                    flag_float = true;
                }

                // Allow for commas
                if(char === ',') {
                    continue;
                }

                buffer += char;
                continue;
            }
        }

        if(flag_keyword) {
            if(!isAlpha(char)) {
                flag_keyword = false;

                // Case insensitivity
                buffer = buffer.toLocaleLowerCase(buffer);

                switch(buffer) {
                    case 'exit':
                        process.exit();

                        break;

                    case 'set':
                        // setting a variable
                        flag_var = true;

                    default:
                        // Standard Math.[sin|cos|tan|log] operations
                        if(stdOps.indexOf(buffer) !== -1) {
                            flag_math = true;
                            flag_math_op = Math[buffer];
                        }

                        // Custom operations: fact (factorial)
                        if(buffer in customOps) {
                            flag_math = true;
                            flag_math_op = customOps[buffer];
                        }

                        // If not mathematical function, it must be a variable
                        if(!flag_math) {
                            // Are we setting a variable?
                            if(flag_var) {
                                flag_var_name = buffer;
                            } else {
                                // We are referencing a variable, not setting it
                                if(buffer in this.vars) {
                                    buffer = this.vars[buffer].toString();
                                    flag_number = true;
                                    // Check if variable is float
                                    if(buffer.indexOf('.') !== -1) {
                                        flag_float = true;
                                    }
                                    --iter;
                                    continue;
                                }
                            }
                        }
                }

                buffer = '';
            } else {
                buffer += char;
                continue;
            }
        }

        // Are we parsing an operator?
        if(flag_operator) {
            // Are we ending an operator?
            if(!isSpecial(char) || char === '-' || char === '+') {
                // reset flags
                flag_operator = false;
                cur_operator = buffer;
                // reset buffer
                buffer = '';
                flag_operator_waiting = true;

                if(char === '-' || char === '+') {
                    flag_number = true;
                    buffer += char;
                    continue;
                }
            } else {
                buffer += char;
                continue;
            }
        }

        // A number has started
        if(isNumeric(char)) {
            flag_number = true;
            buffer += char;
            continue;
        }

        // A keyword, variable or function has started
        if(isAlpha(char)) {
            flag_keyword = true;
            buffer += char;
            continue;
        }

        // An operator has started
        if(isSpecial(char)) {
            if((char === '-' || char === '+') && (flag_operator_waiting || flag_math)) {
                buffer += char;
                flag_number = true;
                continue;
            } else {
                flag_operator = true;
                buffer += char;
                continue;
            }
        }

        // Parse parentheses
        if(char === '(') {
            var bIndex = expression.indexOf(')', iter + 1);

            if(bIndex !== -1) {
                // extract and independtly evaluate this sub-expression, in a recursive fashion
                buffer = this.evaluate(expression.substring(iter + 1, bIndex)).toString();
                flag_number = true;
                flag_float = buffer.indexOf('.') !== -1;
                iter = bIndex;

                continue;
            } else {
                console.log("Invalid bracket syntax");
                break;
            }
        }
    }

    // We are setting a variable
    if(flag_var) {
        this.vars[flag_var_name] = total;
    }

    return total;
};

var calc = new Calc();

process.stdin.resume();
process.stdin.setEncoding('utf8');
process.stdin.on('data', function(expression) {
    console.log(calc.evaluate(expression))
    process.stdout.write("> ");
});
console.log("Welcome to calc.js:");
process.stdout.write("> ");

I have updated the code, see the older version in the edit history.

\$\endgroup\$
2
  • \$\begingroup\$ 42(42) doesn't work. 5(^2) returns 0. Negative powers don't work. \$\endgroup\$
    – Quill
    Jan 31, 2016 at 0:12
  • \$\begingroup\$ It never used to parse negative numbers, can't believe I forgot about negative numbers haha. Anyway, I've implemented them now. \$\endgroup\$
    – thephpdev
    Jan 31, 2016 at 11:01

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Browse other questions tagged or ask your own question.