Proyecto: Operaciones de la calculadora
Clase 5 de 610 min de lectura
Tienes una calculadora preciosa que sabe escribir números… y nada más. Hoy le enseñamos lo único que se le pide a una calculadora: calcular.
Vas a implementar tres cosas:
- Los botones de operaciones (+, -, ×, ÷).
- El botón igual (=) para obtener el resultado.
- La validación de errores, como la división por cero.
El estado que necesitamos#
Para operar hay que tener memoria. Piensa en 5 + 3: cuando escribes el 3, la calculadora tiene que acordarse del 5 y de que querías sumar. Amplía las variables al principio de calculadora.js:
// Elementos del DOMconst displayActual = document.getElementById('operacion-actual')const displayAnterior = document.getElementById('operacion-anterior')// Estado de la calculadoralet numeroActual = '0'let numeroAnterior = ''let operador = nulllet debeReset = falseAdemás de numeroAnterior, hay dos variables nuevas:
operadorguarda la operación elegida (+,-,×o÷).debeResetes una bandera que indica si el próximo número debe empezar de cero.
¿Para qué sirve esa bandera? Cuando pulsas 5 y +, el display sigue mostrando 5. Si ahora pulsas 3, quieres que reemplace al 5, no que se pegue detrás y aparezca 53.
Refactorizamos con una función de actualizar display#
Vamos a actualizar las dos líneas del display desde muchos sitios, así que mejor tener una única función que lo haga:
function actualizarDisplay() { displayActual.textContent = numeroActual displayAnterior.textContent = numeroAnterior}Ahora saca la lógica de añadir un número a su propia función. Es la misma de la clase anterior, con una novedad al principio: mirar la bandera debeReset.
function agregarNumero(numero) { if (debeReset) { numeroActual = '0' debeReset = false } if (numero === '.' && numeroActual.includes('.')) return if (numeroActual === '0' && numero !== '.') { numeroActual = numero } else { numeroActual += numero } actualizarDisplay()}Con eso, los listeners de los botones numéricos se quedan en una sola línea útil:
document.querySelectorAll('.btn-numero').forEach(boton => { boton.addEventListener('click', () => { agregarNumero(boton.dataset.numero) })})Implementar la selección de operador#
Cuando el usuario pulsa un operador, hay que hacer tres cosas:
- Guardar el número actual como «número anterior».
- Guardar qué operación ha elegido.
- Preparar el display para recibir el segundo número.
function seleccionarOperador(op) { // Si ya hay una operación pendiente, calcularla primero if (operador && !debeReset) { calcular() } operador = op numeroAnterior = numeroActual + ' ' + op debeReset = true actualizarDisplay()}¿Y ese if (operador && !debeReset) del principio? Cubre el caso de quien encadena operaciones. Si pulsas 5 + 3 + sin pasar por el igual, primero se calcula 5 + 3 = 8 y después se prepara la siguiente operación con ese 8.
Solo queda conectar los botones, igual que hicimos con los numéricos:
document.querySelectorAll('.btn-operador').forEach(boton => { boton.addEventListener('click', () => { seleccionarOperador(boton.dataset.operador) })})La función de calcular#
Este es el corazón de la calculadora. Coge los dos números, aplica la operación y muestra el resultado. Es la función más larga del proyecto, pero léela de arriba abajo: comprobar, convertir, operar y guardar.
function calcular() { if (!operador || debeReset) return const anterior = parseFloat(numeroAnterior) const actual = parseFloat(numeroActual) if (isNaN(anterior) || isNaN(actual)) return let resultado try { switch (operador) { case '+': resultado = anterior + actual break case '-': resultado = anterior - actual break case '×': resultado = anterior * actual break case '÷': if (actual === 0) { throw new Error('No se puede dividir por cero') } resultado = anterior / actual break default: return } // Redondear para evitar errores de punto flotante resultado = Math.round((resultado + Number.EPSILON) * 100000000) / 100000000 numeroActual = resultado.toString() numeroAnterior = '' operador = null debeReset = true } catch (error) { mostrarError(error.message) return } actualizarDisplay()}Lo importante, por partes:
parseFloatconvierte los strings en números para poder operar. Ya no queremos concatenar.switchejecuta una operación u otra según el operador guardado.- La división por cero lanza un error con
throw, que recoge elcatchde más abajo. - El redondeo del final evita un clásico de JavaScript:
0.1 + 0.20.30000000000000004Nadie quiere ver eso en una calculadora.
debeReset estaba a true, así que empieza un número nuevo: '2', no '82'.Mostrar errores de forma amigable#
Cuando algo falla, como al dividir por cero, se lo contamos al usuario con un mensaje temporal:
function mostrarError(mensaje) { displayActual.textContent = 'Error' displayAnterior.textContent = mensaje setTimeout(() => { limpiar() }, 2000)}El display muestra Error y, en la línea de arriba, el motivo. El setTimeout espera 2 segundos y después llama a limpiar() para dejar la calculadora lista otra vez.
Actualizar los botones clear, delete e igual#
Falta adaptar los botones de la clase anterior al nuevo estado. Limpiar ahora implica reiniciar las cuatro variables, así que se merece su propia función:
function limpiar() { numeroActual = '0' numeroAnterior = '' operador = null debeReset = false actualizarDisplay()}document.getElementById('clear').addEventListener('click', limpiar)document.getElementById('delete').addEventListener('click', () => { if (numeroActual.length > 1) { numeroActual = numeroActual.slice(0, -1) } else { numeroActual = '0' } actualizarDisplay()})document.getElementById('igual').addEventListener('click', calcular)Fíjate en que a addEventListener le pasas limpiar y calcular sin paréntesis. Le entregas la función para que la ejecute él cuando llegue el clic.
Prueba la calculadora#
Abre index.html en el navegador y haz estas pruebas:
| Pulsas | Debe pasar |
|---|---|
8 + 2 = | Muestra 10 |
10 ÷ 3 = | Muestra el resultado redondeado |
5 ÷ 0 = | Muestra Error y el mensaje «No se puede dividir por cero» |
5 + 3 + 2 = | Encadena: primero 5 + 3 = 8, luego 8 + 2 = 10 |
Código completo hasta aquí#
El calculadora.js entero, para que compares con el tuyo:
// Elementos del DOMconst displayActual = document.getElementById('operacion-actual')const displayAnterior = document.getElementById('operacion-anterior')// Estadolet numeroActual = '0'let numeroAnterior = ''let operador = nulllet debeReset = falsefunction actualizarDisplay() { displayActual.textContent = numeroActual displayAnterior.textContent = numeroAnterior}function agregarNumero(numero) { if (debeReset) { numeroActual = '0' debeReset = false } if (numero === '.' && numeroActual.includes('.')) return if (numeroActual === '0' && numero !== '.') { numeroActual = numero } else { numeroActual += numero } actualizarDisplay()}function seleccionarOperador(op) { if (operador && !debeReset) calcular() operador = op numeroAnterior = numeroActual + ' ' + op debeReset = true actualizarDisplay()}function calcular() { if (!operador || debeReset) return const anterior = parseFloat(numeroAnterior) const actual = parseFloat(numeroActual) if (isNaN(anterior) || isNaN(actual)) return let resultado try { switch (operador) { case '+': resultado = anterior + actual; break case '-': resultado = anterior - actual; break case '×': resultado = anterior * actual; break case '÷': if (actual === 0) throw new Error('No se puede dividir por cero') resultado = anterior / actual break default: return } resultado = Math.round((resultado + Number.EPSILON) * 100000000) / 100000000 numeroActual = resultado.toString() numeroAnterior = '' operador = null debeReset = true } catch (error) { mostrarError(error.message) return } actualizarDisplay()}function mostrarError(mensaje) { displayActual.textContent = 'Error' displayAnterior.textContent = mensaje setTimeout(() => limpiar(), 2000)}function limpiar() { numeroActual = '0' numeroAnterior = '' operador = null debeReset = false actualizarDisplay()}// Event listenersdocument.querySelectorAll('.btn-numero').forEach(boton => { boton.addEventListener('click', () => agregarNumero(boton.dataset.numero))})document.querySelectorAll('.btn-operador').forEach(boton => { boton.addEventListener('click', () => seleccionarOperador(boton.dataset.operador))})document.getElementById('igual').addEventListener('click', calcular)document.getElementById('clear').addEventListener('click', limpiar)document.getElementById('delete').addEventListener('click', () => { if (numeroActual.length > 1) { numeroActual = numeroActual.slice(0, -1) } else { numeroActual = '0' } actualizarDisplay()})En resumen
- El estado de la calculadora son cuatro variables:
numeroActual,numeroAnterior,operadorydebeReset. - Una única función,
actualizarDisplay(), pinta ese estado en pantalla. parseFloatconvierte los strings del display en números antes de operar.- Los errores se lanzan con
throwy se recogen contry/catchpara avisar al usuario.
Ya tienes una calculadora funcional. En la siguiente clase le ponemos soporte de teclado y un historial de operaciones.