|
|
<!DOCTYPE html> |
|
|
<html lang="en"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<title>SymCalc - Symbolic Calculator</title> |
|
|
<script src="https://cdn.tailwindcss.com"></script> |
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> |
|
|
<script src="https://cdn.jsdelivr.net/pyodide/v0.23.4/full/pyodide.js"></script> |
|
|
<style> |
|
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Source+Code+Pro:wght@400;600&display=swap'); |
|
|
|
|
|
:root { |
|
|
--primary: #4f46e5; |
|
|
--primary-light: #e0e7ff; |
|
|
--text: #1f2937; |
|
|
--text-light: #6b7280; |
|
|
--border: #e5e7eb; |
|
|
--bg: #f9fafb; |
|
|
} |
|
|
|
|
|
body { |
|
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; |
|
|
color: var(--text); |
|
|
line-height: 1.5; |
|
|
} |
|
|
|
|
|
.mono { |
|
|
font-family: 'Source Code Pro', monospace; |
|
|
} |
|
|
|
|
|
.document { |
|
|
max-width: 800px; |
|
|
margin: 0 auto; |
|
|
background: white; |
|
|
box-shadow: 0 1px 3px rgba(0,0,0,0.05); |
|
|
border-radius: 8px; |
|
|
overflow: hidden; |
|
|
} |
|
|
.analysis-box { |
|
|
background-color: #f8fafc; |
|
|
border: 1px solid #e5e7eb; |
|
|
padding: 16px; |
|
|
margin-top: 12px; |
|
|
border-radius: 8px; |
|
|
font-family: 'Inter', sans-serif; |
|
|
color: #334155; |
|
|
box-shadow: 0 1px 2px rgba(0,0,0,0.05); |
|
|
} |
|
|
.analysis-title { |
|
|
font-weight: bold; |
|
|
color: #4f46e5; |
|
|
margin-bottom: 6px; |
|
|
} |
|
|
.analysis-item { |
|
|
margin-left: 12px; |
|
|
position: relative; |
|
|
} |
|
|
.analysis-item:before { |
|
|
content: "•"; |
|
|
position: absolute; |
|
|
left: -12px; |
|
|
color: #4f46e5; |
|
|
} |
|
|
.analysis-tip { |
|
|
background-color: #e0e7ff; |
|
|
padding: 8px; |
|
|
border-radius: 4px; |
|
|
margin-top: 8px; |
|
|
font-style: italic; |
|
|
} |
|
|
.symbol-btn { |
|
|
transition: all 0.2s ease; |
|
|
} |
|
|
.symbol-btn:hover { |
|
|
transform: translateY(-2px); |
|
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); |
|
|
} |
|
|
.history-item:hover { |
|
|
background-color: #f3f4f6; |
|
|
cursor: pointer; |
|
|
} |
|
|
.fade-in { |
|
|
animation: fadeIn 0.3s ease-in-out; |
|
|
} |
|
|
@keyframes fadeIn { |
|
|
from { opacity: 0; transform: translateY(10px); } |
|
|
to { opacity: 1; transform: translateY(0); } |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
<body class="bg-gray-50 min-h-screen p-6"> |
|
|
<div class="document"> |
|
|
<header class="border-b border-gray-200 px-6 py-4"> |
|
|
<div class="flex items-center justify-between"> |
|
|
<div class="flex items-center space-x-3"> |
|
|
<i class="fas fa-infinity text-indigo-600 text-2xl"></i> |
|
|
<h1 class="text-2xl font-semibold">SymCalc</h1> |
|
|
<span class="text-sm text-gray-500">Symbolic Mathematics Calculator</span> |
|
|
</div> |
|
|
<div class="flex space-x-4"> |
|
|
<button class="text-gray-500 hover:text-indigo-600 transition" title="History"> |
|
|
<i class="fas fa-history"></i> |
|
|
</button> |
|
|
<button class="text-gray-500 hover:text-indigo-600 transition" title="Documentation"> |
|
|
<i class="fas fa-question-circle"></i> |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
</header> |
|
|
|
|
|
<main class="p-6"> |
|
|
<section class="mb-8"> |
|
|
<div id="loading" class="hidden text-indigo-600 text-sm mb-2 font-medium">Calculating...</div> |
|
|
<div class="mb-1 text-sm text-gray-500 font-medium">Input Expression</div> |
|
|
<div class="relative"> |
|
|
<textarea |
|
|
id="formulaInput" |
|
|
class="w-full bg-white text-gray-900 text-lg mono resize-none outline-none border border-gray-200 rounded p-3 mb-2 focus:ring-2 focus:ring-indigo-200 focus:border-indigo-500" |
|
|
rows="2" |
|
|
placeholder="Enter a mathematical expression (e.g. 2x + 3 = 7, sin(π/2), √(16))" |
|
|
spellcheck="false" |
|
|
></textarea> |
|
|
<div id="latexPreview" class="absolute right-3 top-3 text-gray-400 text-sm hidden"> |
|
|
<span class="bg-white px-2 py-1 rounded">LaTeX Preview</span> |
|
|
</div> |
|
|
</div> |
|
|
<div id="latexOutput" class="text-center p-2 bg-gray-50 border border-gray-200 rounded hidden"> |
|
|
<div class="katex-display"></div> |
|
|
</div> |
|
|
|
|
|
<div class="mt-6 mb-1 text-sm text-gray-500 font-medium">Result</div> |
|
|
<div id="result" class="text-xl font-medium text-gray-900 mono p-3 bg-gray-50 rounded border border-gray-200 min-h-[60px]"></div> |
|
|
<div id="steps" class="mt-3 hidden"> |
|
|
<div class="text-sm text-gray-500 font-medium mb-1">Solution Steps</div> |
|
|
<div id="stepsContent" class="text-sm mono p-3 bg-blue-50 rounded border border-blue-200"></div> |
|
|
</div> |
|
|
<div id="error" class="mt-2 text-sm text-red-500"></div> |
|
|
<div id="sympyCode" class="hidden mt-3"> |
|
|
<div class="text-sm text-gray-500 font-medium mb-1">SymPy Code</div> |
|
|
<pre class="text-xs mono p-3 bg-gray-100 rounded overflow-x-auto"></pre> |
|
|
</div> |
|
|
</section> |
|
|
|
|
|
<section class="mb-8"> |
|
|
<div class="mb-3 text-sm text-gray-500 font-medium">Common Symbols</div> |
|
|
<div class="grid grid-cols-8 gap-2 mb-4"> |
|
|
<button onclick="addSymbol('π')" class="p-2 bg-gray-100 hover:bg-gray-200 rounded text-sm mono">π</button> |
|
|
<button onclick="addSymbol('e')" class="p-2 bg-gray-100 hover:bg-gray-200 rounded text-sm mono">e</button> |
|
|
<button onclick="addSymbol('√(')" class="p-2 bg-gray-100 hover:bg-gray-200 rounded text-sm mono">√</button> |
|
|
<button onclick="addSymbol('^')" class="p-2 bg-gray-100 hover:bg-gray-200 rounded text-sm mono">^</button> |
|
|
<button onclick="addSymbol('!')" class="p-2 bg-gray-100 hover:bg-gray-200 rounded text-sm mono">!</button> |
|
|
<button onclick="addSymbol('(')" class="p-2 bg-gray-100 hover:bg-gray-200 rounded text-sm mono">(</button> |
|
|
<button onclick="addSymbol(')')" class="p-2 bg-gray-100 hover:bg-gray-200 rounded text-sm mono">)</button> |
|
|
<button onclick="addSymbol('+')" class="p-2 bg-gray-100 hover:bg-gray-200 rounded text-sm mono">+</button> |
|
|
</div> |
|
|
|
|
|
<div class="grid grid-cols-8 gap-2 mb-4"> |
|
|
<button onclick="addSymbol('-')" class="p-2 bg-gray-100 hover:bg-gray-200 rounded text-sm mono">-</button> |
|
|
<button onclick="addSymbol('*')" class="p-2 bg-gray-100 hover:bg-gray-200 rounded text-sm mono">×</button> |
|
|
<button onclick="addSymbol('/')" class="p-2 bg-gray-100 hover:bg-gray-200 rounded text-sm mono">÷</button> |
|
|
<button onclick="addSymbol('sin(')" class="p-2 bg-gray-100 hover:bg-gray-200 rounded text-sm mono">sin</button> |
|
|
<button onclick="addSymbol('cos(')" class="p-2 bg-gray-100 hover:bg-gray-200 rounded text-sm mono">cos</button> |
|
|
<button onclick="addSymbol('tan(')" class="p-2 bg-gray-100 hover:bg-gray-200 rounded text-sm mono">tan</button> |
|
|
<button onclick="addSymbol('ln(')" class="p-2 bg-gray-100 hover:bg-gray-200 rounded text-sm mono">ln</button> |
|
|
<button onclick="addSymbol('log(')" class="p-2 bg-gray-100 hover:bg-gray-200 rounded text-sm mono">log</button> |
|
|
</div> |
|
|
|
|
|
<div class="flex space-x-3"> |
|
|
<button onclick="calculate()" class="px-4 py-2 bg-indigo-600 hover:bg-indigo-700 text-white rounded text-sm font-medium flex items-center"> |
|
|
<i class="fas fa-equals mr-2"></i> Calculate |
|
|
</button> |
|
|
<button onclick="analyzeExpression()" class="px-4 py-2 bg-white hover:bg-gray-50 border border-gray-200 rounded text-sm font-medium flex items-center"> |
|
|
<i class="fas fa-search mr-2"></i> Analyze |
|
|
</button> |
|
|
<button onclick="clearAll()" class="px-4 py-2 bg-white hover:bg-gray-50 border border-gray-200 rounded text-sm font-medium flex items-center"> |
|
|
<i class="fas fa-trash-alt mr-2"></i> Clear |
|
|
</button> |
|
|
</div> |
|
|
</section> |
|
|
|
|
|
<section id="historyPanel" class="hidden mt-8 border-t border-gray-200 pt-6"> |
|
|
<div class="flex justify-between items-center mb-4"> |
|
|
<h3 class="font-medium text-gray-900">Calculation History</h3> |
|
|
<button onclick="clearHistory()" class="text-gray-500 hover:text-red-500 text-sm flex items-center"> |
|
|
<i class="fas fa-trash-alt mr-1"></i> Clear All |
|
|
</button> |
|
|
</div> |
|
|
<div id="historyList" class="space-y-3"></div> |
|
|
</section> |
|
|
</div> |
|
|
|
|
|
<footer class="border-t border-gray-200 px-6 py-4 text-center text-gray-500 text-sm"> |
|
|
<p>SymCalc v1.0 · Symbolic Mathematics Calculator</p> |
|
|
</footer> |
|
|
</div> |
|
|
|
|
|
<script> |
|
|
let currentExpression = ''; |
|
|
let history = JSON.parse(localStorage.getItem('calcHistory')) || []; |
|
|
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', () => { |
|
|
updateScreen(); |
|
|
renderHistory(); |
|
|
|
|
|
|
|
|
document.querySelector('.fa-history').parentElement.addEventListener('click', toggleHistoryPanel); |
|
|
|
|
|
|
|
|
document.addEventListener('keydown', handleKeyPress); |
|
|
}); |
|
|
|
|
|
function handleKeyPress(e) { |
|
|
const input = document.getElementById('formulaInput'); |
|
|
const cursorPos = input.selectionStart; |
|
|
const textBeforeCursor = input.value.substring(0, cursorPos); |
|
|
|
|
|
|
|
|
if (e.key === '(' && !e.shiftKey) { |
|
|
const lastWord = textBeforeCursor.split(/[\s\+\-\*\/\^\(\)]/).pop(); |
|
|
if (['sin', 'cos', 'tan', 'log', 'ln', 'sqrt'].includes(lastWord)) { |
|
|
e.preventDefault(); |
|
|
const newPos = cursorPos - lastWord.length; |
|
|
input.value = input.value.substring(0, newPos) + |
|
|
lastWord + '()' + |
|
|
input.value.substring(cursorPos); |
|
|
input.setSelectionRange(newPos + lastWord.length + 1, |
|
|
newPos + lastWord.length + 1); |
|
|
return; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (e.key === 'Enter' && e.shiftKey) { |
|
|
e.preventDefault(); |
|
|
addSymbol('\n'); |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
if (e.key === 'Tab') { |
|
|
e.preventDefault(); |
|
|
const lastWord = textBeforeCursor.split(/[\s\+\-\*\/\^\(\)]/).pop(); |
|
|
const matches = ['pi', 'theta', 'alpha', 'beta', 'gamma'] |
|
|
.filter(sym => sym.startsWith(lastWord.toLowerCase())); |
|
|
if (matches.length === 1) { |
|
|
const match = matches[0]; |
|
|
const newPos = cursorPos - lastWord.length; |
|
|
input.value = input.value.substring(0, newPos) + |
|
|
match + |
|
|
input.value.substring(cursorPos); |
|
|
input.setSelectionRange(newPos + match.length, |
|
|
newPos + match.length); |
|
|
} |
|
|
return; |
|
|
} |
|
|
|
|
|
const keyMap = { |
|
|
'0': '0', '1': '1', '2': '2', '3': '3', '4': '4', |
|
|
'5': '5', '6': '6', '7': '7', '8': '8', '9': '9', |
|
|
'+': '+', '-': '-', '*': '*', '/': '/', |
|
|
'.': '.', '^': '^', '!': '!', |
|
|
'(': '(', ')': ')', |
|
|
'Enter': '=', |
|
|
'Backspace': 'backspace', |
|
|
'Escape': 'clear', |
|
|
'ArrowUp': 'history-up', |
|
|
'ArrowDown': 'history-down' |
|
|
}; |
|
|
|
|
|
const key = keyMap[e.key]; |
|
|
if (key) { |
|
|
e.preventDefault(); |
|
|
if (key === '=') calculate(); |
|
|
else if (key === 'backspace') backspace(); |
|
|
else if (key === 'clear') clearAll(); |
|
|
else if (key === 'history-up') navigateHistory(-1); |
|
|
else if (key === 'history-down') navigateHistory(1); |
|
|
else addSymbol(key); |
|
|
} |
|
|
} |
|
|
|
|
|
let historyNavigationIndex = -1; |
|
|
let historyNavigationTemp = ''; |
|
|
|
|
|
function navigateHistory(direction) { |
|
|
const input = document.getElementById('formulaInput'); |
|
|
|
|
|
if (historyNavigationIndex === -1) { |
|
|
historyNavigationTemp = input.value; |
|
|
} |
|
|
|
|
|
historyNavigationIndex += direction; |
|
|
|
|
|
if (historyNavigationIndex < -1) { |
|
|
historyNavigationIndex = -1; |
|
|
} else if (historyNavigationIndex >= history.length) { |
|
|
historyNavigationIndex = history.length - 1; |
|
|
} |
|
|
|
|
|
if (historyNavigationIndex === -1) { |
|
|
input.value = historyNavigationTemp; |
|
|
} else { |
|
|
input.value = history[historyNavigationIndex].expression; |
|
|
} |
|
|
|
|
|
input.focus(); |
|
|
} |
|
|
|
|
|
function addSymbol(symbol) { |
|
|
const input = document.getElementById('formulaInput'); |
|
|
const startPos = input.selectionStart; |
|
|
const endPos = input.selectionEnd; |
|
|
const currentValue = input.value; |
|
|
|
|
|
input.value = currentValue.substring(0, startPos) + symbol + currentValue.substring(endPos); |
|
|
input.focus(); |
|
|
input.setSelectionRange(startPos + symbol.length, startPos + symbol.length); |
|
|
} |
|
|
|
|
|
function clearAll() { |
|
|
currentExpression = ''; |
|
|
document.getElementById('error').textContent = ''; |
|
|
updateScreen(); |
|
|
} |
|
|
|
|
|
function backspace() { |
|
|
const input = document.getElementById('formulaInput'); |
|
|
const startPos = input.selectionStart; |
|
|
const endPos = input.selectionEnd; |
|
|
|
|
|
if (startPos === endPos && startPos > 0) { |
|
|
input.value = input.value.substring(0, startPos - 1) + input.value.substring(endPos); |
|
|
input.setSelectionRange(startPos - 1, startPos - 1); |
|
|
} else { |
|
|
input.value = input.value.substring(0, startPos) + input.value.substring(endPos); |
|
|
input.setSelectionRange(startPos, startPos); |
|
|
} |
|
|
input.focus(); |
|
|
} |
|
|
|
|
|
function updateScreen() { |
|
|
|
|
|
} |
|
|
|
|
|
let pyodide; |
|
|
async function initializePyodide() { |
|
|
const loadingEl = document.getElementById('loading'); |
|
|
loadingEl.classList.remove('hidden'); |
|
|
document.getElementById('loadingText').textContent = "Loading SymPy..."; |
|
|
|
|
|
try { |
|
|
pyodide = await loadPyodide({ |
|
|
indexURL: "https://cdn.jsdelivr.net/pyodide/v0.23.4/full/" |
|
|
}); |
|
|
await pyodide.loadPackage(['sympy']); |
|
|
loadingEl.classList.add('hidden'); |
|
|
return true; |
|
|
} catch (error) { |
|
|
document.getElementById('error').innerHTML = ` |
|
|
<div class="bg-red-50 border border-red-200 p-3 rounded"> |
|
|
<div class="font-medium text-red-600">Failed to initialize SymPy:</div> |
|
|
<div class="text-sm">${error.message}</div> |
|
|
</div> |
|
|
`; |
|
|
loadingEl.classList.add('hidden'); |
|
|
return false; |
|
|
} |
|
|
} |
|
|
|
|
|
async function calculate() { |
|
|
if (!pyodide) { |
|
|
const initialized = await initializePyodide(); |
|
|
if (!initialized) return; |
|
|
} |
|
|
|
|
|
const input = document.getElementById('formulaInput'); |
|
|
currentExpression = input.value.trim(); |
|
|
if (!currentExpression) return; |
|
|
|
|
|
const loadingEl = document.getElementById('loading'); |
|
|
loadingEl.classList.remove('hidden'); |
|
|
document.getElementById('loadingText').textContent = "Calculating..."; |
|
|
|
|
|
try { |
|
|
|
|
|
let pythonCode = ` |
|
|
from sympy import * |
|
|
from sympy.parsing.sympy_parser import parse_expr |
|
|
import traceback |
|
|
|
|
|
x, y, z = symbols('x y z') |
|
|
result = None |
|
|
error = None |
|
|
|
|
|
try: |
|
|
if '=' in expr: |
|
|
if expr.count('=') == 1: |
|
|
# Single equation solving |
|
|
lhs, rhs = expr.split('=', 1) |
|
|
lhs_expr = parse_expr(lhs.strip()) |
|
|
rhs_expr = parse_expr(rhs.strip()) |
|
|
solution = solve(Eq(lhs_expr, rhs_expr)) |
|
|
if isinstance(solution, dict): |
|
|
result = "\\n".join([f"{k} = {v}" for k,v in solution.items()]) |
|
|
elif isinstance(solution, list): |
|
|
result = "\\n".join([f"Solution {i+1}: {s}" for i,s in enumerate(solution)]) |
|
|
else: |
|
|
result = str(solution) |
|
|
else: |
|
|
# System of equations |
|
|
equations = [eq.strip() for eq in expr.split(';') if '=' in eq] |
|
|
syms = set() |
|
|
for eq in equations: |
|
|
syms.update([str(s) for s in parse_expr(eq.split('=')[0].strip()).free_symbols]) |
|
|
syms = sorted(syms, key=lambda s: str(s)) |
|
|
eqs = [Eq(*map(parse_expr, eq.split('=', 1))) for eq in equations] |
|
|
solution = solve(eqs, syms) |
|
|
if solution: |
|
|
if isinstance(solution, list): |
|
|
result = "\\n".join([f"Solution {i+1}: {s}" for i,s in enumerate(solution)]) |
|
|
else: |
|
|
result = "\\n".join([f"{k} = {v}" for k,v in solution.items()]) |
|
|
else: |
|
|
result = "No solution found" |
|
|
else: |
|
|
# Expression evaluation |
|
|
expr_parsed = parse_expr(expr) |
|
|
result = str(expr_parsed.evalf(4)) |
|
|
except Exception as e: |
|
|
error = traceback.format_exc() |
|
|
`; |
|
|
|
|
|
|
|
|
setTimeout(() => { |
|
|
document.getElementById('sympyCode').textContent = sympyCode; |
|
|
document.getElementById('sympyCode').classList.remove('hidden'); |
|
|
try { |
|
|
|
|
|
if (!currentExpression.trim()) throw "Please enter an expression"; |
|
|
if (currentExpression.length > 200) throw "Expression too long (max 200 chars)"; |
|
|
|
|
|
|
|
|
if (currentExpression.includes('=')) { |
|
|
|
|
|
const equations = currentExpression.split(/[\n,;]+/).map(eq => eq.trim()).filter(eq => eq.includes('=')); |
|
|
if (equations.length > 1) { |
|
|
|
|
|
const vars = new Set(); |
|
|
const equationsList = []; |
|
|
|
|
|
for (const eq of equations) { |
|
|
const parts = eq.split('='); |
|
|
if (parts.length !== 2) throw "Each equation must have exactly one '='"; |
|
|
|
|
|
const left = parts[0].trim().replace(/\s+/g, ''); |
|
|
const right = parts[1].trim().replace(/\s+/g, ''); |
|
|
|
|
|
|
|
|
const rearranged = `(${left}) - (${right})`; |
|
|
equationsList.push(rearranged); |
|
|
|
|
|
|
|
|
const varRegex = /([a-zA-Z])/g; |
|
|
let match; |
|
|
while ((match = varRegex.exec(left + right)) !== null) { |
|
|
vars.add(match[1]); |
|
|
} |
|
|
} |
|
|
|
|
|
if (equationsList.length !== vars.size) { |
|
|
throw `Need ${vars.size} independent equations to solve for ${[...vars].join(', ')}`; |
|
|
} |
|
|
|
|
|
|
|
|
sympyCode += `sol = solve([${equationsList.map(eq => `Eq(${eq}, 0)`).join(', ')}], [${[...vars].join(', ')}])\n`; |
|
|
|
|
|
try { |
|
|
|
|
|
const solution = {}; |
|
|
[...vars].forEach(v => solution[v] = `solution_for_${v}`); |
|
|
|
|
|
let resultText = ''; |
|
|
for (const [varName, value] of Object.entries(solution)) { |
|
|
resultText += `${varName} = ${value}\n`; |
|
|
} |
|
|
|
|
|
document.getElementById('result').textContent = resultText; |
|
|
addToHistory(currentExpression, resultText); |
|
|
return; |
|
|
} catch (e) { |
|
|
throw `Failed to solve system: ${e.message || e}`; |
|
|
} |
|
|
|
|
|
throw `System with ${vars.size} variables requires ${vars.size} equations`; |
|
|
} |
|
|
|
|
|
|
|
|
const parts = currentExpression.split('='); |
|
|
if (parts.length !== 2) throw "Each equation must have exactly one '='"; |
|
|
|
|
|
const left = parts[0].trim(); |
|
|
const right = parts[1].trim(); |
|
|
|
|
|
|
|
|
const variables = new Set(); |
|
|
const varRegex = /([a-zA-Z])/g; |
|
|
let match; |
|
|
|
|
|
while ((match = varRegex.exec(left + right)) !== null) { |
|
|
variables.add(match[1]); |
|
|
} |
|
|
|
|
|
if (variables.size === 0) { |
|
|
|
|
|
const leftValue = eval(left.replace(/×/g, '*').replace(/÷/g, '/').replace(/−/g, '-')); |
|
|
const rightValue = eval(right.replace(/×/g, '*').replace(/÷/g, '/').replace(/−/g, '-')); |
|
|
if (Math.abs(leftValue - rightValue) < 1e-10) { |
|
|
document.getElementById('result').textContent = "Equation is true"; |
|
|
} else { |
|
|
document.getElementById('result').textContent = "Equation is false"; |
|
|
} |
|
|
addToHistory(currentExpression, leftValue - rightValue); |
|
|
return; |
|
|
} |
|
|
else if (variables.size === 1) { |
|
|
|
|
|
const varName = [...variables][0]; |
|
|
sympyCode += `sol = solve(Eq(${left}, ${right}), ${varName})\n`; |
|
|
|
|
|
try { |
|
|
|
|
|
const solution = `solution_for_${varName}`; |
|
|
document.getElementById('result').textContent = `${varName} = ${solution}`; |
|
|
addToHistory(currentExpression, solution); |
|
|
return; |
|
|
} catch (e) { |
|
|
throw `Failed to solve equation: ${e.message || e}`; |
|
|
} |
|
|
} |
|
|
else { |
|
|
|
|
|
const varList = [...variables].join(', '); |
|
|
document.getElementById('result').textContent = `Multi-variable equation: ${currentExpression}`; |
|
|
document.getElementById('error').textContent = `Variables: ${varList}\nTo solve for ${variables.size > 1 ? 'these variables' : 'this variable'}, you need ${variables.size} independent equation${variables.size > 1 ? 's' : ''}.`; |
|
|
addToHistory(currentExpression, NaN); |
|
|
return; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
sympyCode += `expr = ${currentExpression.replace(/=/g, '==')}\n`; |
|
|
|
|
|
if (variables.length > 0) { |
|
|
sympyCode += `result = expr.subs({${variables.map(v => `${v}: 1`).join(', ')}})\n`; |
|
|
} else { |
|
|
sympyCode += `result = N(expr)\n`; |
|
|
} |
|
|
|
|
|
|
|
|
const sanitized = expr.replace(/Math\./g, ''); |
|
|
|
|
|
const invalidVars = sanitized.match(/[^a-zA-Z\s\d\.\+\-\*\/\^\(\)\,]/g); |
|
|
if (invalidVars) { |
|
|
const uniqueVars = [...new Set(invalidVars)]; |
|
|
throw `Invalid character${uniqueVars.length > 1 ? 's' : ''}: ${uniqueVars.join(', ')}`; |
|
|
} |
|
|
|
|
|
|
|
|
if (/[xyz]/.test(sanitized) && !currentExpression.includes('=')) { |
|
|
const vars = [...new Set(sanitized.match(/[xyz]/g))]; |
|
|
const varList = vars.join(', '); |
|
|
|
|
|
if (vars.length === 1 && /^[+-]?\d*[xyz]([+-]\d+)?$/.test(sanitized.replace(/\s/g, ''))) { |
|
|
const match = sanitized.match(/^([+-]?\d*)([xyz])([+-]\d+)?$/); |
|
|
const coeff = match[1] ? (match[1] === '+' ? 1 : match[1] === '-' ? -1 : parseFloat(match[1])) : 1; |
|
|
const varName = match[2]; |
|
|
const constant = match[3] ? parseFloat(match[3]) : 0; |
|
|
|
|
|
const analysis = `<div class="analysis-box"> |
|
|
<div class="analysis-title">Linear Expression Analysis</div> |
|
|
<div class="analysis-item">Form: <span class="font-mono">${coeff !== 1 ? coeff : ''}${varName} ${constant >= 0 ? '+' : ''}${constant !== 0 ? constant : ''}</span></div> |
|
|
<div class="analysis-item">Slope (coefficient of ${varName}): <span class="font-bold">${coeff}</span></div> |
|
|
<div class="analysis-item">Y-intercept: <span class="font-bold">${constant}</span></div> |
|
|
|
|
|
<div class="analysis-tip"> |
|
|
<div class="font-bold mb-1">To evaluate:</div> |
|
|
<div>1. Assign value to ${varName} (e.g. <span class="font-mono bg-gray-100 px-1">${varName}=5</span>)</div> |
|
|
<div>2. Or make equation to solve (e.g. <span class="font-mono bg-gray-100 px-1">${currentExpression}=7</span>)</div> |
|
|
</div> |
|
|
</div>`; |
|
|
throw analysis; |
|
|
} else { |
|
|
const analysis = `<div class="analysis-box"> |
|
|
<div class="analysis-title">Expression Analysis</div> |
|
|
<div class="analysis-item">Contains variable${vars.length > 1 ? 's' : ''}: <span class="font-bold">${varList}</span></div> |
|
|
<div class="analysis-item">Not an equation (missing '=')</div> |
|
|
|
|
|
<div class="analysis-tip"> |
|
|
<div class="font-bold mb-1">To evaluate this expression:</div> |
|
|
<div>1. Assign values to variables (e.g. <span class="font-mono bg-gray-100 px-1">${vars[0]}=5</span>)</div> |
|
|
<div>2. Or make it an equation (e.g. <span class="font-mono bg-gray-100 px-1">${currentExpression}=7</span>)</div> |
|
|
</div> |
|
|
</div>`; |
|
|
throw analysis; |
|
|
} |
|
|
throw analysis; |
|
|
} |
|
|
|
|
|
|
|
|
if (expr.includes('**') && expr.split('**')[1].match(/\d{3,}/)) { |
|
|
console.warn("Warning: Very large exponent may cause performance issues"); |
|
|
} |
|
|
|
|
|
let result; |
|
|
try { |
|
|
result = eval(expr); |
|
|
if (typeof result !== 'number' || isNaN(result)) { |
|
|
throw "Calculation did not produce a valid number"; |
|
|
} |
|
|
|
|
|
|
|
|
if (Math.abs(result) > 1e100) { |
|
|
console.warn("Warning: Result is extremely large - may be inaccurate"); |
|
|
} |
|
|
} catch (e) { |
|
|
throw `Calculation error: ${e.message || e}`; |
|
|
} |
|
|
document.getElementById('result').textContent = formatResult(result); |
|
|
document.getElementById('error').textContent = ''; |
|
|
|
|
|
|
|
|
addToHistory(currentExpression, result); |
|
|
|
|
|
} catch (error) { |
|
|
if (typeof error === 'string' && error.startsWith('<div')) { |
|
|
document.getElementById('error').innerHTML = error; |
|
|
} else { |
|
|
document.getElementById('error').innerHTML = ` |
|
|
<div class="analysis-box"> |
|
|
<div class="flex items-center text-red-500 mb-2"> |
|
|
<i class="fas fa-exclamation-circle mr-2"></i> |
|
|
<div class="font-medium">SymPy Calculation Error</div> |
|
|
</div> |
|
|
<div class="text-sm mb-3">${error}</div> |
|
|
<div class="text-xs mono bg-gray-100 p-2 rounded mb-3">${sympyCode}</div> |
|
|
<div class="analysis-tip bg-red-50 p-3 rounded-lg"> |
|
|
<div class="flex items-center text-red-600 font-medium mb-1"> |
|
|
<i class="fas fa-lightbulb mr-2"></i> |
|
|
<div>SymPy Suggestions</div> |
|
|
</div> |
|
|
<ul class="list-disc pl-5 space-y-1 text-sm"> |
|
|
${error.includes('variables') ? '<li>Use symbols() to define variables first</li>' : ''} |
|
|
${error.includes('solve') ? '<li>Ensure equations are properly formatted</li>' : ''} |
|
|
<li>Use simplify() for complex expressions</li> |
|
|
<li>Check SymPy documentation for function syntax</li> |
|
|
</ul> |
|
|
</div> |
|
|
</div> |
|
|
`; |
|
|
} |
|
|
document.getElementById('result').textContent = ''; |
|
|
console.error("Calculation error:", error); |
|
|
} finally { |
|
|
loadingEl.classList.add('hidden'); |
|
|
} |
|
|
}, 10); |
|
|
} |
|
|
|
|
|
function formatResult(num) { |
|
|
if (num === undefined || num === null) return ''; |
|
|
if (!Number.isFinite(num)) { |
|
|
return num > 0 ? "Infinity" : "-Infinity"; |
|
|
} |
|
|
if (Number.isInteger(num)) return num; |
|
|
|
|
|
|
|
|
if (Math.abs(num) > 1e12 || (Math.abs(num) < 1e-4 && num !== 0)) { |
|
|
return num.toExponential(6).replace(/(\.\d*?[1-9])0+e/, '$1e'); |
|
|
} |
|
|
|
|
|
|
|
|
const str = num.toFixed(8); |
|
|
if (str.indexOf('.') !== -1) { |
|
|
return str.replace(/\.?0+$/, ''); |
|
|
} |
|
|
return str; |
|
|
} |
|
|
|
|
|
function addToHistory(expression, result, steps = null, sympyCode = null) { |
|
|
const historyItem = { |
|
|
expression, |
|
|
result, |
|
|
steps, |
|
|
sympyCode, |
|
|
timestamp: new Date().toISOString(), |
|
|
id: Date.now().toString() |
|
|
}; |
|
|
|
|
|
history.unshift(historyItem); |
|
|
|
|
|
|
|
|
if (history.length > 20) history.pop(); |
|
|
|
|
|
localStorage.setItem('calcHistory', JSON.stringify(history)); |
|
|
renderHistory(); |
|
|
return historyItem; |
|
|
} |
|
|
|
|
|
function renderHistory() { |
|
|
const historyList = document.getElementById('historyList'); |
|
|
historyList.innerHTML = ''; |
|
|
|
|
|
history.forEach((item, index) => { |
|
|
const historyItem = document.createElement('div'); |
|
|
historyItem.className = 'p-3 history-item fade-in'; |
|
|
historyItem.innerHTML = ` |
|
|
<div class="flex justify-between items-start"> |
|
|
<div> |
|
|
<div class="text-xs text-gray-400">${new Date(item.timestamp).toLocaleString()}</div> |
|
|
<div class="font-mono text-gray-700">${item.expression}</div> |
|
|
</div> |
|
|
<div class="font-medium text-indigo-600">= ${formatResult(item.result)}</div> |
|
|
</div> |
|
|
`; |
|
|
|
|
|
historyItem.addEventListener('click', () => { |
|
|
currentExpression = item.expression; |
|
|
updateScreen(); |
|
|
document.getElementById('result').textContent = formatResult(item.result); |
|
|
}); |
|
|
|
|
|
historyList.appendChild(historyItem); |
|
|
}); |
|
|
} |
|
|
|
|
|
function clearHistory() { |
|
|
history = []; |
|
|
localStorage.setItem('calcHistory', JSON.stringify(history)); |
|
|
renderHistory(); |
|
|
} |
|
|
|
|
|
function toggleHistoryPanel() { |
|
|
const panel = document.getElementById('historyPanel'); |
|
|
panel.classList.toggle('hidden'); |
|
|
} |
|
|
|
|
|
function analyzeExpression() { |
|
|
const input = document.getElementById('formulaInput'); |
|
|
const expr = input.value.trim(); |
|
|
if (!expr) return; |
|
|
|
|
|
let analysis = ''; |
|
|
|
|
|
|
|
|
analysis += `<div class="analysis-box"> |
|
|
<div class="flex items-center mb-3"> |
|
|
<i class="fas fa-search text-indigo-500 mr-2"></i> |
|
|
<div class="analysis-title text-lg font-semibold text-gray-800">Expression Analysis</div> |
|
|
</div> |
|
|
<div class="grid grid-cols-2 gap-3 mb-3"> |
|
|
<div class="bg-white p-2 rounded border border-gray-100"> |
|
|
<div class="text-xs text-gray-500">Type</div> |
|
|
<div class="font-medium">${expr.includes('=') ? 'Equation' : 'Expression'}</div> |
|
|
</div> |
|
|
<div class="bg-white p-2 rounded border border-gray-100"> |
|
|
<div class="text-xs text-gray-500">Length</div> |
|
|
<div class="font-medium">${expr.length} chars</div> |
|
|
</div> |
|
|
</div>`; |
|
|
|
|
|
|
|
|
const variables = [...new Set(expr.match(/[a-zA-Z]/g) || [])]; |
|
|
const operators = [...new Set(expr.match(/[\+\-\*\/\^]/g) || [])]; |
|
|
|
|
|
if (variables.length > 0 || operators.length > 0) { |
|
|
analysis += `<div class="grid grid-cols-2 gap-3 mb-3">`; |
|
|
if (variables.length > 0) { |
|
|
analysis += `<div class="bg-white p-2 rounded border border-gray-100"> |
|
|
<div class="text-xs text-gray-500">Variables</div> |
|
|
<div class="font-mono text-indigo-600">${variables.join(', ')}</div> |
|
|
</div>`; |
|
|
} |
|
|
if (operators.length > 0) { |
|
|
analysis += `<div class="bg-white p-2 rounded border border-gray-100"> |
|
|
<div class="text-xs text-gray-500">Operators</div> |
|
|
<div class="font-mono text-indigo-600">${operators.map(op => { |
|
|
if (op === '*') return '×'; |
|
|
if (op === '/') return '÷'; |
|
|
return op; |
|
|
}).join(', ')}</div> |
|
|
</div>`; |
|
|
} |
|
|
analysis += `</div>`; |
|
|
} |
|
|
|
|
|
|
|
|
const functions = [...new Set(expr.match(/(sin|cos|tan|ln|log|sqrt)\(/g) || [])]; |
|
|
if (functions.length > 0) { |
|
|
analysis += `<div class="analysis-item"> |
|
|
<span class="font-medium">Functions:</span> |
|
|
<span class="font-mono bg-gray-100 px-1 rounded">${functions.join('</span>, <span class="font-mono bg-gray-100 px-1 rounded">')}</span> |
|
|
</div>`; |
|
|
} |
|
|
|
|
|
|
|
|
const constants = [...new Set(expr.match(/(π|e)/g) || [])]; |
|
|
if (constants.length > 0) { |
|
|
analysis += `<div class="analysis-item"> |
|
|
<span class="font-medium">Constants:</span> |
|
|
<span class="font-mono bg-gray-100 px-1 rounded">${constants.join('</span>, <span class="font-mono bg-gray-100 px-1 rounded">')}</span> |
|
|
</div>`; |
|
|
} |
|
|
|
|
|
|
|
|
const openParens = (expr.match(/\(/g) || []).length; |
|
|
const closeParens = (expr.match(/\)/g) || []).length; |
|
|
if (openParens !== closeParens) { |
|
|
analysis += `<div class="analysis-item text-red-500"> |
|
|
<i class="fas fa-exclamation-triangle mr-1"></i> |
|
|
Unbalanced parentheses (${openParens} open, ${closeParens} close) |
|
|
</div>`; |
|
|
} |
|
|
|
|
|
|
|
|
if (expr.match(/\/\s*0/g)) { |
|
|
analysis += `<div class="analysis-item text-red-500"> |
|
|
<i class="fas fa-exclamation-triangle mr-1"></i> |
|
|
Potential division by zero detected |
|
|
</div>`; |
|
|
} |
|
|
if (expr.match(/\^\-?\d+/g)) { |
|
|
analysis += `<div class="analysis-item"> |
|
|
<i class="fas fa-info-circle text-blue-500 mr-1"></i> |
|
|
Contains exponentiation operations |
|
|
</div>`; |
|
|
} |
|
|
if (expr.match(/\!/g)) { |
|
|
analysis += `<div class="analysis-item"> |
|
|
<i class="fas fa-info-circle text-blue-500 mr-1"></i> |
|
|
Contains factorial operations |
|
|
</div>`; |
|
|
} |
|
|
|
|
|
|
|
|
if (expr.includes('=')) { |
|
|
const parts = expr.split('='); |
|
|
if (parts.length === 2) { |
|
|
const left = parts[0].trim(); |
|
|
const right = parts[1].trim(); |
|
|
|
|
|
analysis += `<div class="analysis-item mt-3"> |
|
|
<div class="font-medium">Equation Analysis:</div> |
|
|
<div class="grid grid-cols-2 gap-2 mt-2"> |
|
|
<div class="bg-gray-50 p-2 rounded"> |
|
|
<div class="text-xs text-gray-500">Left Side</div> |
|
|
<div class="font-mono">${left}</div> |
|
|
</div> |
|
|
<div class="bg-gray-50 p-2 rounded"> |
|
|
<div class="text-xs text-gray-500">Right Side</div> |
|
|
<div class="font-mono">${right}</div> |
|
|
</div> |
|
|
</div> |
|
|
</div>`; |
|
|
|
|
|
if (variables.length > 0) { |
|
|
analysis += `<div class="analysis-item"> |
|
|
<div class="font-medium">Solution Strategy:</div> |
|
|
<div class="mt-1 text-sm"> |
|
|
${variables.length === 1 ? |
|
|
`To solve for ${variables[0]}, isolate the variable on one side` : |
|
|
`Need ${variables.length} independent equations to solve for ${variables.join(', ')}`} |
|
|
</div> |
|
|
</div>`; |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
analysis += `<div class="analysis-tip bg-indigo-50 p-3 rounded-lg mt-4"> |
|
|
<div class="flex items-center text-indigo-600 font-medium mb-2"> |
|
|
<i class="fas fa-lightbulb mr-2"></i> |
|
|
<div>Quick Tips</div> |
|
|
</div> |
|
|
<ul class="list-disc pl-5 space-y-1 text-sm"> |
|
|
<li>Use '=' to create equations (e.g. 2x+3=7)</li> |
|
|
<li>Assign values to variables (e.g. x=5) before evaluation</li> |
|
|
<li>Press 'Calculate' or Enter to evaluate</li> |
|
|
${variables.length > 0 ? `<li>For ${variables.length > 1 ? 'systems of equations' : 'equations'}, provide ${variables.length} equation${variables.length > 1 ? 's' : ''}</li>` : ''} |
|
|
${functions.length > 0 ? `<li>Trigonometric functions use radians by default</li>` : ''} |
|
|
<li>Use parentheses to control operation order</li> |
|
|
</ul> |
|
|
</div></div>`; |
|
|
|
|
|
document.getElementById('error').innerHTML = analysis; |
|
|
document.getElementById('result').textContent = ''; |
|
|
} |
|
|
</script> |
|
|
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=shism/symcalc" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> |
|
|
</html> |