Advertisement
Echo89

math2.js - An Arithmetic Expression Parser

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