# math2.js - An Arithmetic Expression Parser

Jun 17th, 2016
88
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 = {
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.
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.
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.
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.
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.
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.
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.
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.
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. */
RAW Paste Data