Saltar al contenido principal
11DOM y Eventos

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 = false

Además de numeroAnterior, hay dos variables nuevas:

  • operador guarda la operación elegida (+, -, × o ÷).
  • debeReset es 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:

  1. Guardar el número actual como «número anterior».
  2. Guardar qué operación ha elegido.
  3. 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:

  • parseFloat convierte los strings en números para poder operar. Ya no queremos concatenar.
  • switch ejecuta una operación u otra según el operador guardado.
  • La división por cero lanza un error con throw, que recoge el catch de más abajo.
  • El redondeo del final evita un clásico de JavaScript:
Consola
Escribes:0.1 + 0.2
Devuelve:0.30000000000000004

Nadie quiere ver eso en una calculadora.

Infografía
El estado de la calculadora en una operación
5.Pulsas 2: tras el resultado debeReset estaba a true, así que empieza un número nuevo: '2', no '82'.
Paso 5 de 5

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:

PulsasDebe 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, operador y debeReset.
  • Una única función, actualizarDisplay(), pinta ese estado en pantalla.
  • parseFloat convierte los strings del display en números antes de operar.
  • Los errores se lanzan con throw y se recogen con try/catch para avisar al usuario.

Ya tienes una calculadora funcional. En la siguiente clase le ponemos soporte de teclado y un historial de operaciones.