mirror of
https://github.com/smogon/pokemon-showdown.git
synced 2026-04-26 02:39:38 -05:00
ESLint has a whole new config format, so I figure it's a good time to make the config system saner. - First, we no longer have separate eslint-no-types configs. Lint performance shouldn't be enough of a problem to justify the relevant maintenance complexity. - Second, our base config should work out-of-the-box now. `npx eslint` will work as expected, without any CLI flags. You should still use `npm run lint` which adds the `--cached` flag for performance. - Third, whatever updates I did fixed style linting, which apparently has been bugged for quite some time, considering all the obvious mixed-tabs-and-spaces issues I found in the upgrade. Also here are some changes to our style rules. In particular: - Curly brackets (for objects etc) now have spaces inside them. Sorry for the huge change. ESLint doesn't support our old style, and most projects use Prettier style, so we might as well match them in this way. See https://github.com/eslint-stylistic/eslint-stylistic/issues/415 - String + number concatenation is no longer allowed. We now consistently use template strings for this.
210 lines
6.5 KiB
TypeScript
210 lines
6.5 KiB
TypeScript
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 = `<strong>${baseResult}</strong> = ${resultDisplay}`;
|
|
} else {
|
|
resultStr = `<strong>${resultDisplay}</strong>`;
|
|
}
|
|
this.sendReplyBox(`${expression}<br />= ${resultStr}`);
|
|
} catch (e: any) {
|
|
this.sendReplyBox(
|
|
Utils.html`${expression}<br />= <span class="message-error"><strong>Invalid input:</strong> ${e.message}</span>`
|
|
);
|
|
}
|
|
},
|
|
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.`,
|
|
],
|
|
};
|