A scientific calculator that follows the app theme and supports keyboard input.
+
`;
}
@@ -125,162 +188,399 @@ function initCalculator() {
const display = document.getElementById("calcDisplay");
if (!display) return;
let expression = "";
+ const operatorChars = new Set(["+", "-", "×", "÷", "^", "*", "/", "−", "%"]);
function update() {
display.textContent = expression || "0";
}
- function format(expr) {
+ function normalize(expr) {
return expr
.replace(/÷/g, "/")
.replace(/×/g, "*")
- .replace(/\^/g, "**");
+ .replace(/−/g, "-");
+ }
+
+ function sanitize(expr) {
+ let cleaned = expr.trim();
+ while (cleaned && /[+\-×÷^−*/.%]$/.test(cleaned)) {
+ cleaned = cleaned.slice(0, -1);
+ }
+ return cleaned;
+ }
+
+ function factorial(value) {
+ if (!Number.isFinite(value) || value < 0 || Math.floor(value) !== value) {
+ throw new Error("Factorial is only defined for non-negative integers");
+ }
+ let result = 1;
+ for (let index = 2; index <= value; index += 1) {
+ result *= index;
+ }
+ return result;
+ }
+
+ function evaluateExpression(input) {
+ let index = 0;
+
+ function peek(length = 1) {
+ return input.slice(index, index + length);
+ }
+
+ function consume(length = 1) {
+ index += length;
+ }
+
+ function parseNumber() {
+ const match = input.slice(index).match(/^(?:\d*\.\d+|\d+\.?\d*)/);
+ if (!match) throw new Error("Expected number");
+ consume(match[0].length);
+ return Number(match[0]);
+ }
+
+ function parsePrimary() {
+ if (peek() === "(") {
+ consume();
+ const value = parseExpression();
+ if (peek() !== ")") throw new Error("Missing closing parenthesis");
+ consume();
+ return value;
+ }
+ return parseNumber();
+ }
+
+ function parsePostfix() {
+ let value = parsePrimary();
+ while (peek() === "!") {
+ consume();
+ value = factorial(value);
+ }
+ return value;
+ }
+
+ function parseUnary() {
+ if (peek() === "+") {
+ consume();
+ return parseUnary();
+ }
+ if (peek() === "-") {
+ consume();
+ return -parseUnary();
+ }
+ return parsePostfix();
+ }
+
+ function parsePower() {
+ let value = parseUnary();
+ if (peek() === "^") {
+ consume();
+ const exponent = parsePower();
+ value = Math.pow(value, exponent);
+ }
+ return value;
+ }
+
+ function parseTerm() {
+ let value = parsePower();
+ while (index < input.length) {
+ const operator = peek();
+ if (operator === "*" || operator === "/" || operator === "%") {
+ consume();
+ const right = parsePower();
+ if (operator === "*") value *= right;
+ else if (operator === "/") {
+ if (right === 0) throw new Error("Cannot divide by zero");
+ value /= right;
+ } else {
+ if (right === 0) throw new Error("Cannot divide by zero");
+ value %= right;
+ }
+ continue;
+ }
+ break;
+ }
+ return value;
+ }
+
+ function parseExpression() {
+ let value = parseTerm();
+ while (index < input.length) {
+ const operator = peek();
+ if (operator === "+" || operator === "-") {
+ consume();
+ const right = parseTerm();
+ value = operator === "+" ? value + right : value - right;
+ continue;
+ }
+ break;
+ }
+ return value;
+ }
+
+ const result = parseExpression();
+ if (index !== input.length || Number.isNaN(result) || !Number.isFinite(result)) {
+ throw new Error("Invalid expression");
+ }
+ return result;
}
function safeEval(expr) {
try {
- if (!expr) return "";
- let result = eval(format(expr));
- if (result === undefined) return "";
- if (isNaN(result)) return "Error";
- return String(result);
+ const cleaned = sanitize(expr);
+ if (!cleaned) return "";
+ return String(evaluateExpression(normalize(cleaned)));
} catch {
return "Error";
}
}
+ function getNumericValue() {
+ const evaluated = safeEval(expression || "0");
+ if (!evaluated || evaluated === "Error") return 0;
+ const numericValue = Number(evaluated);
+ return Number.isNaN(numericValue) ? 0 : numericValue;
+ }
+
+ function isOperator(char) {
+ return operatorChars.has(char);
+ }
+
+ function getLastToken(expr) {
+ return expr.split(/[+\-×÷^*/()−%]/).pop();
+ }
+
+ function clearIfError() {
+ if (expression === "Error") expression = "";
+ }
+
+ function appendDigit(digit) {
+ clearIfError();
+ expression += digit;
+ update();
+ }
+
+ function appendDecimal() {
+ clearIfError();
+ const lastToken = getLastToken(expression);
+ if (lastToken.includes(".")) return;
+ if (!expression || isOperator(expression.slice(-1)) || expression.slice(-1) === "(") {
+ expression += "0.";
+ } else {
+ expression += ".";
+ }
+ update();
+ }
+
+ function appendOperator(operator) {
+ clearIfError();
+ if (!expression) {
+ if (operator === "-" || operator === "−" || operator === "(") {
+ expression = operator;
+ update();
+ }
+ return;
+ }
+ const lastChar = expression.slice(-1);
+ if (operator === "(") {
+ expression += /[0-9)]$/.test(lastChar) ? "×(" : "(";
+ update();
+ return;
+ }
+ if (operator === ")") {
+ const openCount = (expression.match(/\(/g) || []).length - (expression.match(/\)/g) || []).length;
+ if (openCount <= 0 || isOperator(lastChar) || lastChar === "(") return;
+ expression += ")";
+ update();
+ return;
+ }
+ if (isOperator(lastChar)) {
+ expression = expression.slice(0, -1) + operator;
+ } else {
+ expression += operator;
+ }
+ update();
+ }
+
function applyFunction(type) {
try {
- let value = eval(format(expression || "0"));
+ const value = getNumericValue();
let result;
+
+ // Convert degrees to radians for JS Math functions
+ const radians = value * (Math.PI / 180);
+
+ // Helper to fix JS floating point precision errors (e.g., making sin(180) exactly 0)
+ const cleanFloat = (num) => (Math.abs(num) < 1e-10 ? 0 : num);
+
switch (type) {
- case "sin": result = Math.sin(value); break;
- case "cos": result = Math.cos(value); break;
- case "tan": result = Math.tan(value); break;
- case "sqrt": result = Math.sqrt(value); break;
- case "square": result = value * value; break;
- case "inv": result = 1 / value; break;
+ case "sin":
+ result = cleanFloat(Math.sin(radians));
+ break;
+ case "cos":
+ result = cleanFloat(Math.cos(radians));
+ break;
+ case "tan":
+ // Tangent of 90, 270, etc., is undefined
+ if (value % 180 === 90 || value % 180 === -90) {
+ result = "Error";
+ } else {
+ result = cleanFloat(Math.tan(radians));
+ }
+ break;
+ case "log":
+ result = value > 0 ? Math.log10(value) : "Error";
+ break;
+ case "ln":
+ result = value > 0 ? Math.log(value) : "Error";
+ break;
+ case "factorial":
+ result = factorial(value);
+ break;
+ case "sqrt":
+ result = value < 0 ? "Error" : Math.sqrt(value);
+ break;
+ case "square":
+ result = value * value;
+ break;
+ case "inv":
+ result = value === 0 ? "Error" : 1 / value;
+ break;
+ default:
+ result = "Error";
}
- if (isNaN(result)) return "Error";
- return String(result);
+
+ expression = result === "Error" || Number.isNaN(result) ? "Error" : String(result);
+ update();
} catch {
- return "Error";
+ expression = "Error";
+ update();
}
}
-
- function clearIfFinished() {
- if (expression === "Error" || expression === "NaN") {
- expression = "";
- }
+ function evaluateCurrent() {
+ const evaluated = safeEval(expression);
+ expression = evaluated || "";
+ update();
+ }
+
+ function deleteLast() {
+ clearIfError();
+ expression = expression.slice(0, -1);
+ update();
+ }
+
+ function clearExpression() {
+ expression = "";
+ update();
}
- document.querySelectorAll(".calc-btn").forEach((btn) => {
+ document.querySelectorAll(".calc-btn").forEach(btn => {
btn.addEventListener("click", () => {
- clearIfFinished();
-
+ clearIfError();
const value = btn.dataset.value;
const action = btn.dataset.action;
if (value !== undefined) {
- if (value === ".") {
-
- const lastOperand = expression.split(/[\+\-\*\/\^\(\)]/).pop();
- if (lastOperand.includes(".")) return;
- }
- expression += value;
- update();
+ if (value === ".") appendDecimal();
+ else appendDigit(value);
return;
}
if (!action) return;
-
switch (action) {
case "clear":
- expression = "";
+ clearExpression();
break;
case "delete":
- if (expression === "Infinity" || expression === "-Infinity") {
- expression = "";
- } else {
- expression = expression.slice(0, -1);
- }
+ deleteLast();
break;
case "=":
- expression = safeEval(expression);
+ evaluateCurrent();
break;
case "sin":
case "cos":
case "tan":
+ case "log":
+ case "ln":
+ case "factorial":
case "sqrt":
case "square":
case "inv":
- expression = applyFunction(action);
+ applyFunction(action);
break;
- case "^":
+ case "mod":
+ appendOperator("%");
+ break;
+ case "(":
+ case ")":
case "+":
case "-":
- case "*":
- case "/":
-
- const lastChar = expression.slice(-1);
- if (["+", "-", "*", "/", "^"].includes(lastChar)) {
- expression = expression.slice(0, -1) + action;
- } else {
- expression += action;
- }
+ case "−": // Catching both standard minus and typographic minus
+ case "×":
+ case "÷":
+ case "^":
+ case "%":
+ appendOperator(action);
break;
default:
- expression += action;
+ appendOperator(action);
}
- update();
});
});
- document.addEventListener("keydown", (e) => {
- const key = e.key;
- if (!document.getElementById("calcDisplay")) return;
+ if (window.calcKeydownHandler) {
+ document.removeEventListener("keydown", window.calcKeydownHandler);
+ }
- // Whitelist allowed keys to prevent typing letters
- const allowedKeys = ["Enter", "Backspace", "Escape", "=", "+", "-", "*", "/", "^", ".", "(", ")"];
- if (allowedKeys.includes(key) || /^[0-9]$/.test(key)) {
+ window.calcKeydownHandler = (e) => {
+ if (expression === "Error") expression = "";
+
+ if (/^\d$/.test(e.key)) {
e.preventDefault();
- } else {
+ appendDigit(e.key);
return;
}
- clearIfFinished();
+ if (e.key === ".") {
+ e.preventDefault();
+ appendDecimal();
+ return;
+ }
- if (/^[0-9]$/.test(key)) {
- expression += key;
- } else if (key === ".") {
- const lastOperand = expression.split(/[\+\-\*\/\^\(\)]/).pop();
- if (!lastOperand.includes(".")) {
- expression += ".";
- }
- } else if (["+", "-", "*", "/", "^"].includes(key)) {
- const lastChar = expression.slice(-1);
- if (["+", "-", "*", "/", "^"].includes(lastChar)) {
- expression = expression.slice(0, -1) + key;
- } else {
- expression += key;
- }
- } else if (key === ")" || key === "(") {
- expression += key;
- } else if (key === "Enter" || key === "=") {
- expression = safeEval(expression);
- } else if (key === "Backspace") {
- if (expression === "Infinity" || expression === "-Infinity") {
- expression = "";
- } else {
- expression = expression.slice(0, -1);
- }
- } else if (key === "Escape" || key.toLowerCase() === "c") {
- expression = "";
+ if (["+", "-", "*", "/", "^", "(", ")", "%"].includes(e.key)) {
+ e.preventDefault();
+ if (e.key === "*") appendOperator("×");
+ else if (e.key === "/") appendOperator("÷");
+ else if (e.key === "-") appendOperator("−");
+ else appendOperator(e.key);
+ return;
}
- update();
- });
+ if (e.key === "!") {
+ e.preventDefault();
+ applyFunction("factorial");
+ return;
+ }
+
+ if (e.key === "Enter" || e.key === "=") {
+ e.preventDefault();
+ evaluateCurrent();
+ return;
+ }
+
+ if (e.key === "Backspace") {
+ e.preventDefault();
+ deleteLast();
+ return;
+ }
+
+ if (e.key === "Escape" || e.key.toLowerCase() === "c") {
+ e.preventDefault();
+ clearExpression();
+ }
+ };
+
+ document.addEventListener("keydown", window.calcKeydownHandler);
update();
}
\ No newline at end of file