import { Utils } from '../../lib'; type Operator = '^' | 'negative' | '%' | '/' | '*' | '+' | '-' | '('; interface Operators { precedence: number; associativity: "Left" | "Right"; } const OPERATORS: { [k in Operator]: Operators } = { "^": { precedence: 5, associativity: "Right", }, "negative": { precedence: 4, associativity: "Right", }, "%": { precedence: 3, associativity: "Left", }, "/": { precedence: 3, associativity: "Left", }, "*": { precedence: 3, associativity: "Left", }, "+": { precedence: 2, associativity: "Left", }, "-": { precedence: 2, associativity: "Left", }, "(": { precedence: 1, associativity: "Right", }, }; const BASE_PREFIXES: { [base: number]: string } = { 2: "0b", 8: "0o", 10: "", 16: "0x", }; function parseMathematicalExpression(infix: string) { // Shunting-yard Algorithm -- https://en.wikipedia.org/wiki/Shunting-yard_algorithm const outputQueue: string[] = []; const operatorStack: Operator[] = []; infix = infix.replace(/\s+/g, ""); const infixArray = infix.split(/([+\-*/%^()])/).filter(token => token); let isExprExpected = true; for (const token of infixArray) { if (isExprExpected && "+-".includes(token)) { if (token === '-') operatorStack.push('negative'); } else if ("^%*/+-".includes(token)) { if (isExprExpected) throw new SyntaxError(`Got "${token}" where an expression should be`); const op = OPERATORS[token as Operator]; let prevToken = operatorStack[operatorStack.length - 1] || '('; let prevOp = OPERATORS[prevToken]; while (op.associativity === "Left" ? op.precedence <= prevOp.precedence : op.precedence < prevOp.precedence) { outputQueue.push(operatorStack.pop()!); prevToken = operatorStack[operatorStack.length - 1] || '('; prevOp = OPERATORS[prevToken]; } operatorStack.push(token as Operator); isExprExpected = true; } else if (token === "(") { if (!isExprExpected) throw new SyntaxError(`Got "(" where an operator should be`); operatorStack.push(token as Operator); isExprExpected = true; } else if (token === ")") { if (isExprExpected) throw new SyntaxError(`Got ")" where an expression should be`); while (operatorStack.length && operatorStack[operatorStack.length - 1] !== "(") { outputQueue.push(operatorStack.pop()!); } operatorStack.pop(); isExprExpected = false; } else { if (!isExprExpected) throw new SyntaxError(`Got "${token}" where an operator should be`); outputQueue.push(token); isExprExpected = false; } } if (isExprExpected) throw new SyntaxError(`Input ended where an expression should be`); while (operatorStack.length > 0) { const token = operatorStack.pop()!; if (token === '(') continue; outputQueue.push(token); } return outputQueue; } function solveRPN(rpn: string[]): [number, number] { let base = 10; const resultStack: number[] = []; for (let token of rpn) { if (token === 'negative') { if (!resultStack.length) throw new SyntaxError(`Unknown syntax error`); resultStack.push(-resultStack.pop()!); } else if (!"^%*/+-".includes(token)) { if (token.endsWith('h')) { // Convert h suffix for hexadecimal to 0x prefix token = `0x${token.slice(0, -1)}`; } else if (token.endsWith('o')) { // Convert o suffix for octal to 0o prefix token = `0o${token.slice(0, -1)}`; } else if (token.endsWith('b')) { // Convert b suffix for binary to 0b prefix token = `0b${token.slice(0, -1)}`; } if (token.startsWith('0x')) base = 16; if (token.startsWith('0b')) base = 2; if (token.startsWith('0o')) base = 8; let num = Number(token); if (isNaN(num) && token.toUpperCase() in Math) { // @ts-expect-error Math consts should be safe num = Math[token.toUpperCase()]; } if (isNaN(num) && token !== 'NaN') { throw new SyntaxError(`Unrecognized token ${token}`); } resultStack.push(num); } else { if (resultStack.length < 2) throw new SyntaxError(`Unknown syntax error`); const a = resultStack.pop()!; const b = resultStack.pop()!; switch (token) { case "+": resultStack.push(a + b); break; case "-": resultStack.push(b - a); break; case "*": resultStack.push(a * b); break; case "/": resultStack.push(b / a); break; case "%": resultStack.push(b % a); break; case "^": resultStack.push(b ** a); break; } } } if (resultStack.length !== 1) throw new SyntaxError(`Unknown syntax error`); return [resultStack.pop()!, base]; } export const commands: Chat.ChatCommands = { math: "calculate", calculate(target, room, user) { if (!target) return this.parse('/help calculate'); let base = 0; const baseMatchResult = (/\b(?:in|to)\s+([a-zA-Z]+)\b/).exec(target); if (baseMatchResult) { switch (toID(baseMatchResult[1])) { case 'decimal': case 'dec': base = 10; break; case 'hexadecimal': case 'hex': base = 16; break; case 'octal': case 'oct': base = 8; break; case 'binary': case 'bin': base = 2; break; default: return this.errorReply(`Unrecognized base "${baseMatchResult[1]}". Valid options are binary or bin, octal or oct, decimal or dec, and hexadecimal or hex.`); } } const expression = target.replace(/\b(in|to)\s+([a-zA-Z]+)\b/g, '').trim(); if (!this.runBroadcast()) return; try { const [result, inferredBase] = solveRPN(parseMathematicalExpression(expression)); if (!base) base = inferredBase; let baseResult = ''; if (Number.isFinite(result) && base !== 10) { baseResult = `${BASE_PREFIXES[base]}${result.toString(base).toUpperCase()}`; if (baseResult === expression) baseResult = ''; } let resultStr = ''; const resultTruncated = parseFloat(result.toPrecision(15)); let resultDisplay = resultTruncated.toString(); if (resultTruncated > 10 ** 15) { resultDisplay = resultTruncated.toExponential(); } if (baseResult) { resultStr = `${baseResult} = ${resultDisplay}`; } else { resultStr = `${resultDisplay}`; } this.sendReplyBox(`${expression}
= ${resultStr}`); } catch (e: any) { this.sendReplyBox( Utils.html`${expression}
= Invalid input: ${e.message}` ); } }, calculatehelp: [ `/calculate [arithmetic question] - Calculates an arithmetical question. Supports PEMDAS (Parenthesis, Exponents, Multiplication, Division, Addition and Subtraction), pi and e.`, `/calculate [arithmetic question] in [base] - Returns the result in a specific base. [base] can be bin, oct, dec or hex.`, ], };