Echo89

ES5 Expression Parser

Sep 15th, 2016
81
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. var ExpressionParser = (function() {
  2.     'use strict';
  3.  
  4.     var NUMERIC_CHARSET = '01234567890.',
  5.         ALPHA_CHARSET = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_',
  6.         OPERATOR_CHARSET = '+-/*^%',
  7.         WHITE_SPACE_REGEX = /\s/;
  8.  
  9.     var MathFunctions = {
  10.         sin: function(radians) { return Math.sin(radians); },
  11.         cos: function(radians) { return Math.cos(radians); },
  12.         tan: function(radians) { return Math.tan(radians); },
  13.         fact: function (value) {
  14.             var iter,
  15.                 multiplier,
  16.                 returnValue = value;
  17.  
  18.             for(multiplier = value - 1; multiplier > 0; --multiplier) {
  19.                 returnValue *= multiplier;
  20.             }
  21.  
  22.             return returnValue;
  23.         },
  24.         exp: function (value) { return Math.exp(value); },
  25.         sqrt: function (value) { return Math.sqrt(value); },
  26.         ceil: function (value) { return Math.ceil(value); },
  27.         floor: function (value) { return Math.floor(value); },
  28.         abs: function (value) { return Math.abs(value); },
  29.         acos: function (value) { return Math.acos(value); },
  30.         asin: function (value) { return Math.asin(value); },
  31.         atan: function (value) { return Math.atan(value); },
  32.         round: function (value) { return Math.round(value); }
  33.     };
  34.  
  35.     var Helpers = {
  36.         isNumeric: function(char) { return NUMERIC_CHARSET.indexOf(char) !== -1; },
  37.         isAlpha: function(char) { return ALPHA_CHARSET.indexOf(char) !== -1; },
  38.         isOperator: function(char) { return OPERATOR_CHARSET.indexOf(char) !== -1; },
  39.         isMathFunction: function(keyword) { return typeof MathFunctions[keyword] === 'function'; },
  40.         isWhitespace: function(char) { return WHITE_SPACE_REGEX.test(char); },
  41.         radians: function(angle) { return angle * Math.PI / 180; }
  42.     };
  43.  
  44.     var OperatorFunctions = {
  45.         '+': function(left, right) { return left + right },
  46.         '-': function (left, right) { return left - right },
  47.         '/': function (left, right) { return left / right },
  48.         '*': function (left, right) { return left * right },
  49.         '%': function (left, right) { return left % right },
  50.         '^': function (left, right) { return Math.pow(left, right); }
  51.     };
  52.  
  53.     function EP() {
  54.         'use strict';
  55.  
  56.         this.variables = {
  57.             pi: Math.PI,
  58.             PI: Math.PI,
  59.             e: Math.E,
  60.             E: Math.E,
  61.             rand: function() { return Math.random() }
  62.         };
  63.  
  64.         this.readOnlyVariables = Object.keys(this.variables).map(function(varName) { return varName; });
  65.     };
  66.  
  67.     /* Sets a variable */
  68.     EP.prototype.setVariable = function(name, value) {
  69.         'use strict';
  70.  
  71.         if(this.readOnlyVariables.indexOf(name) !== -1) {
  72.             throw new Error('Cannot set read only variable "' + name + '"');
  73.         }
  74.  
  75.         this.variables[name] = value;
  76.     };
  77.  
  78.     /* Gets a variable */
  79.     EP.prototype.getVariable = function(name) {
  80.         'use strict';
  81.  
  82.         if(this.isVariable(name)) {
  83.             var variable = this.variables[name];
  84.  
  85.             if(typeof variable === 'function') {
  86.                 return variable();
  87.             }
  88.  
  89.             return variable;
  90.         }
  91.  
  92.         return undefined;
  93.     };
  94.  
  95.     /* Checks if a variable exists */
  96.     EP.prototype.isVariable = function(name) {
  97.         'use strict';
  98.  
  99.         return this.variables.hasOwnProperty(name);
  100.     };
  101.  
  102.     /* Parse an expression */
  103.     EP.prototype.parse = function(expression) {
  104.         'use strict';
  105.  
  106.         var tokens = this.tokenize(expression);
  107.  
  108.         tokens = this.parseTokens(tokens);
  109.  
  110.         var tokensLength = tokens.length,
  111.             iter,
  112.             value = null,
  113.             last_number = null,
  114.             flag_assignment = false;
  115.  
  116.         for(iter = 0; iter < tokensLength; ++iter) {
  117.             // Get the value
  118.             if(tokens[iter][0] === 'number') {
  119.                 value = tokens[iter][1];
  120.             }
  121.  
  122.             if(tokens[iter][0] === 'assignment') {
  123.                 if(
  124.                     iter - 1 === 0 &&                   // Check there is a keyword previous
  125.                     iter + 1 < tokensLength &&          // Check there is a value to set next
  126.  
  127.                     tokens[iter - 1][0] === 'keyword'
  128.                 ) {
  129.                     flag_assignment = true;
  130.                 } else {
  131.                     throw new Error('Unexpected assignment');
  132.                 }
  133.             }
  134.         }
  135.  
  136.         if(flag_assignment) {
  137.             this.setVariable(tokens[0][1], value);
  138.         }
  139.  
  140.         return value;
  141.     };
  142.  
  143.     /* Parse tokens */
  144.     EP.prototype.parseTokens = function(tkns) {
  145.         'use strict';
  146.  
  147.         var tokens = tkns;
  148.  
  149.         tokens = this.parseVariables(tokens);
  150.         tokens = this.parseBrackets(tokens);
  151.         tokens = this.parseNegatives(tokens);
  152.         tokens = this.parseFunctions(tokens);
  153.         tokens = this.parseOperations(tokens);
  154.  
  155.         return tokens;
  156.     };
  157.  
  158.     EP.prototype.parseBrackets = function(tkns) {
  159.         'use strict';
  160.  
  161.         var tokens = tkns,
  162.             tokensLength = tokens.length,
  163.             bracketDepth = 0,
  164.             bracketIndex = 0,
  165.             iter;
  166.  
  167.         for(iter = 0; iter < tokensLength; ++iter) {
  168.             if(tokens[iter][0] === 'bracket') {
  169.                 if(tokens[iter][1] === ')' && bracketDepth === 0) {
  170.                     throw new Error('Invalid bracket syntax');
  171.                 }
  172.  
  173.                 if(bracketDepth > 0) {
  174.                     if(tokens[iter][1] === ')') {
  175.                         --bracketDepth;
  176.                     }
  177.  
  178.                     if(bracketDepth === 0) {
  179.                         var leftSide = tokens.slice(0, bracketIndex),
  180.                             parsed = this.parseTokens(tokens.slice(bracketIndex + 1, iter)),
  181.                             rightSide = tokens.slice(iter + 1);
  182.  
  183.                         tokens = leftSide.concat(parsed, rightSide);
  184.                         iter += tokens.length - tokensLength;
  185.                         tokensLength = tokens.length;
  186.                     }
  187.                 }
  188.  
  189.                 if(tokens[iter][1] === '(') {
  190.                     if(bracketDepth === 0) {
  191.                         bracketIndex = iter;
  192.                     }
  193.  
  194.                     ++bracketDepth;
  195.                 }
  196.             }
  197.         }
  198.  
  199.         if(bracketDepth > 0) {
  200.             throw new Error('Invalid bracket syntax');
  201.         }
  202.  
  203.         return tokens;
  204.     };
  205.  
  206.     EP.prototype.parseNegatives = function(tkns) {
  207.         'use strict';
  208.  
  209.         var tokens = tkns,
  210.             tokensLength = tokens.length,
  211.             iter;
  212.  
  213.         for(iter = 0; iter < tokensLength; ++iter) {
  214.             // Logic for a negative number
  215.             if(
  216.                 tokens[iter][0] === 'operator' &&
  217.                 (
  218.                     tokens[iter][1] === '-' ||          // Check it's a minus symbol
  219.                     tokens[iter][1] === '+'             // Or a plus symbol
  220.                 ) &&
  221.                 (
  222.                     iter - 1 < 0 ||                     // Either there is no previous token...
  223.                     tokens[iter - 1][0] !== 'number'    // Or it's not a number
  224.                 ) &&
  225.                 iter + 1 < tokensLength &&              // Check there is a proceeding token
  226.                 tokens[iter + 1][0] === 'number'        // And it's a number
  227.             ) {
  228.                 // Make the next number a negative
  229.                 tokens[iter + 1][1] = tokens[iter][1] === '-' ? -tokens[iter + 1][1] : tokens[iter + 1][1];
  230.                 // Remove this token from stack
  231.                 tokens.splice(iter, 1);
  232.                 --tokensLength;
  233.                 --iter;
  234.                 continue;
  235.             }
  236.         }
  237.  
  238.         return tokens;
  239.     };
  240.  
  241.     EP.prototype.parseVariables = function(tkns) {
  242.         'use strict';
  243.  
  244.         var tokens = tkns,
  245.             tokensLength = tokens.length,
  246.             iter;
  247.  
  248.         for(iter = 0; iter < tokensLength; ++iter) {
  249.             if(tokens[iter][0] === 'keyword') {
  250.                 if(
  251.                     !Helpers.isMathFunction(tokens[iter][1]) && // Check it's not a function
  252.                     (
  253.                         iter === tokensLength - 1 ||            // Either this is the last token
  254.                         tokens[iter + 1][0] !== 'assignment'    // Or the next token is not an assignment
  255.                     )
  256.                 ) {
  257.                     // Check variable exists
  258.                     if(this.isVariable(tokens[iter][1])) {
  259.                         if(
  260.                             iter - 1 >= 0 &&
  261.                             tokens[iter - 1][0] === 'number'
  262.                         ) {
  263.                             tokens[iter][0] = 'number';
  264.                             tokens[iter][1] = this.getVariable(tokens[iter][1]) * tokens[iter - 1][1];
  265.  
  266.                             var leftSide = tokens.slice(0, iter - 1),
  267.                                 rightSide = tokens.slice(iter);
  268.  
  269.                             tokens = leftSide.concat(rightSide);
  270.  
  271.                             --iter;
  272.                             --tokensLength;
  273.                         } else {
  274.                             tokens[iter][0] = 'number';
  275.                             tokens[iter][1] = this.getVariable(tokens[iter][1]);
  276.                         }
  277.  
  278.                         continue;
  279.                     } else {
  280.                         throw new Error('Undefined variable "' + tokens[iter][1] + '"');
  281.                     }
  282.                 }
  283.             }
  284.         }
  285.  
  286.         return tokens;
  287.     };
  288.  
  289.     EP.prototype.parseFunctions = function(tkns) {
  290.         'use strict';
  291.  
  292.         var tokens = tkns,
  293.             tokensLength = tokens.length,
  294.             iter;
  295.  
  296.         for(iter = 0; iter < tokensLength; ++iter) {
  297.             if(tokens[iter][0] === 'keyword' && tokens[iter][1] in MathFunctions) {
  298.                 if(
  299.                     iter + 1 < tokensLength &&          // Check this is not the last token
  300.                     tokens[iter + 1][0] === 'number'    // And the last next token is a number
  301.                 ) {
  302.                     // Apply math function
  303.                     tokens[iter + 1][1] = MathFunctions[tokens[iter][1]](tokens[iter + 1][1]);
  304.                     // Remove this token from stack
  305.                     tokens.splice(iter, 1);
  306.                     --tokensLength;
  307.                     --iter;
  308.                 } else {
  309.                     throw new Error('unexpected function "' + tokens[iter][1] + '"');
  310.                 }
  311.             }
  312.         }
  313.  
  314.         return tokens;
  315.     };
  316.  
  317.     EP.prototype.parseOperations = function(tkns) {
  318.         'use strict';
  319.  
  320.         // Order of operations
  321.         var operators = ['^', '/', '*', '+', '-'],
  322.             tokens = tkns,
  323.             self = this;
  324.  
  325.         operators.forEach(function(operator) {
  326.             tokens = self.parseOperator(tokens, operator);
  327.         });
  328.  
  329.         return tokens;
  330.     };
  331.  
  332.     EP.prototype.parseOperator = function(tkns, oprtr) {
  333.         'use strict';
  334.  
  335.         var tokens = tkns,
  336.             operator = oprtr,
  337.             tokensLength = tokens.length,
  338.             iter;
  339.  
  340.         for(iter = 0; iter < tokensLength; ++iter) {
  341.             var token = tokens[iter],
  342.                 token_type = token[0],
  343.                 token_value = token[1];
  344.  
  345.             if(token_type === 'operator' && token_value === operator) {
  346.                 if(
  347.                     iter - 1 >= 0 &&                        // Check there is a previous token
  348.                     iter + 1 < tokensLength &&              // Check there is a next token
  349.                     tokens[iter - 1][0] === 'number' &&     // Check the previous token is a number
  350.                     tokens[iter + 1][0] === 'number'        // Check the next token is a number
  351.                 ) {
  352.                     tokens[iter + 1][1] = OperatorFunctions[token_value](tokens[iter - 1][1], tokens[iter + 1][1]);
  353.  
  354.                     var leftSide = tokens.slice(0, iter - 1),
  355.                         rightSide = tokens.slice(iter + 1);
  356.  
  357.                     // Replace sub-expression with the result value
  358.                     tokens = leftSide.concat(rightSide);
  359.                     iter += tokens.length - tokensLength;
  360.                     tokensLength = tokens.length;
  361.  
  362.                     continue;
  363.                 } else {
  364.                     throw new Error('unexpected operator "' + tokens[iter][1] + '"');
  365.                 }
  366.             }
  367.         }
  368.  
  369.         return tokens;
  370.     };
  371.  
  372.     /**
  373.      * Split expression into tokens
  374.      */
  375.     EP.prototype.tokenize = function(expr) {
  376.         'use strict';
  377.  
  378.         // TOKENIZER VARS
  379.         var expression = expr + ' ', // Append space so that the last character before that space is tokenised
  380.             expressionLength = expression.length,
  381.             iter,
  382.             tokens = [ ],
  383.             buffer = '';
  384.  
  385.         // FLAGS
  386.         var flag_numeric = false,
  387.             flag_keyword = false,
  388.             flag_operator = false,
  389.             flag_sciNotation = false;
  390.  
  391.         // Iterate through expression
  392.         for(iter = 0; iter < expressionLength; ++iter) {
  393.             var char = expression.charAt(iter),
  394.                 char_isNumeric = Helpers.isNumeric(char),
  395.                 char_isOperator = Helpers.isOperator(char),
  396.                 char_isAlpha = Helpers.isAlpha(char);
  397.  
  398.             if(flag_keyword) {
  399.                 // We've reached the end of the keyword
  400.                 if(!char_isAlpha) {
  401.                     flag_keyword = false;
  402.                     tokens.push(['keyword', buffer]);
  403.                     buffer = '';
  404.                 }
  405.             }
  406.  
  407.             if(flag_numeric) {
  408.                 // We've reached the end of the number
  409.                 if(!char_isNumeric) {
  410.                     if(char === 'e') {
  411.                         flag_sciNotation = true;
  412.                         buffer += char;
  413.                         continue;
  414.                     }
  415.  
  416.                     if(flag_sciNotation && (char === '+' || char === '-')) {
  417.                         flag_sciNotation = false;
  418.                         buffer += char;
  419.                         continue;
  420.                     }
  421.  
  422.                     // Skip char if comma, so we can allow for formatted numbers
  423.                     if(char === ',' && iter + 1 < expressionLength && Helpers.isNumeric(expression[iter + 1])) {
  424.                         continue;
  425.                     }
  426.  
  427.                     var parsingFunction = parseInt;
  428.  
  429.                     if(buffer.indexOf('.') !== -1) { // Check for float
  430.                         parsingFunction = parseFloat;
  431.                     }
  432.  
  433.                     tokens.push(['number', new Number(buffer)]);
  434.  
  435.                     flag_numeric = false;
  436.                     flag_sciNotation = false;
  437.                     buffer = '';
  438.                 } else {
  439.                     flag_sciNotation = false;
  440.                 }
  441.             }
  442.  
  443.             if(char_isNumeric) {                     // Check for a number
  444.                 flag_numeric = true;
  445.                 buffer += char;
  446.             } else if(char_isAlpha) {                // Check for a keyword
  447.                 flag_keyword = true;
  448.                 buffer += char;
  449.             } else if(char_isOperator) {             // Check for an operator
  450.                 tokens.push(['operator', char]);
  451.             } else if(char === '(') {                // Check for parentheses
  452.                 tokens.push(['bracket', '(']);
  453.             } else if(char === ')') {                // Check for closing parentheses
  454.                 tokens.push(['bracket', ')']);
  455.             } else if(char === '=') {                // Check for assignment
  456.                 tokens.push(['assignment', char]);
  457.             } else if(!Helpers.isWhitespace(char)) { // Check for whitespace
  458.                 throw new Error('Unexpected char "' + char + '"');
  459.             }
  460.         }
  461.  
  462.         return tokens;
  463.     };
  464.  
  465.     return EP;
  466. })();
RAW Paste Data