Created
December 8, 2023 13:59
-
-
Save kybernetikos/152ee63b65fbec5de372a63aee0fc62a to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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