├── .gitignore ├── favicon.ico ├── thumbnail.png ├── README.md ├── .github └── workflows │ └── main.yml ├── package.json ├── environment.test.js ├── LICENSE ├── simple.test.js ├── evaluator.test.js ├── index.js ├── simple.js ├── index.html └── evaluator.js /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | .cache/ 3 | node_modules/ 4 | .vscode/ 5 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chidiwilliams/expression-evaluator/HEAD/favicon.ico -------------------------------------------------------------------------------- /thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chidiwilliams/expression-evaluator/HEAD/thumbnail.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # expression-evaluator 2 | 3 | Evaluates Math expressions. 4 | 5 | Supports: 6 | 7 | - Arithmetic operators: `+`, `-`, `*`, `/` 8 | - Brackets 9 | - Comparison operators: `<`, `>`, `==` 10 | - Variables 11 | - Custom functions 12 | 13 | Blog post: https://chidiwilliams.com/evaluator/ 14 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | gh-pages: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: actions/setup-node@v1 12 | with: 13 | node-version: '10.x' 14 | - run: npm install -g yarn 15 | - name: yarn install and build 16 | run: | 17 | yarn install --frozen-lockfile 18 | yarn run build 19 | - uses: crazy-max/ghaction-github-pages@v2 20 | with: 21 | target_branch: gh-pages 22 | build_dir: dist 23 | env: 24 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "expression-evaluator", 3 | "version": "1.0.0", 4 | "main": "evaluator.js", 5 | "homepage": "http://chidiwilliams.github.io/expression-evaluator", 6 | "scripts": { 7 | "test": "node simple.test.js && node evaluator.test.js", 8 | "deploy": "yarn run build && gh-pages -d dist", 9 | "dev": "parcel index.html", 10 | "build": "parcel build index.html --public-url ." 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/chidiwilliams/expression-evaluator.git" 15 | }, 16 | "author": "Chidi Williams", 17 | "license": "ISC", 18 | "bugs": { 19 | "url": "https://github.com/chidiwilliams/expression-evaluator/issues" 20 | }, 21 | "devDependencies": { 22 | "gh-pages": "^2.2.0", 23 | "parcel": "^1.12.4", 24 | "parcel-plugin-ogimage": "^1.1.0", 25 | "parcel-plugin-static-files-copy": "^2.3.1" 26 | }, 27 | "dependencies": { 28 | "canvas-confetti": "^1.3.3" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /environment.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const { evaluate } = require('./evaluator'); 3 | 4 | /** 5 | * These tests are in a different file because they update the state 6 | * of the environment and must be run in the specified order. 7 | */ 8 | const testCases = [ 9 | { 10 | input: 'SET(#FR, 45 + 50)', 11 | output: 95, 12 | message: 'setting the value of a variable', 13 | }, 14 | { 15 | input: 'SET(#FR, $FR + 5)', 16 | output: 100, 17 | message: 'updating value of already set variable', 18 | }, 19 | { 20 | input: '$FR * 10', 21 | output: 1000, 22 | message: 'accessing a set variable', 23 | }, 24 | ]; 25 | 26 | testCases.forEach((test) => { 27 | assertEqual(evaluate(test.input), test.output, `evaluator: ${test.message}`); 28 | }); 29 | 30 | function assertEqual(actual, expected, message) { 31 | assert.deepStrictEqual( 32 | actual, 33 | expected, 34 | `${message}: expected: ${expected}, got: ${actual}`, 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Chidi Williams 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /simple.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const { tokenize, toRPN, evalRPN, evaluate } = require('./simple'); 3 | 4 | const testCases = [ 5 | { 6 | input: '2 + 2', 7 | tokens: [2, '+', 2], 8 | rpn: [2, 2, '+'], 9 | output: 4, 10 | message: 'simple', 11 | }, 12 | { 13 | input: '0.3 * 0.95', 14 | tokens: [0.3, '*', 0.95], 15 | rpn: [0.3, 0.95, '*'], 16 | output: 0.285, 17 | message: 'with decimals', 18 | }, 19 | { 20 | input: '1 * 3 )', 21 | tokens: [1, '*', 3, ')'], 22 | rpn: [1, 3, '*'], 23 | output: 3, 24 | message: 'with unbalanced parantheses', 25 | }, 26 | { 27 | input: '24 + 5 * (6 - 3 ^ 2 ^ 2)', 28 | tokens: [24, '+', 5, '*', '(', 6, '-', 3, '^', 2, '^', 2, ')'], 29 | rpn: [24, 5, 6, 3, 2, '^', 2, '^', '-', '*', '+'], 30 | output: -351, 31 | message: 'with operators with different precedences', 32 | }, 33 | ]; 34 | 35 | testCases.forEach((test) => { 36 | assertEqual(tokenize(test.input), test.tokens, `tokenize: ${test.message}`); 37 | assertEqual(toRPN(test.tokens), test.rpn, `toRPN: ${test.message}`); 38 | assertEqual(evalRPN(test.rpn), test.output, `evalRPN: ${test.message}`); 39 | assertEqual(evaluate(test.input), test.output, `evaluator: ${test.message}`); 40 | }); 41 | 42 | function assertEqual(actual, expected, message) { 43 | assert.deepStrictEqual( 44 | actual, 45 | expected, 46 | `${message}: expected: ${expected}, got: ${actual}`, 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /evaluator.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const { evalRPN, evaluate, toRPN, tokenize } = require('./evaluator'); 3 | require('./simple.test'); 4 | require('./environment.test'); 5 | 6 | const testCases = [ 7 | { 8 | input: 'MAX(50, 10)', 9 | tokens: ['MAX', '(', 50, ',', 10, ')'], 10 | rpn: [50, 10, 'MAX'], 11 | output: 50, 12 | message: 'with MAX function', 13 | }, 14 | { 15 | input: '54 + SQRT(49)', 16 | tokens: [54, '+', 'SQRT', '(', 49, ')'], 17 | rpn: [54, 49, 'SQRT', '+'], 18 | output: 61, 19 | message: 'with SQRT function', 20 | }, 21 | { 22 | input: '54 + SQRT(49) * 8', 23 | tokens: [54, '+', 'SQRT', '(', 49, ')', '*', 8], 24 | rpn: [54, 49, 'SQRT', 8, '*', '+'], 25 | output: 110, 26 | message: 'with function and operators with different precedences', 27 | }, 28 | { 29 | input: 'IF(54 < 3, 6, 9) + 20', 30 | tokens: ['IF', '(', 54, '<', 3, ',', 6, ',', 9, ')', '+', 20], 31 | rpn: [54, 3, '<', 6, 9, 'IF', 20, '+'], 32 | output: 29, 33 | message: 'with function and operators with different precedences', 34 | }, 35 | { 36 | input: '$PI * 78 + $E', 37 | tokens: ['$PI', '*', 78, '+', '$E'], 38 | rpn: ['$PI', 78, '*', '$E', '+'], 39 | output: 247.7625088084629, 40 | message: 'with predefined variable', 41 | }, 42 | ]; 43 | 44 | testCases.forEach((test) => { 45 | assertEqual(tokenize(test.input), test.tokens, `tokenize: ${test.message}`); 46 | assertEqual(toRPN(test.tokens), test.rpn, `toRPN: ${test.message}`); 47 | assertEqual(evalRPN(test.rpn), test.output, `evalRPN: ${test.message}`); 48 | assertEqual(evaluate(test.input), test.output, `evaluator: ${test.message}`); 49 | }); 50 | 51 | function assertEqual(actual, expected, message) { 52 | assert.deepStrictEqual( 53 | actual, 54 | expected, 55 | `${message}: expected: ${expected}, got: ${actual}`, 56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import confetti from 'canvas-confetti'; 2 | import * as evaluator from './evaluator'; 3 | 4 | const inputElement = document.querySelector('#input'); 5 | const outputElement = document.querySelector('#output'); 6 | const envElement = document.querySelector('#env'); 7 | 8 | inputElement.addEventListener('input', (event) => { 9 | runEvaluator(event.target.value); 10 | }); 11 | 12 | function runEvaluator(expression) { 13 | try { 14 | const result = evaluator.evaluate(expression); 15 | outputElement.innerText = result ?? ''; 16 | outputElement.classList.remove('error'); 17 | 18 | if (result === Infinity) { 19 | runConfetti(); 20 | } 21 | } catch (error) { 22 | outputElement.innerText = error; 23 | outputElement.classList.add('error'); 24 | } 25 | 26 | const envStr = Object.entries(evaluator.environment) 27 | .map(([k, v]) => `${k}=${v}`) 28 | .join(', '); 29 | 30 | envElement.innerText = envStr; 31 | } 32 | 33 | // https://www.kirilv.com/canvas-confetti/#fireworks 34 | function runConfetti() { 35 | const duration = 5 * 1000; 36 | const animationEnd = Date.now() + duration; 37 | const defaults = { startVelocity: 30, spread: 360, ticks: 60, zIndex: 0 }; 38 | 39 | function randomInRange(min, max) { 40 | return Math.random() * (max - min) + min; 41 | } 42 | 43 | const interval = setInterval(function () { 44 | const timeLeft = animationEnd - Date.now(); 45 | 46 | if (timeLeft <= 0) { 47 | return clearInterval(interval); 48 | } 49 | 50 | const particleCount = 50 * (timeLeft / duration); 51 | confetti( 52 | Object.assign({}, defaults, { 53 | particleCount, 54 | origin: { x: randomInRange(0.1, 0.3), y: Math.random() - 0.2 }, 55 | }), 56 | ); 57 | confetti( 58 | Object.assign({}, defaults, { 59 | particleCount, 60 | origin: { x: randomInRange(0.7, 0.9), y: Math.random() - 0.2 }, 61 | }), 62 | ); 63 | }, 250); 64 | } 65 | 66 | runEvaluator(inputElement.value); 67 | -------------------------------------------------------------------------------- /simple.js: -------------------------------------------------------------------------------- 1 | function tokenize(input) { 2 | let scanner = 0; 3 | const tokens = []; 4 | 5 | while (scanner < input.length) { 6 | const char = input[scanner]; 7 | 8 | if (/[0-9]/.test(char)) { 9 | let digits = ''; 10 | 11 | while (scanner < input.length && /[0-9\.]/.test(input[scanner])) { 12 | digits += input[scanner++]; 13 | } 14 | 15 | const number = parseFloat(digits); 16 | tokens.push(number); 17 | continue; 18 | } 19 | 20 | if (/[+\-/*()^]/.test(char)) { 21 | tokens.push(char); 22 | scanner++; 23 | continue; 24 | } 25 | 26 | if (char === ' ') { 27 | scanner++; 28 | continue; 29 | } 30 | 31 | throw new Error(`Invalid token ${char} at position ${scanner}`); 32 | } 33 | 34 | return tokens; 35 | } 36 | 37 | function toRPN(tokens) { 38 | const operators = []; 39 | const out = []; 40 | 41 | for (let i = 0; i < tokens.length; i++) { 42 | const token = tokens[i]; 43 | 44 | if (typeof token === 'number') { 45 | out.push(token); 46 | continue; 47 | } 48 | 49 | if (/[+\-/*<>=^]/.test(token)) { 50 | while (shouldUnwindOperatorStack(operators, token)) { 51 | out.push(operators.pop()); 52 | } 53 | operators.push(token); 54 | continue; 55 | } 56 | 57 | if (token === '(') { 58 | operators.push(token); 59 | continue; 60 | } 61 | 62 | if (token === ')') { 63 | while (operators.length > 0 && operators[operators.length - 1] !== '(') { 64 | out.push(operators.pop()); 65 | } 66 | operators.pop(); 67 | continue; 68 | } 69 | 70 | throw new Error(`Unparsed token ${token} at position ${i}`); 71 | } 72 | 73 | for (let i = operators.length - 1; i >= 0; i--) { 74 | out.push(operators[i]); 75 | } 76 | 77 | return out; 78 | } 79 | 80 | const precedence = { '^': 3, '*': 2, '/': 2, '+': 1, '-': 1 }; 81 | 82 | function shouldUnwindOperatorStack(operators, nextToken) { 83 | if (operators.length === 0) { 84 | return false; 85 | } 86 | 87 | const lastOperator = operators[operators.length - 1]; 88 | return precedence[lastOperator] >= precedence[nextToken]; 89 | } 90 | 91 | function evalRPN(rpn) { 92 | const stack = []; 93 | 94 | for (let i = 0; i < rpn.length; i++) { 95 | const token = rpn[i]; 96 | 97 | if (/[+\-/*^]/.test(token)) { 98 | stack.push(operate(token, stack)); 99 | continue; 100 | } 101 | 102 | // token is a number 103 | stack.push(token); 104 | } 105 | 106 | return stack.pop(); 107 | } 108 | 109 | function operate(operator, stack) { 110 | const a = stack.pop(); 111 | const b = stack.pop(); 112 | 113 | switch (operator) { 114 | case '+': 115 | return b + a; 116 | case '-': 117 | return b - a; 118 | case '*': 119 | return b * a; 120 | case '/': 121 | return b / a; 122 | case '^': 123 | return Math.pow(b, a); 124 | default: 125 | throw new Error(`Invalid operator: ${operator}`); 126 | } 127 | } 128 | 129 | function evaluate(input) { 130 | return evalRPN(toRPN(tokenize(input))); 131 | } 132 | 133 | module.exports = { 134 | tokenize, 135 | toRPN, 136 | evalRPN, 137 | evaluate, 138 | }; 139 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 14 | 15 | 16 | 17 | 18 | 19 | 23 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 38 | 42 | 43 | Expression Evaluator 44 | 119 | 120 | 121 |
122 |

Expression Evaluator

123 | 124 |
125 | 148 |
149 | 150 |
151 |
155 | 162 |
163 | 164 |
165 |
ENV>
166 |
167 | > 168 |
169 |
170 |
171 | 172 | 179 |
180 | 181 | 182 | 183 | -------------------------------------------------------------------------------- /evaluator.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ####### # # ###### ###### ####### ##### ##### ### ####### # # 3 | * # # # # # # # # # # # # # # # ## # 4 | * # # # # # # # # # # # # # # # # 5 | * ##### # ###### ###### ##### ##### ##### # # # # # # 6 | * # # # # # # # # # # # # # # # 7 | * # # # # # # # # # # # # # # # ## 8 | * ####### # # # # # ####### ##### ##### ### ####### # # 9 | * 10 | * ####### # # # # # # # ####### ####### ###### 11 | * # # # # # # # # # # # # # # # 12 | * # # # # # # # # # # # # # # # 13 | * ##### # # # # # # # # # # # # ###### 14 | * # # # ####### # # # ####### # # # # # 15 | * # # # # # # # # # # # # # # # 16 | * ####### # # # ####### ##### # # # ####### # # 17 | */ 18 | 19 | // We'll set the default environment to contain a few handy math constants 20 | const defaultEnvironment = { 21 | PI: Math.PI, 22 | E: Math.E, 23 | }; 24 | 25 | // And then we'll initialize the current environment to the default 26 | let environment = { ...defaultEnvironment }; 27 | 28 | /** 29 | * Returns an array of tokens from the input expression string. 30 | */ 31 | function tokenize(input) { 32 | // First, we'll initialize a `scanner` index to track how much 33 | // of the input string we've covered 34 | let scanner = 0; 35 | 36 | // We'll also need an array to put the tokens we find 37 | const tokens = []; 38 | 39 | // While we haven't reached the end of the input... 40 | while (scanner < input.length) { 41 | // Get the next character 42 | const char = input[scanner]; 43 | 44 | // If the character is a number... 45 | if (/[0-9]/.test(char)) { 46 | // Create a string to hold all the digits in the number 47 | let digits = ''; 48 | 49 | // While we have more digits (or a period) in the number and we haven't 50 | // gotten to the end of the input... 51 | while (scanner < input.length && /[0-9\.]/.test(input[scanner])) { 52 | // Collect all the digits 53 | digits += input[scanner++]; 54 | } 55 | 56 | // Convert the digits to a number and push the number to the array of tokens 57 | const number = parseFloat(digits); 58 | tokens.push(number); 59 | continue; 60 | } 61 | 62 | // If the character is a symbol... 63 | if (/[+\-/*(),^<>=]/.test(char)) { 64 | // Push it to the array of tokens 65 | tokens.push(char); 66 | scanner++; 67 | continue; 68 | } 69 | 70 | // If the character is white space... 71 | if (char === ' ') { 72 | // Ignore it 73 | scanner++; 74 | continue; 75 | } 76 | 77 | // If the character is the first character of a name, 78 | // like the name of a function or variable or variable pointer 79 | if (/[A-Z$#]/.test(char)) { 80 | // Create a string to hold all the characters in the name 81 | let name = ''; 82 | 83 | // While we have more characters that make up the name and we haven't 84 | // gotten to the end of the input... 85 | while (scanner < input.length && /[A-Z$#]/.test(input[scanner])) { 86 | // Collect all the characters in the name 87 | name += input[scanner++]; 88 | } 89 | 90 | // Then push the full name to the array of tokens 91 | tokens.push(name); 92 | continue; 93 | } 94 | 95 | // If we can't recognize the character, we'll throw an error 96 | throw new Error(`Invalid token ${char} at position ${scanner}`); 97 | } 98 | 99 | // After collecting all the tokens in the expression, we'll return them 100 | return tokens; 101 | } 102 | 103 | /** 104 | * Converts the tokens in infix notation to Reverse Polish notation 105 | */ 106 | function toRPN(tokens) { 107 | // First, we'll set up a stack to hold operators we're not yet ready to 108 | // add to the final expression 109 | const operators = []; 110 | 111 | // ...and an array to hold the final RPN expression 112 | const out = []; 113 | 114 | // For each token in the infix expression... 115 | for (let i = 0; i < tokens.length; i++) { 116 | const token = tokens[i]; 117 | 118 | // If the token is a number... 119 | if (typeof token === 'number' || /^[$#]/.test(token)) { 120 | // We'll push it to `out` 121 | out.push(token); 122 | continue; 123 | } 124 | 125 | // If the token is an operator or a function name... 126 | if (/[+\-/*<>=^A-Z]/.test(token)) { 127 | // While there are operators in the `operators` stack with a higher 128 | // precedence than the current token, we'll unwind them `operators` on to `out` 129 | while (shouldUnwindOperatorStack(operators, token)) { 130 | out.push(operators.pop()); 131 | } 132 | 133 | // And then, push the current token to the `operators` stack 134 | operators.push(token); 135 | continue; 136 | } 137 | 138 | // If the token is a left parenthesis symbol... 139 | if (token === '(') { 140 | // We'll push it to the `operators` stack 141 | operators.push(token); 142 | continue; 143 | } 144 | 145 | // If the token is a right parenthesis symbol... 146 | if (token === ')') { 147 | // While there are operators in the `operators` stack, and we haven't reached the 148 | // last left parenthesis symbol, we'll unwind them on to `out` 149 | while (operators.length > 0 && operators[operators.length - 1] !== '(') { 150 | out.push(operators.pop()); 151 | } 152 | 153 | // We no longer need the left parenthesis symbol, so we'll discard it 154 | operators.pop(); 155 | continue; 156 | } 157 | 158 | // If the token is a comma... 159 | if (token === ',') { 160 | // While there are operators in the `operators` stack, and we haven't reached the 161 | // last left parenthesis symbol, we'll unwind them on to `out` 162 | while (operators.length > 0 && operators[operators.length - 1] !== '(') { 163 | out.push(operators.pop()); 164 | } 165 | continue; 166 | } 167 | 168 | // If we can't recognize the token, we'll throw an error 169 | throw new Error(`Invalid token ${token}`); 170 | } 171 | 172 | // Finally we'll unwind all the remaining operators on to `out` 173 | for (let i = operators.length - 1; i >= 0; i--) { 174 | out.push(operators[i]); 175 | } 176 | 177 | // And then return `out` 178 | return out; 179 | } 180 | 181 | // BODMAS, PEMDAS 182 | // Exponentiation > [multiplication, division] > [addition, subtraction] 183 | const precedence = { 184 | '^': 3, 185 | '*': 2, 186 | '/': 2, 187 | '+': 1, 188 | '-': 1, 189 | }; 190 | 191 | // Returns true if the topmost operator in the `operators` stack has a 192 | // precedence higher than or equal to the precedence of `nextToken` 193 | function shouldUnwindOperatorStack(operators, nextToken) { 194 | if (operators.length === 0) { 195 | return false; 196 | } 197 | 198 | const lastOperator = operators[operators.length - 1]; 199 | return /[A-Z]/.test(lastOperator) || (precedence[lastOperator] && precedence[lastOperator] >= precedence[nextToken]); 200 | } 201 | 202 | /** 203 | * Evaluates the RPN expression 204 | */ 205 | function evalRPN(rpn) { 206 | const stack = []; 207 | 208 | // For each token in the RPN expression... 209 | for (let i = 0; i < rpn.length; i++) { 210 | const token = rpn[i]; 211 | 212 | // If the token is an operator... 213 | if (/[+\-/*^<>=]/.test(token)) { 214 | // Operate on the stack and push the result back on to the stack 215 | stack.push(operate(token, stack)); 216 | continue; 217 | } 218 | 219 | // If the token is a variable... 220 | if (/^\$/.test(token)) { 221 | // We'll try to get its value from the environment (remember to skip the '$' character) 222 | const value = environment[token.slice(1)]; 223 | 224 | // If the variable has not been set in the environment, we'll throw an error 225 | if (value === undefined) { 226 | throw new Error(`${token} is undefined`); 227 | } 228 | 229 | // But if it has, we'll push the value to the stack 230 | stack.push(value); 231 | continue; 232 | } 233 | 234 | // If the token is a function name... 235 | if (/^[A-Z]/.test(token)) { 236 | // Apply the function on the stack and push the result to the stack 237 | stack.push(apply(token, stack)); 238 | continue; 239 | } 240 | 241 | // If the token is a number or a variable pointer, push it to the stack 242 | if (typeof token === 'number' || /^\#/.test(token)) { 243 | stack.push(token); 244 | continue; 245 | } 246 | 247 | // If we can't recognize the token, we'll throw an error 248 | throw new Error(`Invalid token ${token}`); 249 | } 250 | 251 | // The value left on the stack is the final result of the evaluation 252 | return stack.pop(); 253 | } 254 | 255 | /** 256 | * Returns the result of appyling the mathematical operator on the stack. 257 | * The operator here is either an arithmetic or a logical operator, so we 258 | * only need two operands from the stack for any operator. 259 | */ 260 | function operate(operator, stack) { 261 | const b = stack.pop(); 262 | const a = stack.pop(); 263 | 264 | switch (operator) { 265 | case '+': 266 | return a + b; 267 | case '-': 268 | return a - b; 269 | case '*': 270 | return a * b; 271 | case '/': 272 | return a / b; 273 | case '^': 274 | return Math.pow(a, b); 275 | case '<': 276 | return a < b; 277 | case '>': 278 | return a > b; 279 | case '=': 280 | return a === b; 281 | default: 282 | throw new Error(`Invalid operator: ${operator}`); 283 | } 284 | } 285 | 286 | /** 287 | * Returns the result of applying the function onto the stack. 288 | * 289 | * Functions may have any number of arguments. But each function must have a 290 | * definite number of arguments. i.e. X(a, b) and Y(a, b, c) are possible, but 291 | * X(a, b) and X(a, b, c) are not possible. 292 | * 293 | * The function arguments are in right-to-left order in the stack, i.e. the rightmost 294 | * argument is at the top of the stack, and so on. 295 | */ 296 | function apply(func, stack) { 297 | // MAX(a, b) returns the larger of a and b 298 | if (func === 'MAX') { 299 | const b = stack.pop(); 300 | const a = stack.pop(); 301 | return Math.max(a, b); 302 | } 303 | 304 | // SQRT(a) returns the square-root of a 305 | if (func === 'SQRT') { 306 | const a = stack.pop(); 307 | return Math.sqrt(a); 308 | } 309 | 310 | // IF(a, b, c) returns b if a is true. Else, it returns c 311 | if (func === 'IF') { 312 | const ifFalse = stack.pop(); 313 | const ifTrue = stack.pop(); 314 | const predicate = stack.pop(); 315 | return predicate ? ifTrue : ifFalse; 316 | } 317 | 318 | // SET(#a, b) sets the variable a to the value b 319 | if (func === 'SET') { 320 | const value = stack.pop(); 321 | const key = stack.pop(); 322 | environment[key.slice(1)] = value; 323 | return value; 324 | } 325 | 326 | // If we can't recognize the function, we'll throw an error 327 | throw new Error(`Undefined function: ${func}`); 328 | } 329 | 330 | /** 331 | * Finally, in the `evaluator` function, we'll link all stages 332 | * of the evaluation together. 333 | * 334 | * tokenize -> toRPN -> evalRPN 335 | */ 336 | function evaluate(input) { 337 | return evalRPN(toRPN(tokenize(input))); 338 | } 339 | 340 | /** 341 | * Resets the environment back to the default 342 | */ 343 | function reset() { 344 | environment = { ...defaultEnvironment }; 345 | } 346 | 347 | module.exports = { 348 | tokenize, 349 | toRPN, 350 | evalRPN, 351 | evaluate, 352 | reset, 353 | environment, 354 | }; 355 | --------------------------------------------------------------------------------