From 168a195bb0ae9698b3a60abac2a91150722fb5c9 Mon Sep 17 00:00:00 2001 From: SV Date: Sun, 24 May 2026 16:03:46 +0530 Subject: [PATCH] fix: resolve calculator logic and UI bugs --- web-app/js/projects.js | 273 +------------ web-app/js/projects/calculator.js | 612 ++++++++++++++++++++++-------- 2 files changed, 457 insertions(+), 428 deletions(-) diff --git a/web-app/js/projects.js b/web-app/js/projects.js index 05bdd56..a5f930f 100644 --- a/web-app/js/projects.js +++ b/web-app/js/projects.js @@ -591,278 +591,7 @@ function initPascalTriangle() { generatePascal(); // Initial generation } -// ============================================ -// CALCULATOR -// ============================================ -function getCalculatorHTML() { - return ` -
-

🧮 Calculator

-
-
0
-
- - - - - - - - - - - - - - - - - - - - - - - -
-
-
- - - `; -} - -function initCalculator() { - const display = document.getElementById("calcDisplay"); - if (!display) return; - let expression = ""; - - function update() { - display.textContent = expression || "0"; - } - - function format(expr) { - return expr - .replace(/÷/g, "/") - .replace(/×/g, "*") - .replace(/\^/g, "**"); - } - - function safeEval(expr) { - try { - if (!expr) return ""; - let result = eval(format(expr)); - if (result === undefined) return ""; - if (isNaN(result)) return "Error"; - return String(result); - } catch { - return "Error"; - } - } - - function applyFunction(type) { - try { - let value = eval(format(expression || "0")); - let result; - 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; - } - if (isNaN(result)) return "Error"; - return String(result); - } catch { - return "Error"; - } - } - - - function clearIfFinished() { - if (expression === "Error" || expression === "NaN") { - expression = ""; - } - } - - document.querySelectorAll(".calc-btn").forEach((btn) => { - btn.addEventListener("click", () => { - clearIfFinished(); - - 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(); - return; - } - - if (!action) return; - - - switch (action) { - case "clear": - expression = ""; - break; - case "delete": - if (expression === "Infinity" || expression === "-Infinity") { - expression = ""; - } else { - expression = expression.slice(0, -1); - } - break; - case "=": - expression = safeEval(expression); - break; - case "sin": - case "cos": - case "tan": - case "sqrt": - case "square": - case "inv": - expression = applyFunction(action); - break; - case "^": - case "+": - case "-": - case "*": - case "/": - - const lastChar = expression.slice(-1); - if (["+", "-", "*", "/", "^"].includes(lastChar)) { - expression = expression.slice(0, -1) + action; - } else { - expression += action; - } - break; - default: - expression += action; - } - update(); - }); - }); - - document.addEventListener("keydown", (e) => { - const key = e.key; - if (!document.getElementById("calcDisplay")) return; - - // Whitelist allowed keys to prevent typing letters - const allowedKeys = ["Enter", "Backspace", "Escape", "=", "+", "-", "*", "/", "^", ".", "(", ")"]; - if (allowedKeys.includes(key) || /^[0-9]$/.test(key)) { - e.preventDefault(); - } else { - return; - } - - clearIfFinished(); - - 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 = ""; - } - update(); - }); - - update(); -} +// Calculator module is in js/projects/calculator.js. // ============================================ // FIBONACCI diff --git a/web-app/js/projects/calculator.js b/web-app/js/projects/calculator.js index 7176e8d..66c98bd 100644 --- a/web-app/js/projects/calculator.js +++ b/web-app/js/projects/calculator.js @@ -1,122 +1,185 @@ function getCalculatorHTML() { return ` -
-

🧮 Ultra Pro Calculator

+
+

🧮 Calculator

+

A scientific calculator that follows the app theme and supports keyboard input.

-
+
0
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
`; } @@ -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