From bef740d554e457d92c05d4759e847074b1887382 Mon Sep 17 00:00:00 2001 From: delta student <04.sainiKhushi@gmail.com> Date: Sat, 25 Oct 2025 18:53:57 +0530 Subject: [PATCH] replace eval() with custom safe expression parse --- Projects/calculator-js/script.js | 324 ++++++++++++++++++++++++++----- 1 file changed, 278 insertions(+), 46 deletions(-) diff --git a/Projects/calculator-js/script.js b/Projects/calculator-js/script.js index 360ceaf1..05cbd4a5 100644 --- a/Projects/calculator-js/script.js +++ b/Projects/calculator-js/script.js @@ -1,3 +1,102 @@ +// const display = document.getElementById('display'); +// const resultDisplay = document.getElementById('result'); +// const buttons = document.querySelectorAll('.buttons button'); + +// let currentExpression = ''; + +// function updateDisplay() { +// display.value = currentExpression; +// } + +// function sanitizeExpression(expr) { +// return expr.replace(/[^0-9+\-*/().%]/g, ''); +// } + +// // function calculateExpression(expr) { +// // const withPercent = expr.replace(/(\d+(\.\d+)?)%/g, '($1/100)'); +// // try { +// // const result = eval(withPercent); +// // if (typeof result === 'number') { +// // if (!isFinite(result)) return 'Error'; +// // return String(Math.round(result * 1e10) / 1e10); +// // } +// // return 'Error'; +// // } catch (e) { +// // return 'Error'; +// // } +// // } + +// +// Option 1: Use math.js library (add to index.html: ) +// +// Or install via npm: npm install mathjs +// + + +// +// Option 1: Use math.js library (add to index.html: ) +// +// Or install via npm: npm install mathjs +// + + + + + + +// buttons.forEach(btn => { +// btn.addEventListener('click', () => { +// const val = btn.dataset.value; +// const action = btn.dataset.action; + +// if (action === 'clear') { +// currentExpression = ''; +// updateDisplay(); +// resultDisplay.value = ''; +// return; +// } +// if (action === 'back') { +// currentExpression = currentExpression.slice(0, -1); +// updateDisplay(); +// resultDisplay.value = ''; +// return; +// } +// if (action === 'equals') { +// const sanitized = sanitizeExpression(currentExpression); +// const result = calculateExpression(sanitized); +// resultDisplay.value = result; +// return; +// } + +// currentExpression += val; +// updateDisplay(); +// resultDisplay.value = ''; +// }); +// }); + +// document.addEventListener('keydown', (e) => { +// // Only digits, operators, and decimal +// if ("0123456789+-*/.%".includes(e.key)) { +// currentExpression += e.key; +// updateDisplay(); +// resultDisplay.value = ''; +// } +// // Evaluate on Enter +// else if (e.key === 'Enter') { +// e.preventDefault(); // Prevent form submission +// const sanitized = sanitizeExpression(currentExpression); +// resultDisplay.value = calculateExpression(sanitized); +// } +// // Delete last character +// else if (e.key === 'Backspace') { +// e.preventDefault(); // Prevent browser back navigation +// currentExpression = currentExpression.slice(0, -1); +// updateDisplay(); +// resultDisplay.value = ''; +// } +// // Clear using Escape key +// else if (e.key === 'Escape') { +// e.preventDefault(); +// currentExpression = ''; +// updateDisplay(); +// resultDisplay.value = ''; +// } +// }); + const display = document.getElementById('display'); const resultDisplay = document.getElementById('result'); const buttons = document.querySelectorAll('.buttons button'); @@ -12,10 +111,13 @@ function sanitizeExpression(expr) { return expr.replace(/[^0-9+\-*/().%]/g, ''); } +// Custom expression parser - replaces eval() function calculateExpression(expr) { + // Convert percentages to division by 100 const withPercent = expr.replace(/(\d+(\.\d+)?)%/g, '($1/100)'); + try { - const result = eval(withPercent); + const result = evaluateExpression(withPercent); if (typeof result === 'number') { if (!isFinite(result)) return 'Error'; return String(Math.round(result * 1e10) / 1e10); @@ -26,62 +128,192 @@ function calculateExpression(expr) { } } -buttons.forEach(btn => { - btn.addEventListener('click', () => { - const val = btn.dataset.value; - const action = btn.dataset.action; +// Tokenizer: converts string expression into tokens +function tokenize(expr) { + const tokens = []; + let i = 0; + + while (i < expr.length) { + const char = expr[i]; + + // Skip whitespace + if (char === ' ') { + i++; + continue; + } + + // Handle numbers (including decimals) + if (char >= '0' && char <= '9' || char === '.') { + let num = ''; + while (i < expr.length && (expr[i] >= '0' && expr[i] <= '9' || expr[i] === '.')) { + num += expr[i]; + i++; + } + tokens.push({ type: 'number', value: parseFloat(num) }); + continue; + } + + // Handle operators + if (char === '+' || char === '-' || char === '*' || char === '/') { + tokens.push({ type: 'operator', value: char }); + i++; + continue; + } + + // Handle parentheses + if (char === '(') { + tokens.push({ type: 'leftParen' }); + i++; + continue; + } + + if (char === ')') { + tokens.push({ type: 'rightParen' }); + i++; + continue; + } + + // Unknown character - skip it + i++; + } + + return tokens; +} + +// Evaluator: uses Shunting Yard algorithm to handle operator precedence +function evaluateExpression(expr) { + const tokens = tokenize(expr); + const outputQueue = []; + const operatorStack = []; + + // Define operator precedence + const precedence = { + '+': 1, + '-': 1, + '*': 2, + '/': 2 + }; + + // Convert to Reverse Polish Notation (RPN) using Shunting Yard + for (const token of tokens) { + if (token.type === 'number') { + outputQueue.push(token); + } else if (token.type === 'operator') { + while ( + operatorStack.length > 0 && + operatorStack[operatorStack.length - 1].type === 'operator' && + precedence[operatorStack[operatorStack.length - 1].value] >= precedence[token.value] + ) { + outputQueue.push(operatorStack.pop()); + } + operatorStack.push(token); + } else if (token.type === 'leftParen') { + operatorStack.push(token); + } else if (token.type === 'rightParen') { + while ( + operatorStack.length > 0 && + operatorStack[operatorStack.length - 1].type !== 'leftParen' + ) { + outputQueue.push(operatorStack.pop()); + } + // Pop the left parenthesis + if (operatorStack.length > 0) { + operatorStack.pop(); + } + } + } + + // Pop remaining operators + while (operatorStack.length > 0) { + outputQueue.push(operatorStack.pop()); + } + + // Evaluate RPN expression + const evalStack = []; + + for (const token of outputQueue) { + if (token.type === 'number') { + evalStack.push(token.value); + } else if (token.type === 'operator') { + if (evalStack.length < 2) { + throw new Error('Invalid expression'); + } + + const b = evalStack.pop(); + const a = evalStack.pop(); + + let result; + switch (token.value) { + case '+': + result = a + b; + break; + case '-': + result = a - b; + break; + case '*': + result = a * b; + break; + case '/': + if (b === 0) { + return Infinity; // Will be caught as 'Error' by !isFinite check + } + result = a / b; + break; + default: + throw new Error('Unknown operator'); + } + + evalStack.push(result); + } + } + + if (evalStack.length !== 1) { + throw new Error('Invalid expression'); + } + + return evalStack[0]; +} + +// Rest of your button handling code remains the same +buttons.forEach(button => { + button.addEventListener('click', () => { + const action = button.dataset.action; + const value = button.dataset.value; if (action === 'clear') { currentExpression = ''; - updateDisplay(); resultDisplay.value = ''; - return; - } - if (action === 'back') { + updateDisplay(); + } else if (action === 'backspace') { currentExpression = currentExpression.slice(0, -1); updateDisplay(); - resultDisplay.value = ''; - return; - } - if (action === 'equals') { + } else if (action === 'equals') { const sanitized = sanitizeExpression(currentExpression); const result = calculateExpression(sanitized); resultDisplay.value = result; - return; + } else { + currentExpression += value; + updateDisplay(); } - - currentExpression += val; - updateDisplay(); - resultDisplay.value = ''; }); }); +// Keyboard support document.addEventListener('keydown', (e) => { - // Only digits, operators, and decimal - if ("0123456789+-*/.%".includes(e.key)) { - currentExpression += e.key; - updateDisplay(); - resultDisplay.value = ''; - } - // Evaluate on Enter - else if (e.key === 'Enter') { - e.preventDefault(); // Prevent form submission - const sanitized = sanitizeExpression(currentExpression); - resultDisplay.value = calculateExpression(sanitized); - } - // Delete last character - else if (e.key === 'Backspace') { - e.preventDefault(); // Prevent browser back navigation - currentExpression = currentExpression.slice(0, -1); - updateDisplay(); - resultDisplay.value = ''; - } - // Clear using Escape key - else if (e.key === 'Escape') { - e.preventDefault(); - currentExpression = ''; - updateDisplay(); - resultDisplay.value = ''; - } -}); - + if (e.key >= '0' && e.key <= '9' || '+-*/.()%'.includes(e.key)) { + currentExpression += e.key; + updateDisplay(); + } else if (e.key === 'Enter') { + const sanitized = sanitizeExpression(currentExpression); + const result = calculateExpression(sanitized); + resultDisplay.value = result; + } else if (e.key === 'Backspace') { + e.preventDefault(); + currentExpression = currentExpression.slice(0, -1); + updateDisplay(); + } else if (e.key.toLowerCase() === 'c') { + currentExpression = ''; + resultDisplay.value = ''; + updateDisplay(); + } +}); \ No newline at end of file