Sesion Laboratorio 6 Practica 2 2 - jesusgpa/2023-2024-CSAAI GitHub Wiki

Sesión Laboratorio 6: Práctica 2-2

  • Tiempo: 2h
  • Fecha: Jueves 29 de Febrero de 2024
  • Objetivos de la sesión:
    • Practicar con ejemplos
    • Implementar una calculadora básica
    • Aprenderemos a utilizar una máquina de estados para implementar la calculadora y otras aplicaciones

Contenido

Introducción

Seguimos trabajando en la calculadora básica.

En esta sesión puedes encontrar las pistas y los elementos necesarios para implementar una versión simple de la calculadora.

Tu objetivo será entender bien lo que se está haciendo, y luego refinarlo y pulir los detalles.

Calculadora Versión 0

Como en cualquier reto de programación, existen infinitas soluciones para resolver el mismo problema.

En esta sección daremos algunas pinceladas de cómo se puede implementar una calculadora muy básica.

La función eval

Un enfoque sencillo para implementar la calculadora es usar la función eval().

Lo que hace es evaluar la expresión contenida en la cadena que se le pasa como parámetro y devuelve el número cálculo.

Así, si invocamos a la función eval() sobre la cadena "1+2", nos devuelve el 3.

Lo comprobamos en la consola javascript del navegador.

El problema de la calculadora se reduce ahora a ser capaces de generar una cadena con la expresión que queremos luego evaluar.

Calculadora de dos dígitos

Vamos a crear una calculadora mini con sólo dos dígitos: el 1 y el 2. Además añadimos la tecla de sumar (+), la tecla para calcular el resultado (=) y una para inicializar todo a 0 y comenzar una nueva expresión: en total 5 teclas.

Usaremos botones HTML para las teclas (etiqueta <button>).

Cada tecla tiene su propio identificador.

Este es el fichero HTML: calc0-01.html:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script src="calc0-01.js" defer></script>
    <title>Calculadora 0</title>
</head>
<body>
    <p id="display">0</p>
    <button type="button" id="boton1">1</button>
    <button type="button" id="boton2">2</button>
    <button type="button" id="suma">+</button>
    <br>
    <br>
    <button type="button" id="clear">AC</button>
    <button type="button" id="igual">=</button>
</body>
</html>

El programa en Javascript obtiene todos los elementos del interfaz: display + botones y establece las funciones de retrollamada para cada botón.

  • Fichero javacript: calc0-01.js
console.log("Ejecutando JS...");


//-- Elementos de la interfaz de la calculadora
display = document.getElementById("display")
boton1 = document.getElementById("boton1")
boton2 = document.getElementById("boton2")
suma = document.getElementById("suma")
igual = document.getElementById("igual")
clear = document.getElementById("clear")

//-- Funciones de retrollamada de los botones
//-- Cada vez que se aprieta un botón se actúa
//-- sobre la cadena: añadiendo dígito, operador +
//-- poniendo a cero o evaluando la expresión

// -- Insertar dígito 1
boton1.onclick = () => {
  display.innerHTML += "1";
}

//-- Insertar dígito 2
boton2.onclick = () => {
  display.innerHTML += "2";
}

//-- Insertar símbolo de sumar
suma.onclick = () => {
  display.innerHTML += "+";
}

//-- Evaluar la expresión
igual.onclick = () => {
  display.innerHTML = eval(display.innerHTML);
}

//-- Poner a cero la expresión
clear.onclick = () => {
  display.innerHTML = "0";
}

Al apretar las teclas 1,2 y suma, se insertan en el párrafo display cada uno de sus símbolos correspondientes: el 1, el 2 ó el "+".

Se utiliza el operador "+=" para añadirlos al final.

El botón de igual es el que llama a la función de evaluación, leyendo la expresión insertada en el párrafo y colocando el resultado.

Y finalmente el botón de AC lo que hace es eliminar la expresión que hubiese sustituyéndolo por el carácter "0"

En esta animación vemos el funcionamiento:

Todavía hay que pulir muchos detalles, pero con muy poquitas líneas de código hemos conseguido tener algo que empieza a comportarse como una calculadora 🙂

Usando el atributo value de los botones

Los botones de HTML tienen un atributo que se llama value.

Su función es asignar un valor a cada botón.

Este valor lo podemos leer en cualquier momento.

Lo vamos a utilizar para dar un valor a cada tecla.

Así, las teclas numéricas tendrán un valor igual a su número.

La tecla de la suma tendrá un valor igual a "+".

Modificamos el HTML:

  • Fichero calc0-02.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script src="calc0-02.js" defer></script>
    <title>Calculadora 0.2</title>
</head>
<body>
    <p id="display">0</p>
    <button type="button" id="boton1" value="1">1</button>
    <button type="button" id="boton2" value="2">2</button>
    <button type="button" id="suma" value="+">+</button>
    <br>
    <br>
    <button type="button" id="clear">AC</button>
    <button type="button" id="igual">=</button>
</body>
</html>

Y este es el nuevo programa en Javascript.

Es muy parecido al anterior.

  • Fichero calc0-02.j2
console.log("Ejecutando JS...");


//-- Elementos de la interfaz de la calculadora
display = document.getElementById("display")
boton1 = document.getElementById("boton1")
boton2 = document.getElementById("boton2")
suma = document.getElementById("suma")
igual = document.getElementById("igual")
clear = document.getElementById("clear")

//-- Funciones de retrollamada de los botones
//-- Cada vez que se aprieta un botón se actúa
//-- sobre la cadena: añadiendo dígito, operador +
//-- poniendo a cero o evaluando la expresión

// -- Insertar digito 1
boton1.onclick = () => {
  display.innerHTML += boton1.value;
}

//-- Insertar digito 2
boton2.onclick = () => {
  display.innerHTML += boton2.value;
}

//-- Insertar simbolo de sumar
suma.onclick = () => {
  display.innerHTML += suma.value;
}

//-- Evaluar la expresión
igual.onclick = () => {
  display.innerHTML = eval(display.innerHTML);
}

//-- Poner a cero la expresión
clear.onclick = () => {
  display.innerHTML = "0";
}

Ahora no tenemos "valores mágicos" para las funciones de retrollamada de los botones, sino que el valor a introducir en el párrafo display se lee directamente del valor del botón.

Es una pequeña mejora, pero que nos permitirá simplificar la configuración de las retrollamadas en las mejoras siguientes.

Leyendo el elemento target de los eventos

Hasta ahora no lo hemos usado, pero cuando ocurre el evento de click, el navegador prepara un objeto llamado ev que contiene información sobre ese evento.

En concreto, la propiedad ev.target contiene el elemento html sobre el que se ha hecho click.

Esto nos permite unificar el código javascript un poquito más, haciendo que la implementación de las retrollamadas de los botones 1, 2 y "+" sea exactamente igual:

// -- Digito 1
boton1.onclick = (ev) => {
  display.innerHTML += ev.target.value;
}

//-- Digito 2
boton2.onclick = (ev) => {
  display.innerHTML += ev.target.value;
}

//-- Simbolo de sumar
suma.onclick = (ev) => {
  display.innerHTML += ev.target.value;
}

Agrupando botones en la misma clase

Si agrupamos en una misma clase todos los botones que son dígitos, podremos obtener un Array de todos ellos, y lo podremos recorrer con un bucle.

De esta forma podemos asignar sus funciones de retrollamada de forma más compacta.

En el HTML metemos a los botones 1 y 2 en dentro de la clase dígito:

  <button type="button" id="boton1" value="1" class="digito">1</button>
  <button type="button" id="boton2" value="2" class="digito">2</button>

Y en el programa javascript usamos el método getElementsByClassName() para obtener una colección de todos los botones de tipo dígito:

console.log("Ejecutando JS...");

display = document.getElementById("display")
suma = document.getElementById("suma")
igual = document.getElementById("igual")
clear = document.getElementById("clear")

//-- Obtener una colección con todos los elementos
//-- de la clase dígito
digitos = document.getElementsByClassName("digito")

//-- Establecer la misma función de retrollamada
//-- para todos los botones de tipo dígito
for (let boton of digitos) {

    //-- Se ejecuta cuando se pulsa un boton
    //-- que es un dígito
    boton.onclick = (ev) => {
        display.innerHTML += ev.target.value;
        console.log("DIGITO!!!");
    }
}

//-------- Resto de funciones de retrollamada

//-- Insertar simbolo de sumar
suma.onclick = (ev) => {
  display.innerHTML += ev.target.value;
}

//-- Evaluar la expresión
igual.onclick = () => {
  display.innerHTML = eval(display.innerHTML);
}

//-- Poner a cero la expresión
clear.onclick = () => {
  display.innerHTML = "0";
}

Como en este ejemplo sólo tenemos 2 botones de dígito, no se nota mucho la diferencia.

Pero ahora este código nos vale para cualquier número de dígitos (10 para decimal, y 16 para hexadecimal).

Esto mismo lo podemos hacer con los botones de tipo operación: suma, resta, producto y división.

Los agrupamos en una clase y asignamos sus funciones de retrollamada con un bucle.

Calculadora Versión 1: Estados

En la calculadora no sólo es importante qué tecla se aprieta, sino cuándo se aprieta.

El orden importa mucho.

Por ejemplo, tras el reset (botón AC) hay que introducir un número.

No sería válido introducir un operador. En este estado inicial, las entradas de operadores se deben ignorar.

También la entrada de la tecla "=", porque no hay nada que calcular todavía.

Una forma de resolver este tipo de problemas es usando una máquina de estados.

Hay un estado inicial y para poder realizar el cálculo hay que llegar al estado final.

Entre medias hay una serie de estado posibles y sus transiciones.

Sólo se pasa de un estado a otro cuando se reciben entradas (en nuestro ejemplo pulsaciones de teclas).

Así, por ejemplo, si estamos en el estado inicial y llega un dígito, cambiamos al estado Operando 1: en el que tenemos una expresión que tiene un dígito del primer operando.

Esto lo dibujamos en un diagrama de estados.

Los diagramas de estado no son únicos: a cada uno se le puede ocurrir uno diferentes.

Este sería un ejemplo de uno que tiene 4 estados.

El significado es el siguiente. Inicialmente la calculadora está en el estado INIT.

En este estado sólo procesa dígitos.

Si llega algo que no es un dígito: se ignora o se informa de error.

Sólo en caso de que llegue un dígito pasamos al estado OP1.

En este estado pueden llegar un dígito o bien un operador.

Si llega un dígito lo procesamos y nos quedamos en el mismo estado.

Pero si llega un operador significa que el primer operando ya ha sido introducido.

Tras la introducción del operador pasamos al estado OPERATION, donde sólo esperamos recibir un dígito (si se recibe otro operador sería erróneo, o la tecla igual).

El siguiente estado OP2 es el estado final.

Si llegamos aquí es que el usuario ha introducido el operando 1, el operador y al menos un dígitos del operando 2.

El usuario puede seguir introduciendo dígitos o bien pulsar la tecla igual.

Sólo en este estado es donde la expresión que tenemos es correcta, y por tanto se puede calcular.

En cualquier momento, si se pulsa la tecla AC, se vuelve al estado inicial.

¿Cómo se programa esto en Javascript? De muchas formas.

Será necesario tener una variable de estado, que contenga el estado actual.

Para determinarlo podemos usar constantes asignadas a números: así a cada estado le damos un número diferente.

//-- Estados de la calculadora
const ESTADO = {
  INIT: 0,
  OP1: 1,
  OPERATION: 2,
  OP2: 3,
}

Luego definimos nuestra variable de estado, que inicialmente deberá tener el valor del estado inicial:

//-- Variable de estado
//-- Por defecto su valor será el del estado inicial
let estado = ESTADO.INIT;

Necesitamos tener tres funciones de retrollamada: una para los dígitos, otra para los operandos y otra para la tecla igual, que calcula el resultado final.

Para cada una de ellas habrá que comprobar el estado de la calculadora y determinar qué operaciones se pueden o no se pueden hacer.

Este sería un fragmento de la función de retrollamada de cuando llega un dígito:

//-- Ha llegado un dígito
function number(num)
{
  //-- Segun el estado hacemos una cosa u otra
  if (estado == ESTADO.INIT) {
    display.innerHTML = num;
    estado = ESTADO.OP1;
  } 
  // .......... Resto del código
}

Ejemplo: Acción especial en el estado inicial

Como ejemplo de un programa que usa estados vamos a hacer que no aparezca el "0" inicial al introducir el primer dígito.

Cuando arranca la culadora en el display está el valor "0".

Sin embargo, al introducir el primer dígito, como se añade al display, lo que nos aparece es "01".

El primer dígito introducido es por tanto especial.

En vez de añadirlo hay que asignarlo directamente a display.innerHTML.

En este código se muestra cómo está solucionado usando la máquina de estados:

console.log("Ejecutando JS...");

display = document.getElementById("display")
suma = document.getElementById("suma")
igual = document.getElementById("igual")
clear = document.getElementById("clear")

//-- Estados de la calculadora
const ESTADO = {
    INIT: 0,
    OP1: 1,
    OPERATION: 2,
    OP2: 3
}
 
 //-- Variable de estado de la calculadora
 //-- Al comenzar estamos en el estado inicial
 let estado = ESTADO.INIT;   

//-- Función de retrollamada de los dígitos
function digito(ev)
{
    //-- Se ha recibido un dígito
    //-- Según en qué estado se encuentre la calculadora
    //-- se hará una cosa u otra

    //-- Si es el primer dígito, no lo añadimos,
    //-- sino que lo mostramos directamente en el display
    if (estado == ESTADO.INIT) {

        display.innerHTML = ev.target.value;

        //-- Pasar al siguiente estado
        estado = ESTADO.OP1;

    } else {
       
        //--En cualquier otro estado lo añadimos
        display.innerHTML += ev.target.value;

        //-- Y nos quedamos en el mismo estado
        //-- Ojo! Este ejemplo sólo implementa el primer
        //-- estado del diagrama. Habría que tener en 
        //-- cuenta el resto... lo debes hacer en tu práctica
    } 
    
}


//-- Obtener una colección con todos los elementos
//-- de la clase dígito
digitos = document.getElementsByClassName("digito")

//-- Establecer la misma función de retrollamada
//-- para todos los botones de tipo dígito
for (let boton of digitos) {

    //-- Se ejecuta cuando se pulsa un botón
    //-- que es un dígito. Para que el código sea 
    //-- mas legible la función de retrollamada se
    //-- escribe como una función normal (digito)
    boton.onclick = digito;
}

//-------- Resto de funciones de retrollamada

//-- Operación de sumar
suma.onclick = (ev) => {

    //-- Insertar simbolo de sumar
    display.innerHTML += ev.target.value;

    //-- ¡Ojo! Aquí se inserta el + siempre!
    //-- Para que la calculadora funcione bien
    //-- sólo se debe permitir insertar el operador
    //-- en el estado OP1, y debe cambiar el estado
    //-- a OPERATION (según el diagrama de estados)
  
}

//-- Evaluar la expresión
igual.onclick = () => {
  
    //-- Calcular la expresión y añadirla al display
    display.innerHTML = eval(display.innerHTML);

    //-- ¡Ojo! Aquí se hace siempre!
    //-- Sólo se debe permitir que eso se haga
    //-- si se está en el estado final (OP2)
  
}

//-- Poner a cero la expresión
//-- Y volver al estado inicial
clear.onclick = () => {
  display.innerHTML = "0";
  estado = ESTADO.INIT;
}

Para que el código sea más legible, se usa la función dígito como función de callback de los dígitos, en lugar de utilizar su versión compacta.

En esta función, según el estado en el que se encuentre la calculadora, el dígito recibido se procesa de una forma u otra:

Si es el primer dígito se asigna a display.innerHTML.

Si NO es el primer dígito entonces se añade, manteniendo lo que antes hubiese en el display

En cualquier momento, si se aprieta la tecla AC, se muestra un 0 en el display y se vuelve al estado inicial

En esta animación se muestra el funcionamiento.

Práctica recomendada

  • Haz los ejemplos del 1 al 9 de la sesión de Teoría: S5: Javascript II. Súbelos al repo en P2/S5
  • Haz los ejemplos mostrados en esta sesión del laboratorio. Súbelos al repo en P2/L6

Autor

Jesús Parrado Alameda (jesusgpa)

Creditos

Licencia

Enlaces

⚠️ **GitHub.com Fallback** ⚠️