0

I'm building a calculator that is supposed to calculate the contents of a string. For this I am using a Function object, however, upon running the code I get a value of undefined. I'm assuming this has something to do with the global scope of the Function object, but I can't see a way of debugging whats going on in that function. Passing it a local variable would solve the problem, but I can't figure out how.

let addListeners = function () {
    screens = document.querySelectorAll("[class=screen]");
    operationsButtons = document.querySelectorAll("[class^=operations_button]");

    initAttributes();
    addNumberButtonListeners();
    addOperationsListeners();
    addOtherButtons();
}


function addNumberButtonListeners() {
    numberButtons = document.querySelectorAll("[id^=number]");
    numberButtons.forEach(button => {
        button.addEventListener("click", function () {
            let buttonNumber = button.innerText;
            screens.forEach(screen => {
                screen.numberLast = true;
                if (screen.isDefault) {
                    screen.innerText = buttonNumber;
                    screen.isDefault = false;
                    if (screen.id == "little_screen") {
                        screen.value = screen.innerText;
                    }
                }
                else {
                    screen.innerText += buttonNumber;
                    if (screen.id == "little_screen") {
                        screen.value = screen.innerText;
                    }
                }

            })

        })
    });
}

function addOperationsListeners() {
    let littlescreen = document.querySelector("[id=little_screen]");
    let bigscreen = document.querySelector("[id=big_screen]");
    operationsButtons.forEach(button => {
        button.addEventListener("click", function () {


            try {
                if (littlescreen.numberLast == false) throw button.innerText;
                littlescreen.innerText = (littlescreen.innerText + button.innerText);
                bigscreen.isDefault = true;
                littlescreen.numberLast = false;
                littlescreen.value = littlescreen.innerText;
            }

            catch (e) {
                let str = littlescreen.innerText;
                littlescreen.innerText = (str.slice(0, -1) + button.innerText);
                littlescreen.value = littlescreen.innerText;
                console.log(e + " twice");
            }
        })

    })
}


function addOtherButtons() {
    allClear = function () {
        button = document.querySelector("[id=all_clear]");
        button.addEventListener("click", function () {
            screens.forEach(screen => {
                screen.innerText = "0";
                screen.isDefault = true;
                if (screen.id == "big_screen") {
                    screen.numberLast = false;
                }
            })

        })
    }

    equalsButton = function () {
        let littlescreen = document.querySelector("[id=little_screen]");
        button = document.querySelector("[id=equals]");
        button.addEventListener("click", function () {
            screens.forEach(screen => {
                screen.isDefault = true;
                if (screen.id == "big_screen") {
                    screen.numberLast = false;
                    // Function I can't get to work.
                    //littlescreen.innertext is string to be calculated.

                    console.log(littlescreen.innerText);
                    let calculate = function () {
                    screen = document.querySelector("[id=little_screen]");
                    screen.innerText = screen.innerText.slice(0, -1);
                    return screen.innerText;
                    }
                    console.log(calculate());

                }
                else {
                    littlescreen.innerText = (littlescreen.innerText + button.innerText);
                }
            })

        })
    }

    //add pow()
    //add decimal
    //add +/-
    allClear();
    equalsButton();
}



function initAttributes() {
    screens.forEach(screen => {
        Object.defineProperty(screen, "isDefault", {
            value: true,
            writable: true,
        });

        if (screen.id == "little_screen") {
            Object.defineProperty(screen, "numberLast", {
                value: false,
                writable: true,
            });
            Object.defineProperty(screen, "value", {
                value: null,
                writable: true,
            });
        }
        console.log(screen);
    });
}



addListeners()
.container{
    display: flex;
    justify-content: center;
}

.calccontainer{
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;

    height: 100vh;
    width:calc((2/3) * 100vh);

    background: #D6D1B1;
    border: 3px solid gray;
    border-radius: 20px;
}

.screencontainer{
    display: flex;
    flex-direction: column;
    justify-content: space-around;
    align-items: center;

    height: 20vh;
    width: 80%;

    background: #eef5db;
    border: 3px solid gray;
    border-radius: 20px;

    margin: 5%;
    margin-top: 7%;

}

#little_screen, #big_screen{
    width: 80%;
    text-align: right;
    font-family: 'Seven Segment', sans-serif;
    overflow: hidden;
}

#little_screen{
    height: 25%;
    font-size: 5vh;
}

#big_screen{
    height: 50%;
    font-size: 10vh;
}

.buttoncontainer{
    display: flex;
    flex-direction: row;
    flex-wrap: wrap;
    justify-content: space-between;
    align-items: center;

    height: 70vh;
    width: 80%;

    border-radius: 20px;

    margin: 0 5% 5% 5%;
}

.number_button, .operations_button, .other_button{
    display: flex;
    justify-content: center;
    align-items: center;
    height: 10vh;
    width: 20%;

    border: 3px solid gray;
    border-radius: 15px;

}
#all_clear{
    width: 46%;
    flex-shrink: 0;

    background: #E3C498;
}

[id^="number"], #positive_negative, #decimal {
    background: #BAE9C4;
}

[id^="button"] {
    background: #F0B67F;
}

#equals{
    background: #fe5f55;
}
<!DOCTYPE html>
<html>

<head>
  <meta charset="UTF-8">
  <title>Page Title</title>
  <link rel="stylesheet" href="style.css">
  <script src="script.js" defer></script>
  <link href="http://fonts.cdnfonts.com/css/seven-segment" rel="stylesheet">
</head>

<body style="margin: 0;">
  <!-- Removes white border around page-->
  <div class="container" id="container">

    <div class="calccontainer" id="calccontainer">

      <div class="screencontainer" id="screencontainer">

        <div class="screen" id="little_screen">0</div>
        <div class="screen" id="big_screen">0</div>
      </div>


      <div class="buttoncontainer" id="innercontainer">

        <div class="other_button" id="all_clear">AC</div>
        <div class="other_button" id="button2">Xy</div>
        <div class="operations_button" id="button3">/</div>
        <div class="number_button" id="number7">7</div>
        <div class="number_button" id="number8">8</div>
        <div class="number_button" id="number9">9</div>
        <div class="operations_button" id="button7">*</div>
        <div class="number_button" id="number4">4</div>
        <div class="number_button" id="number5">5</div>
        <div class="number_button" id="number6">6</div>
        <div class="operations_button" id="button11">-</div>
        <div class="number_button" id="number1">1</div>
        <div class="number_button" id="number2">2</div>
        <div class="number_button" id="number3">3</div>
        <div class="operations_button" id="button15">+</div>
        <div class="other_button" id="positive_negative">+/-</div>
        <div class="number_button" id="number0">0</div>
        <div class="other_button" id="decimal">.</div>
        <div class="other_button" id="equals">=</div>
      </div>
    </div>
  </div>
</body>

</html>

I know using eval() would work, but I'm trying to stay away from it. I have a console.log that gives the value of the string that's supposed to calculate that triggers what you try to calculate something by pressing the equals button.

EDIT: So I made a couple changes and am no longer getting undefined, however now I'm just getting the string passed to me without a calculation.

let calculate = function () {
                        screen = document.querySelector("[id=little_screen]");
                        screen.innerText = screen.innerText.slice(0, -1);
                        return screen.innerText;
  • I'm not sure what you're trying to do with `new function` that then calls `new Function(...)`. You only need `new Function(...)` if you're creating the function dynamically, which is no safer than using `eval()`. – Barmar May 18 '22 at 16:07
  • You probably want `let calculate = function() { ... }` not `new function` there. `new function()` would be for creating an instance of an anonymous class. – Barmar May 18 '22 at 16:08
  • Ok gotcha, that's what I had before, but still wasn't finding a way to calculate my string. I saw the new Function() as an answer to a similar question posted previously. – Jayden Dafoe-Dunn May 18 '22 at 16:16
  • Note that `new Function` is considered the same as `eval` for code execution security purposes so there's really not much difference, they're both equally "bad" =) The "real" solution that courses that ask you to make a calculator eventually steer you towards (after asking you to try to make things work by directly using strings), is to write a string parser/tokenizer that you use to turn "math in text form" into "a tree of values and operations, nested in order of operation", which you then use to run the maths instead. – Mike 'Pomax' Kamermans May 18 '22 at 16:22
  • I think I understand what you're saying, will give it a shot. – Jayden Dafoe-Dunn May 18 '22 at 16:27
  • You can use a mapping like `{'+': (a, b) => a+b, '-': (a, b) => a-b, ...}`. – Barmar May 18 '22 at 17:41
  • read this question: https://stackoverflow.com/questions/6479236/calculate-string-value-in-javascript-not-using-eval is what you need – Andrea Fini May 19 '22 at 08:57

1 Answers1

0

You normally don't want "untrusted code" to be executed, that's the reason of the "eval is bad" rule. The means of running that untrusted code doesn't really matter however, therefore using eval or creating "new functions" doesn't really matter.

However, in the application you seem to have at the moment, the only code that would get executed is the one which gets written by pressing the calculator buttons. Unless that code is getting shared somehow, or there is a way to initialize the application with an arbitrary code, there is no real risk of using eval. The worst that could happen is that a user executes his own malicious code on his own computer (not great hacking skills).

If the code is persisted somehow, you either need to clean it before passing it through eval. You could do this by executing a regular expression on it and remove everything that is in the character class [^0-9+\-*\/\.], as far as I can tell there is no big harm that could be done with the characters +, -, *, /, ., and 0 through 9.

const code = 'alert("bad stuff"); 1+1';
const safeCode = code.replaceAll(/[^0-9+\-*\/\.]/g, '');
console.log(eval(safeCode));
// expected output: 2

Once you would like to integrate more complicated stuff, say a sqrt() function, this approach does no longer work, and you begin entering the magical realm of tokenizers (these split the string into manageable parts), parsers (these order these parts in a hierarchical tree), and interpreters (these evaluate the tree). These days, parsers are often written with the help of so called "parser generators". Those are special programs that generate the programming code for parsers by starting with a formal declaration of the language you would like to create.
Wikipedia curates a comparative list of parser generators here.

AFAIK, the output of running most of those generated parsers on an input string is either an "AST" or a "CST" (those are "Abstract Syntax Tree" and "Concrete Syntax Tree"). The difference between the two essentially is given by how closely they resemble the given code. It's detailed a bit more in this SO answer. The AST is then normally interpreted (directly executed) or compiled to machine or byte code, or "transpiled" to another language (like "Typescript" to "Javascript", or any language to LLVM).

Of course, since the output of the parser generators is "just code" (although often times unreadable code), it's entirely possible to write one by hand. There are many types of "Parsers" (depending on the complexity of the code they need to parse), but the one type that you would want to write by hand these days is probably a "recursive descent parser", those aren't necessarily the most "efficient" ones, but they are powerful (like, you could totally write one to parse stuff like Javascript) and they are kinda logical enough so that we mere mortal can understand what happens. There's a really good free "online book" out there that takes you step by step through the process of writing a "Scanner" (or Tokenizer), a "Recursive Descent Parser" and an "Interpreter". You can find it here (I sincerely hope it stays out there for many years to come; but maybe: to all the people, make me a favor and back the pdf version up - we don't want to loose this gem).

I've written for you a very simple example scanner, parser and interpreter (simplified for the purpose here) that runs simple mathematical expressions (like the ones from your app) here:

class Scanner {
  tokens = []
  start = 0
  current = 0
  source = ''
  
  scanTokens(source) {
    this.source = source
    this.tokens = []
    this.start = 0
    this.current = 0
    
    while(!this.isAtEnd()) {
      this.start = this.current
      this.scanToken()
    }
    
    return this.tokens
  }
  
  isAtEnd() {
    return this.current >= this.source.length
  }
  
  scanToken() {
    const c = this.advance()
    switch(c) {
      case '+':
      case '-':
      case '*':
      case '/':
        this.tokens.push({type: c})
        break;
        
      default:
        if(this.isDigit(c)) {
          this.number()
        } else {
          throw 'Unknown Token: ' + c
        }
    }
  }
  
  advance() {
    this.current += 1;
    return this.source.charAt(this.current - 1)
  }
  
  peek() {
    if(this.isAtEnd()) return undefined;
    
    return this.source.charAt(this.current)
  }
  
  peekNext() {
    if(this.isAtEnd()) return undefined;
    
    return this.source.charAt(this.current + 1)
  }
  
  isDigit(c) {
    return c >= '0' && c <= '9'
  }
  
  number() {
    while(this.isDigit(this.peek())) {
      this.advance()
    }
    
    if(this.peek() === '.' && this.isDigit(this.peekNext())) {
      this.advance() 
      while(this.isDigit(this.peek())) {
        this.advance()
      }
    }
    
    this.tokens.push({
      type: 'number', 
      value: parseFloat(this.source.substring(this.start, this.current))
    })
  }
}

class Parser {
  tokens = null
  current = 0
  
  parse(tokens) {
    this.tokens = tokens;
    this.current = 0;
    
    return this.expression()
  }
  
  expression() {
    return this.sumOrSubtraction()
  }
  
  sumOrSubtraction() {
    let left = this.multiplicationOrDivision()
    let op;
    
    while(op = this.match('+', '-')) {
      const right = this.multiplicationOrDivision();
      left = {
        type: 'binary',
        operator: op.type, 
        left: left,
        right: right
      }
    }
    
    return left;
  }
  
  multiplicationOrDivision() {
    let left = this.unary()
    let op;
    
    while(op = this.match('*', '/')) {
      const right = this.unary();
      left = {
        type: 'binary',
        operator: op.type, 
        left: left,
        right: right
      }
    }
    
    return left;
  }
  
  unary() {
    let op = this.match('-')
    if(op) {
      return {
        type: 'unary',
        operator: '-',
        right: this.unary()
      }
    }
    
    const right = this.consume('number', 'number expected')
    return {
      type: 'number',
      value: right.value
    }
  }
  
  consume(type, error) {
    if(this.check(type)) {
      return this.advance()
    }
    
    throw error
  }
  
  match(...types) {
    if(this.check(...types)) {
      return this.advance()
    }
    
    return false
  }
  
  check(...types) {
    if (this.isAtEnd()) return false
    
    return types.some(t => t === this.tokens[this.current].type)
  }
  
  isAtEnd() {
    return this.current >= this.tokens.length;
  }
  
  advance() {
    const v = this.tokens[this.current]
    this.current += 1
    return v;
  }
}

function evaluate(node) {
  switch(node.type) {
    case 'number': return node.value
    case 'binary': 
      switch(node.operator) {
        case '+': return evaluate(node.left) + evaluate(node.right);
        case '-': return evaluate(node.left) - evaluate(node.right);
        case '*': return evaluate(node.left) * evaluate(node.right);
        case '/': return evaluate(node.left) / evaluate(node.right);
      }
      
      throw 'unknown binary operator ' + node.operator
    case 'unary': 
      switch(node.operator) {
        case '-': return -evaluate(node.right)
      }
      throw 'unknown unary operator ' + node.operator
  }
  
  throw 'unkwown node type ' + node.type
}

const expr = '-100+7-9+4*2*3.265';

console.log('For the expression', expr, '...')

const tokens = new Scanner().scanTokens(expr)
console.log('The tokens are:', tokens)
const ast = new Parser().parse(tokens)
console.log('The ast nodes are:', ast)
console.log('Interpreter Result', evaluate(ast))
console.log('Eval Result', eval(expr))

The console output shows you the initial string, the scanned tokens, the parsed tree and the result (together with a comparison made with "eval").

Roman
  • 5,888
  • 26
  • 47