0

I'm making a program that can understands human words, and so far it's going great.

My current standpoint is understanding math equations. You can add, subtract, multiply and divide very easily with as many numbers as you'd like, but I'm wondering how I can do addition then multiply the result, like this:

const source = require("./source.js")

var main = source.interpret(
  "add 4 and 4 multiply by 4",
)

source.output(main)

And it should output:

64

Yes I know that there is an easier way of doing this math equation, however in any calculator of any sort you should be able to this kind of switching in any context.

How can I accomplish this? Here is the full source code;

index.js:

const source = require("./source.js")

var main = source.interpret(
  "add 4 and 4 together then multiply the result by 4",
)

source.output(main)

source.js:

function output(main) {
  console.log(main)
}

function interpret(str) {
  const dl = str.split(' ');
  const operator = dl.shift(x => x.includes("add", "subtract", "multiply", "divide"))
  const numbers = dl.filter(x => Number(x))
  switch (operator) {
    case "add":
      return numbers.reduce((a, b) => Number(a) + Number(b));
    case "subtract":
      return numbers.reduce((a, b) => Number(a) - Number(b));
    case "multiply":
      return numbers.reduce((a, b) => Number(a) * Number(b));
    case "divide":
      return numbers.reduce((a, b) => Number(a) / Number(b));
  }
}

module.exports = {interpret, output}
1BL1ZZARD
  • 225
  • 2
  • 14

1 Answers1

2

The main problem with your interpret function is that after finding a single operator, it will perform that operation on all numbers and immediately return. We can’t simply reduce all the numbers using the first operation we find, because it’s possible that some numbers are related to other operations! In the expression add 2 and 2 multiply by 3, the 3 is related to the multiply operation!

This means that we can't process the entire input like that. An alternative is to iterate over the input, and depending on the operator we find, we perform the related action.

To simplify, let's consider that there's only the add operation. What are we expecting next? It could be [number] and [number], but also, it can be by [number]. The first one just adds the two numbers, but in the second, it should add the new number to the last operation.

A side note: your shift and filter functions are parsing the input, and the switch case is interpreting the parsed structure. Your “human language” is actually a programming language! add 2 and 2 is analogous to 2 + 2 in JavaScript, just different. With that, I will introduce you to some programming language theory terms, it can be easier to search for more help if you deep dive in the topic.

Considering the last paragraph, let's refactor interpret:

// from https://stackoverflow.com/questions/175739/how-can-i-check-if-a-string-is-a-valid-number
function isNumeric(str) {
  if (typeof str != "string") return false
  return !isNaN(str) && !isNaN(parseFloat(str))
}

function interpret(input) {
  const tokens = input.split(' ') // in fancy programming language terms, 
  // this is a lexical analysis step
  // note that we are not supporting things like
  // double spaces, something to think about!

  let state = 0 // we are keeping the results from our operation here

  for (i = 0; i < tokens.length; i++) {
    const t = tokens[i] // to keep things shorter
    switch (t) {
      case "add": // remember: there's two possible uses of this operator
        const next = tokens[i + 1]
        if (next == "by") {
          // we should add the next token (hopefully a number!) to the state
          state += parseFloat(tokens[i + 2])
          i += 2 // very important! the two tokens we read should be skipped
          // by the loop. they were "consumed".
          continue // stop processing. we are done with this operation
        }

        if (isNumeric(next)) {
          const a = tokens[i + 2] // this should be the "and"
          if (a != "and") {
            throw new Error(`expected "and" token, got: ${a}`)
          }
          const b = parseFloat(tokens[i + 3])
          state = parseFloat(next) + b
          i += 3 // in this case, we are consuming more tokens
          continue
        }

        throw new Error(`unexpected token: ${next}`)  
    }
  }

  return state
}

const input = `add 2 and 2 add by 2 add by 5`
console.log(interpret(input))

There's a lot to improve from this code, but hopefully, you can get an idea or two. One thing to note is that all your operations are "binary operations": they always take two operands. So all that checking and extracting depending if it's by [number] or a [number] and [number] expression is not specific to add, but all operations. There's many ways to write this, you could have a binary_op function, I will go for possibly the least maintainable option:

// from https://stackoverflow.com/questions/175739/how-can-i-check-if-a-string-is-a-valid-number
function isNumeric(str) {
  if (typeof str != "string") return false
  return !isNaN(str) && !isNaN(parseFloat(str))
}

function isOperand(token) {
  const ops = ["add", "multiply"]
  if (ops.includes(token)) {
    return true
  }

  return false
}

function interpret(input) {
  const tokens = input.split(' ') // in fancy programming language terms, 
  // this is a lexical analysis step
  // note that we are not supporting things like
  // double spaces, something to think about!

  let state = 0 // we are keeping the results from our operation here

  for (i = 0; i < tokens.length; i++) {
    const t = tokens[i] // to keep things shorter
    if (!isOperand(t)) {
      throw new Error(`expected operand token, got: ${t}`)  
    }

    // all operators are binary, so these variables will hold the operands
    // they may be two numbers, or a number and the internal state
    let a, b;

    const next = tokens[i + 1]
    if (next == "by") {
      // we should add the next token (hopefully a number!) to the state
      a = state
      b = parseFloat(tokens[i + 2])
      i += 2 // very important! the two tokens we read should be skipped
      // by the loop. they were "consumed".
    } 
    else if (isNumeric(next)) {
      const and = tokens[i + 2] // this should be the "and"
      if (and != "and") {
        throw new Error(`expected "and" token, got: ${and}`)
      }
      a = parseFloat(next)
      b = parseFloat(tokens[i + 3])
      i += 3 // in this case, we are consuming more tokens 
    } else {
      throw new Error(`unexpected token: ${next}`)  
    }

    switch (t) {
      case "add": 
        state = a + b
        break;
      case "multiply":
        state = a * b
    }
  }

  return state
}

const input = `add 2 and 2 add by 1 multiply by 5`
console.log(interpret(input)) // should log 25

There's much more to explore. We are writing a "single-pass" interpreter, where the parsing and the interpreting are tied together. You can split these two, and have a parsing function that turns the input into a structure that you can then interpret. Another point is precedence, we are applying the operation in the order they appear in the expression, but in math, multiplication should be done first than addition. All of these problems are programming language problems.

If you are interested, I deeply recommend the book http://craftinginterpreters.com/ for a gentle introduction on writing programming languages, it will definitely help in your endeavor.

Eduardo Thales
  • 401
  • 1
  • 8
  • Welcome to SO, Eduardo! In a way, I guess you could see it as a programming language, I find that to be not really far-fetched, reasonable, and appropriate. Can you edit your post to do that last part in code? ("What you need is instead of returning, you keep the resulting value, say, in a variable, then find the next operation and use the last result with the next number.") I'm having difficulty figuring out how to split the actual equation into bits to add 4+4, detect if the answer was return, then get the answer, then using another operator (if the user is actually doing this). – 1BL1ZZARD Jun 14 '22 at 01:26
  • Hm, yes, I apologize! It adds some complication in how you should process your input, and my last part is almost meaningless, now that I'm reading. I will edit in a bit. – Eduardo Thales Jun 14 '22 at 01:49
  • One question if I may ask, I want the programming language's math to follow PEMDAS rules and not just move left to right, how can I do that? – 1BL1ZZARD Jun 14 '22 at 23:08