Skip to content

Instantly share code, notes, and snippets.

@kybernetikos
Created December 8, 2023 13:59
Show Gist options
  • Save kybernetikos/152ee63b65fbec5de372a63aee0fc62a to your computer and use it in GitHub Desktop.
Save kybernetikos/152ee63b65fbec5de372a63aee0fc62a to your computer and use it in GitHub Desktop.
class Op {
constructor(symbol, precedence, leftAssociate, apply) {
Object.assign(this, {symbol, precedence, leftAssociate, apply})
}
process(stack) {
if (this.apply === null) return
const args = []
for (let i = 0; i < this.apply.length; i++) {
const arg = stack.pop()
if (arg === undefined) {
throw new Error(`Stack Exhausted, not enough arguments for ${this.symbol}, found only (${args.join(", ")})`)
}
args.unshift(arg)
}
stack.push(this.apply.apply(null, args))
}
}
const INFIX_OPERATORS = {
"+": new Op('+', 1, true, (a, b) => a + b),
"-": new Op('-', 1, true, (a, b) => a - b),
"*": new Op('*', 2, true, (a, b) => a * b),
"/": new Op('/', 2, true, (a, b) => a / b),
"^": new Op('^', 5, false, (a, b) => Math.pow(a, b)),
"=": new Op('=', 0, false, (a, b) => a == b),
")" : new Op(')', -1, false, null)
}
const PREFIX_OPERATORS = {
"-": new Op('-', 4, false, (a) => -a),
"(" : new Op('(', -1, false, null)
}
// This splitting regexp assumes that operators are single characters
const RE = (()=>{
const ALL_OP_SYMBOLS = Array.from(new Set(Object.keys(INFIX_OPERATORS).concat(Object.keys(PREFIX_OPERATORS))))
const literalOps = '\\' + ALL_OP_SYMBOLS.join('\\')
return new RegExp(`(?<=[^${literalOps}])(?=[${literalOps}])|(?<=[${literalOps}])(?=[^${literalOps}])|(?<=[${literalOps}])(?=[${literalOps}])|[\s]+`)
})()
const peek = (stack) => stack[stack.length - 1]
const doLeftOp = (leftOp, rightOp) => {
if (rightOp.symbol === '(') return false
if (rightOp.apply !== null && leftOp.apply !== null && rightOp.apply.length !== leftOp.apply.length) {
return false
}
if (rightOp.leftAssociate) {
return rightOp.precedence <= leftOp.precedence
} else {
return rightOp.precedence < leftOp.precedence
}
}
function parse(str) {
const operandStack = [], operatorStack = []
let lastTokenWasOperand = false
for (const part of str.split(RE)) {
const num = Number.parseFloat(part)
if (!isNaN(num)) {
operandStack.push(num)
lastTokenWasOperand = true
} else {
let operators = lastTokenWasOperand ? INFIX_OPERATORS : PREFIX_OPERATORS
if (part in operators) {
const thisOp = operators[part]
let stackOp = peek(operatorStack)
while (stackOp !== undefined && doLeftOp(stackOp, thisOp)) {
stackOp.process(operandStack)
operatorStack.pop()
stackOp = peek(operatorStack)
}
if (thisOp.symbol === ')') {
// clear open bracket from operator stack
operatorStack.pop()
lastTokenWasOperand = true
} else {
operatorStack.push(thisOp)
lastTokenWasOperand = false
}
} else {
throw new Error(`Parse error, '${part}' not recognised`)
}
}
}
while (operatorStack.length > 0) {
operatorStack.pop().process(operandStack)
}
if (operandStack.length !== 1) {
throw new Error(`Parse error, operand stack not empty at end. Contains [${operandStack.join(", ")}]`)
}
return operandStack.pop()
}
console.log(parse("2^1^2"))
console.log(parse("5-1-2"))
console.log(parse("2^1^2=5-1-2"))
console.log(parse("1+(2-1)*2"))
console.log(parse("1--8"))
console.log(parse("1--8"))
console.log(parse("2^-1"))
console.log(parse("-2*-5"))
console.log(parse("2*-2^3"))
console.log(parse("-2^2"))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment