Quantcast
Channel: Adictos al trabajo
Viewing all 989 articles
Browse latest View live

Conexión HTTP cliente-servidor con TypeScript

$
0
0

Antes de empezar recordar que este tutorial forma parte de una cadena de tutoriales en las que pretendo simplemente probar tecnologías actuales. Aquí abajo podrás encontrarlos :

 

También  me gustaría que vieras este video de 2 minutos para entender dónde estamos y a dónde vamos


Mi equipo es Mac

macOS Catalina
Versión 10.15.2

MacBook Pro (15-inch, 2018)
Procesador 2,9 GHz Intel Core i9 de 6 núcleos
Memoria 32 GB 2400 MHz DDR4
Gráficos Intel UHD Graphics 630 1536 MB


Si queremos hacer un juego y mostrar/guardar los mejores resultados de los jugadores tendremos que almacenarlos en un servidor central.

Hay muchos modos de hacerlo y vamos a probar ahora el más sencillo (que no es el definitivo pero hemos dicho que el objetivo es aprender las bases), que es que el servidor http nos retorne una respuesta JSON y que el cliente, nuestro programa TypeScript, lo muestre por pantalla.

Lo primero que vamos a aprender a hacer es una petición HTTP en TypeScript básica.

Para ello tenemos que poder instalar los comandos http de node/JavaScript por los que con npm instalamos. Esta es la secuencia que voy a ejecutar.

npm i @types/node

npm install acorn

npm audit fix

 

Realmente quería instalar @types para tener acceso a los tipos básicos.

Al leer el log he visto que daba errores y requería la instalación manual de acorn.

Al instalar el último paquete he visto que había vulnerabilidades y he ejecutado fix.

Esta es la salida de la pantalla de la secuencia.

Hay un dicho “si veo lejos es porque me apoyo en hombres de gigantes” (creo que de Newton) así que buscando en internet he encontrado este interesante tutorial, que usa servicios de la NASA, para ver cómo hacer la comunicación cliente/servidor.

https://www.twilio.com/blog/2017/08/http-requests-in-node-js.html.

Vamos a usar ese servicio para jugar.

Simplemente vamos a probar (corrigiendo errores simples porque faltan tipos) y podemos ver la respuesta de la NASA.

Funciona, pero de momento no me es demasiado satisfactorio, porque voy a tratar de usar librerías y usar clases que me hagan todo más sencillo para hacerlo un poco más TypeScript.

Seguiremos este guion:

– Vamos a aprender a hacer peticiones al servidor sin programar, usando programas que nos lo faciliten y monitorizando lo que pasa, incluso que nos generen el código.

– Luego vamos a crear objetos TypeScript y vincularles la respuesta JSON obtenida de un modo asíncrono.

– Finalmente veremos un modo alternativo de conectarnos sincronamente (con fetch y promesas).

Empezamos con programas para hacer peticiones. Un herramienta muy fácil de usar para hacer peticiones HTTP es https://reqbin.com/.

Podemos ver mejor la respuesta que nos ha dado la NASA.

{

    «date»: «2020-01-27»,

    «explanation»: «Where do comet tails come from?  There are no obvious places on the nuclei of comets from which the jets that create comet tails emanate.  One of the best images of emerging jets is shown in the featured picture, taken in 2015 by ESA’s robotic Rosetta spacecraft that orbited Comet 67P/Churyumov-Gerasimenko (Comet CG) from 2014 to 2016.  The picture shows plumes of gas and dust escaping numerous places from Comet CG’s nucleus as it neared the Sun and heated up.  The comet has two prominent lobes, the larger one spanning about 4 kilometers, and a smaller 2.5-kilometer lobe connected by a narrow neck. Analyses indicate that evaporation must be taking place well inside the comet’s surface to create the jets of dust and ice that we see emitted through the surface.  Comet CG (also known as Comet 67P) loses in jets about a meter of radius during each of its 6.44-year orbits around the Sun, a rate at which will completely destroy the comet in only thousands of years. In 2016, Rosetta’s mission ended with a controlled impact onto Comet CG’s surface.   Outreach Astronomers: Future APOD writers sought.»,

    «hdurl»: «https://apod.nasa.gov/apod/image/2001/Comet67P_Rosetta_1024.jpg»,

    «media_type»: «image»,

    «service_version»: «v1»,

    «title»: «Comet CG Evaporates»,

    «url»: «https://apod.nasa.gov/apod/image/2001/Comet67P_Rosetta_1024.jpg»

}

Pero también podemos hacer otro tipo de peticiones.

De este modo sencillo separamos dos problemas, la programación de front y de back y simplemente negociamos la interfaz (luego nos tendríamos que preocupar de la seguridad).

Otra herramienta que podemos utilizar para el mismo propósito, bastante más avanzada, es Postman https://www.getpostman.com/

Yo investigaría un poco más sobre las capacidades de esta herramienta a la hora de probar y construir juegos de pruebas de APIs (ahí lo dejo).

En mi caso, como sólo estoy haciendo un tutorial voy a usar la versión experimental de PostMan:  https://www.getpostman.com/downloads/canary

Creamos nuestra cuenta o nos logamos con una existente.

Al contestar el correo electrónico de confirmación de cuenta de correo nos salta a la página Web. Aprovechad y navegad un poco por las APIs que encontrareis.

Si pincháis en API Evangelist podréis encontrar algunas para jugar, como la de la NASA:

Dentro de estas 4 APIs disponible y la última es la de la NASA.

Decimos de ejecutarla en Postman local, el programa que hemos instalado.

Este es el aspecto de la primera pantalla de Postman.

Si vamos a la lengüeta de Collections encontramos la que hemos descargado.

Ahora podemos ver todas las posibles peticiones (79) y vemos que nos genera una URL (incompleta, ya que falta la clave para usar el API que proporciona el servidor de la misma).

Vamos a la página Web de la NASA ya por curiosidad y vemos la descripción de la API en https://api.nasa.gov/

Podéis registrar vuestra clave o seguir usando la de demo, tal como explica en las instrucciones. Es interesante que leáis sobre las limitaciones de uso.

Cambiamos la URL para poner la Key de Demo y damos enviar (SEND).

La respuesta podemos comprobar que es un documento JSON.

Mirad que buena gente que hasta te crea el código (SNIPPETS), en distintos lenguajes, que necesitas para hacer la invocación.

Este es nuestro código generado por la herramienta:

var request = require('request');

var options = {

  'method': 'GET',

  'url': 'https://api.nasa.gov/planetary/apod?api_key=DEMO_KEY&date=2019-09-13&hd=true',

  'headers': {

  }

};

request(options, function (error, response) {

  if (error) throw new Error(error);

  console.log(response.body);

});

Lo podemos ejecutar en nuestro proyecto base sin cambiar nada.

Ahora vamos a modificar el código de nuestro proyecto y comprender algunos problemas con la ejecución asíncrona.

Paraos a mirar: hacemos la petición GET, se invoca una función y el programa sigue. Si retornamos el valor de propiedades de la clase no están todavía actualizadas.

Este es todo el código.

let request = require("request");

class GestorNoticiasNASA {
  peticion: any = null;
  respuesta: any = "todavía Nada";
  URL: string = "";

  constructor(URL: string) {
    this.URL = URL;
  }

  anima = (error: any, response: any) => {
    if (error) {
      throw new Error(error);
    } else {
      console.log("----------- Dentro de la función --------------");
      console.log(response.body);
      this.respEst = response.body;

      console.log("----------- Estructura --------------");
      console.log(this.respEst);
    }
  };

  recupera() {
    //creamos estructura
    let options = {
      method: "GET",
      url: this.URL,
      headers: {}
    };

    this.peticion = request(options, this.anima);
    console.log("----------- Despues de la función --------------");
    console.log(this.respuesta);
  }
}

let gestorNASA = new GestorNoticiasNASA(
  "https://api.nasa.gov/planetary/apod?api_key=DEMO_KEY&date=2019-09-13&hd=true"
);

gestorNASA.recupera();
console.log("----------- Fuera de la función--------------");
console.log(gestorNASA.respuesta);

for (var i = 0; i < 5; i++) {
  setTimeout(function() {
    console.log(new Date()); //It's you code
  }, (i + i + 1) * 1000);
}

console.log("-----------final del retraso asincrono --------------");
console.log(gestorNASA.respuesta);

Fijaros en los mensajes de log.

Esto no será relevante si actualizamos elementos de la página Web que no requieran comportamientos síncronos.

Vamos a hacer un pequeño cambio para ver como poder manipular la respuesta como un objeto TypeScript, mediante un interfaz, y el comando JSON.parse(string).

Usaremos la interfaz para filtrar los campos que queremos recuperar.

Fijaros en esta línea en concreto:       console.log(objeto.copyright);

Este es el código de nuestro proyecto.

let request = require("request");

interface INoticiaNasa {
  copyright: string;
  date: string;
  explanation: string;
  hdurl: string;
  media_type: string;
  service_version: string;
  title: string;
  url: string;
}

class GestorNoticiasNASA {
  peticion: any = null;
  URL: string = "";

  constructor(URL: string) {
    this.URL = URL;
  }

  anima = (error: any, response: any) => {
    if (error) {
      throw new Error(error);
    } else {
      let objeto: INoticiaNasa = JSON.parse(response.body);
      console.log(objeto.copyright);
    }
  };

  recupera() {
    let options = { method: "GET", url: this.URL, headers: {} };

    this.peticion = request(options, this.anima);
  }
}

let gestorNASA = new GestorNoticiasNASA(
  "https://api.nasa.gov/planetary/apod?api_key=DEMO_KEY&date=2019-09-13&hd=true"
);

gestorNASA.recupera();

Si queremos comportamientos síncronos tendremos que utilizar otros sistemas. El API de fetch, compatible con muchos navegadores, combinado con una técnica llamada promesas, nos permite el comportamiento que deseamos.

Os invito a visitar este enlace. https://www.todojs.com/api-fetch-el-nuevo-estandar-que-permite-hacer-llamadas-http/

El código JavaScript para utilizar fetch es muy sencillo.

Compilamos y lanzamos con Parcel.

Y al ejecutar podemos ver volcada la respuesta en la pantalla y podríamos tener el comportamiento síncrono en caso de desearlo.

No vamos a avanzar más con el ejemplo porque ahora nos interesa más FireBase.

 

La entrada Conexión HTTP cliente-servidor con TypeScript se publicó primero en Adictos al trabajo.


Consumer Driven Contract: testeando servicios con Pact.

$
0
0

Índice

¿Qué son los Consumer Driven Contract?

Es un patrón que nos permite testear y comprobar las diferentes interacciones entre los diferentes servicios de nuestro ecosistema y sus clientes.

En estos tests existen un mínimo de dos actores, un consumidor, que puede ser el cliente, y un proveedor, que será el servicio en sí mismo. El consumidor capturará sus expectativas respecto al proveedor y lo almacenará en un fichero que llamará ‘contrato’, y es lo que usará para realizar los tests. Un ‘contrato’ sería la definición de la forma en que va a comportarse nuestro servicio en base a una solicitud. Es decir, nosotros vamos a definir que el cliente necesitará por ejemplo, los campos nombre, apellido, y edad, por tanto lo que se va a comprobar es que esto campos le siguen llegando, independientemente de que lleguen otros campos que quizás utilicen o no otros clientes, de esta forma aunque el servicio se haya actualizado con nuevos campos, sabemos que los clientes que utilicen la versión anterior siguen teniendo soporte y que no hemos ‘roto’ nada.

Ventajas de usar este patrón

Cuando queremos probar una aplicación que se comunica con otros servicios o APIs, tenemos dos opciones:

– Desplegar todos los servicios y realizar un test E2E.

– Realizar un mock de los servicios y probar la aplicación de forma individual mediante test unitarios.

Vamos a analizar las dos opciones, vamos con la primera, tests end-to-end.

Ventajas:

– Se simula el entorno de producción.

– Se testea una comunicación real con los servicios.

Desventajas:

– Nos puede llevar mucho tiempo desplegar todos los servicios cada vez que queramos testear la aplicación.

– Introduce dependencias

– Feedback muy lento

– Muy frágil

Ahora la segunda opción, tests unitarios:

Ventajas:

– Poco tiempo de ejecución

– No requiere de infraestructura

Desventajas:

– Los servicios serían mocks que quizás no tuvieran nada que ver con la realidad

– Podrías pasar todos los test, ir a producción y fallar

En general es muy útil cuando tenemos arquitecturas basadas en microservicios, algo que está muy de moda ahora mismo.

¿Qué es Pact?

Pact es un framework de código abierto que nos permite implementar consumer driven contract de una manera simple y rápida. Tiene una comunidad bastante activa y soporta múltiples lenguajes. Puedes acceder a documentación mediante este enlace.

Logo Pact

A su vez, este framework, incluye una herramienta llamada Pact Broker, que nos permitirá compartir los contratos de forma remota.

Ejemplo con Pact y Spring Boot.

Para este ejemplo vamos a crear dos aplicaciones, una será el consumidor y otra el proveedor. Consistirá en una aplicación que devolverá los datos de un usuario cuando se le haga una petición. Empezamos creando un proyecto Maven que tenga 2 módulos: Consumer y Provider. 

En el pom.xml del proyecto ‘padre’, añadiremos las dependencias necesarias:

  • spring-boot-starter-web
  • lombok
  • spring-boot-starter-test
  • pact-jvm-consumer-java8_2.12
  • pact-jvm-provider
  • pact-jvm-consumer
  • pact-jvm-provider-spring

Y como ‘parent’, spring-boot-starter-parent.

Usaremos también la librería de Lombok, esto es opcional, es simplemente para ahorrarnos código en algunas clases. Una vez tenemos esto listo, empezaremos por el módulo del consumidor.

Consumidor

Empezamos creando la aplicación que será el consumidor, dado que será la que genere el ‘contrato’, para ello creamos un nuevo proyecto Maven. Con la siguiente estructura:

  • src
    • main
      • java
        • ConsumerApplication.java
        • User.java
    • test
      • java
        • ConsumerContractTest.java

Creamos nuestra clase modelo, User.class:

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class User {
    private String name;
    private String city;
    private int age;
}

Mediante las etiquetas @Getter y @Setter, Lombok nos genera los ‘getter’ y ‘setter’.

Ahora, la clase desde donde vamos a ‘consumir’ la API, ConsumerApplication.class:

public class ConsumerApplication {

    private final RestTemplate restTemplate;

    public ConsumerApplication(@Value("${user-service.base-url}") String baseUrl) {
        this.restTemplate = new RestTemplateBuilder().rootUri(baseUrl).build();
    }

    public User getUser (String id) {
        return restTemplate.getForObject("/users/" + id, User.class);
    }
}

Ahora vamos a crear la clase de los test que van a generar los ‘contratos’, vamos a llamarla ConsumerContractTest:

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE,
properties = "user-service.base-url:http://localhost:8080",
classes = ConsumerApplication.class)
public class ConsumerContractTest {

Añadimos esta regla para configurar nuestro ‘mock server’, el valor a null sería el host, en este caso al ser ‘localhost’ no es necesario especificarlo.

  @Rule
    public PactProviderRuleMk2 provider = new PactProviderRuleMk2("Provider", null, 8080, this);
    @Autowired
    private ConsumerApplication consumerApplication;

Aquí le indicamos que inicie el ‘mock server’, con la interacción definida en el método con el nombre ‘pactUserExists’.

    @PactVerification(fragment = "pactUserExists")
    @Test
    public void userExists() {
        User user = consumerApplication.getUser("1");
        assertEquals(user.getName(), "Pepe");
    }

Por último, en este método vamos a definir las interacciones, como vemos hemos hecho un ejemplo con un usuario de nombre Pepe, que vive en la ciudad de Madrid y tiene 22 años.

@Pact(consumer = "Consumer")
    public RequestResponsePact pactUserExists(PactDslWithProvider builder) {
        return builder.given("User 1 exists")
                .uponReceiving("A request to /users/1")
                .path("/users/1")
                .method("GET")
                .willRespondWith()
                .status(200)
                .body(LambdaDsl.newJsonBody(o -> o
                    .stringType("name", "Pepe")
                    .stringType("city", "Madrid")
                    .numberType("age", 22)
                    ).build())
                .toPact();
    }
}

Si ejecutamos la clase ConsumerContractTest, nos generará un fichero como el siguiente:
{
    "provider": {
        "name": "Provider"
    },
    "consumer": {
        "name": "Consumer"
    },
    "interactions": [
        {
            "description": "A request to /users/1",
            "request": {
                "method": "GET",
                "path": "/users/1"
            },
            "response": {
                "status": 200,
                "headers": {
                    "Content-Type": "application/json; charset=UTF-8"
                },
                "body": {
                    "age": 22,
                    "city": "Madrid",
                    "name": "Pepe"
                },
                "matchingRules": {
                    "body": {
                        "$.name": {
                            "matchers": [
                                {
                                    "match": "type"
                                }
                            ],
                            "combine": "AND"
                        },
                        "$.city": {
                            "matchers": [
                                {
                                    "match": "type"
                                }
                            ],
                            "combine": "AND"
                        },
                        "$.age": {
                            "matchers": [
                                {
                                    "match": "number"
                                }
                            ],
                            "combine": "AND"
                        }
                    },
                    "header": {
                        "Content-Type": {
                            "matchers": [
                                {
                                    "match": "regex",
                                    "regex": "application/json;\\s?charset=(utf|UTF)-8"
                                }
                            ],
                            "combine": "AND"
                        }
                    }
                }
            },
            "providerStates": [
                {
                    "name": "User 1 exists"
                }
            ]
        }
    ...

Este será nuestro ‘contrato’, con el que vamos a verificar siempre el servicio, nos lo genera en la carpeta target/pacts/<archivo>.json, ahora este fichero lo pegamos en nuestro proveedor, dentro de la carpeta src/pacts/<archivo>.json, y con esto estaría todo configurado por parte de nuestro consumidor. Ahora vamos con el proveedor.

Proveedor

Creamos un nuevo proyecto Maven, que tendrá la siguiente estructura:

  • src
    • main
      • java
        • producer
          • User.java
          • UserController.java
          • UserService.java
          • UserServiceApplication.java
    • pacts
      • <archivo>.json
    • test
      • java
        • ContractTest.java

La clase User será nuestra clase modelo al igual que en el consumidor, pero a esta le añadiremos las etiquetas @Data y @Builder de Lombok.

La clase UserController, controlará las peticiones:

@RestController
public class UserController {
    private final UserService userService;

    public UserController (UserService userService) {
        this.userService = userService;
    }

    @GetMapping("/users/{userId}")
    public User getUser(@PathVariable String userId) {
        return userService.findUser(userId);
    }
}

Ahora creamos nuestra clase UserService, que simulará devolver un objeto de tipo User:

@Service
public class UserService {
    public User findUser(String userId) {
        return User.builder()
                .name("Paco")
                .city("Madrid")
                .age(22)
                .build();
    }
}

Los valores no tienen que coincidir con el ejemplo que hemos creado anteriormente, debido a que el test de contrato solo va a comprobar que los campos que utiliza el consumidor son los que mínimamente ofrece el proveedor.

Y por último nuestra clase UserServiceApplication, que iniciará la aplicación:

@SpringBootApplication
public class UserServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(UserServiceApplication.class, args);
    }
}

Y ahora, ya solo nos queda crear nuestra clase de test, que la llamaremos ContractTest:

@RunWith(SpringRestPactRunner.class)
@Provider("Provider")
@PactFolder("src/pacts")
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = UserServiceApplication.class)
public class ContractTest {
    @TestTarget
    public final Target target = new SpringBootHttpTarget();

    @State("User 1 exists")
    public void user1Exists() {
    }
}

Con la etiqueta @PactFolder le indicamos dónde encontrar el ‘contrato’ necesario, en la etiqueta @State colocamos el mismo nombre que tenemos dentro del ‘contrato’ en providerStates, para saber qué está testeando. Ahora ejecutamos la clase test y debería darnos ok, podemos jugar y cambiar el nombre de los campos o eliminar alguno para ver como falla, si añadimos uno más no fallará, debido a que seguirá enviando los mínimos que necesita el consumidor. Os dejo el código en Github.

Conclusiones

Como vemos, implementar los test de contrato mediante el framework de Pact es bastante sencillo, y nos permite asegurarnos de que nuestro consumidor proveedor se entienden a la perfección.

La entrada Consumer Driven Contract: testeando servicios con Pact. se publicó primero en Adictos al trabajo.

Consultando Json en columna de bases de datos relacionales. Haciendo consultas a Json en Oracle

$
0
0

Consultando Json en columna de bases de datos relacionales. Haciendo consultas a Json en Oracle

Estamos acostumbrados a pensar en modelos relacionales para nuestros sistemas y en el camino tenemos el problema de diccionarios y estructuras que no queremos guardar en una columna hasta que pensamos que sería ideal guardar y consultar datos no relacionales y que mantengan una estructura variable como Json.

Esta pregunta en la mayoría de casos se responde en bases de datos no relacionales y a su vez en estructuras Json para explotar sus múltiples ventajas y sencillez.

Pero no existe solamente esta respuesta. También podemos lograrlo desde nuestro contexto relacional.

Hoy les traemos una implementación de persistencia y consulta de Json en columnas de Bases de Datos Relacionales de Oracle.

Esta es una serie de artículos que ejemplifican cómo utilizar Json en Bases de datos Oracle, Mysql y Postgres.

A partir de la versión Oracle Database 12.1.0.2 brinda soporte para persistencia de Json dentro de bases de datos relacionales.

Es importante aclarar que en Oracle no existe un tipo Json nativo pero estas versiones incorporaron soporte a partir de funciones nativas.

Para este artículo utilizamos como ejemplo el siguiente modelo.

Debemos implementar un sistema que permita guardar información relativa a una Persona y los registros de las Pesadas que se realiza la misma en balanzas para darle seguimiento a un plan de dieta saludable.

Teniendo esta información vamos a necesitar consultar como va el plan de dieta de cierta Persona y su progreso a partir de la información de las pesadas. Además necesitamos el peso promedio de las personas asociadas al sistema.

Model uml

 

Las pesadas tienen la siguiente información.

[
  {
    "id": 1,
    "balanza": "bal1",
    "peso": 80
  },
  {
    "id": 2,
    "balanza": "bal1",
    "peso": 78
  },
  {
    "id": 3,
    "balanza": "bal2",
    "peso": 77
  }
]

 

Primero vamos a crear la consulta para persistir este modelo.

En la creación de la tabla podemos notar que Pesadas es CLOB. Recordando que Oracle no tiene tipo de dato nativo para Json, normalmente debemos utilizar un tipo de dato que permita almacenar gran volumen de datos por eso elegimos CLOB.

CREATE TABLE PERSONA
(
  ID NUMBER NOT NULL
, NOMBRE VARCHAR2(20) NOT NULL
, APELLIDO VARCHAR2(20) NOT NULL
, USUARIO VARCHAR2(20) NOT NULL
, EMAIL VARCHAR2(50) NOT NULL
, PESADAS CLOB
, CONSTRAINT PERSONA_PK PRIMARY KEY  (ID) ENABLE
);

 

Luego, debemos crear una CONSTRAINT para que Oracle valide PESADAS como un Json. Esto impedirá que tengamos en el campo PESADAS json no válidos.

ALTER TABLE PERSONA ADD CONSTRAINT PC
CHECK  (PESADAS is JSON)  ENABLE;

 

Ya tenemos el modelo en Oracle.
Insertemos datos de prueba:

ALTER TABLE PERSONA ADD CONSTRAINT PC
CHECK  (PESADAS is JSON)  ENABLE;
```

>Ya tenemos el modelo en Oracle.
 Insertemos datos de prueba:

```sql
INSERT INTO PERSONA (ID,NOMBRE,APELLIDO,USUARIO,PESADAS,EMAIL) values (1,'alam','brito','alambrito'
,
'{"id": 1,"balanza": "bal1","pesoActual": 77 ,"pesadas":
[{
"fecha": "2020-01-01",
"peso": 80
}, {
"fecha": "2020-02-01",
"peso": 77
}
]}'
,'alambrito@cip.com');

Insert into PERSONA (ID,NOMBRE,APELLIDO,USUARIO,PESADAS,EMAIL) values (2,'Abran','la puerta','abranlapuerta'
,
'{"id": 1,"balanza": "bal1","pesoActual": 100,"pesadas":
[{
"fecha": "2020-01-01",
"peso": 120
}, {
"fecha": "2020-06-01",
"peso": 100}
]}'
,'abranlapu@cip.com');

 

Ahora vamos a consultar como va el plan de dieta de “Alam” y su progreso a partir de la información de las pesadas.

SELECT pesos FROM persona ,
JSON_TABLE(pesadas, '$.pesadas[*]'COLUMNS("pesos" NUMBER (10) PATH '$.peso')) pesos
WHERE NOMBRE = 'alam';

Resultado:

PESOS
----------
        80
        77

 

En esta consulta hemos utilizado la función JSON_TABLE para consultar valores de un arreglo dentro de un Json.

Podríamos además consultar el peso promedio de las personas que acuden al sistema.

SELECT  avg(JSON_VALUE(pesadas, '$.pesoActual' RETURNING NUMBER)) as promedio FROM persona;

 

Resultado

PROMEDIO
----------
      88.5

 

En la consulta que vemos, utilizamos la función JSON_VALUE para obtener un elemento del Json y luego le hacemos avg para el promedio.

SELECT pesos FROM SYSTEM.persona , 
JSON_TABLE(pesadas, '$.pesadas[*]'COLUMNS("pesos" NUMBER (10) PATH '$.peso')) pesos 
WHERE NOMBRE = 'alam'

 

Ventajas

  1. A partir de la versión Oracle 12.1.0.2 podemos consultar Json en base de datos relacionales sin utilizar otros tipos de almacenamiento.
  2. Utilizando las funciones de Oracle para Json, las consultas son rápidas y emulan comportamientos relacionales.
  3. Las consultas SQL a Json son muy similares a las de otros motores como Mysql o Postgres.
  4. Los campos internos de Json son indexables para ganar en performance.

Desventajas

  1. Las versiones anteriores de Oracle 12.1.0.2 no cuentan con todo el potencial descrito.
  2. Las funciones de Oracle para Json necesitan un estudio previo para su utilización.
  3. Las funciones de Json en base de datos relacionales varían para Oracle, Mysql o Postgres.
  4. Algunos framework no brindan soporte para funciones de Json, por lo que se deben ejecutar consultas nativas.

Conclusiones

En el presente artículo hemos mostrado cómo crear y consultar campos Json en una base de datos relacionales Oracle. A partir de esto tenemos la oportunidad de darle mayor valor y protagonismo a nuestras bases de datos. En próximos artículos estaremos ejemplificando cómo mejorar la performance de las consultas Json e implementar y consultar Json en bases de datos Mysql o Postgres.

La entrada Consultando Json en columna de bases de datos relacionales. Haciendo consultas a Json en Oracle se publicó primero en Adictos al trabajo.

Creación de relaciones entre documentos de una base de datos MongoDB con Mongoose

$
0
0

En este tutorial con la ayuda de Mongoose aprenderemos a “crear relaciones” entre diferentes documentos extraídos de una base de datos NoSql cómo MongoDB.

Índice de contenidos

1. Introducción

Actualmente estoy desarrollando una pequeña API con NestJS y MongoDB. Uno de los “problemas” que me encontré fue a la hora de devolver objetos anidados.

Gracias a Mongoose esto no será un problema.

2. Entorno

El tutorial está escrito usando el siguiente entorno:

  • Hardware: Portátil MacBook Pro 15′ (2,7 Ghz Intel Core i7, 16GB DDR3).
  • Sistema Operativo: Mac OS Catalina 10.15.3
  • Entorno de desarrollo: Visual Studio Code
  • NestJS 6.14.2
  • TypeScript 3.7.4

3. ¿Que es mongoose?

Mongoose es una biblioteca que nos permite definir esquemas en nuestra API con la misma estructura que se está utilizando en nuestros documentos de MongoDB.

4. Instalación y configuración

Usando npm podemos instalarlo

npm i mongoose

Una vez instalado procedemos a definir nuestra conexión con la base de datos, en este caso estamos utilizando MongoDB.

Debemos importar en el app.module de nuestro proyecto el módulo de mongoose y asignarle la conexión a nuestra base de datos.

MongooseModule.forRoot('mongodb://localhost/mongodb_test')

Ahora debemos definir nuestros esquemas, en mi caso son dos: alumno y curso.

El esquema alumno será el encargado de tener la referencia al documento de curso.

  • Alumno

import * as mongoose from 'mongoose';
export const StudentSchema = new mongoose.Schema({
 name: String,
 surname: String,
 courses: [
   {
     type: mongoose.Schema.Types.ObjectId,
     ref: 'Course',
   },
 ],
});

En el esquema alumno podemos ver que tiene como atributo un array de cursos debido a que un alumno puede estar inscrito en diferentes cursos. Hacemos referencia a un objeto de tipo Course.

  • Curso

import * as mongoose from 'mongoose';

export const CourseSchema = new mongoose.Schema({
 name: String,
});

En el esquema curso tenemos simplemente su nombre.

No es necesario asignarles una id ya que Moongose se encarga de hacerlo.

4.1. Populate

Una vez realizados los esquemas ya podremos agregar en nuestro servicio de alumno, que es el encargado de realizar la consulta en la base de datos, la llamada al método populate() que nos permitirá anidar el objeto alumno con sus respectivos cursos.

async getStudents(): Promise<Student[]> {
  return this.studentModel.find()
  .populate({ path: 'courses', model: 'Course' })
  .exec();
}

En el método encargado de recoger todos los alumnos le decimos que queremos poblar esa lista de alumnos con los datos del documento cursos de la base de datos, cuyo id del curso sea el mismo que aparece en el array de cursos de los alumnos. De esta manera es cómo Mongoose nos permite realizar esta relación.

4.2. Realizando la petición

Una vez definido nuestro endpoint en el controlador, podremos realizar la petición y ver el resultado.

[
  {
    "courses": [
      {
        "_id": "5e454efb7e322f0012fa11b5",
        "name": "curso_java"
       }],
    "_id": "5e454efb7e322f0012fa13h6",
    "name": "Autentia",
    "surname": "Real Business Solutions",
    "__v": 0
    }
]

4.3. Utilizando mongoose-autopopulate

Otra forma que tenemos de hacer este “populate” es con el plugin mongoose-autopopulate.

npm i mongoose-autopopulate

Una vez instalada, su uso es muy fácil, basta con agregar en nuestro esquema:

import * as mongoose from 'mongoose';

export const StudentSchema = new mongoose.Schema({
 name: String,
 surname: String,
 courses: [
   {
     type: mongoose.Schema.Types.ObjectId,
     ref: 'Course',
     autopopulate: true,
   },
 ],
});

StudentSchema.plugin(require('mongoose-autopopulate'));

Con esto ya no tendríamos qué hacer nada más.

5. Conclusiones

Trabajar con una base de datos como MongoDB por primera vez puede resultar raro, pero con la ayuda de Mongoose esta tarea se vuelve mucho más fácil.

6. Referencias

La entrada Creación de relaciones entre documentos de una base de datos MongoDB con Mongoose se publicó primero en Adictos al trabajo.

Dockeriza tu API – Explicado para principiantes

$
0
0

El propósito de este tutorial es aprender a dockerizar una aplicación realizada con NestJS y otra con SpringBoot, aunque se puede realizar con cualquier aplicación que desarrollemos.

Índice de contenidos

1. Introducción

Gracias a Docker podemos construir entornos sencillos, de manera muy fácil y que se pueden ejecutar en cualquier máquina sin importar el sistema operativo que tenga.

2. Entorno

El tutorial está escrito usando el siguiente entorno:

  • Hardware: Portátil MacBook Pro 15′ (2,7 Ghz Intel Core i7, 16GB DDR3).
  • Sistema Operativo: Mac OS Catalina 10.15.3
  • Entorno de desarrollo: Visual Studio Code
  • NestJS 6.14.2
  • TypeScript 3.7.4

3. ¿Qué es docker?

Por si no conoces docker te dejo por aquí la definición de wikipedia.

Docker es un proyecto de código abierto que automatiza el despliegue de aplicaciones dentro de contenedores de software, proporcionando una capa adicional de abstracción y automatización de virtualización de aplicaciones en múltiples sistemas operativos.

4. Creación de imagen

Para poder crear un contenedor con nuestra aplicación, la primera tarea que debemos hacer es crear una imagen de esta.

Gracias a un fichero llamado “Dockerfile” podremos crear nuestra imagen. En este fichero definiremos las configuraciones necesarias para la correcta creación de nuestra imagen.

4.1. Definición del fichero DockerFile de aplicación hecha con NestJS.

FROM node:10
WORKDIR /app
COPY ./package.json ./
RUN npm install
COPY . .
RUN npm run build
CMD ["npm", "run", "start:prod"]

Explicación de cada propiedad:

  • FROM: en este caso al ser una aplicación realizada con NestJS es necesario utilizar node. En esta ocasión utilizaremos la versión 10 de node.
  • WORKDIR: es el directorio interno del contenedor en el cual se copiará el proyecto
  • COPY: copia el package.json de nuestro proyecto para después realizar la instalación.
  • RUN: Realizamos la instalación de los paquetes
  • COPY: Copiamos todo el sistema de archivos
  • RUN: Construimos nuestra aplicación
  • CMD: Escribimos el comando con el que se ejecutará la aplicación dentro del contenedor.

4.2. Definición del fichero DockerFile de aplicación hecha con SpringBoot.

FROM demo/oracle-java:8
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} api.jar
ENTRYPOINT ["java","-jar","/api.jar"]

Explicación de cada propiedad:
  • FROM: en este caso al ser una aplicación realizada con SpringBoot es necesario utilizar java. En esta ocasión utilizaremos la versión 8 de java.
  • ARG: Definimos el nombre de variable que contendrá la referencia al path de nuestro .jar
  • COPY: copia el archivo .jar a nuestro contenedor y lo renombre a api.jar
  • ENTRYPOINT: definimos el comando que ejecutará nuestra aplicación dentro del contenedor.

4.3. Construcción de la imagen.

El fichero Dockerfile debe estar en la raíz de nuestro proyecto

En el terminal situándonos en la raíz del proyecto, ejecutamos en siguiente comando para crear la imagen de nuestra aplicación.

docker build -t [nombre_de_imagen] .

Una vez termine el proceso podremos comprobar que nuestra imagen se ha creado ejecutando el siguiente comando: 

docker image ls

5. Creación de contenedor

Para crear el contenedor lo haremos con la ayuda de docker-compose.

version: '3.3'
services:
 nest-api:
   image: nest-api #nombre de la imagen que hemos creado
   restart: always
   container_name: nest-api
   ports:
     - '3000:3000'

Y simplemente ejecutando:

docker-compose up -d

Tendremos nuestro contenedor creado.

6. Conclusiones

Dockerizar una aplicación nos facilitará el trabajo a la hora de realizar cualquier instalación o despliegue en diferentes sistemas.

7. Referencias

La entrada Dockeriza tu API – Explicado para principiantes se publicó primero en Adictos al trabajo.

Testing funcional con Puppeteer

$
0
0

En este tutorial veremos las funciones más básicas de la herramienta Puppeteer y la integraremos con Jest para crear una suite de testing funcional.

¿Qué es Puppeteer?

Puppeteer es una herramienta, desarrollada principalmente por Google, que nos permite utilizar un navegador Chromium/Chrome headless a través del protocolo DevTools. Se ejecuta sobre NodeJS y se instala a través de npm.

Headless
Un navegador headless se ejecuta dentro de un proceso del terminal. Por lo que no es necesario que haya un navegador «real» abierto.

DevTools
El protocolo que permite a Puppeteer instrumentar, inspeccionar y debuggear Chromium.
Este protocolo expone una API que permite utilizar el navegador desde fuera del mismo.

Gráfico que muestra la interacción entre Puppeteer, el protocolo Devtools, chrome, y una aplicación web

Puppeteer puede usarse para:

  • Automatización de procesos web.
  • Testing de extensiones de Chrome.
  • Testing funcional de aplicaciones web.

Este último será el foco de este tutorial.

Instalación

Puppeteer se instala como una dependencia en un proyecto de npm.

npm install puppeteer 
# o yarn add puppeteer

Hay que tener en cuenta que la librería viene empaquetada con un ejecutable de Chromium. Por lo que ocupa bastante espacio en el disco (~280 MB).

Si no queremos que se descargue el ejecutable, podemos instalar puppeteer-core, que solo incluye la librería de Puppeteer. En este caso deberemos especificar la ubicación del ejecutable que utilizará.

Uso de la API

En esta sección crearemos algunos scripts sencillos con Puppeteer.
Cada ejemplo estará en un archivo JavaScript que podemos ejecutar con Node:

node nombre_fichero.js argumento1 argument2

Creando un navegador

Todos los ejemplos tienen esta estructura:

// Importamos la librería de Puppeteer.
const puppeteer = require("puppeteer");

// Obtenemos el primer argumento del comando.
const [url] = process.argv.slice(2);

(async () => {
  // Lanzamos un nuevo navegador.
  const browser = await puppeteer.launch();
  // Abrimos una nueva página.
  const page = await browser.newPage();

  // Vamos a la URL.
  await page.goto(url);

  // Cerramos la página y el navegador.
  await page.close();
  await browser.close();
})();

Si ejecutamos este script con

node script.js https://google.com
 , veremos que aparentemente no hace nada. Esto es porque actualmente se está ejecutando en modo headless.

Ahora, si añadimos estos parámetros a la función de

launch
:

const browser = await puppeteer.launch({
  headless: false, // Especificamos que el navegador no es headless
  slowMo: 1000 // Añadimos un delay de 1 segundo entre cada comando.
});

Podemos ver el proceso en tiempo real y con un delay de un segundo entre comandos.

Obtener el título de la página

const puppeteer = require("puppeteer");

const [url] = process.argv.slice(2);

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();

  await page.goto(url);

  // "page" contiene muchas funciones
  // que nos permiten obtener información de la página.
  // Prueba alguna!
  const title = await page.title();
  console.log("Page title: " + title);

  await browser.close();
})();

Si ejecutamos este script con

node script.js https://google.com
  deberíamos tener esta respuesta:
Page title: Google

Modificando el Viewport

Hasta ahora, estamos utilizando el Viewport por defecto. Si queremos modificarlo para, digamos, simular un dispositivo móvil, podemos hacerlo con:

await page.setViewport({
  width: 1080,
  height: 1920
});

Esto daría al Viewport una resolución de 1080p.

También existe la posibilidad de sobrescribir el Viewport por defecto con:

const browser = await puppeteer.launch({
  defaultViewport: {
    width: 1080,
    height: 1920
  }
});

Y si queremos probar sobre un dispositivo específico, existe el objeto

devices
 dentro de

puppeteer
 que contiene muchos dispositivos:
puppeteer.devices["LG Optimus L70 landscape"]

Capturando la pantalla

Una vez en una página, podemos usar la función

screenshot
 para capturar la página en una imagen y guardarla en el disco:

await page.screenshot({ "/directorio/nombre_fichero.png", type: "png" });

evaluate()

La función

evaluate
 nos permite ejecutar código en el contexto de la página del navegador de Puppeteer.

Con este código obtenemos el HTML de la página:

await page.screenshot({ "/directorio/nombre_fichero.png", type: "png" });

Selectores

$
  y
$$
  funcionan como
document.querySelector
y
document.querysSelectorAll
  respectivamente. Nos devuelven la referencia de los elementos seleccionados.

Este código devuelve la referencia al primer enlace y las referencias a todos los parrafos respectivamente:

const firstLink = await page.$("a");
const allParagraphs = await page.$$("p");

$eval
  y
$$eval
se parecen a los anteriores, pero en lugar de devolver las referencias, las pasan como parámetro al callback provisto:

Este código devuelve el texto del primer enlace y todos los párrafos respectivamente.

const firstLinkText = await page.$eval("a", link => link.textContent);
const allParagraphsText = await page.$$eval("p", links =>
  links.map(link => link.textContent)
);

Click

Para hacer click sobre un elemento, tenemos varias opciones disponibles:

// La función click de page se encarga de seleccionar y clickar
await page.click("selector");

// Podemos seleccionar y clickar por separado
const element = await page.$("selector");
await element.click()

// También podemos utilizar click() en el contexto de la página
await page.$eval("selector", element => element.click());

Por lo general, la primera opción suele ser la más robusta.

Keyboard

En el siguiente ejemplo, se introducen las credenciales en un formulario de login:

// Seleccionamos del input de usuario
const username = await page.$("[name=username]");

// Se hace focus sobre el elemento
await username.focus();

// Simulamos el teclado
await page.keyboard.type("the user name");

// Pasamos el focus al siguiente elemento
await page.keyboard.press("Tab");

// Simulamos el teclado
await page.keyboard.type("the password");

// Pulsamos enter
await page.keyboard.press("Enter");

Si quieres aprender en profundidad sobre la API de Puppeteer, échale un vistazo a la documentación oficial (pptr.dev).

Integración con Jest

Para integrar Puppeteer con Jest, utilizaremos la librería jest-puppeteer que se encarga de conectar el navegador con la ejecución de los tests. Para instalar las dependencias:

npm install jest-puppeteer puppeteer jest
# o yarn add jest-puppeteer puppeteer jest

En la carpeta raíz del proyecto, creamos los archivos de configuración

jest.config.js
  y
jest-puppeteer.config.js
 :
module.exports = {
  verbose: true,
  preset: "jest-puppeteer"
};

module.exports = {
  launch: {
    dumpio: true,
    // Aquí podemos introducir los argumentos que normalmente irían en la función launch()
    slowMo: 1
  }
};

Y en el

package.json
  se configura:
// ...
"scripts": {
  "test": "jest"
}
// ...

jest-puppeteer
  se encarga de lanzar el navegador y la página por nosotros y expone un
browser
y
page
  global.

Nuestro primer test con Puppeteer probará que el título de

google.com
  es el correcto. En
google-title.test.js
:
describe("Google Title", () => {
  test("The title is 'Google'", async () => {
    const expected = "Google";
    await page.goto("https://google.com");

    const actual = await page.title();

    expect(actual).toEqual(expected);
  });
});

Si ahora ejecutamos los tests, veremos su resultado:

npm test

Cuando la suite de tests acaba, se nos presenta un resumen.

PASS  src/tests/google-title.test.js
  Google Title
    ✓ the title is 'Google' (12ms)
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        0.418s, estimated 1s

En este tutorial hemos visto las funciones básicas de Puppeteer y cómo integrarla con Jest para nuestros tests funcionales.

La entrada Testing funcional con Puppeteer se publicó primero en Adictos al trabajo.

WeWalk: Un bastón inteligente para ciegos

$
0
0

En esta review vamos a ver las características de un dispositivo que ha caído en nuestras manos. Se trata de WeWalk, un bastón inteligente para ciegos. Ha sido una suerte poder probarlo en Autentia, ya que es una de esas tecnologías que llaman bastante la atención, pero uno no sabe si son realmente efectivas. ¿Nos acompañáis en este vídeo?

 

La entrada WeWalk: Un bastón inteligente para ciegos se publicó primero en Adictos al trabajo.

Tecnologías del habla

$
0
0

 

Introducción

Las tecnologías del habla, son aquellas que buscan asimilar el habla humana (imperfectas), dando paso a los sistemas de diálogo hablado (SDH) que las usan como medio de comunicación con el usuario. En la actualidad existen diferentes tecnologías del habla, como lo son:

  • El reconocimiento automático del habla.
  • La síntesis de voz mejor conocida por “Texto a Voz”.
  • La autentificación del locutor.
  • La identificación del lenguaje.
  • El entendimiento del lenguaje natural, entre otras.

Para efectos del presente artículo se describen las tecnologías del habla más comúnmente empleadas por los sistemas de diálogo hablado como lo son “El reconocimiento automático del habla” y “La síntesis de voz”. De igual forma se describirán las limitaciones, errores y desafíos a los que se enfrentan estas tecnologías.

1. El Reconocimiento Automático del habla

El reconocimiento automático del habla (Automatic Speech Recognition: ASR) soportado por computadora, busca reconocer las palabras producidas por el usuario para realizar una alguna tarea específica. Mediante la información análoga de un canal de audio que percibe el habla humana, el audio es transferido al ASR para que este realice una transcripción digital de la señal y la interprete (ver [​1​] para mayor información sobre este proceso). Dicho de otra forma, la computadora capta el habla producida por el usuario y la analiza para procesarla y generar un resultado que se aproxime a lo hablado por el usuario. Para llevar a cabo el reconocimiento automático del habla, es necesario emplear un conjunto de técnicas que nos permitan obtener una aproximación fiable de la palabra o frase pronunciada por el usuario.

En la figura 1, se muestran los principales componentes de un sistema de reconocimiento automático del habla, en esta figura se aprecia que el habla obtenida del usuario es transformada en una señal digitalizada (normalmente dividida en frecuencias de voz de 10 a 20 milisegundos cada una) y analizada para obtener un conjunto de métricas que permitan la búsqueda de la palabra más adecuada. Esta búsqueda emplea modelos acústicos, léxicos y de lenguaje que se asocian al comportamiento de un lenguaje y son soportados por un conjunto de datos de voz previamente empleados para entrenamiento [​2​].

Figura 1. Componentes de un sistema típico de reconocimiento automático del habla.

Cabe hacer mención, que un ASR puede estar especializado para el trabajo con un solo usuario (dependencia del locutor) o para diferentes tipos de usuarios (independencia del locutor), estos sistemas ASRs son descritos a continuación.

1.1 Dependencia del locutor

En la dependencia del locutor se requiere de un proceso de entrenamiento con la voz del usuario final. Durante el entrenamiento el sistema solicita al usuario que haga la pronunciación de una serie de frases o palabras para un posterior almacenamiento de información auditiva, la cual es resultado del análisis de su voz. Una vez terminado el entrenamiento, el usuario puede hablar de manera libre, continúa y espontánea para que el reconocedor procese su habla y devuelva un resultado aproximado a las palabras que el usuario pronunció. Un ejemplo de sistemas populares que emplean dependencia del locutor son el sistema Dragon Dictate de la compañía Nuance. Estos sistemas permiten al usuario realizar el dictado de algún documento en las aplicaciones de MS Office o con las aplicaciones de un sistema operativo como lo es el escritorio de MS Windows.

Una evaluación comparativa entre ASRs comerciales en español como lo son ViaVoice de IBM, Dragon Naturally Speaking de Nuance, FreeSpeech de Phillips Speech Product y Voice Xpress Professional de L&H fue realizada por los laboratorios de PC Magazine (PC Labs). Sus resultados argumentan que el reconocimiento del habla con dependencia del locutor llega a alcanzar desde 95% hasta 98% de precisión en el proceso del reconocimiento del habla [​3​].

1.2 Independencia del locutor.

En la independencia del locutor, el ASR analiza el habla humana para reconocer las palabras dichas sin tener una especialización particular en alguna persona, es decir, puede reconocer el habla de diferentes personas. En este proceso no existe un entrenamiento especializado por cada usuario.

Un ejemplo de sistemas que emplean independencia del locutor son los servicios de reservaciones de aviones o trenes que hacen uso del reconocimiento de voz. Estos sistemas atienden a diferentes tipos de personas las cuales tienen diferentes formas o estilos de hablar ante el sistema.

Víctor Zue, director asociado de los laboratorios del MIT y considerado como uno de los pioneros en la investigación de las tecnologías del habla, argumenta: “El alto desempeño en el reconocimiento del habla independiente del locutor es posible. Los sistemas de reconocimiento del habla comerciales están ahora disponibles, pero la tasa de errores es aún más alta que 10 veces la de los humanos, aún para tareas simples” [​4​].

1.3 Errores en el reconocimiento automático del habla

El reconocimiento automático del habla no es perfecto y pueden ocurrir errores durante la realización de sus tareas. En el trabajo expuesto en [​1​]se muestran los diferentes tipos de errores que pueden ocurrir durante el proceso de reconocimiento del habla, estos errores se describen en la tabla 1.

Error

Descripción del error

Borrado

La palabra o frase es ignorada cuando la persona habló en voz baja y el reconocedor la trato como ruido proveniente del ambiente.

Inserción

El ruido proveniente del ambiente o un sonido producido por el usuario de manera no intencional como un suspiro, llegó a ser interpretado por el reconocedor como una palabra pronunciada.

Rechazo

El usuario habló pero el reconocedor no fue capaz de reconocer lo dicho por el usuario.

Sustitución

El reconocedor regresa un resultado diferente al que dijo el usuario.

Tabla 1. Errores comunes ocurridos en el proceso de reconocimiento automático del habla.

1.4 Limitaciones en el reconocimiento automático del habla

Las personas cuando hablamos cometemos errores, los reconocedores automáticos del habla comenten más, ya que este “intenta” hacer un reconocimiento de las palabras pronunciadas por el usuario. El desempeño de un reconocedor se define en función de la exactitud de sus resultados. Para conocer la exactitud del desempeño del reconocedor, a cada una de las palabras se les asocia un nivel de confianza respectivo. Este nivel de confianza es un indicativo de la probabilidad con que la palabra fue reconocida, por ejemplo un 83% de exactitud.

La siguiente lista muestra un conjunto de factores que intervienen en el desempeño de un reconocedor automático del habla [​1​].

      1. La exactitud en el reconocimiento del habla es normalmente mejor en ambientes donde no existe mucho ruido.
      2. La calidad del hardware empleado para captar el habla del usuario puede mejorar el proceso de reconocimiento del habla.
      3. Cuando los usuarios hablan de forma clara y concisa se obtienen mejores resultados.
      4. Cuando los sistemas emplean gramáticas simples (poco confusas y complejas) se obtienen mejores resultados.
      5. La acentuación de las palabras y la forma en que las personas hablan son un factor que influye durante el reconocimiento del habla.
      6. Las palabras que suenan acústicamente similar son más difíciles de distinguir.

Solo algunos de estos factores como 2 y 4 pueden ser solucionados ya sea en el desarrollo del sistema o en la elección de las herramientas de soporte como el hardware. Sin embargo, se debe considerar que existen factores humanos como la acentuación de las palabras, el estado de ánimo, entre otros, que afectan al desempeño de esta tecnología.

1.5 Desafíos en el reconocimiento automático del habla

La labor por mejorar el reconocimiento del habla automático es una tarea que requiere de un esfuerzo que afronte constantemente los desafíos que esta tecnología presenta para lograr un desempeño apropiado. La siguiente lista describe un conjunto de desafíos a los que se enfrenta esta tecnología del habla [​7​].

    1. Variabilidad de los patrones del habla: Diferentes personas hablan el mismo idioma y la pronunciación de las palabras difiere una de otra. Interpretar la variabilidad del habla de una persona a otra ha conducido al desarrollo de un análisis de patrones complejo. Comprender las diferentes pausas, las tasas del discurso y los cambios en el volumen se ha vuelto una tarea compleja y difícil.
    1. Poder de procesamiento: A mediados de los 80’s una nueva técnica conocida como Modelos de Markov Ocultos mejoraron la habilidad de asociar las relaciones entre palabras. Esto provocó una tecnología de cómputo intensivo que eventualmente condujo al desarrollo de poderosas aplicaciones que emplearon el reconocimiento del habla. El buen desempeño del reconocimiento del habla en tiempo real requiere mucho poder de procesamiento para los sistemas de diálogo hablado.
    2. Ruido de fondo: Dado que las personas hacen acceso a los sistemas de diálogo hablado desde un teléfono, el ruido de fondo como el viento, la música, los murmullos, entre otros, han sido un factor que afecta al desempeño del reconocedor.
    3. Reconocimiento del habla continua: los sistemas que necesitan procesar el habla continua requieren de un alto poder de procesamiento. Dado que el usuario habla de una manera natural y continua, es difícil distinguir qué sonidos están asociados a determinadas palabras.
    4. Gramáticas: El buen trabajo en el desarrollo de las gramáticas promueve un buen desempeño para el reconocedor automático del habla, sin embargo, existen problemas con las palabras que están fuera del contexto del reconocedor. En el trabajo realizado en [​6​] se muestran un conjunto de técnicas que afrontan este problema para el idioma en español.

Los desafíos presentados anteriormente nos proporcionan un enfoque de la necesidad de una investigación y trabajo arduo a realizar para mejorar el desempeño de esta tecnología del habla.

2. La síntesis de voz

La síntesis de voz es el proceso por el cual la computadora produce habla a partir de un texto conformado por un conjunto de caracteres, al proceso de síntesis de voz se le conoce como “Texto A Voz” (Text To Speech: TTS). Para generar la voz por parte de la computadora, se realiza un análisis minucioso del texto a sintetizar, esto se hace con el fin de generar una correcta pronunciación y acentuación adecuada de cada una de las palabras a reproducir de manera auditiva al usuario (prosodia). En la figura 2, se muestra la configuración estándar de un sistema TTS, en los sistemas que emplean variantes de este estándar, la síntesis ha logrado una calidad de habla buena, congruente, fácil de entender al usuario y de uso práctico para los fines que persigue la síntesis de voz [​2​].

Figura 2. Configuración estándar de un sistema TTS.

Para llevar a cabo el proceso de síntesis, es necesario realizar un procesamiento detallado del texto a sintetizar, en el trabajo descrito en [​7​] se detalla el proceso por el cual el texto llega a convertirse en voz:

  1. Análisis estructural: En este proceso se realiza un análisis del texto para determinar dónde inician y terminan las palabras o frases. En la mayoría de los lenguajes, la puntuación y formateo de datos son procesados mediante este análisis.
  2. Pre-procesamiento de texto: Este proceso involucra un análisis del texto para construcciones especiales del lenguaje como lo son los acrónimos, fechas, números, direcciones de correo, etc.
  3. Conversión texto a fonema: En este proceso cada palabra es convertida en un fonema, el cual es la unidad mínima y básica del sonido del habla.
  4. Análisis prosódico: Este análisis consiste en el procesamiento de la estructura de la oración, palabras y fonemas. Esto involucra el pausado, el ritmo, el tiempo, el énfasis con que las palabras deben ser pronunciadas.
  5. Producción: Finalmente, la información de los fonemas y la prosodia son usadas para producir el habla auditiva. Existen diferentes técnicas para producir habla como la concatenación de fragmentos de audio o mediante una síntesis formante, la cual emplea técnicas de procesamiento de señales teniendo como soporte el sonido de los fonemas y el efecto de la prosodia.

2.1 Errores en la síntesis de voz

Los errores de la síntesis de voz ocurren normalmente durante el proceso de síntesis. A continuación, se muestran algunos de los errores más comunes que pueden ocurrir durante el proceso de síntesis [​7​]:

    1. Análisis estructural: La puntuación y el formateo de los datos no son siempre consistentes. Por ejemplo, los puntos en la palabra “D.E.A.” pueden ser confundidos como el final de una oración.
    2. Pre-procesamiento de texto: No siempre es posible que el sintetizador reconozca todos los acrónimos, formatos de fechas, o direcciones de correo electrónico de manera fiable.
    3. Conversión texto a fonema: Los sintetizadores pueden generar fonemas a partir de palabras confusas las cuales no llegan a ser claras al usuario, ejemplo de estas son apellidos, nombres de personas o empresas, marcas de productos, entre otros.
    4. Análisis prosódico: Los sintetizadores no siempre pueden producir una oración clara y entendible al usuario. Por lo que es necesario considerar la existencia de palabras que necesiten énfasis, pausas entre palabras o la modificación de la velocidad del sintetizador al generar el habla.
    5. Producción: el habla generada por los sintetizadores normalmente llega a sonar artificial por lo que se llega a crear un ambiente de confusión y esfuerzo por entender la voz sintetizada por parte del usuario.

2.2 Limitantes en la síntesis de voz

La síntesis de voz aún requiere de un mayor trabajo e investigación. Existen limitantes que afectan su desempeño y calidad de audio. El trabajo expuesto en [​7​], se proporciona información sobre un conjunto de limitantes a los que se enfrenta la síntesis de voz, que de manera breve se describen aquí:

    1. Las voces sintetizadas no son lo suficientemente expresivas. Aún no pueden simular las emociones humanas, tales como la alegría, la tristeza, o el enojo.
    2. Actualmente las voces sintetizadas en su mayoría son masculinas o femeninas de personajes maduros. Por lo que la existencia de voces de niños, viejos o jóvenes es escasa.
    3. Es necesario realizar investigaciones que satisfagan el manejo de estilos formales e informales del habla así como las variaciones de los dialectos. Por lo que se requiere de un estudio detallado de estos estilos del lenguaje, así como de sus dialectos y modelos sociolingüísticos.

2.3 Desafíos en la síntesis de voz

La síntesis de voz aún no es perfecta y el audio con que se genera aún requiere de una mayor calidad. Actualmente existe la necesidad de un trabajo arduo para obtener una buena calidad de síntesis de voz en la prosodia. Los sintetizadores actuales llegan a ser muy artificiales en las voces que estos producen, y cuando las personas escuchan estos tipos de voces se distraen o se sorprenden al escucharlas, lo cual afecta la comprensión y percepción de la calidad del habla [​1​ y ​​8​].

3. Resumen

En el presente artículo se presentaron las tecnologías del habla haciendo un principal énfasis en sus errores, limitantes y desafíos a las que estas se enfrentan. Las tecnologías del habla, aún no son perfectas y requieren de un esfuerzo y trabajo arduo en investigación para mejorar el desempeño de estas. El empleo de estas tecnologías son de gran beneficio para la humanidad, ya que permiten que diferentes tipos de personas puedan acceder a diferentes tipos de recursos de una computadora, y de esta manera beneficiarse al incrementar su productividad personal.

4. Referencias

[ 1 ] Robert D. Rodman. ​“Computer Speech Technology”, Artech House Publishers. 1999. (pp 101, 140, 213).

[ 2 ] R. A. Cole, J. Mariani, H. Uszkoreit, A. Zaenen and V. Zue​. “Survey State of the Art in Human Language Technology”.​ Cambridge University Press, 1996.

[ 3 ] PC Labs, ​“Tecnología de reconocimiento de voz”. PC Magazine en español, Marzo del 2000. (página 81).

[ 4 ] Victor Zue, ​“Beyond Recognition, to Understanding”, Speech Technology Magazine, October November 1998.

5 ] Converse, ​“The Benefits of a Conversational Voice User Interface in a Voice Portal”​. International Engineering Consortium (IEC). 2002.

[ 6 ] Cuayahuitl, Kirschning y Serridge. ​“Técnicas para mejorar el reconocimiento de voz en presencia de habla fuera del vocabulario”. Universidad de las Américas Puebla, primavera del 2000.

[ 7 ] Sun Microsystems. ​“Java Speech Programmer Guide Version 1.0”,​ October 26 de 1998. (pp 9, 10, 11 y 16)

[ 8 ] Kéller, E., & Zéller-Keller, “Challenges and New Uses for Speech Synthesis”. Dundee, Scotland, August 2000.

Otras ligas de interés

Google’s SpecAugment achieves state-of-the-art speech recognition without a language

model.

https://venturebeat.com/2019/04/22/googles-specaugment-achieves-state-of-the-art-speech-recognition-without-a-language-model/

State-of-the-art Speech Recognition With Sequence-to-Sequence Models

State-of-the-art Speech Recognition With Sequence-to-Sequence Models

Amazon Alexa

https://developer.amazon.com/en-US/alexa/alexa-skills-kit/asr

La entrada Tecnologías del habla se publicó primero en Adictos al trabajo.


Montando un entorno de integración continua local para Apps mobile (iOS,Android)

$
0
0

Índice de contenidos

  1. Introducción
  2. Entorno
  3. Configuración
  4. Xcode bots
  5. Jenkins
  6. Android SDK
  7. Configurando Jenkins
  8. Conclusiones

1. Introducción

Hoy en día existen muchas soluciones para proveer a los desarrollos mobile de un entorno de integración continua. En muchos casos, lo más fácil, menos costoso, más mantenible y más rápido es utilizar servicios de terceros (SaaS) como puedan ser Bitrise, Buddybuild, etc, que se conectan con nuestro repositorio en la nube.

Para bien o para mal, en función del cliente con el que nos toque trabajar, es posible que no podamos usar ninguno de estos servicios, bien porque el propio cliente no lo apruebe o a consecuencia de sus propias restricciones de seguridad (un repositorio privado sin acceso exterior y sin permiso para conectar vía VPN por ejemplo). Ante ese panorama tenemos dos opciones:

  • No tener integración continua (opción mala)
  • Montar nuestro propio entorno de integración continua (opción menos mala)

2. Entorno

El tutorial está escrito usando el siguiente entorno:

  • Hardware: Mac mini (2018) (3 GHz Intel Core i5, 16 GB 2667 MHz DDR4)
  • Sistema Operativo: Mac OS X Catalina 10.15.3
  • Jenkins 2.204.5
  • Fastlane 2.143.0
  • Xcode 11.3.1
  • Android SDK (command line tools) latest

3. Configuración

En el caso del desarrollo backend no es tan inusual que los equipos monten sus propios entornos de integración continua (en adictos tenemos unos cuantos tutoriales al respecto sobre Jenkins), pero en el caso del desarrollo mobile hay alguna que otra limitación.

Nuestra App tiene versiones nativas en iOS y Android. Para poder compilar la App de Android, necesitaremos tener instalado el SDK en nuestro servidor. En el caso de iOS, además necesitamos sí o sí que nuestro servidor ejecute macOS, es imposible a día de hoy compilar cualquier App de iOS desde otro sistema operativo. Así pues, se decidió un Mac Mini como servidor de integración continua tanto para la App de Android como la de iOS. Para ello en nuestro Mac Mini necesitamos lo siguiente:

  • Xcode: Se encargará de la compilación de la App de iOS y actuará como Xcode server para pasar los tests.
  • Android SDK: Necesario para poder compilar la App de Android
  • Jenkins: Se utilizará para pasar los tests de Android y en general para automatizar cualquier otra tarea, como la generación de betas nightly.
  • Fastlane: Utilidad muy conocida en el mundo mobile, sobre todo en iOS para ayudar a realizar tareas. En nuestro caso la utilizamos para generar las betas de iOS y Android y desplegarlas a nuestra plataforma de distribución de betas.

4. Xcode Bots

Para pasar los tests de iOS, en nuestro caso hemos optado por utilizar bots los bots de Xcode. Los bots de Xcode tienen un comportamiento peculiar, por una lado el server (en nuestro caso el Mac Mini) se encargará de compilar y ejecutar los tests pero el Bot se crea desde nuestro equipo.

Lo primero que debemos hacer es habilitar Xcode server en nuestro servidor, para ello abrimos Xcode -> Preferencias -> Server & Bots, y ahí activamos el server.

"XcodeServer"

Una vez activado Xcode Server, en local configuramos Xcode para que se conecte con nuestro server en Xcode -> Preferences -> Accounts

"XcodeServer"

Con esto ya podemos crear el bot desde nuestra máquina local en Product -> Create bot, en el que un asistente nos guiará paso a paso. Nos preguntará que si queremos añadir el servidor como miembro del equipo para gestionar el firmado, el repositorio, el esquema, las acciones a realizar (tests, cobertura, etc) y las acciones posteriores.

5. Jenkins

Para simplificarnos la vida, lo más fácil es en el caso de macOS es instalar Jenkins con brew.

brew install jenkins

Podemos cambiar la configuración (ip, puerto, etc) desde el fichero:

/usr/local/Cellar/jenkins/2.x.x/homebrew.mxcl.jenkins.plist (reemplazar 2.x.x por la versión que corresponda)

Si además queremos que se ejecute al iniciar sesión también podemos añadirlo como daemon con:

brew services start jenkins

Una vez instalado deberíamos poder acceder a Jenkins desde nuestro navegador (por defecto http://localhost:8080).

La primera vez que arrancamos Jenkins tendremos que seguir los pasos que nos indique para completar la instalación.

6. Android SDK

Antes de que Jenkins pueda pasar los test de Android, debemos instalar el SDK de Android. No es necesario descargar Android Studio completo, solo nos interesa el SDK. Para ello accedemos a https://developer.android.com/studio#downloads, y en «Download options», bajo «Command line tools only» podremos descargar el zip correspondiente para macOS.
Una vez descargado lo descomprimimos donde más nos convenga (en nuestro caso lo hemos dejado en el home de jenkins ~/.jenkins). Tendremos que aceptar las licencias, para ello abrimos una terminal, nos desplazamos hasta el directorio del SDK, en nuestro caso:

~/.jenkins/android_sdk/tools/bin

Y ejecutamos:

./sdkmanager –licenses

Nos preguntará si aceptamos las condiciones. Esto es necesario o si no fallará cuando intentemos compilar nuestra App Android desde Jenkins. Con esto ya deberíamos ser capaces de crear nuestros jobs de Jenkins para pasar los tests.

7. Configurando Jenkins

Empezaremos creando la variable global ANDROID_HOME. Para ello desde Manage Jenkins -> Configure System, bajo Global properties creamos una variable ANDROID_HOME apuntando a nuestro SDK.

"Jenkins"

Tambien nos aseguramos de tener instalados los plugins de Gradle, HTML Publisher Plugin y Text-finder desde Manage Jenkins -> Plugins. Adicionalmente también podemos instalar algún plugin para notificar sobre los resultados de los jobs en función a nuestras preferencias/recursos (por ejemplo, el de Slack).

Con esto ya deberíamos poder crear nuestro job, para ello desde el dashboard principal de Jenkins, pulsamos New Item -> Freestyle Project. Configuramos el repositorio, los build triggers, y pulsamos Add build step -> Invoke gradle script. Marcamos Use Gradle Wrapper e indicamos el task de Gradle con el que pasar los tests.

"Jenkins"

Para visualizar los resultados de una forma más cómoda, en Post-build actions, añadimos Publish HTML reports, indicando la carpeta de nuestro proyecto en la cual se almacenan los resultados de los tests.

"Report"

Por último, en nuestro caso para que pase todos los tests y no se interrumpa al primer fallo, en nuestro fichero build.gradle tenemos configurado lo siguiente:

testOptions {
   unitTests.returnDefaultValues = true
   unitTests.all {
       setIgnoreFailures(true)
   }
}

Esto aunque por una parte permite que el job de Jenkins pase todos los tests y nos diga cuales pasan y cuales no hace que el job se marque como estable a pesar de que fallan tests. Como solución rápida optamos por utilizar el plugin de Jenkins «Text-finder» para identificar cuando no han pasado todos los tests.

"Finder"

Con esto ya seríamos capaces de pasar los tests en nuestra App Android

8. Conclusiones

El tener que configurar uno mismo su propio entorno de integración continua es un proceso bastante arduo, complicado y además requiere armarse de paciencia y un mantenimiento periódico. En la parte positiva es que ofrece mucha flexibilidad, ya que puedes utilizar cualquier herramienta que tengas a tu alcance para automatizar aquellas tareas que uno se proponga.

Si para colmo de males el cliente cuenta con un proxy esa dificultad se eleva al cuadrado, aunque se puede paliar con herramientas como SquidMan para no tener repartidos las credenciales por varios scripts diferentes.

La entrada Montando un entorno de integración continua local para Apps mobile (iOS,Android) se publicó primero en Adictos al trabajo.

Cursores múltiples y otras opciones y extensiones sencillas y útiles en Visual Studio Code

$
0
0

Antes de empezar recordar que este tutorial forma parte de una cadena de tutoriales en las que pretendo simplemente probar tecnologías actuales. Aquí abajo podrás encontrarlos :

 

También  me gustaría que vieras este video de 2 minutos para entender dónde estamos y a dónde vamos


Mi equipo es Mac

macOS Catalina
Versión 10.15.2

MacBook Pro (15-inch, 2018)
Procesador 2,9 GHz Intel Core i9 de 6 núcleos
Memoria 32 GB 2400 MHz DDR4
Gráficos Intel UHD Graphics 630 1536 MB


Cuando imparto formación sobre metodologías ágiles a directivos siempre les insisto sobre un concepto Lean: amplificar aprendizaje. También gobernarlo, que no es poco.

Voy a poneros un ejemplo: pregunto a todos los asistentes cuál es la herramienta que más utilizan en el día a día.

Normalmente contestan que el correo electrónico.

Si les preguntas si saben siquiera para que valen la mitad de los menús de su herramienta de correo se quedan perplejos. ¿Podrían usar alguna opción de su menú para hacer el trabajo más sencillo?

¡¡¡La gente no conoce cómo se utilizan correctamente las herramientas !!!

También les comento qué pasaría si ellos mismos o cualquiera de sus subordinados recibiera el aviso de que su madre se ha caído de una escalera y está en el hospital. Todos me dicen que se irían corriendo y faltarían varios días. Al preguntarles ¡ ¿Pasaría algo? Todos contestan que realmente no, un poco de desajuste pero se viviría en el mismo caos controlable que siempre.

Por tanto, si una persona deja varias horas de hacer su trabajo para aprender sobre esos menús que no conoce y luego comparte un documento y una charla con sus compañeros ¿sería productivo para el colectivo? ¿No podría haber faltado esa misma persona ese tiempo empleado y hubiera afectado lo mismo?

Bueno, queda claro que hay pocas excusas para no dedicar un poquito de tiempo a mejorar el conocimiento de las herramientas. Obviamente esto no significa que se deba hacer anárquicamente, a criterio de cada persona o constantemente. Lo que digo es que hay que dedicar tiempo a mejorar el conocimiento base y compartirlo.

Recordad el contexto de lo que estoy haciendo, superar un reto (aunque lo he dejado unos días para terminar de revisar mi último libro)

Voy a repasar cosas tan sencillas como cursores múltiples y otras opciones contextuales y extensiones que ofrece el universo de MS Visual Studio Code para hacer la programación más sencilla.

En este ejemplo declaro unas variables. Parece que dar a una variable el nombre a y b no es lo más razonable. Tenemos que plantearnos siempre ¿cómo de mantenible será el código que generemos?

Cambiar en nombre de las variables en su definición y sus usos en Visual Studio Code es muy sencillo. Pulsamos el botón derecho aparece el menú contextual.

Fijaros que podemos dar a renombrar símbolo, lo mismo que si pulsamos F2.

Aparece una caja de texto interactiva y podemos introducir el nuevo nombre.

Que se sustituye en todo el documento.

Ahora bien, podemos querer hacer cambios en más de un punto del código pero selectivamente. Para ello podemos utilizar los multi-cursores.

Si pulsamos Command + Click, nos aparece un cursor en cada punto donde lo hagamos. Aquí imaginad que he escrito un punto y coma después de un case en vez de dos puntos. Puedo crear un cursor múltiple y borrarlo e insertar dos puntos y se hacen en todas las líneas a la vez.

Así quedaría el resultado, simplemente escribiendo una vez.

Podemos, a partir de la selección de una palabra seleccionar todas las ocurrencias.

Simplemente pongo el cursor en la palabra, por ejemplo coche y selecciono todas sus ocurrencias.

Ya me aparece un cursor múltiple sobre ellas

Cambiamos la palabra coche por vehículo y se producen los cambios en todos los cursores.

Podríamos luego aplicar el primer cambio, solo a la variable para renombrarla otra vez al gusto.

En el entorno hay muchas más cosas útiles: si restamos ya medio cegatos, como me va ya pasando, podemos querer hacer las fuentes más grandes.

Con Editor Font In las podemos ir aumentando a nuestro gusto. Con Editor Font Reset las dejamos como estaban.

Si queremos hacerlo, no solo con las pantallas de edición, con todo el IDE lo podemos hacer con View Zoom In.

Otra cosa que es muy útil, cuando alguien te interrumpe, es volver al último punto de edición con Command K y Command Q. Dejando pensado Command pulsamos kq.

Cuando te estas volviendo loco porque tienes muchas llaves anidadas y no encuentras la que corresponde (y mira que te entiendo bien) la opción Go to Bracket hace las cosas más sencillas.

Para evitar este problema definitivamente puede utilizar una extensión llamada Bracket Pair Colorized

De este modo, visualmente se puede comprobar cómo casan los ámbitos con colores.

Otra extensión que nos puede valer es Bracket Highlighter que nos permite ver en otro estilo los elementos de un ámbito.

Si pinchamos en la llave vemos todos los elementos afectados en Italica y un poco más grandes.

Algo bastante útil es crearte un fichero de los comandos que más utilizas y con la opción Run Selected Text del terminar y así no tienes que escribirlos.

Hay un montón de extensiones que nos pueden ser también muy útiles. Por ejemplo quiero poder abrir un fichero cualquiera en un navegador.

Podéis ver que por defecto no tenemos esa opción en un menú contextual.

Con el plugin Open In Browser

Ya disponemos de la opción

Una opción super interesante es disponer de Bookmarks. Cuando está tocando código entre distintos ficheros, por ejemplo html, css y TypeScript, es una buena opción saltar entre puntos concretos. Instalamos la extensión Bookmarks.

Y nos aparece una nueva opción en el menú contextualmente.

Podemos acceder a ellos en un nuevo menú en la barra de la izquierda, aunque lo más cómodo es saltar al anterior y al siguiente.

Ya que estamos usando Jest vamos a instalar una extensión para ejecutar test selectivamente llamado

Si usamos nuestro comando estándar vemos que se ejecutan los dos test que hemos creado

Después de instalar el plugin tenemos un menú contextual para ejecutar solo la sección seleccionada.

Vemos que ignora uno de ellos.

Bueno, supongo que lo habéis captado. Dedicar unos minutos a investigar las opciones de la herramienta puede hacer que vivas más cómodo en decenas de ocasiones, simplemente por no haberte parado a investigar un rato.

Esto de aprender a manejar las herramientas debería ser un taller obligatorio en el proceso de OnBoarding de cualquier empresa y seguro que en cada re-edición alguien podría aportar algún truco o información sobre una extensión nueva que nos hiciera la vida más fácil.

La entrada Cursores múltiples y otras opciones y extensiones sencillas y útiles en Visual Studio Code se publicó primero en Adictos al trabajo.

Java Compiler Tree API y cómo manipular el árbol AST

$
0
0

índice de contenidos

1. Introducción

Probablemente alguna vez te has preguntado cómo funcionan internamente algunos de los frameworks o de las tools más famosas como Sonar, Lombok o incluso IDEs como IntelliJ o Eclipse. En este post veremos qué es el AST (Abstract Syntax Tree) y cómo interactuar con él. La clasificación de estas clases podemos encontrarla en el siguiente overview oficial. Muchas de las clases citadas en el post quedaron ocultas a partir del proyecto Jigsaw (Java 9) como se especifica en la JEP 220.


** IMPORTANTE: El ejemplo que vamos a ver no está pensado para ser usado en un proyecto final ya que haremos uso de un API interno de la JDK que no está sujeto a retrocompatibilidad. Vamos a mostrar el uso de este API para poder ver cómo funciona y sus características, pero el uso de esta queda bajo nuestro propio riesgo.

2. Proceso de compilación de Java y generación del AST

Antes de entrar en materia primero debemos entender (aunque sea a alto nivel) cómo funciona el compilador de Java. El proceso de compilación consta, a grandes rasgos, de tres fases a la hora de compilar un fichero y generar el bytecode:

Parse and Enter

En la fase «Parse» se escanean todos los ficheros *.java, se tokenizan y se genera el árbol AST. Una buena analogía sería comparar el AST con un árbol DOM para aclarar el concepto. Una vez tokenizada la clase se pasa a la fase «Enter», en la cual se registran todos los símbolos y se añaden como entradas en la tabla de símbolos del compilador (palabras clave, nombres de métodos, variables, scopes, etc). Seguro que alguna vez os habéis encontrado con el típico error «Cannot find the symbol». Más sobre compiladores y tablas de símbolos aquí.

Annotation Processing

En el ejemplo propuesto usaremos esta fase como punto de entrada a la compilación. Esta fase es controlada por la clase com.sun.tools.javac.processing.JavacProcessingEnvironment. El procesado de anotaciones funciona por «rounds» o rondas, y es un paso que se realiza previamente a la compilación de las clases. Al arrancar la primera ronda se ejecutan los procesadores de las anotaciones definidos y, si alguna de estos procesadores genera nuevo código fuente, se realizarán rondas posteriores para tener en cuenta este nuevo código.
Cuando los procesadores de anotaciones han sido ejecutados se evalúa de nuevo si es necesaria una ronda posterior para compilar y procesar nuevo código fuente y, si es necesario, se generará un nuevo objeto JavaCompiler que lee y procesa el nuevo código fuente y genera una nueva ronda. Este proceso se repite hasta que no hay más anotaciones que procesar.

Analyse and Generate

Una vez que las dos fases anteriores se han ejecutado el compilador pasa a analizar la sintáxis del árbol generado contra la tabla de símbolos creada en la fase «Parse and Enter» de cara a generar los ficheros .class. El análisis y la generación de los ficheros .class se realiza a partir de una serie de visitors y no se garantiza que estos visitors se ejecuten en un orden o formato concretos por optimización de memoria, aunque sí se garantiza que cada elemento a procesar será procesado en algún momento. Aunque la fase de análisis contiene múltiples pasos internos (que pueden ser consultados aquí), en resumen, una vez completada la fase de análisis se generará el fichero .class.

3. Java Compiler API

El Java Compiler API es un conjuto de interfaces y clases que nos dan acceso al compilador de Java. Las principales acciones que podemos llevar a cabo usando este API son compilar clases programáticamente y obtener acceso al diagnóstico del compilador, también programáticamente. Esto puede ser interesante en algunas ocasiones, de hecho, en el ejemplo planteado usaremos este API para desarrollar un test unitario en el cual compilaremos una clase con el propósito de leer el bytecode que genera. El conjunto de clases e interfaces que forman este API están definidas dentro de la JSR 199.

4. Java Compiler Tree API y AST

El Java Compiler Tree API define un conjunto de clases e interfaces que nos proveen acceso al árbol AST generado en la primera fase de compilación. A través de estas interfaces y de sus clases de utilidades podemos intervenir en el proceso de compilación leyendo la estructura del árbol AST aunque no podamos modificarlo. Si nos fijamos de qué está compuesto el API (dentro de la paquetería com.sun.source.tree) en su mayoría son interfaces y solo hay unas pocas clases de utilidades que nos permiten usar un patrón Visitor, como por ejemplo la clase TreePathScanner, el cual nos habilita recorrer cualquier clase, método, constructor, imports, variables, etc.

Pero si son interfaces, ¿qué las implementa? Para responder a eso tenemos que salir del API público y analizar una librería que hasta la JDK 8 venía dentro del directorio /lib de la JDK. Hablamos de la librería tools.jar que comentaremos en el siguiente apartado.

4.1 tools.jar

El uso de esta librería nos abre una nueva puerta: la posibilidad, no solo de leer el árbol AST, sino de modificarlo. Todas las interfaces que se declaran en API público son implementadas por clases que se encuentran en esta librería y, a su vez, extienden de una clase abstracta llamada com.sun.tools.javac.tree.JCTree y cuyos tipos concretos se crean a partir de una factoría llamada com.sun.tools.javac.tree.TreeMaker. Además, veremos la clase com.sun.tools.javac.tree.TreeTranslator que extiende el com.sun.tools.javac.tree.JCTree.Visitor para facilitar la tarea de modificación del AST.

Como hemos comentado anteriormente, con la modularización que vino con Java 9 (https://openjdk.java.net/jeps/220), las librerías rt.jar y tools.jar son borradas y sus clases quedan encapsuladas dentro de un módulo que no se exportan en la JDK por lo que, a menos que se le indique lo contrario, están ocultas. Y por un buen motivo, y es que no aseguran retrocompatibilidad de dichas clases por lo que debemos tener cuidado de dónde usamos estas clases. En la JEP 261 se indica lo siguiente:

…The –add-exports and –add-opens options must be used with great care. You can use them to gain access to an internal API of a library module, or even of the JDK itself, but you do so at your own risk: If that internal API is changed or removed then your library or application will fail…

5. Ejemplo propuesto. Anotación @DTO para generar getters / setters en tiempo de compilación

A continuación vamos a ver un ejemplo completo usando todo lo explicado anteriormente y en el que desarrollaremos una anotación Java que llamaremos @DTO y cuyo procesador se encargará de leer y modificar el árbol AST para incluir métodos getter y setter de todas las variables de instancia de la clase en tiempo de compilación.

5.1 First things first. Empecemos por un test

Lo primero que vamos a implementar es un test unitario partiendo de una clase que solo define sus atributos de instancia y marcada con la anotación que hemos creado:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface DTO {}

@DTO
public class PersonSample {
    private String name;
    private int age;
    private UUID id;
    private Other other;
    private byte[] arr;

}

Dentro del test, obtendremos una referencia al compilador de Java para compilar la clase de prueba marcada con la anotación y posteriormente procesarla la anotación.

@Test
public void testDTOAnnotationProcessor() throws ClassNotFoundException {
    final JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
    final DiagnosticCollector diagnostics = new DiagnosticCollector();
    final StandardJavaFileManager manager = compiler.getStandardFileManager(diagnostics, null, null );

    final Path path = Paths.get("src/main/java/com/sergio/dto/PersonSample.java");
    Iterable sources = manager.getJavaFileObjects(path.toFile());
    final JavaCompiler.CompilationTask task = compiler.getTask(null, manager, diagnostics,
                    null, null, sources);

    task.setProcessors(Collections.singletonList(new DTOAnnotationProcessor()));
    final boolean compilationSuccesss = task.call();
    if (!compilationSuccesss) {
...

Si la invocación al método call() retorna false implica que la clase contiene errores de compilación, por lo que haremos uso de las clases de diagnóstico incluidas en el Compiler API para mostrar el error.

if (!compilationSuccess) {
     diagnostics.getDiagnostics().forEach(d -> {
       if (d.getKind() == Diagnostic.Kind.ERROR) {
           System.out.println(d.getMessage(null));
           System.out.println("Line error: " + d.getLineNumber());
           System.out.println("Column number: " + d.getColumnNumber());
       }
     });
     fail();
}

Si la compilación es correcta, crearemos nuestro propio ClassLoader para refrescar el bytecode generado después del procesamiento de la anotación y validaremos que existen un método getter y setter por cada atributo de la clase.

ClassLoader parentClassLoader = PersonSample.class.getClassLoader();
ReloaderClassLoader loader = new ReloaderClassLoader(parentClassLoader);
loader.setClassUrl("src/main/java/com/sergio/dto/PersonSample.class");
Class clazz = loader.loadClass("com.sergio.dto.PersonSample");
Set declaredMethods = Stream.of(clazz.getDeclaredMethods()).map(Method::getName).collect(toSet());
assertThat(declaredMethods).contains("getId", "getName");
assertThat(declaredMethods).contains("setId", "setName");

Una vez definido el test, y estando este en rojo, podemos empezar a ver el código que se encarga de procesar la anotación y añadir los nuevos métodos al AST.

5.2 El Procesador de anotaciones

Para poder intervenir en el proceso de compilación (más info aquí) podemos hacerlo de tres formas: Usando un Doclet, implementando un Plugin o usando un procesador de anotaciones. En este ejemplo usaremos la última de las opciones donde la JSR 269 es la especificación donde se define. Para crear un procesador de anotaciones basta con extender la clase javax.annotation.processing.AbstractProcessor, indicarle qué anotación(es) procesa y qué versión de Java soporta.

Al extender esta clase nos obliga a implementar el método process que, como su nombre indica, se encarga de procesar la lógica de la anotación.

@SupportedAnnotationTypes("com.sergio.dto.DTO")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class DTOAnnotationProcessor extends AbstractProcessor {

    private JavacProcessingEnvironment env;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        this.env = (JavacProcessingEnvironment) processingEnv;
        super.init(processingEnv);
    }

    @Override
    public boolean process(Set annotations, RoundEnvironment environment) {
        final Context context = env.getContext();
        final Trees trees = Trees.instance(env);

        if (!environment.processingOver()) { // in order to avoid processing twice in the last round
            for (Element codeElement : environment.getRootElements()) {
                if (codeElement.getKind() != ElementKind.CLASS) continue;
                JCTree tree = (JCTree) trees.getTree(codeElement);
                tree.accept(new GetterAndSetterJavaTranslator(context));
            }
        }

        return false; // false as default behaviour. Process subsequent annotations if exists.
    }
}

En este caso, la lógica de procesamiento será la de acceder al árbol AST para el/los elementos en cuestión y pasarlos por una implementación propia de un TreeTranslator. Una vez identificado el elemento que queremos procesar se accede al visitor a través del método accept del árbol, al cual le pasamos nuestra clase para añadir las modificaciones oportunas. Para ello extendemos la clase TreeTranslator sobrescribiendo el método visitClassDef:

public class GetterAndSetterJavaTranslator extends TreeTranslator {

    public GetterAndSetterJavaTranslator(Context context) {
        this.context = context;
    }

    @Override
    public void visitClassDef(JCTree.JCClassDecl clazz) {

        super.visitClassDef(clazz);

        final boolean isAnnotated = isClassAnnotated(clazz);

        if (isAnnotated) {

            final TreeMaker maker = TreeMaker.instance(context);
            final Names names = Names.instance(context);
            final Symtab symb = Symtab.instance(context);

            final com.sun.tools.javac.util.List getters = createGetters(maker, names, clazz);
            final com.sun.tools.javac.util.List setters = createSetters(maker, names, symb, clazz);

            clazz.defs = clazz.defs.appendList(getters).appendList(setters);
            result = clazz;

            System.out.println("class after translate = " + result);
        }

    }
 ...
}

Es importante preguntar si la clase está marcada con la anotación creada y solo procesarla en tal caso. Al extender de TreeTranslator se nos da acceso al atributo result que será el atributo al cual se le añaden los métodos getters y setters como se muestra continuación.

/**
* public <memberType> get<MemberName>() {
*  return <memberName>;
* }
*/
private com.sun.tools.javac.util.List createGetters(TreeMaker maker, Names names, ClassTree node) {
   return com.sun.tools.javac.util.List.from(node.getMembers().stream()
         .filter(m -> m.getKind() == Tree.Kind.VARIABLE)
         .map(m -> {
               final VariableTree varTree = (VariableTree) m;
               final Tree type = varTree.getType();
               final Name name = names.fromString(varTree.getName().toString());
               final Name methodName = names.fromString("get" + capitalize(name));
               final JCTree.JCExpression memberName = maker.Ident(name);
               final JCTree.JCBlock block = maker.Block(0, List.of(maker.Return(memberName)));
               return maker.MethodDef(
                     maker.Modifiers(Flags.PUBLIC),  // method access modifier
                     methodName,                     // method name
                     generateReturnType(type),       // return type
                     List.nil(),                     // generic type parameters
                     List.nil(),                     // parameter list
                     List.nil(),                     // throws clause
                     block,                          // method body
                     null                // default methods (for interface declaration)
               ).getTree();
          }).collect(Collectors.toList()));
    }

Usamos las clases de utilidades que nos proporciona la librería tools.jar para generar los métodos getter. La clase más importante aquí es com.sun.tools.javac.tree.TreeMaker, que es la factoría que nos permite crear cualquier subtipo de la clas com.sun.tools.javac.tree.JCTree. En el ejemplo usamos el maker para generar un com.sun.tools.javac.tree.MethodDef (definición de método), además de crear el bloque de código que se encuentra dentro del getter. En este caso creará un tipo com.sun.tools.javac.tree.JCReturn (una definición de tipo retorno) que nos permite devolver el miembro de la clase. Veámos cómo crear los setters:

/**
* public void set<MemberName>(<MemberType> <MemberName>) {
*   this.<member> = <MemberName>;
* }
*/
private com.sun.tools.javac.util.List<JCTree> createSetters(TreeMaker maker, Names names, Symtab symb, ClassTree node) {
    return com.sun.tools.javac.util.List.from(node.getMembers().stream()
        .filter(m -> m.getKind() == Tree.Kind.VARIABLE)
        .map(m -> {
               final VariableTree varTree = (VariableTree) m;
               final Name methodName = names.fromString("set" + capitalize(names.fromString(varTree.getName().toString())));
               final Name name = names.fromString(varTree.getName().toString());
               final JCTree.JCExpression memberName = maker.Ident(name);

               final JCTree.JCVariableDecl param = generateParameter(varTree, maker, names, symb);
               final JCTree.JCAssign assign = maker.Assign(memberName, maker.Ident(param.getName()));
               final JCTree.JCExpressionStatement exec = maker.Exec(assign);
               final JCTree.JCBlock block = maker.Block(0, List.of(exec));
                    
               return maker.MethodDef(
                     maker.Modifiers(Flags.PUBLIC),  // method access modifier
                     methodName,                     // method name
                     maker.TypeIdent(TypeTag.VOID),  // return type
                     List.nil(),                     // generic type parameters
                     List.of(param),                 // parameter list
                     List.nil(),                     // throws clause
                     block,                          // method body
                     null                // default methods (for interface declaration)    
               ).getTree();
        }).collect(Collectors.toList()));
}

La creación de los setters difiere de los getters en el bloque de código que se declara dentro del método, ya que no retornamos nada, sino que asignamos un parámetro a un miembro de la clase. Usando el TreeMaker creamos un tipo com.sun.tools.javac.tree.JCAssign el cual recibe las expresiones a asignar a la izquierda y a la derecha.

Una vez definidos los métodos y asignados al attributo result cuando ejecutemos el test unitario deberíamos ver el fichero .class de la clase de ejemplo junto con un método getter y setter por cada miembro de la clase, además de ver el test en verde.

// IntelliJ API Decompiler stub source generated from a class file
// Implementation of methods is not available

package com.sergio.dto;

@com.sergio.dto.DTO
public class PersonSample {
    private java.lang.String name;
    private int age;
    private java.util.UUID id;
    private com.sergio.dto.Other other;
    private byte[] arr;

    public PersonSample() { /* compiled code */ }

    public java.lang.String getName() { /* compiled code */ }

    public int getAge() { /* compiled code */ }

    public java.util.UUID getId() { /* compiled code */ }

    public com.sergio.dto.Other getOther() { /* compiled code */ }

    public byte[] getArr() { /* compiled code */ }

    public void setName(java.lang.String s) { /* compiled code */ }

    public void setAge(int i) { /* compiled code */ }

    public void setId(java.util.UUID uuid) { /* compiled code */ }

    public void setOther(com.sergio.dto.Other other) { /* compiled code */ }

    public void setArr(byte[] bytes) { /* compiled code */ }
}

6. Conclusiones

Dado que es un contenido denso para ser leído he creído necesario dar apoyo visual del contenido con un screencast, divido en dos partes, desarrollando desde cero un ejemplo muy parecido a este y que os he dejado en en apartado 7. Referencias.

Aunque no sea algo que vayamos a usar todos los días sí que merece la pena ver cómo muchas de las herramientas que usamos día a día están implementadas internamente. Conocer un poco cómo funcionan las tripas siempre nos puede aportar visión además de ser tremendamente divertido.

Y recordad: lo visto en este post puede daros más de un dolor de cabeza si lo usáis sin pensar dos veces en las consecuencias.

7. Referencias

Ejemplo completo

Vídeos relacionados

Documentación

La entrada Java Compiler Tree API y cómo manipular el árbol AST se publicó primero en Adictos al trabajo.

El juego de la Vida en TypeScript con TDD y medida de cobertura en Jest

$
0
0

Antes de empezar recordar que este tutorial forma parte de una cadena de tutoriales en las que pretendo simplemente probar tecnologías actuales. Aquí abajo podrás encontrarlos :

También  me gustaría que vieras este video de 2 minutos para entender dónde estamos y a dónde vamos


Mi equipo es Mac

macOS Catalina
Versión 10.15.2

MacBook Pro (15-inch, 2018)
Procesador 2,9 GHz Intel Core i9 de 6 núcleos
Memoria 32 GB 2400 MHz DDR4
Gráficos Intel UHD Graphics 630 1536 MB


Aunque me propuse un reto construyendo un Arcanoid en TypeScript, que todavía tengo que completar (ver referencias) antes quiero revisar un poquito el lenguaje (recordad que no hablo como experto sino como estudiante).

Un buen ejemplo para manejar arrays y hacer pruebas unitarias y TDD puede ser crear el juego de la vida de Conway: https://es.wikipedia.org/wiki/Juego_de_la_vida

Las reglas son sencillas. Creas dentro de un tablero de un tamaño fijo una semilla (que voy a crear aleatoriamente) que representan células y se gobierna en base a tres reglas.

1 – Si en una casilla vacía hay 3 células alrededor vivas, nace una célula.

2 – Si en una casilla hay una célula vida y alrededor solo 2 o tres vivas, sobrevive.

3 – En cualquier otro caso, la célula de esa casilla muere, por superpoblación.

Para empezar voy a crear un test que voy a llamar tdd.test.ts.

El código lo voy a guardar en índex.ts y para visualizarlo voy a crear una página HTML llamada índex.html.

También crearé un fichero de comando por si necesito usar alguno desde el terminal.

 

El fichero index.html es sencillo.

Solamente tiene un canvas de 300 x 300 donde pintar los elementos.

Este canvas tiene un id=“areadejuego”

 

<html>
  <head>
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta http-equiv="Expires" content="0" />
    <meta http-equiv="Last-Modified" content="0" />
    <meta htt p-equiv="Cache-Control" content="no-cache, mustrevalidate" />
    <meta http-equiv="Pragma" content="no-cache" />
    <style>
      canvas {
        border: solid;
      }
    </style>
  </head>
  <body>
    <h2>El juego de la vida TDD con TypeScript</h2>

    <canvas width=300 height= 300 id="areajuego"></canvas>

    <script src="./index.ts"></script>
  </body>
</html>

 

El fichero índex.ts inicialmente solo va a tener el enlace entre el html y JavaScript.

window.addEventListener("load", main);

function main() {
  let juego: JuegoDeLaVidaTDD;

  let micanvas: HTMLCanvasElement = document.getElementById(
    "areajuego"
  ) as HTMLCanvasElement;

  let ctx: CanvasRenderingContext2D | null = micanvas.getContext("2d");

  if (ctx == null) {
    console.log("Imposible recuperar contexto pintado");
    return;
  }

  juego = new JuegoDeLaVidaTDD(ctx);
  juego.timer = setInterval(juego.anima, 100);
}

Como veis, nos hace solamente falta una clase llamada JuegoDeLaVidaTDD

Y necesitamos que tenga un timer y una función que se ejecute periódicamente.

El comportamiento es sencillo: se calculará una nueva generación en cada ciclo y se pintará.

anima = () => {
    console.log("Animamos el juego");
    this.nuevaGeneracion();
    this.pinta();
  };

El código nos queda tal que así

class JuegoDeLaVidaTDD {
  timer: any;
  tama = { anchoCanvas: 0, altoCanvas: 0 };
  dim = { columnas: 0, filas: 0 };
  color: string = "FFFFFF";
  tamaCuadrado: number = 5;
  matriz: number[][];
  ctx: CanvasRenderingContext2D;

  constructor(ctx: CanvasRenderingContext2D) {
    this.ctx = ctx;
    this.tama.anchoCanvas = ctx.canvas.width;
    this.tama.altoCanvas = ctx.canvas.height;

    this.dim.columnas = this.tama.anchoCanvas / this.tamaCuadrado;
    this.dim.filas = this.tama.altoCanvas / this.tamaCuadrado;

    this.matriz = this.inicializaMatrizAleatoria(
      this.dim.columnas,
      this.dim.filas
    );
  }

  inicializaMatrizAleatoria(columnas: number, filas: number) {
    let matrizAux = new Array<Array<number>>();

    for (let fila = 0; fila < filas; fila++) {
      let filaArray: number[] = new Array<number>();

      for (let columna = 0; columna < columnas; columna++) {
        filaArray.push(Math.round(Math.random() * 2));
      }

      matrizAux.push(filaArray);
    }

    return matrizAux;
  }

  nuevaGeneracion() {}

  pintaCasilla(columna: number, fila: number, color: string) {
    this.ctx.fillStyle = color;
    this.ctx.fillRect(
      columna * this.tamaCuadrado + 1,
      fila * this.tamaCuadrado + 1,
      this.tamaCuadrado - 2,
      this.tamaCuadrado - 2
    );
  }

  pinta() {
pinta() {
    this.ctx.fillStyle = "#FFFFFF";
    this.ctx.fillRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);

    for (let fila = 0; fila < this.back.dim.filas; fila++) {
      for (let columna = 0; columna < this.back.dim.columnas; columna++) {
        if (this.back.matriz[fila][columna] == 0) {
          this.color = "#FFFFFF";
        } else if (this.back.matriz[fila][columna] == 1) {
          this.color = "#0000FF";
        }

        this.pintaCasilla(fila, columna, this.color);
      }
    }
  }


  anima = () => {
    console.log("Animamos el juego");
    this.nuevaGeneracion();
    this.pinta();
  };
}

window.addEventListener("load", main);

function main() {
  let juego: JuegoDeLaVidaTDD;

  let micanvas: HTMLCanvasElement = document.getElementById(
    "areajuego"
  ) as HTMLCanvasElement;

  let ctx: CanvasRenderingContext2D | null = micanvas.getContext("2d");

  if (ctx == null) {
    console.log("Imposible recuperar contexto pintado");
    return;
  }

  juego = new JuegoDeLaVidaTDD(ctx);
  juego.timer = setInterval(juego.anima, 100);
}

Este es el resultado que obtenemos. Un esqueleto donde se han inicializado a 1 aleatoriamente las casillas.

Bueno, tal vez tendríamos que preguntarnos si no le estoy dando antes de empezar demasiada responsabilidad en a la clase del juego porque métodos como inicializaMatrizAleatoria podrían estar en una clase dedicada a operaciones con matrices.

Además, si el constructor de la clase principal requiere un objeto CanvasRenderingContext2D

¿cómo pretendemos pasárselo desde un test? Tendremos acoplada la capa de presentación de la capa de cálculos.

Podríamos desdoblar nuestro código en dos clases, una dedicada a presentación y otra a las estructuras de datos y operaciones.

export class JuegoDeLaVidaTDDBack {
  matriz: number[][];
  dim = { columnas: 0, filas: 0 };

  constructor(columnas: number, filas: number) {
    this.dim.columnas = columnas;
    this.dim.filas = filas;

    this.matriz = this.inicializaMatrizAleatoria(columnas, filas);
  }


  inicializaMatrizAleatoria(
    columnas: number,
    filas: number
  ): Array<Array<number>> {
    let matrizAux = new Array<Array<number>>();

    for (let fila = 0; fila < filas; fila++) {
      let filaArray: number[] = new Array<number>();

      for (let columna = 0; columna < columnas; columna++) {
        filaArray.push(Math.round(Math.random() * 2));
      }

      matrizAux.push(filaArray);
    }

    return matrizAux;
  }

  nuevaGeneracion() {}
}

export class JuegoDeLaVidaTDDFront {
  timer: any;

  color: string = "FFFFFF";
  tamaCuadrado: number = 5;
  ctx: CanvasRenderingContext2D;
  back: JuegoDeLaVidaTDDBack;

  constructor(ctx: CanvasRenderingContext2D) {
    this.ctx = ctx;

    this.back = new JuegoDeLaVidaTDDBack(
      this.ctx.canvas.width / this.tamaCuadrado,
      this.ctx.canvas.height / this.tamaCuadrado
    );
  }

  pintaCasilla(columna: number, fila: number, color: string) {
    this.ctx.fillStyle = color;
    this.ctx.fillRect(
      columna * this.tamaCuadrado + 1,
      fila * this.tamaCuadrado + 1,
      this.tamaCuadrado - 2,
      this.tamaCuadrado - 2
    );
  }

  consigueNuevaGeneracion() {}

  pinta() {
    this.ctx.fillStyle = "#FFFFFF";
    this.ctx.fillRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);

    for (let fila = 0; fila < this.back.dim.filas; fila++) {
      for (let columna = 0; columna < this.back.dim.columnas; columna++) {
        if (this.back.matriz[fila][columna] == 0) {
          this.color = "#FFFFFF";
        } else if (this.back.matriz[fila][columna] == 1) {
          this.color = "#0000FF";
        }

        this.pintaCasilla(fila, columna, this.color);
      }
    }
  }

  anima = () => {
    console.log("Animamos el juego");
    this.consigueNuevaGeneracion();
    this.pinta();
  };
}

window.addEventListener("load", main);

function main() {
  let juego: JuegoDeLaVidaTDDFront;

  let micanvas: HTMLCanvasElement = document.getElementById(
    "areajuego"
  ) as HTMLCanvasElement;

  let ctx: CanvasRenderingContext2D | null = micanvas.getContext("2d");

  if (ctx == null) {
    console.log("Imposible recuperar contexto pintado");
    return;
  }

  juego = new JuegoDeLaVidaTDDFront(ctx);
  juego.timer = setInterval(juego.anima, 100);
}

Vamos ahora a crear la estructura del fichero de test.

Inicialmente vamos a crear una función que nos retorne con certeza si el cálculo de los vecinos vivos de una célula son los correctos.

Para ello, voy a crear un conjunto de datos y voy a llamar a la clase del juego para que me lo calcule y comprobemos que el algoritmo es correcto.

Esta es la matriz inicial

let muestra1: number[][] = [
  [1, 1, 1],
  [0, 0, 1],
  [1, 0, 1]
];

 

 

Primero escribimos el test y lo ejecutamos.

Fallará porque no existen los métodos adecuados en la clase del juego.

Es decir, se llama Test Driven Development o TDD porque el test me dice los métodos que tengo que escribir.

Si ahora aportamos los métodos que faltan.

setMatriz(matriz: number[][]) {
    this.matriz = matriz;

    this.dim.filas = matriz.length;
    this.dim.columnas = matriz[0].length;
  }


  calculaVecinosVivos(columna: number, fila: number) {
    return 0;
  }

Ahora falla porque no está haciendo bien el cálculo, ya que siempre retoma 0 calculaVecinosVivos

Por tanto, tenemos que escribir el algoritmo.

Este es el código. Realmente creamos unos índices para recorrer las casillas alrededor y sumar los unos. Tenemos que tener cuidado con los indices en el caso de que la fila o columna sea cero o estemos en ola última.

calculaVecinosVivos(columna: number, fila: number): number {
    let filaInicio: number = fila;
    let filaFin: number = fila;
    let columnaInicio: number = columna;
    let columnaFin: number = columna;

    let contador: number = 0;

    if (fila > 0) {
      filaInicio--;
    }
    if (fila < this.dim.filas - 1) {
      filaFin++;
    }

    if (columna > 0) {
      columnaInicio--;
    }
    if (columna < this.dim.columnas - 1) {
      columnaFin++;
    }

    for (let a = filaInicio; a <= filaFin; a++) {
      for (let b = columnaInicio; b <= columnaFin; b++) {
        if (fila == a && columna == b) {
          // ignorar la propia casilla
        } else {
          contador += this.matriz[a][b];
        }
      }
    }

    return contador;
  }

Y ahora podemos incluir todos los test que queramos de un modo sencillo, estando seguro de que el cálculo siempre estará bien o el test nos lo soplará.

Si os fijáis, el porcentaje de cobertura de pruebas automáticas en este caso se está limitando a la función calculaVecinosVivos.

Podemos ampliar la cobertura y también verificar que el resultado de una nueva generación completa es lo que esperamos.

Creamos el método nuevaGeneracion  que luego tendremos que implementar porque nos fallará la compilación.

Para comprobar el resultado creo una matriz resultado1 con lo que tiene que salir y tras procesar la nueva generación verificamos automáticamente.

Por tanto, cuando toquemos en un futuro la función que calcula los vecinos o la nueva generación podremos tener una red de seguridad que nos proporcionarán los test.

Este es el código completo.

xport class JuegoDeLaVidaTDDBack {
  matriz: number[][];
  dim = { columnas: 0, filas: 0 };

  constructor(columnas: number, filas: number) {
    this.dim.columnas = columnas;
    this.dim.filas = filas;

    this.matriz = this.inicializaMatriz(columnas, filas, true);
  }

  setMatriz(matriz: number[][]) {
    this.matriz = matriz;

    this.dim.filas = matriz.length;
    this.dim.columnas = matriz[0].length;
  }

  calculaVecinosVivos(columna: number, fila: number): number {
    let filaInicio: number = fila;
    let filaFin: number = fila;
    let columnaInicio: number = columna;
    let columnaFin: number = columna;

    let contador: number = 0;

    if (fila > 0) {
      filaInicio--;
    }
    if (fila < this.dim.filas - 1) {
      filaFin++;
    }

    if (columna > 0) {
      columnaInicio--;
    }
    if (columna < this.dim.columnas - 1) {
      columnaFin++;
    }

    for (let a = filaInicio; a <= filaFin; a++) {
      for (let b = columnaInicio; b <= columnaFin; b++) {
        if (fila == a && columna == b) {
          // ignorar la propia casilla
        } else {
          contador += this.matriz[a][b];
        }
      }
    }
    return contador;
  }

  inicializaMatriz(
    columnas: number,
    filas: number,
    aleatoria = true
  ): Array<Array<number>> {
    let matrizAux = new Array<Array<number>>();

    for (let fila = 0; fila < filas; fila++) {
      let filaArray: number[] = new Array<number>();

      for (let columna = 0; columna < columnas; columna++) {
        if (aleatoria == true) {
          filaArray.push(Math.round(Math.random() * 2));
        } else {
          filaArray.push(0);
        }
      }

      matrizAux.push(filaArray);
    }

    return matrizAux;
  }

  nuevaGeneracion() : Array<Array<number>>{
    let matrizAux: number[][] = this.inicializaMatriz(
      this.dim.columnas,
      this.dim.filas,
      false
    );

    for (let fila = 0; fila < this.dim.filas; fila++) {
      for (let columna = 0; columna < this.dim.columnas; columna++) {
        let vecinos: number = this.calculaVecinosVivos(columna, fila);

        if (this.matriz[fila][columna] == 0 && vecinos == 3) {
          matrizAux[fila][columna] = 1;
        } else if (
          this.matriz[fila][columna] == 1 &&
          (vecinos == 2 || vecinos == 3)
        ) {
          matrizAux[fila][columna] = 1;
        } else {
          matrizAux[fila][columna] = 0;
        }
      }
    }

    return matrizAux;
  }
}

export class JuegoDeLaVidaTDDFront {
  timer: any;

  color: string = "FFFFFF";
  tamaCuadrado: number = 5;
  ctx: CanvasRenderingContext2D;
  back: JuegoDeLaVidaTDDBack;

  constructor(ctx: CanvasRenderingContext2D) {
    this.ctx = ctx;

    this.back = new JuegoDeLaVidaTDDBack(
      this.ctx.canvas.width / this.tamaCuadrado,
      this.ctx.canvas.height / this.tamaCuadrado
    );
  }

  pintaCasilla(columna: number, fila: number, color: string) {
    this.ctx.fillStyle = color;
    this.ctx.fillRect(
      columna * this.tamaCuadrado + 1,
      fila * this.tamaCuadrado + 1,
      this.tamaCuadrado - 2,
      this.tamaCuadrado - 2
    );
  }

  consigueNuevaGeneracion() {
    this.back.matriz = this.back.nuevaGeneracion();
  }

  pinta() {
    this.ctx.fillStyle = "#FFFFFF";
    this.ctx.fillRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);

    for (let fila = 0; fila < this.back.dim.filas; fila++) {
      for (let columna = 0; columna < this.back.dim.columnas; columna++) {
        if (this.back.matriz[fila][columna] == 0) {
          this.color = "#FFFFFF";
        } else if (this.back.matriz[fila][columna] == 1) {
          this.color = "#0000FF";
        }

        this.pintaCasilla(fila, columna, this.color);
      }
    }
  }

  anima = () => {
    console.log("Animamos el juego");
    this.consigueNuevaGeneracion();
    this.pinta();
  };
}

window.addEventListener("load", main);

function main() {
  let juego: JuegoDeLaVidaTDDFront;

  let micanvas: HTMLCanvasElement = document.getElementById(
    "areajuego"
  ) as HTMLCanvasElement;

  let ctx: CanvasRenderingContext2D | null = micanvas.getContext("2d");

  if (ctx == null) {
    console.log("Imposible recuperar contexto pintado");
    return;
  }

  juego = new JuegoDeLaVidaTDDFront(ctx);
  juego.timer = setInterval(juego.anima, 100);
}

Y este es nuestro test.

let muestra1: number[][] = [
  [1, 1, 1],
  [0, 0, 1],
  [1, 0, 1]
];

let resultado1: number[][] = [
  [0, 1, 1],
  [1, 0, 1],
  [0, 1, 0]
];

import { JuegoDeLaVidaTDDBack } from "../index";

test("Recupera número de vecinos", () => {
  let juego: JuegoDeLaVidaTDDBack = new JuegoDeLaVidaTDDBack(0, 0);
  juego.setMatriz(muestra1);
  expect(juego.calculaVecinosVivos(0, 0)).toBe(1);
  expect(juego.calculaVecinosVivos(1, 0)).toBe(3);
  expect(juego.calculaVecinosVivos(2, 0)).toBe(2);
  expect(juego.calculaVecinosVivos(2, 1)).toBe(3);
  expect(juego.calculaVecinosVivos(2, 2)).toBe(1);
});

test("Comprueba nueva generación", () => {
  let juego: JuegoDeLaVidaTDDBack = new JuegoDeLaVidaTDDBack(0, 0);
  juego.setMatriz(muestra1);
  let matrizResuelta: number[][] = juego.nuevaGeneracion();

  for (let fila = 0; fila < matrizResuelta.length; fila++) {
    for (let columna = 0; columna < matrizResuelta[fila].length; columna++) {
      console.log("Procesando fila " + fila + " columna " + columna);
      expect(matrizResuelta[fila][columna]).toBe(resultado1[fila][columna]);
    }
  }
});

Podemos configurar Jest para que nos diga la cobertura de código que tenemos en jest.config.js

module.exports = {
  collectCoverage: true,
  coverageDirectory: "./coverage",

  roots: ["<rootDir>/src"],
  testMatch: [
    "**/__tests__/**/*.+(ts|tsx|js)",
    "**/?(*.)+(spec|test).+(ts|tsx|js)"
  ],
  transform: {
    "^.+\\.(ts|tsx)$": "ts-jest"
  }
};

En la carpeta seleccionada almacenan los resultados.

Y comprobamos la cantidad de veces que se testa cada porción.

Y el porcentaje de cobertura. Recordad que esto las una herramienta y que los números nos pueden llegar a esclavizar. Si decimos “yo no hago código que tenga menos del 80% de cobertura” es posible que trabajemos solo para ese número probando cosas que no hay que probar.

Y podemos ver nuestro resultado, Obviamente animado queda mucho más chulo.

Os puedo decir que es hipnótico ver la progresión de las células en cada partida.

Bueno, espero que os haya gustado. Poner test unitarios en nuestra vida y trabajar desde el principio pensado en ello, como nos obliga TDD tiene que efectos secundarios desacoplar capas y hacer código verificable.

Si veis algún fallo o algo mejorable no dudéis en comentarlo.

La entrada El juego de la Vida en TypeScript con TDD y medida de cobertura en Jest se publicó primero en Adictos al trabajo.

Algoritmos de clasificación con RStudio

$
0
0

Aprenderemos qué son los algoritmos de clasificación con RStudio y para qué sirven. Mostraremos los árboles de decisión y el modelo de bosque aleatorio. Aprenderemos a medir las bondades del modelo mediante matrices de confusión y curvas ROC para ver cómo de preciso es.

Índice de contenidos

1. Introducción a los Algoritmos de clasificación con RStudio

Dado un conjunto de datos, muchas veces queremos saber cómo clasificar datos nuevos en base a datos que ya tenemos. Imaginemos que tenemos acceso a millones de analíticas de sangre de pacientes desde 1990. Y sabemos qué personas murieron 1 año después de esos análisis. Sería interesante poder entrenar un modelo que dado unos análisis nuevos nos alertara sobre los pacientes que se morirán en el próximo año.

Otro ejemplo: al pedir una hipoteca nos piden un montón de información. Algunas preguntas son normales: ingresos, otras deudas, etc., pero suelen incorporar una pregunta aparentemente inocua, y es «en qué sector trabajas». Os puedo adelantar que los algoritmos de clasificación aciertan bastante sobre qué personas tienen más posibilidades de convertirse en morosas. El mismo estudio se aplica a cualquier tipo de seguro: del hogar, de accidentes, de vida, médico, etc.

Para todos estos sistemas es fundamental saber catalogar qué tipo de cliente/paciente eres y con la mayor fiabilidad posible.

Se usa mucho en comercio electrónico, para saber qué tipo de cliente eres, o con qué periodicidad volverás. Para centrar los esfuerzos de fidelización en quienes les merezcan la pena. En fin, hay infinidad de aplicaciones.

1.1. Nuestro objetivo

En este artículo vamos a seguir con alguno de los ejemplos que hemos visto en artículos previos. En él obteníamos la rentabilidad de los pisos en función del precio del m2 de compra y el precio del m2 de alquiler. Cuanto menor fuera el de compra y mayor el de alquiler, mayor rentabilidad. Esos datos los teníamos geoposicionados, y en base a distancia de paradas de metro.

Cabe hacernos la siguiente pregunta: dada una localización exacta que cumpliera el precio medio de la zona ¿sería una buena idea comprar un piso para alquilar ahí? De alguna forma, dadas unas coordenadas debemos saber si es una buena inversión o no.

2. Particionando el juego de datos

Lo primero que debemos hacer es leer el juego de datos.

#seteamos el directorio de trabajo
setwd("/lab/adictosaltrabajo/rstudio/06")

# leemos los precios que vimos en el ejemplo 02
precios <- readRDS("./precios.rds")

#creamos la columna con el rendimiento anualizado
precios$rendimiento <- (12 * precios$rental)/precios$sale

Ahí tenemos una nueva columna que indica una aproximación al rendimiento anual de un piso. Pero nos interesa clasificarla en categorías. Podríamos clasificar los datos en 5 niveles, según su rendimiento.

# creamos un factor con la rentabilidad
precios$rentabilidad <- cut(precios$rendimiento, 
                            breaks = 5, 
                            labels = c("Muy Bajo","Bajo","Medio","Alto","Muy Alto"))

View(precios)

Algoritmos de clasificación con RStudio

Hemos creado una nueva columna «rentabilidad», donde se ven las categorías (son factores) que hemos dispuesto.

Ahora queremos dividir nuestro dataFrame en dos conjuntos de datos. Un conjunto de entrenamiento, y otro conjunto de test. La idea es que usaremos en de entrenamiento para elaborar un modelo de predicción. Y luego veremos cómo de bueno es ese modelo cotejándolo con el conjunto de test.

Así que lo primero que necesitamos es una forma de dividir aleatoriamente nuestro dataFrame fijándonos en la columna «rentabilidad». Para ello emplearemos la librería caret.
Le indicamos que para el conjunto de entrenamiento queremos el 70% de las filas, y que no las devuelva como una lista, si no como un array.

install.packages("caret")
library(caret)

filas.entrenamiento <- createDataPartition(precios$rentabilidad, p = 0.7, list = FALSE)

De esta forma, tenemos un array con los IDs de las filas de entrenamiento y podemos seleccionar fácilmente el subconjunto del dataFrame que se corresponde con el conjunto de entrenamiento (229 filas) y cuál con el de test (96 filas).

#El conjuto de entrenamiento son esas filas y todas las columnas
conjuntoEntrenamiento <- precios[filas.entrenamiento,]

#Y el conjunto de test son las filas restantes, y todas las columnas
conjuntoTest <- precios[-filas.entrenamiento,]

3. Árboles de Decisión

El primero de los algoritmos de clasificación con RStudio que vamos a utilizar son los árboles de decisión. Para ello cargamos la librería rpart y le indicamos que deseamos crear un modelo, que nos prediga la «rentabilidad» en base a la «latitud y longitud», usando el conjunto de entrenamiento.

install.packages("rpart")
library(rpart)

# vamos a hacer un modelo de clasificación basado en árboles de decisión
# estudiando la rentabilidad en base a la localizacion geografica (latitud, longitud)
modeloArbolDecicion <- rpart(
            rentabilidad ~ lat + lng,
            data = precios[filas.entrenamiento,],
            method = "class")

Me gustaría llamar la atención sobre varios puntos:

  • Por un lado, indicamos la variable independiente en función de las variables dependientes: rentabilidad en función de latitud y longitud. Realmente es lo mismo que una función con dos valores funcion(lat,lng) y se indica de la siguiente manera rentabilidad ~ lat + lng
  • Por otro lado, el conjunto de datos sobre el que calculamos el modelo, no es el conjunto total, si no que es el subconjunto de entrenamiento data = precios[filas.entranamiento, ]
  • El método que indicamos para calcular el modelo es el de clasificación. Existen otros muchos posibles métodos como són: anova, poisson, exponencial,…

¿Y qué pinta tendría este árbol de decisión?

Nuestro nodo raíz nos indica que tiene 229 items. En la primera bifurcación mira que la latitud sea mayor que 40.40384 dejando a ese lado 156 casos y al otro 73. El Árbol sigue ramificándose, y así hasta llegar a los nodos hojas, que son marcados con asteriscos. Es decir, dada una ubicación nueva, con su latitud y longitud, podemos aplicar el árbol de decisión y ver en qué nodo hoja final cae.

Lo cierto es que no es muy atractivo visualmente. Vamos a usar otra librería para pintar este árbol de decisión.

#pintamos el arbol de decision
install.packages("rpart.plot")
library(rpart.plot)

prp(modeloArbolDecicion, type = 2, extra = 102)

En base a este árbol de decisión, podemos clasificar las filas del conjunto de test y añadir los resultados en una columna nueva.

#hacemos una predicción sobre el conjunto de test
prediccionArbolDecicion <- predict(modeloArbolDecicion, precios[-filas.entrenamiento,], type="class")

#añadimos la predicción en una columna nueva
precios[-filas.entrenamiento,c("P(arbol_decision)")] <- prediccionArbolDecicion

Ahí vemos la nueva columna P(arbol_decisión) junto a la columna rentabilidad. Hay filas donde pone N/A. Eso es porque sólo hemos hecho la predicción para el conjunto de test, no para el de entrenamiento. Y vemos que los valores que ha predicho parece que coinciden bastante. Sólo estamos viendo el principio del dataFrame. ¿Será siempre igual?

Necesitamos algún mecanismo para saber cómo de eficaz es un modelo.

4. Matrices de confusión

Una matriz de confusión es una tabla de doble entrada. Por un lado en el eje Y tenemos los valores reales, y por otro en el eje X los valores predichos.
tabla[valores reales, valores predichos]

Veamos cómo sería:

rentabilidad <- precios[-filas.entrenamiento,c("rentabilidad")]
matrizDeConfusionArbolDecicion <- table(rentabilidad, 
                                        prediccionArbolDecicion, 
                                        dnn = c("retabilidad","rentabilidad predicha"))

matrizDeConfusionArbolDecicion
> #clasificacion de aciertos vs fallos
> matrizDeConfusionArbolDecicion
           
           rentabilidad predicha
retabilidad Muy Bajo Bajo Medio Alto Muy Alto
   Muy Bajo       18    3     0    3        0
   Bajo           11   21     0    3        0
   Medio           0    0    12    3        6
   Alto            0    1     4    1        3
   Muy Alto        0    0     0    1        6

Los valores que hemos «clavado» son los que están en la diagonal principal. Hemos predicho como «muy Bajo», había 18 que realmente eran «muy bajo», pero había 11 que eran realmente «Bajo». En fin hemos acertado en algunos valores, pero otros los hemos clasificado mal. ¿Habría alguna forma de verlo en porcentajes?

Podemos hacer la probabilidad distribuida por columnas (valores predichos) y mostrarla en porcentaje

#probabilidad por COLUMNAS en porcentaje
round(100 * prop.table(matrizDeConfusionArbolDecicion,2))

           rentabilidad predicha
retabilidad Muy Bajo Bajo Medio Alto Muy Alto
   Muy Bajo       62   12     0   27        0
   Bajo           38   84     0   27        0
   Medio           0    0    75   27       40
   Alto            0    4    25    9       20
   Muy Alto        0    0     0    9       40

Por ejemplo, para la clasificación «Muy Alta» hemos acertado en el 40% de los casos, pero hemos clasificado mal un 20% que realmente son «Alto» y un 40% de valores que realmente son «Medio».

5. Bosque aleatorio

Otro de los algoritmos de clasificación con RStudio es el randomForest, llamado de bosque aleatorio. Este algoritmo realmente nos lanza n árboles de decisión distintos y los combina para obtener mejores resultados. En este ejemplo usaremos la librería «randomForest» y crearemos 200 árboles para generar nuestro modelo.

#veamos otros algoritmos de clasificacion con RStudio: randomForest
install.packages("randomForest")
library(randomForest)


modeloRandomForest <- randomForest(x = precios[filas.entrenamiento,c("lat","lng")],
                                   y = precios[filas.entrenamiento,c("rentabilidad")], 
                                   ntree = 200)

prediccionRandomForest <- predict(modeloRandomForest, precios[-filas.entrenamiento,], type="class")

#añadimos la predicción en una columna nueva
precios[-filas.entrenamiento,c("P(bosque_aleatorio)")] <- prediccionRandomForest

¿Nos habremos equivocado más o menos que antes? Parece a simple vista que nos hemos equivocado menos.

Pero para esto está la matriz de confusión.

matrizDeConfusionRandomForest <- table(rentabilidad, 
                                       prediccionRandomForest, 
                                       dnn = c("retabilidad","rentabilidad predicha"))

           rentabilidad predicha
rentabilidad Muy Bajo Bajo Medio Alto Muy Alto
   Muy Bajo       20    4     0    0        0
   Bajo            1   32     2    0        0
   Medio           0    1    19    0        1
   Alto            0    1     2    4        2
   Muy Alto        0    0     0    0        7


#probabilidad por COLUMNAS en porcentaje
round(100 * prop.table(matrizDeConfusionRandomForest,2))

           rentabilidad predicha
rentabilidad Muy Bajo Bajo Medio Alto Muy Alto
   Muy Bajo       95   11     0    0        0
   Bajo            5   84     9    0        0
   Medio           0    3    83    0       10
   Alto            0    3     9  100       20
   Muy Alto        0    0     0    0       70

Vaya, parece que en la diagonal principal tengo porcentajes mucho más altos. Mi modelo explica con éxito el 95% de los clasificados como «Muy Bajo», el 84% de los «Bajo», el 83% de los casos catalogados como rentabilidad «Media», explica el 100% de los casos con rentabilidad «Alta». Y un 70% de los «Muy Alta»

Aún así clasifica como «Muy Alta» un 20% que son realmente «Alta» y un 10% que son realmente «Media».

Con este modelo, podría arriesgarme y no equivocarme mucho.

6. Curva ROC

Normalmente las clasificaciones pueden tomar dos posibles valores: o te conviertes en cliente o no. Eres moroso, o lo no eres. Mañana va a llover, o no. Normalmente se trata de una clasificación binaria. Es una buena zona para invertir o no.

Vamos a crear una nueva columna en nuestro data frame, de forma que si es Alto o Muy Alto, es una buena opción de inversión.

# creamos un factor con la rentabilidad
precios$inversion <- cut(precios$rendimiento, 
                            breaks = 5, 
                            labels = c("NO","NO","NO","SI","SI"))

# creamos un modelo de Bosque Aleatorio con el conjunto de entrenamiento
modRF <- randomForest(x = precios[filas.entrenamiento,c("lat","lng")],
                                   y = precios[filas.entrenamiento,c("inversion")], 
                                   ntree = 200)

# hacemos una predicción sobre el conjunto de test
predRF <- predict(modRF, precios[-filas.entrenamiento,], type="class")
precios[-filas.entrenamiento,c("predRF")] <- predRF

# visualizamos los resultados sólo del conjunto de test
View(precios[-filas.entrenamiento,])

Estamos partiendo del supuesto de que equivocarnos en nuestra predicción es malo. Nuestra predicción será mejor cuanto menos equivocaciones tenga ¿no?. Pero aquí ahora cabe hacerse una pregunta importante: ¿Es igual de malo predecir que es una mala inversión cuando realmente es buena, que predecir que es una buena inversión y que luego resulte ser una pésima inversión? Veremos que no.

6.1. Un poquito de teoría

En este caso un falso positivo es mucho peor que el error contrario. A veces no es tan importante maximizar el número de aciertos, como minimizar el número de falsos positivos. Aquí hay que hablar de sensibilidad y especificidad. Imaginemos que tenemos que clasificar a pacientes por cierta enfermedad y el tratamiento es la amputación de una extremidad. Ahí un falso positivo sería imperdonable.

Vamos a dar una serie de definiciones:

  • Verdadero positivo: VP, cuando predecimos que es positivo y realmente es positivo. Por ejemplo, hemos aventurado que el paciente está enfermo, y realmente lo está.
  • Falso positivo: FP, cuando predecimos que es positivo pero realmente es negativo. Nuestra predicción afirma que está enferme cuando en realidad no lo está
  • Verdadero negativo: VN, cuando predecimos que es negativo y realmente es negativo. Por ejemplo, para un paciente el modelo predice que no está enfermo, y efectivamente no lo está
  • Falso negativo: FN, cuando predecimos que es negativo pero realmente es positivo. En nuestro ejemplo, hemos predicho que el paciente no tiene la enfermedad, y sin embargo sí la tiene

Está claro que los aciertos no nos preocupan, pero los errores sí:
Cuado predecimos que no está enfermo, pero realmente sí lo está, podríamos estarle tratando contra la enfermedad y mejorar su estado de salud. En el caso contrario, cuando el tratamiento es muy agresivo, como puede ser la amputación de una extremidad, si damos falsos positivos, estaremos procediendo a la amputación de personas que realmente están sanas.

Se define así la sensibilidad: el ratio de verdaderos positivos sobre la tasa de aciertos (verdaderos positivos + falsos negativos)
Y la especificidad como: el ratio de verdaderos negativos entre los negativos originales (falsos positivos + verdaderos negativos)

Lo primero que debemos entender es que no son términos complementarios. La suma de uno y otro no nos dan 1.

6.2. Aplicando estos conceptos en la matriz de confusión

Veamas como es la matriz de confusión de la predicción de nuestro modelo.

inversion <- precios[-filas.entrenamiento, c("inversion")]
matrizDeConfusionRF <- table(inversion, 
                             predRF, 
                             dnn = c("inversion","pred"))

#clasificacion de aciertos vs fallos
matrizDeConfusionRF

> matrizDeConfusionRF
         pred
inversion NO SI
       NO 80  0         # 80 - VN: Verdadero Negativo
       SI  4 12         #  4 - FN: Falso negativo
                        #  0 - FP: Falso positivo
         PREDICHO       # 12 - VP: Verdaderos positivos
REAL     VN  FP
         FN  VP

Hemos predicho que:

  • 80 elementos son mala inversión y efectivamente lo son.
  • 12 elementos son una buena inversión y efectivamente lo son.
  • 4 elementos los hemos calificado de mala inversión cuando realmente era una buena inversión.
  • Y no hemos calificado de buena inversión ninguno que sea realmente malo.

6.3. obteniendo el ROC con la probabilidad del SI

Así que en apariencia nuestro modelo parece muy bueno ¿no?. El algoritmo de bosque aleatorio nos ha clasificado las filas en SI y NO, pero con qué probabilidad son SI y NO. Hacemos una predicción, pero que en lugar de decir que Sí o NO, nos diga también las probabilidades.

# hacemos una predicción pero en lugar de class que nos de la probabilidad
# predRF <- predict(modRF, precios[-filas.entrenamiento,], type="class")
  probRF <- predict(modRF, precios[-filas.entrenamiento,], type="prob")

Ahora vamos a instalar la librería pROC para hacer curvas ROC y ver cómo de bueno es nuestro modelo y la relación entre sensibilidad y 1-especificidad. Cuando más a la izquierda y arriba mejor.

install.packages("pROC")
library(pROC)

#comparamos el valor original con la probabilidad del SI  
roc.objRF <- roc( inversion, probRF[,2], auc = TRUE, ci = TRUE  )

plot(roc.objRF)

Vemos que la curva sube casi vertical y a partir del 75%-80% empiezo a clasificar como falsos negativos, algunos SIes.

Una medida que nos ayuda mucho es el «Area bajo la curva». Cuando más cercana a 1 mejor.

Podemos acceder al area bajo la curva con la propiedad auc (area under curve):

> roc.objRF$auc
Area under the curve: 0.9906

Nuestra estimación de Bosque Aleatorio es buenísima. Tiene un 0.9906.

7. Conclusiones

Este artículo ha sido especialmente denso. Pero es la base para cualquier concepto relacionado con los algoritmos de clasificación en RStudio, o en cualquier otro sistema..

En este tutorial hemos aprendido:

  • qué son los algoritmos de clasificación en RStudio y para qué sirven
  • árboles de decisión
  • modelo del bosque aleatorio
  • matrices de confusión
  • ver cómo de bueno es nuestro modelo
  • curvas ROC
  • particionar el juego de datos y crear conjuntos de entrenamiento y de pruebas

Enlaces y referencias

La entrada Algoritmos de clasificación con RStudio se publicó primero en Adictos al trabajo.

Configuración de Cucumber.js y Jest-Cucumber en Visual Studio Code con TypeScript

$
0
0

Antes de empezar recordar que este tutorial forma parte de una cadena de tutoriales en las que pretendo simplemente probar tecnologías actuales. Aquí abajo podrás encontrarlos :

 

También  me gustaría que vieras este video de 2 minutos para entender dónde estamos y a dónde vamos


Mi equipo es Mac

macOS Catalina
Versión 10.15.2

MacBook Pro (15-inch, 2018)
Procesador 2,9 GHz Intel Core i9 de 6 núcleos
Memoria 32 GB 2400 MHz DDR4
Gráficos Intel UHD Graphics 630 1536 MB


En el último tutorial de la cadena (https://www.adictosaltrabajo.com/2020/03/18/el-juego-de-la-vida-en-typescript-con-tdd-y-medida-de-cobertura-en-jest/), en el que veíamos el juego de la vida, aprendidos como podemos hacer TDD con TypeScript.

Lo suyo, cuando construimos software, es que definamos un proyecto en base a historias de usuario y que cada historia, para estar Ready tenga unos criterios de aceptación.

Estos criterios de aceptación deben estar escritos por personal no técnico (en Product Owner o en su lenguaje). Sería interesante investigar un poco los conceptos de ATDD y BDD.

Vamos a verlo con un ejemplo sencillo instalando Cucumber en nuestro proyecto en Visual Studio Code y TypeScript. Nos va a permitir escribir los test en lenguaje natural (aunque no quita entender lo que hay detrás).

Tengo que darle las gracias a Elliot DeNolf porque con su tutorial es francamente sencillo resolver la primera parte, instalar Cucumber. Voy a procurar complementarlo un poquito.

https://dev.to/denolfe/cucumber-js-with-typescript-44cg

Lo primero que voy a hacer es instalar una extensión de Visual Studio Code llamada Cucumber (Gherkin) Full Support. Con ella vamos a manejar los ficheros de un modo más cómodo.

Usaré un fichero de texto (comandos.txt) donde almacenar los comandos que vamos usando en el terminal.

Para instalar estas son las instrucciones:

Instalar cucumber

npm i -D cucumber cucumber-tsflow cucumber-pretty ts-node typescript chai

npm i -D @types/cucumber @types/chai

Tendríamos que investigar un poquito más esto de las vulnerabilidades (os dejo el comando).

npm audit fix

Para que nos funcione Cucumber tenemos que habilitar las anotaciones en nuestro proyecto a través de la activación de “Decorators”. Os recomiendo visitar el link: https://www.typescriptlang.org/docs/handbook/decorators.html

Nos vamos al fichero tsconfig.json y activamos el parámetro: “experimentalDecorators : true”.

Una vez instalado Cucumber, la extensión y los decoradores creamos una carpeta features y dentro un fichero calculadora.feature, que tendrá el DSL de Gherkin con los escenarios de prueba escritos en lenguaje de alto nivel.

Establecemos las condiciones iniciales con Given, lo que sucede con When y lo que queremos comprobar con Then. También, si queremos encadenar operaciones, podemos usar And.

# calculadora.feature
Feature: Operaciones básicas

    Scenario: Suma simple
        Given Un valor de partida de 200
        When se le suma 100
        Then el resultado debe ser 300

    Scenario: Resta simple
        Given Un valor de partida de 200
        When se le resta 100
        Then el resultado debe ser 100

    Scenario: Operaciones múltiples
        Given Un valor de partida de 200
        When se le suma 300
        And se le resta 400
        Then el resultado debe ser 100

Una vez escrito nuestro fichero de alto nivel tenemos que enlazarlo con el código. Si os fijáis, es una expresión textual con un valor en uno o varios puntos.

import { binding, given, then, when } from "cucumber-tsflow";
import { assert } from "chai";

class calculadoraImp {
  resultado: number = 0;

  suma(operando: number): void {
    this.resultado += operando;
  }
  resta(operando: number): void {
    this.resultado -= operando;
  }
}

@binding()
export class calculadora {
  private cal: calculadoraImp = new calculadoraImp();

  @given(/Un valor de partida de (d*)$/)
  public dadoPrimerOperador(cantidad: number) {
    this.cal.resultado = Number(cantidad);
  }

  @when(/se le suma (d*)$/)
  public suma(cantidad: number) {
    this.cal.suma( Number(cantidad));
  }

  @when(/se le resta (d*)$/)
  public resta(cantidad: number) {
    this.cal.resta( Number(cantidad));
  }

  @then(/el resultado debe ser (d*)$/)
  public resultadoDebeSer(cantidadEsperrada: number) {
    assert.equal(this.cal.resultado, cantidadEsperrada);
  }
}

Y en el fichero package.json añadimos un nuevo comando para lanzar Cucumber.

«lanza cucumber»: «./node_modules/.bin/cucumber-js -p default»,

Este es el aspecto de nuestro fichero de comando.

Y ejecutando el comando vemos como se mezclan las variables del test de alto nivel con las llamadas al patrón de código.

Lo único a destacar es que hay que ser muy estricto con los patrones para que funcione bien: mayúsculas, minúsculas, etc..

Bueno, con esto podemos ver que podemos escribir test de alto nivel y enlazarlo con código de bajo nivel con facilidad.

En los tutoriales anteriores usábamos el Framework de test Jest para probar.

Parece que tiene sentido no cambiar el modo de trabajar.

Por suerte disponemos del paquete jest-cucumber que nos permite enlazar las dos cosas: https://www.npmjs.com/package/jest-cucumber

Las instrucciones están muy bien, os invito a visitar su página.

Instalamos.

Modificamos el fichero jest.config.js para incluir otros patrones de ficheros (steps).

Hay que escribir el código de enlace entre las dos cosas, el fichero de escenarios y fuente. Es lo que podéis ver en esta captura de la documentación:

Vamos a instalar ahora otra extensión llamada Jest-cucumber Code generador que nos va a generar el código particular del test automáticamente.

Una vez instalada la extensión, sobre el fichero calculadora.feature marcamos un escenario y con el botón derecho pulsamos la opción “Generate code from feature”.

Y nos genera las estructura de enlace que necesitamos.

test('Multiplicación simple', ({
  given,
  when,
  then
}) => {
  given(/^Un valor de partida de (.*)$/, (arg0) => {

  });

  when(/^se le multiplica por (.*)$/, (arg0) => {

  });

  then(/^el resultado debe ser (.*)$/, (arg0) => {

  });
});

Este es el código completo que se encuentra en la carpeta de test, llamado cucumber.test.ts.

Completamos las funciones.

import { defineFeature, loadFeature } from "jest-cucumber";

const feature = loadFeature("./features/calculadora.feature");

class calculadora {
  resultado: number = 0;

  suma(operando: number): void {
    this.resultado += operando;
  }
  resta(operando: number): void {
    this.resultado -= operando;
  }
}

defineFeature(feature, test => {
  test("Resta simple", ({ given, when, then }) => {
    let cal: calculadora = new calculadora();

    given(/^Un valor de partida de (.*)$/, arg0 => {
      cal.resultado = Number(arg0);
    });

    when(/^se le resta (.*)$/, arg0 => {
      cal.resultado -= Number(arg0);
    });

    then(/^el resultado debe ser (.*)$/, arg0 => {
      expect(cal.resultado).toBe(Number(arg0));
    });
  });
});

Y lanzamos con Jest como cualquier otro test existente. Vemos lo sencillo que es.

Una de las cosas que podemos hacer es usar las palabras clave en castellano para modelar escenarios.

Solo tenemos que decir en el fichero calculadora.feature que el lenguaje es Español: “es” con #language: es

La única pega que os vais a encontrar son los conflictos a la hora de generar el código, que tenéis que seguir poniendo los patrones en Ingles (supongo que también habría parche para eso, pero no merece la pena).

Una de as cosas interesantes de trabajar con Cucumber es la posibilidad de definir tablas de datos (incluso anidadas como arrays de mapas) ver :https://github.com/cucumber/cucumber-js/blob/master/docs/support_files/data_table_interface.md

Veamos un ejemplo sencillo donde probar muchas operaciones con un array:

Escenario: tabla de datos
Dados los siguientes datos: 
| Operador1 | Operador2 | Operacion | Resultado |
| 10        | 5         | +         | 15        |
| 20        | 10        | -         | 10        |
Entonces las reglas se cumplen

Este es el código generado por la extensión:

test('tabla de datos', ({
  dados,
  entonces
}) => {
  dados('los siguientes datos:', (table) => {

  });

  entonces('las reglas se cumplen', () => {

  });
});

Vamos, antes de empezar, a volcar a la consola el parámetro table donde recibimos los datos para ver la pinta que tiene. Es un array de pares clave-valor con el nombre de las columnas.

La única gracia es que dentro del código del test en TypeScript lo declaremos como array de tipo any para poder hacer foreach let tabla: any[];

Este es el código completo.

import { defineFeature, loadFeature } from "jest-cucumber";

const feature = loadFeature("./features/calculadora.feature");

class calculadora {
  resultado: number = 0;

  suma(operando: number): void {
    this.resultado += operando;
  }
  resta(operando: number): void {
    this.resultado -= operando;
  }
}

defineFeature(feature, test => {
  test("Resta simple", ({ given, when, then }) => {
    let cal: calculadora = new calculadora();

    given(/^Un valor de partida de (.*)$/, arg0 => {
      cal.resultado = Number(arg0);
    });

    when(/^se le resta (.*)$/, arg0 => {
      cal.resultado -= Number(arg0);
    });

    then(/^el resultado debe ser (.*)$/, arg0 => {
      expect(cal.resultado).toBe(Number(arg0));
    });
  });

  test("tabla de datos", ({ given, when }) => {
    let tabla: any[];
    let cal: calculadora;

    given("los siguientes datos:", table => {
      tabla = table;
    });

    when("las reglas se cumplen", () => {
      console.log(tabla);

      tabla.forEach(element => {
        {
          cal = new calculadora();
          cal.resultado = Number(element.Operador1);

          if (element.Operacion == "+") {
            cal.suma(Number(element.Operador2));
          } else if (element.Operacion == "-") {
            cal.resta(Number(element.Operador2));
          } // para otras operaciones
          else {
          }
          expect(cal.resultado).toBe(Number(element.Resultado));
        }
      });
    });
  });
});

Y podemos ver el resultado de la ejecución.

Cambio un dato de la tabla (15 por 16) para verificar que funciona correctamente. Podemos ver cómo el test nos avisa del fallo.

Ya para terminar, vamos a configurar Jest para que genere un informe básico html con test-html-reporter.

Lo instalamos: ver https://www.npmjs.com/package/jest-html-repo

Modificamos el fichero jest.config.js

Añadimos un reporter por defecto y al ejecutar veremos en la consola el path del informe generado en verde.

Ya tenemos la base para entender cómo se configuran reporters, por si queremos añadir alguno más específico.

Bueno, espero que este tutorial os sirva para configurar vuestro entorno y para mejorar vuestro escenario de BDD/TDD con TypeScript.

Como pasa siempre, todo es fácil una vez hecho, pero creedme que lleva un rato hasta que juntas todas las piezas dispersas 😉

La entrada Configuración de Cucumber.js y Jest-Cucumber en Visual Studio Code con TypeScript se publicó primero en Adictos al trabajo.

Accesibilidad y daltonismo a la hora de diseñar

$
0
0

Índice de contenidos

  1. Introducción
  2. Tipos de daltonismo y herramienta
  3. Los colores no son todo
  4. Las texturas ayudan
  5. Siempre tiene que haber contraste
  6. Conclsuiones

 

1.Introducción

El 8% de la población mundial es daltónica, es un dato muy a tener en cuenta, pero pocas veces lo tenemos. Muchas veces estamos en la fase de diseño de un proyecto y definimos tipografía, wireframes, paleta de colores, etc., y no nos paramos a pensar en los usuarios con este tipo de problemas visuales. Tenemos que acordarnos de todas estas personas  y pensar también en ellas, ya que para que un producto sea accesible significa que llegue al mayor número de personas posibles sin hacer distinciones.

Para solventar esta problemática hay una serié de prácticas y herramientas que nos ayudaran a que nuestros diseños (webs o ui) sean más accesibles.

2.Tipos de daltonismo y herramientas

Si estamos diseñando con Sketch, Figma o Adobe XD hay un plugin llamado Stark que te ayuda a previsualizar rápidamente las distintas formas de daltonismo que hay para realizar los ajustes necesarios.

El plugin es gratuito aunque solo te dará opción a las 3 opciones básicas de daltonismo ( Pratanopia, Deuteranopia, Tritanopia), si quieres las demás versiones tendrás que hacerte con la versión de pago.

Si estamos diseñando con Illustator o Photoshop, estos programas de Adobe ofrecen un algunos de los principales grupos de daltónicos (Ver – Ajustes de prueba – ceguera de los colores)

Otra herramienta gratuita y con todos los tipos de daltonismo es Colorblinding, una extensión para el navegador Chrome que puedes utilizar e implementar muy bien si trabajas con Figma.

Aquí podemos ver los distintos tipos de daltonismos y sus diferencias, estas simulaciones han sido realizadas con Colorblinding

Tipos de daltonismo

    1. La protanopia es la carencia de sensibilidad al color rojo, una disfunción visual relacionada con la percepción del color.
    2. La deuteranopía o deuteranopsia es una disfunción visual consistente en alteración para la percepción del color. Los conos de la retina responsables de la recepción de luz con longitud de onda correspondiente al color verde están ausentes o no son funcionales
    3. La tritanopia es una disfunción visual relacionada con la percepción del color. Consiste en la carencia de sensibilidad al color azul, denominada también dicromacia azul.
    4. La protanomalía, o Anomalía del tipo Hart, ​ es una discromatopsia, caracterizada por la falta de sensibilidad, por parte de los conos oculares, a longitudes de onda largas, y por consecuencia, a la incapacidad de distinguir el rojo
    5. La deuteranomalía son imperfecciones donde son sensibles a longitudes de onda media (verde), pero el resultado final es similar a la protanopia, con la excepción de que los rojos no se ven tan oscuro. Deuteranomalía es menos grave de las dos condiciones. Aunque los individuos con deuteranomalía probablemente no pueden percibir los rojos y verdes de la misma manera que lo puede ver la gente sin estos problemas, a menudo pueden distinguir entre las tonalidades de rojos y verdes con relativa precisión
    6. La tritanomalía es una anomalía visual congénita que afecta a la visión de los colores. El individuo que la presenta tiene reducida capacidad para distinguir la diferencia entre algunos tonos de azul y amarillo. Es una variante poco frecuente de discromatopsia o daltonismo que presenta el 0.01 % de la población
    7. La acromatopsia (también llamada monocromatismo) es una enfermedad congénita y no progresiva que consiste en una anomalía de la visión a consecuencia de la cual sólo son percibidos los colores blanco, negro, gris y todas sus tonalidades
    8. La monocromía de cono azul (BCM) es una enfermedad ocular hereditaria que causa una discriminación grave del color, baja visión, nistagmo y fotofobia debido a la ausencia de funcionalidad de las células fotorreceptoras de cono rojo (L) y verde (M) en la retina.

3.Los colores no son todo

Los colores siempre nos han ayudado a diferenciar entre las distintas acciones, cuando algo esta bien se suele representar con el color verde, y cuando es lo contrario con el rojo. Hasta ahora con eso bastaba, ahora ya no. Una buena practica y solución es implementar un mensaje con un título o un icono.

Ejemplo visual

El 99% de todos los daltónicos sufren deficiencia visual al color rojo-verde.

4.Las texturas ayudan

Otra buena practica es agregar texturas y patrones en las zonas donde sea posible para dar énfasis al contraste entre objetos.

Ejemplo visual


Siempre tiene que haber contraste

Otra de las reglas para que nuestro producto sea accesible es comprobar el contraste que hay entre dos colores. La W3C es la encargada de dictar las directrices de accesibilidad de la web.

  • Para poder pasar el certificado AA tiene que haber un ratio de contraste para el texto de 4.5:1. Para los textos grandes y titulares necesitan mínimo un 3:1.
  • Para AAA, el contraste requerido para el texto es 7:1. Para los textos grandes y titulares necesitan mínimo un 4.5:1

 

Para realizar estas métricas hay una selección de herramientas a nuestra disposición.

Stark, ya hemos hablado de ella antes, a parte de ver los distintos tipos de daltonismo también nos permite ver el contraste como la accesibilidad entre dos colores.

Solo tendremos que seleccionar  las dos capas con los colores y seleccionar la opción de Check Contrast en el plugin.

Contraste es una app de escritorio gratuita para Mac OSX que nos permite ver el contraste entre dos colores así como su accesibilidad.

 

Contrast-ratio es una web que al igual que la aplicación anterior nos permite ver el contraste entre dos colores así como su accesibilidad.

Su funcionamiento es muy sencillo, solo tenemos que introducir el color ya sea en formato Hexadecimal o el nombre de color en ingles.


6.Conclusiones

Como has comprobado existen un par de prácticas y herramientas nada costosas de llevar a cabo que nos ayudaran a que nuestro diseño sea más accesible, no es una obligación, pero si un deber intentar hacer que nuestros diseños (webs o apps) sean más accesibles. Gracias a esto nuestras webs se posicionarán mejor y tendrán una mayor accesibilidad para los usuarios.

 

La entrada Accesibilidad y daltonismo a la hora de diseñar se publicó primero en Adictos al trabajo.


Publicidad en aplicaciones, hay vida más allá de Admob

$
0
0

En este tutorial aprenderemos a implementar el SDK de Appodeal para mostrar anuncios en nuestra app.

Índice de contenidos

1. Introducción

Hace unas semanas terminé un proyecto personal con android, entonces llegó el momento de pensar cómo monetizar esta aplicación. Siempre o casi siempre recurrimos a google admob ya que es la más conocida, pero hay vida más allá de google.

2. Entorno

El tutorial está escrito usando el siguiente entorno:

  • Hardware: Portátil MacBook Pro 15′ (2,7 Ghz Intel Core i7, 16GB DDR3).
  • Sistema Operativo: Mac OS Catalina 10.15.3
  • Entorno de desarrollo: Android Studio
  • Lenguaje: Kotlin

3. ¿Qué es Appodeal?

Es un SDK para desarrolladores que unifica todas las redes publicitarias y nos ayuda a conseguir optimizar al máximo nuestras aplicaciones. Es decir, Appodeal reúne los anuncios adecuados de cada red publicitaria para conseguir la mayor monetización posible.

4. ¿Como funciona?

  1. El cliente solicita el anuncio.
  2. El servidor recibe las diferentes ofertas de las diferentes redes publicitarias asociadas -Amazon Products Ads, admob, inmobi, etc.-.
  3. El servidor compara los intereses del usuario con los anuncios recibidos de los proveedores.
  4. El cliente muestra el anuncio que más reporte monetario aporta.

5. ¿Qué diferencias encuentro entre Appodeal y el resto de servicios similares?

La principal diferencia es el alto eCPM ya que tiene implementación con la mayoría de proveedores de anuncios.

6. ¿Qué es eCPM?

En español es, Coste Efectivo Por Mil. Es un método de medición que nos permite ver la cantidad de ganancias que tendremos por cada mil impresiones de los anuncios en nuestra app.

Aquí puedes encontrar más información

7. Implementación en android

La característica principal que hizo que me decantara por de Appodeal fue la facilidad de implementación.

Aquí puede ver su documentación

  • A nivel de app en el build.gradle debemos agregar esta dependecia.

implementation 'com.appodeal.ads:sdk:2.6.2.+'

  • En el archivo AndroidManifest.xml debemos añadir los siguientes permisos.

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />

  • Ahora solo nos queda inicializarlo y mostrar en tipo de anuncio que queremos.
    • Primero creamos el componente en la vista.

<com.appodeal.ads.BannerView
        android:id="@+id/bannerTest"
        android:layout_width="400dp"
        android:layout_height="52dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"/>

    • Ahora nos vamos a nuestra clase.

private val appKey = "tu app key dada por appodeal"
Appodeal.initialize(activity, appKey, Appodeal.BANNER)
Appodeal.setBannerViewId(R.id.bannerTest)
Appodeal.show(this, Appodeal.BANNER_VIEW)

    • Así de fácil tendríamos ya un banner en nuestra aplicación.
  • También tenemos la posibilidad de usar otros tipos de anuncios:
    • INTERSTITIAL
    • REWARDED_VIDEO
    • NON_SKIPPABLE_VIDEO
    • NATIVE
    • MREC
  • Todas las implementaciones de los diferentes tipos de anuncios las puedes encontrar aquí

8. Conclusiones

Appodeal es un excelente SDK que a parte de tener un alto eCPM y conexión con todos los proveedores de anuncios, su implementación es muy fácil.

9. Referencias

La entrada Publicidad en aplicaciones, hay vida más allá de Admob se publicó primero en Adictos al trabajo.

Resumen Clean Code

$
0
0

En este resumen de uno de los libros más leídos en el mundo del software, encontrará las ideas claves y las bases del código limpio, incluye un total de 230 páginas resumidas, los casos prácticos no están resumidos.

Este PDF puede ayudar a aquellos que tengan dudas en leerse el libro, o aquellos que lo hayan leído hace tiempo y quieran refrescar un poco sus ideas.

No es un resumen largo y tedioso, es directo al grano, si quieres profundizar en algún tema siempre puedes comprar el libro, además en el resumen están especificadas las páginas del libro donde aparece algún ejemplo interesante que ver sobre el tema en cuestión.

En cualquier caso, si te gusta el resumen, te recomiendo leerte el libro y sobretodo realizar los casos prácticos que contiene, porque como dice el dicho: “La práctica hace al maestro”.

 

Descargar: CLEAN CODE

La entrada Resumen Clean Code se publicó primero en Adictos al trabajo.

Primeros pasos con Firebase / Firestore: almacenamiento transparente de datos en servidor.

$
0
0

 

Antes de empezar recordar que este tutorial forma parte de una cadena de tutoriales en las que pretendo simplemente probar tecnologías actuales. Aquí abajo podrás encontrarlos :

 

También  me gustaría que vieras este video de 2 minutos para entender dónde estamos y a dónde vamos


Mi equipo es Mac

macOS Catalina
Versión 10.15.2

MacBook Pro (15-inch, 2018)
Procesador 2,9 GHz Intel Core i9 de 6 núcleos
Memoria 32 GB 2400 MHz DDR4
Gráficos Intel UHD Graphics 630 1536 MB


Siguiendo nuestra colección de tutoriales sobre el desarrollo de TypeScript nos quedaba un punto interesante, el almacenamiento y recuperación de datos del servidor.

Cesar Alberca proponía la utilización de Firebase y la verdad es que es una solución muy sencilla y transparente.

Antes de seguir os invito ver este video que me ha ayudado mucho:  Firebase – Ultimate Beginner’s Guide en

https://www.youtube.com/watch?v=9kRgVxULbag

Lo primero que vamos a hacer es abrirnos una cuenta en Firebase.

Una vez creada, podemos darle a “Añadir Proyecto”.

Le he llamado “juegoroberto”.

Te dice que si quieres añadir Analytics. Yo en este caso le he dicho que no.

Se nos crea nuestro nuevo proyecto.

Uso el plan gratuito.

Cuando usemos una aplicación Web o movil debemos decidir el tipo de autenticación.

Podemos utilizar anónima o usuario y contraseña para registrar a priori algunos.

Ahora vamos a crear una base de datos. Voy a elegir Firestore.

Defino el modo de prueba para la base de datos, en 30 días se desactivará sin hacer nada.

Elegimos la ubicación, es este caso eur3 (europe-west). Estos detalles habría que mirarlos con cariño por temas de LOPD y RGPD.

Vamos a crear una colección.

La llamo coleccionjuego.

La bases de datos son estilo MongoDB, noSQL donde cada entrada es un documento JSON.

Vamos a insertar un nuevo documento (con id automático) y dos campos: usuario y Puntuación (no creo que sea una buena idea crear campos con mayusculas ni acentos) pero he probado y funciona todo correctamente.

Ahora vamos a conseguir las credenciales para poder acceder desde una aplicación. Para ello damos a registrar aplicación Web.

Nos genera una entrada que tenemos que simplemente copiar y pegar en nuestro documento TypeScript.

Ahora nos vamos a MS Visual Studio Code.

Si no tenemos el proyecto configurado para node escribiremos npm init para conseguir un fichero package.json.

Como nosotros partimos de nuestro proyecto solamente tendremos que ejecutar npm install -save firefase.

Os recomiendo actualizar las dependencias.

En mi caso he creado un nuevo script en package.json para ejecutarlo de vez en cuando.

  «updatempm»: «npm update –save/–save-dev -f»

Nuestro proyecto tendrá un fichero indexfirebase.html y otro llamado indexfirebase.ts con el código que ejecutaremos.

Modifico el Script de Parcel para arrancar automáticamente ese nuevo html. (indexfirebase.html).

Ahora vamos a instalar las herramientas de Firebase. Usamos el comando:

npm install -g firebase-tools

Una vez instalados los paquetes vamos a hacer login en local para tener acceso a la información del servidor desde nuestro entorno de desarrollo.

Firebase login

Nos pide que nos loguemos con nuestra cuenta y le damos permisos.

Nos dice que todo ha ido bien,

Ahora desde la línea de comando podemos usar el comando: firebase init database

Nos aparecen unas pantallas a modo de menú.

Automáticamente nos crea los ficheros que necesitamos para trabajar: firebase.json

Y database.rules.json

También un fichero .firebaserc

Con esto ya tenemos todo lo que necesitamos para la infraestructura,

Solamente necesitamos escribir el código para acceder a nuestra colección y recuperar un documento.

Y podemos comprobar en la consola cómo lo recuperamos.

Podemos irnos a la Web de firebase e insertar otros documentos.

Por suerte, tenemos también extensiones de MS Visual Studio Code que nos permiten manipular nuestra base de datos sin salir del entorno. Instalamos Firebase Explorer.

Ahora aparece una nueva opción en la que podemos ver la base de datos, colección, y documentos.

Es más, con el botón derecho podemos generar el código para acceder al documento seleccionado.

Vamos ahora a aprender como insertan un documento,

Este es el código (función add).

let nuevosdatos = {
  usuario: "Pepitop",
  Puntuación: "88"
};

console.log("Añadimos documento");

// Add a new document in collection "cities" with ID 'LA'
let setDoc = bd
  .collection("coleccionjuego")
  .add(nuevosdatos);

  let addDoc = bd.collection('coleccionjuego').add({
    usuario: 'Ana',
    Puntuación: 20
  }).then(ref => {
    console.log('Añadido documento con referencia: ', ref.id);
  });

Ahora vamos a ver cómo recuperamos documentos (5) ordenados por puntuación (descendente).

let colleccionResultados = bd.collection("coleccionjuego");
let consulta = colleccionResultados
  .orderBy("Puntuación", "desc")
  .limit(5)
  .get()
  .then(snapshot => {
    if (snapshot.empty) {
      console.log("No se encontraron documentos.");
      return;
    }

    snapshot.forEach(doc => {
      console.log(doc.id, "=>", doc.data());
    });
  })
  .catch(err => {
    console.log("Error recuperando documentos", err);
  });

Podemos ver el resultado en el navegador.

Aquí tenéis el fichero fuente completo con varias operaciones funcionando.

// Firebase App (the core Firebase SDK) is always required and
// must be listed before other Firebase SDKs
let firebase = require("firebase/app");

// Add the Firebase products that you want to use
require("firebase/auth");
require("firebase/firestore");

var firebaseConfig = {
  apiKey: "AIzaSyB-dhySzacXWXrD3bv-PwGC7UB-Ww",
  authDomain: "juegoroberto-ac9a2.firebaseapp.com",
  databaseURL: "https://juegoroberto-ac9a2.firebaseio.com",
  projectId: "juegoroberto-ac9a2",
  storageBucket: "juegoroberto-ac9a2.appspot.com",
  messagingSenderId: "9676380639",
  appId: "1:967619380639:web09fec166a9aa"
};
// Initialize Firebase
firebase.initializeApp(firebaseConfig);

let bd: any = firebase.firestore();

let coleccionDoc: any = bd
  .collection("coleccionjuego")
  .doc("qUlVB9JrQbDjyraailYq");

coleccionDoc.get().then(doc => {
  let datos = doc.data();
  // console.log("Mostramos los datos " + datos.usuario);
});
/*

let nuevosdatos = {
  usuario: "Pepitop",
  Puntuación: "88"
};

console.log("Añadimos documento");

// Add a new document in collection "cities" with ID 'LA'
let setDoc = bd
  .collection("coleccionjuego")
  .add(nuevosdatos);

  let addDoc = bd.collection('coleccionjuego').add({
    usuario: 'Ana',
    Puntuación: 20
  }).then(ref => {
    console.log('Añadido documento con referencia: ', ref.id);
  });

  */

let colleccionResultados = bd.collection("coleccionjuego");
let consulta = colleccionResultados
  .orderBy("Puntuación", "desc")
  .limit(5)
  .get()
  .then(snapshot => {
    if (snapshot.empty) {
      console.log("No se encontraron documentos.");
      return;
    }

    snapshot.forEach(doc => {
      console.log(doc.id, "=>", doc.data());
    });
  })
  .catch(err => {
    console.log("Error recuperando documentos", err);
  });

Bueno, supongo que habréis visto lo fácil que es tener una base de datos en el servidor donde guardar de un modo transparente nuestra información y su recuperación.

Con todos los componentes resueltos ya solo nos queda hacer nuestro programa, nuestro juego de Arkanoid.

La entrada Primeros pasos con Firebase / Firestore: almacenamiento transparente de datos en servidor. se publicó primero en Adictos al trabajo.

Hibernate – OneToOne, OneToMany, ManyToOne y ManyToMany

$
0
0

Analizaremos como funcionan algunas de las anotaciones que proporciona JPA que nos permiten manejar las relaciones de nuestra aplicación.

Índice de Contenidos

  1. Introducción
  2. Relaciones
    1. @OneToOne
    2. @OneToMany – @ManyToOne
    3. @OneToMany (unidireccional)
    4. @ManyToMany

 

1. Introducción

A través de las anotaciones que proporciona JPA cuando usamos Hibernate, podemos gestionar las relaciones entre dos tablas como si de objetos se tratasen. Esto facilita el mapeo de atributos de base de datos con el modelo de objetos de la aplicación. Dependiendo de la lógica de negocio y cómo modelemos, se podrán crear relaciones unidireccionales o bidireccionales.

 

2. Relaciones

2.1 @OneToOne (bidireccional)

La siguiente tabla muestra nuestro modelo de base de datos. student_id es la Foreign Key (a partir de ahora FK) que apunta a student.

modelo base de datos

Lo primero que deberíamos hacer es preguntarnos quién es el propietario de la relación, ya que esto determinará dónde irá la respectiva FK. Un estudiante tiene asociada una matrícula y esa matrícula está asociada a un único estudiante.

Una buena práctica es usar cascade en la entidad padre ya que nos permite propagar los cambios y aplicarlos a los hijos. En nuestro ejemplo, tuition no tiene sentido que exista si student no existe, por lo que student es el que tendrá el rol padre.Lo primero que deberíamos hacer es preguntarnos quién es el propietario de la relación, ya que esto determinará dónde irá la respectiva FK.

Si observamos la imagen anterior, he decidido que la FK la tenga tuition. Podemos decir que tuition es el propietario de la relación o propietario de esa FK (owning side) y student, la no propietaria de la relación, no posee esa FK (non-owning side). Pero, ¿Cómo creo una relación bidireccional en caso de que student quiera obtener las propiedades de tuition? Podríamos pensar en tener otra FK en student apuntando a tuition pero esto generaría una duplicidad innecesaria en nuestro modelo de base de datos. Para poder realizar este mapeo correctamente, entran en juego las anotaciones @JoinColumn y mappedBy.

@Entity
@Table(name = "student")
public class Student {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @OneToOne(mappedBy = "student", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
    private Tuition tuition;

    /* Getters and setters */
}

@Entity
@Table(name = "tuition")
public class Tuition {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private Double fee;

    // Que columna en la tabla Tuition tiene la FK
    @JoinColumn(name = "student_id")
    @OneToOne(fetch = FetchType.LAZY)
    private Student student;

    /* Getters and setters */
}

@JoinColumn nos permite indicar el nombre de la columna a la que queremos hacer referencia en la tabla tuition

Con mappedBy, podemos establecer una relación bidireccional ya que a pesar de tener una única FK, podemos relacionar ambas tablas. Al final, el objetivo de las anotaciones es dejar claro donde está la clave que mapea las relaciones.

orphanRemoval= true especifica que la entidad hijo debe ser eliminada automáticamente por el propio ORM si ha dejado de ser referenciada por una entidad padre. p.ej., tenemos una colección de items y eliminamos uno, ese item ha dejado de tener una referencia y será eliminado. Ojo, no confundir con cascadeType que son operaciones a nivel de base de datos.

fetchType=LAZY, Recupera la entidad solo cuando realmente la necesitamos. Importante destacar que la sesión debe estar abierta para poder invocar al Getter correspondiente y recuperar la entidad, ya que hibernate usa el patrón Proxy (object proxying) . En caso contrario (al cerrar la sesión), la entidad pasaría de estado persistent a detach y se lanzaría una excepción LazyInitializationException.

Vamos a crear un test muy simple para comprobar la sentencia sql que se está ejecutando.

@Test
    @Transactional
    @Rollback(false)
    public void check_sql_statement_when_persisting_in_one_to_one_bidirectional() {

        Student student = new Student();
        student.setName("Jonathan");
                
        Tuition tuition = new Tuition();
        tuition.setFee(150);
        tuition.setStudent(student);
        
        student.setTuition(tuition);

        entityManager.persist(student);
    }

sql

El uso de cascade en la entidad padre, hace que al persistir student se persista también tuition.

Una alternativa en el ejemplo que acabamos de ver es usar @MapsId. Como especifiqué anteriormente, matrícula no tiene sentido  que exista si estudiante no existe, solo puede haber asociada una matrícula  por estudiante. Con @MapsId  estamos especificando a Hibernate que student_id es PK (Primary Key) de tuition pero también es FK de student. Ambas entidades compartirán el mismo valor de identificación y no nos haría falta @GeneratedValue para la generación de nuevos ids en tuition.

modelo base de datos

@Entity
@Table(name = "tuition")
public class Tuition {

    @Id
    private Long id;

    private Double fee;
    
    @MapsId
    @OneToOne(fetch = FetchType.LAZY)
    private Student student;
    
    /* Getters and setters */
}

 

2.2 @OneToMany (bidireccional)

La siguiente tabla muestra nuestro modelo de base de datos. university_id es la FK que apunta a university.

modelo base de datos

Normalmente el owning side en este tipo de relaciones suele estar en el @ManyToOne y el mappedBy iría en la entidad padre.

@Entity
@Table(name = "university")
public class University {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @OneToMany(mappedBy = "university", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Student> students;

    /* Getters and setters */

@Entity
@Table(name = "students")
public class Student {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @ManyToOne()
    @JoinColumn(name = "university_id")
    private University university;

    /* Getters and setters */
}

 

2.3 @OneToMany (unidireccional)

modelo base de datos

En una relación unidireccional @OneToMany, la anotación @JoinColumn hace referencia a la tabla en base de datos del many (student en este caso). Por este motivo, vemos en la siguiente imagen @JoinColumn en la clase University. La clase Student únicamente tendrá los atributos id y name.

@Entity
@Table(name = "university")
public class University {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
    @JoinColumn(name = "university_id")
    private List<Student> students;

    /* Getters and setters */
}

Vamos a hacer el test y comprobar las sentencias sql que se generan.

@Test
    @Transactional
    @Rollback(false)
    public void check_sql_statement_when_persisting_in_one_to_many_unidirectional() {
        University university = new University();
        university.setName("Universidad de Las Palmas de Gran Canaria");

        Student student1 = new Student();
        student1.setName("Ana");

        Student student2 = new Student();
        student2.setName("Jonathan");

        university.setStudents(List.of(student1, student2));

        entityManager.persist(university);
    }

sql

¿Por qué se ejecutan los Update?

Al no indicar a student que debe tener una FK mapeando a university (como hicimos en el ejemplo anterior), Hibernate tiene que ejecutar sentencias adicionales para resolverlo. Una buena práctica es usar @ManyToOne si queremos que sea unidireccional o si no crear directamente una relación bidireccional, de este modo nos ahorraremos la ejecución de queries innecesarias

 

2.4 @ManyToMany (bidireccional)

Nuestro modelo de Base de datos es el siguiente

modelo base de datos

Con @ManyToMany debemos crear una tercera tabla para realizar el mapeo de ambas entidades. Esta tercera tabla tendrá dos FK apuntando a sus respectivas tablas padre. Por lo tanto, student_id apunta a la tabla student y course_id apunta a la tabla course.

@Entity
@Table(name="course")
public class Course {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    private Double fee;

    @ManyToMany(mappedBy = "courses")
    private Set<Student> students;

    /* Getters and setters */
}

@Entity
@Table(name="student")
public class Student {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @ManyToMany(cascade = {
            CascadeType.PERSIST,
            CascadeType.MERGE
    })
    @JoinTable(
            name = "student_course",
            joinColumns = {@JoinColumn(name = "student_id")},
            inverseJoinColumns = {@JoinColumn(name = "course_id")}
    )
    private Set<Course> courses;

    /* Getters and setters */
}

En este ejemplo el owning side es student y es donde se usa la anotación @JoinTable. Con ella, especificamos el nombre de la tabla que realiza el mapeo (student_course). JoinColumns apunta a la tabla del owning side (student) e InverseJoinColumns apunta a la tabla inversa del owning side (course). He decidido usar el cascade Merge y Persist, pero no cascade.Remove ya que si elimino un curso, no quiero que elimine los estudiantes asociados a él.

Como podemos ver en el ejemplo, estoy usando un Set en vez de List en la asociación. Esto es porque usando List, Hibernate elimina las filas del objeto que queremos eliminar y vuelve a insertar las demás. Esto es completamente innecesario e ineficiente.

La siguiente imagen muestra las sentencias sql generadas usando List. En este caso, tenemos un estudiante asociado a 4 cursos y queremos eliminar uno de ellos.


 
Recordemos el uso de Set si queremos evitar este tipo de comportarmiento indeseado

Si quieres leer el tutorial en inglés, puedes encontrarlo en mi perfil de dev.to

La entrada Hibernate – OneToOne, OneToMany, ManyToOne y ManyToMany se publicó primero en Adictos al trabajo.

Cómo pedir y fraccionar tu baja de paternidad en España durante 2020

$
0
0

 

1. Introducción

Hola a todos, como muchos ya sabréis, con los últimos cambios introducidos en material de igualdad de oportunidades en empleo y ocupación a través del Real Decreto-ley 6/2019, de 1 de marzo, durante este 2020 los padres podremos disfrutar de hasta 12 semanas de baja de paternidad.

Una de las cosas interesantes (al menos para mí) es el poder disfrutar de este periodo en varios periodos, hay un periodo de 4 semanas obligatorias tras el parto, pero el resto pueden posponerse, por ejemplo hasta que deje la lactancia materna o empiece a introducir otros alimentos o por que haya un periodo del año donde puedas disfrutar más del nuevo miembro de la familia (esto es muy personal, que cada uno lo lleve a su terreno).

Lo que me ha sorprendido, es que cuando me ha tocado realizar las gestiones a mí (enero 2020), no encontré mucha información, así que me gustaría aportar sobre todo puntos prácticos que espero que os resulten de gran ayuda, a mí me hubiese gustado tenerlos para no ir dándome cabezazos.

Importante tener en cuenta:

Periodos a disfrutar

  • Se pueden fraccionar los periodos, pero inicialmente es obligatorio coger 4 semanas tras el nacimiento.
  • Las 8 semanas restantes se pueden coger durante el primer año de vida del bebé.
  • Para regirte por el calendario de pago estándar (ver siguiente punto) debes coger semanas completas, no días (de hecho parece que hay algunos problemas para pedir periodos que no sean semanas completas).

Fechas de pago

  • Los pagos se realizan el día 25 de cada mes. Por lo que tendrás que estar pendiente para asegurarte que no te quedas casi 2 meses sin cobrar si tu hij@ nace en los primeros días del mes. Os pongo un ejemplo práctico de lo que sería un proceso tipo con varias casuísticas:
      1. Tu hij@ nace un viernes 01 de Enero por la tarde, como la oficina del registro que hay en los hospitales solo esta abierta por las mañanas y solo puedes inscribir al niño en las siguientes 48 horas, el lunes ya tienes que ir al Registro para hacer el trámite.
      2. El día 04 de Enero presentas la documentación en el Registro y obtienes el certificado de inscripción. Ese mismo día solicitas cita en la oficina de la Seguridad Social y te dan cita para el día 26 de Enero en una oficina que no es la tuya, por que en la tuya te dan cita para el 18 de Febrero.
      3. En el mejor de los casos, cobrarás el día 18 de Febrero a través de una transferencia de tu oficina, y en el peor, el 25 de Febrero a través del proceso estándar de pago. En resumen, casi dos meses después de haber cobrado tu última nómina..
  • Si la solicitud la realizas en la oficina física de la Seguridad Social (puedes pedir en cualquiera) que tienes asignada, te pueden hacer la transferencia en el mismo momento de presentar la documentación independientemente de la fecha, si es otra oficina NO.

Documentación que necesitas

NO están considerados aquí los casos de familias numerosas, parados, familias monoparentales, discapacitados, etc que pueden diferir de la información recogida a continuación.

  • DNI o pasaporte.
  • Certificado de baja emitido por la empresa. Tienes que solicitarlo al inicio de la baja. Este certificado te puede ser entregado en mano (o bien a través de correo electrónico en un PDF) o cargado directamente por la empresa (o una gestoría) directamente en el  Sistema RED.
  • Libro de familia o la certificación de la inscripción en el Registro Civil.

 

2. ¿Qué opciones hay para solicitar la baja paternal?

Por suerte tenemos varias opciones telemáticas y físicas, y cada una de ellas tiene pros y contras. En cada uno de los apartados lo que os voy a indicar en primer lugar son los aspectos que debéis tener en cuenta.

Vamos a partir de un caso concreto, Certificado de empresa cargado directamente en el Sistema RED por la empresa.

Debes disponer de un Certificado Digital, que puedes obtener uno a través de la FNMT.

Solicita y Configura tu Certificado Digital en mac con Firefox

Para este proceso he utilizado:

IMPORTANTE: Para la solicitud y la instalación del Certificado Digital, debes tener la misma versión de navegador y hacerlo desde el mismo ordenador.

Para evitar problemas asegúrate de haber desactivado las actualizaciones automáticas de Firefox. Para ello haz clic en Firefox > Preferencias y en la sección Actualizaciones de Firefox seleccionamos Buscar actualizaciones, pero permitirle elegir si instalarlas.

Imagen que muestra la pantalla de preferencias de Firefox
Ventana de preferencias de Firefox

Ahora sí, considerando que ya tengas Java y Firefox instalados:

  1. Solicita tu Certificado. Es un proceso online tras el que recibirás un correo con un Código de Solicitud, puedes iniciarlo a través de la sede electrónica de la Real Casa de la Moneda.
  2. Acredita tu identidad presentándote en una Oficina de Registro (puedes buscar una aquí), esto es una oficina de la Seguridad Social o de las Delegaciones y Administraciones de la AEAT. Debes llevar el Código de Solicitud del paso anterior.
  3. Descarga tu Certificado. Desde este enlace podrás descargar el certificado en tu ordenador.
  4. Instala el certificado en Firefox. Tienes el proceso detallado aquí.
  5. Instala el certificado en el Llavero de tu mac. Esto es FUNDAMENTAL (si pudiese poner «blinkeos» y colores chillones con muchos GIFs horteras, lo haría….), da igual que tengas el certificado descargado, instalado en Firefox, o que te hayas hecho una copia de seguridad (generada por defecto en formato .exec del certificado) en un USB, la aplicación de autofirma falla al leer el certificado y solo funciona si has importado el certificado al llavero del mac antes.

Para poder importar el certificado en tu llavero tienes que exportarlo desde Firefox, recuerda que la fuente que tienes no sirve. Puedes ver como exportar desde Firefox desde aquí. Este certificado sí que se exportará en formato .pkcs12 en lugar del .exec original.

Para importarlo en el llavero, tienes que abrir el llavero en tu mac (para mi lo más rápido es abrir spotlight (pulsa las teclas command+espacio) y escribe «llavero» pulsando intro para abrirlo. Una vez abierto pulsa en «+» en la parte superior derecha, busca el certificado elnombrequesea.pkcs12 que te has exportado de Firefox y selecciónalo para importarlo.

Imagen que muestra el llavero de mac para importar el certificado exportado de Firefox.
Llavero de mac desde donde importar el certificado de Firefox.

2.1. Solicitud del primer periodo a través de TUSS (Tu Seguridad Social)

A tener en cuenta:

  • Solo funciona con Certificado Digital (o Clave Permanente, nada de Cl@ve) que debe estar en vigor.
  • Si tu certificado electrónico es el del DNI, necesitarás tener configurado un lector de DNI en tu mac y al menos en mi caso… he tenido que darlo por imposible.
  • Si usas MacOS y Firefox el proceso es bastante tedioso y nada intuitivo, pero se puede.

Cómo hacerlo:

Necesitarás disponer de Certificado electrónico o Clave permanente, recuerda que Cl@ve no sirve (soy muy pesado, lo sé, pero es por tu bien).

Navega al portal: https://sede-tu.seg-social.gob.es/ y accede usando tu certificado instalado en Firefox. Si el certificado de baja de la empresa ha sido cargado directamente en RED ya te aparecerá una sección FAMILIA indicando que has tenido X hij@s para que puedas tramitar automáticamente la baja. Pulsa PEDIR TU PRESTACIÓN.

Imagen de la pantalla principal de la web TUSS Tu Seguridad Social, donde aparece ua sección FAMILIA desde la que solicitar directamente tu prestación.
Pantalla principal de TUSS desde donde pedir tu prestación.

Empezará un proceso donde nos aparecerá el detalle de la información que ha cargado la empresa a través de Sistema RED, puedes comprobar los datos. Si fuese necesario hacer alguna modificación, habría que hacer de nuevo la solicitud a la empres y esperar que estuviese cargada de nuevo antes de seguir con el proceso. Hacemos clic en PEDIR PRESTACIÓN.

Imagen que muestra una pantalla de TUSS que muestra el primer paso para solicitar tu prestación de paternidad, en ella se muestran los datos de la baja que se ha solicitado a la empresa.
Pantalla de TUSS con los datos de la baja.

Hay una opción «NO pides modificaciones al descanso» que aparece como para completar, hay que indicar que NO se piden modificaciones al descanso. NO os preocupéis, no tiene nada que ver con los periodos que queráis pedir, cada periodo se pide como si fuese una baja nueva.

Imagen de una pantalla de TUSS que muestra los datos personales para pedir la prestación
TUSS pantalla de datos personales para prestación

Una vez hayáis seguido todos los pasos se descargará un archivo que habrá que firmar, Firefox lanzará automáticamente AutoFirma y en ella tendréis que seleccionar usar un certificado del llavero del mac. Es la única forma en la que he conseguido que funcione.

2.2. Solicitud del primer periodo a través de una oficina física de la Seguridad Social

A tener en cuenta:

¡ATENCIÓN! Debido a la situación actual en España con relación al COVID-19, desde el día 16 de Marzo de 2020 no es posible acudir físicamente a una oficina y solo estarán disponibles las opciones telemáticas.

  • Es un trámite que exige si o si cita previa.
  • Hay que solicitar cita con antelación, los plazos para las citas previas están siendo de más de un mes (en mi caso, me reincorporé al trabajo antes de poder acudir a la oficina). Mi recomendación es hacerlo unos 15-20 días antes de la fecha de salida de cuentas (ya sabéis que esto no es matemático).
  • Si solicitas una oficina diferente de la tuya para que te atiendan antes, ten en cuenta que no te van a poder hacer la transferencia en el momento, por lo que tendrás que esperar hasta el siguiente día 25.

Cómo hacerlo:

  1. Configura tu certificado para acceder a TUSS (ya)
  2. Pide cita en la Seguridad Social (al menos 15 días antes de la fecha de salida de cuentas)
  3. Solicita el Certificado de baja de la empresa (al nacer el/la peque)
  4. Solicitud del primer periodo de la baja a través de TUSS
  5. Solicitud del segundo periodo de la baja a través del portal de la Seguridad Social

 

2.3. Solicitud de sucesivos periodos a través de la Sede Electrónica de la Seguridad Social

A tener en cuenta:

  • Como os decía al principio de la entrada, no se puede desde TUSS, esta opción aún no está disponible salvo para el primer periodo que se solicita.

Cómo hacerlo:

Hay que hacerlo directamente a través de la  Sede Electrónica de la Seguridad Social.

Si el enlace anterior no os funciona, podéis generarlo a el a través de TUSS. Para ello, accedéis al portal: https://sede-tu.seg-social.gob.es/.

Como ya no está la opción de solicitar directamente la prestación tenéis que entrar en la opción SIMULAR TU PRESTACIÓN.

Imagen que muestra una opción de TUSS para acceder a simular tu prestación de paternidad
Opción de TUSS para simular tu prestación

Tenéis que intentar solicitar el segundo periodo de baja que queréis, introduciendo la fecha de nacimiento y la fecha de inicio de este segundo periodo de baja (que evidentemente será mayor que la fecha de nacimiento, ya que habréis disfrutado ya del periodo obligatorio que comienza en la misma fecha de nacimiento).

Imagen que muestra la opción de TUSS para simular tu prestación
Pantalla de TUSS para simular tu prestación

Vais a ver que al intentar pedir una fecha de inicio de la baja diferente a la de nacimiento, os va a salir un error.

Imagen que muestra un error al intentar solicitar una simulación para un segundo periodo de baja de parternidad.
Error en la solicitud de simulación, aparece información sobre varios periodos de descanso.

Fijaos en la imagen anterior en que a la izquierda, justo debajo del campo para la fecha de  INICIO DE DESCANSO habrá aparecido una casilla de verificación con el texto «Quiero solicitar el descanso en varios periodos», que tiene un icono de información al lado. Haced clic en el icono. Os aparecerá el siguiente mensaje de información, y al final de este tendréis in texto marcado en verde que es un enlace para iniciar el proceso en la Sede Electrónica de la Seguridad Social.

Imagen que muestra un enlace desde TUSS al proceso de solicitud de la baja de paternidad en la Sede Electrónica de la Seguridad Social
Enlace desde TUSS al proceso de solicitud de la baja de paternidad en la Sede Electrónica de la Seguridad Social

Solo como información adicional, en el proceso que se os abrirá al seguir el enlace anterior, tendréis que rellenar un .PDF editable que adjuntar en el propio proceso, además de este tendréis que adjuntar el .PDF con la información de la baja que os habrá facilitado la empresa.

Espero que os sirva.

La entrada Cómo pedir y fraccionar tu baja de paternidad en España durante 2020 se publicó primero en Adictos al trabajo.

Viewing all 989 articles
Browse latest View live