Fechas y Expresiones Regulares

/artista/ unknow

Fechas y expresiones regulares

En el universo data, es muy frecuente tener que estar lidiando con campos de fechas. Ya sea desde el campo más habitual como es un campo de fecha de auditoría a otra clase de campos que den información relevante de un día, mes, año o periodo concreto. O incluso, tener parametrizado nuestro proceso por variables de ejecución cuyos valores sean fechas para por ejemplo, acotar la ejecución por periodos/rangos de fechas.

No es difícil determinar qué función usar para establecer estas fechas, tenemos desde el propio que viene por defecto en Scala (de java), o si estamos usando Spark por ejemplo, la función current_date () : Column, por ejemplo. Lo interesante y “difícil” es establecer un formato correcto o normalizado de estas fechas. Y más interesante, y por tanto más “difícil”, sería conseguir que para cualquier input de fechas en cualquier formato asumible, acabara teniendo un output con nuestro formato normalizado.

Contenido

Regular Expressions o Regex

Para la normalización de fechas, tenemos muchísimos recursos en internet que dan pistas y/o dan soluciones bastante buenas. En este artículo, voy a explicar como normalizar fechas en Scala mediante expresiones regulares (regular expressions o regex) con el fin aceptar cualquier fecha como input (ser flexibles al input) para la ejecución de nuestro proceso (proceso que va a depender de dicha fecha como parámetro de ejecución).

En las referencias al final del articulo dejaré una serie de enlace a sitios donde explican con buen detalle que son las expresiones regulares y herramientas online dónde podréis juguetear con ellas.

Caso de uso

Desarrollaremos esta idea partiendo de un proceso que: Dado una fecha mostrará por pantalla los valores contenidos en un CSV que comprenden por el campo Date desde esa fecha como input hasta la fecha más reciente.

Código Base

Contenido del CSV

El contenido del csv es una descarga del historial de valores que tiene las acciones de Apple. Esta información la hemos podido recoger desde Yahoo Finance. El contenido es el siguiente:

1. Usando precondición o requires

Si nos fijamos en nuestro código de arriba, cualquier fecha que le demos como input con formato distinto al que internamente conocemos como desarrollador, a ojos de usuario nos devolvería un resultado inesperado
sin entender muy bien por qué.

Por ejemplo, si le pasáramos como input 07/10/2021 [mm/dd/yyyy] (formato americano), 07-10-2021 [mm-dd-yyyy] o incluso 10-07-2021 [dd-mm-yyyy], nos mostraría incongruencias. En estos casos, el proceso podría entender que queremos que busque desde el año 7 o 10, respectivamente.

Por ello, una opción podría ser el uso de requires.

Requires

Los requires son requisitos o precondiciones a nivel de usuario de una funcionalidad, de manera que si cierto valor input no cumple la propiedad que requerimos que verifique, automáticamente para todo lo que esté haciendo para saltarte con un error. Añadido a esto, podemos customizar este error dando más
información sobre cual podría ser el problema.

Veamos un ejemplo

Queremos que las fechas que entren como input sean de la forma yyyy-mm-dd. Una precondición podría ser que el separador fuera ‘-’. Otra también podría ser que el primer número sea el año, es decir, que tenga exactamente 4 caracteres. Es decir:

Con esto, somos capaces de dar información al usuario. Por ejemplo, si ejecutamos con 2021/07/10, nos escupe el siguiente error:

Mientras que, si tenemos como input 07-10-2021

2. Usando Regex

Los requires es un buen paso, pero es un poco coñazo estar haciendo constantemente requires. ¿Qué te parece si empezamos a definir un normalizador de fechas? Para empezar, asumiremos que desde origen, nos llega constantemente dos formatos de fecha.
  • dd-mm-yyyy
  • yyyy/mm/dd

La operativa es muy sencilla, y lo que se define para uno, será igual para otro. Lo primero que haremos será definir la expresión regular de por ejemplo yyyy/mm/dd. Este tiene la forma:
([0-9]{4,})([/])([0-9]{2,})([/])([0-9]{2,})

  • [0-9]{4,} – 4 dígitos del 0 al 9
  • ([/]) – Un separador /
  • ([0-9]{2,}) – 2 dígitos del 0 al 9

Para el primer caso sería ([0-9]{2,})([-])([0-9]{2,})([-])([0-9]{4,}). Nuestra función de normalizar fechas quedaría tal que así:

Por supuesto, esto es tan solo un primer acercamiento. De aquí construiremos un constructor inteligente de manera que nos facilite muchísimo más la semántica del código. Pero para conocer los regex, no está nada mal.

Observación

Si nos fijamos, la expresión regular no es más que un string (encapsulado con las dobles comillas) con los patrones comunes de expresiones regulares, y lo importante, acabado en .r De esta forma, un objeto de tipo String ahora sí es de tipo Regex.

Antes de continuar al último paso de generalizar nuestro normalizador para que sea más flexible, construyamos nuestro datatype con constructor inteligente.

2.1 Smart Constructor - StandardDate

Para todos aquellos que no sepan que es un constructor inteligente o Smart Constructor, próximamente haré un articulo sobre ellos y lo útiles e importantes que son de cara al desarrollo.

Pero por ahora, quedaros con la idea de que son una forma óptima, correcta y eficiente de generar datatypes de manera guiada con la intención de evitar errores de ejecución y/o encapsulamientos sin sentido o incorrectos. Un diseño de patrón de construcción más, básicamente.

De manera que ahora nuestra forma de normalizar sea de la siguiente forma:

Con esto tenemos además, la flexibilidad de poder utilizar los parámetros del constructor day, month, y year del datatype StandardDate. Además de poder hacer pattern matching, que a priori, en nuestro caso de uso, quizás no tiene mucha utilidad, pero podría serlo, y sino, al menos a modo ilustrativo. Nuestro smart constructor es la función build(date: String, sep: String = «-«) .

Con esto, podríamos reconfigurar nuestra función principal de showFrom, evitando los requires, y por tanto evitando que rompa el código y asegurando que (casi) siempre pueda funcionar correctamente.

Casi siempre

Porque técnicamente sólo contempla dos patrones de fechas (dos regex). Deberíamos afinar más con controles de errores como Options, Either, etc.

2.2 Smart Constructor [Boosted] (Optional)

Este apartado es totalmente opcional que no interfiere con el tema que estamos tratando, simplemente vamos a afinar nuestro constructor inteligente para que trabaje de una forma correcta.

Para ello implementaremos una sistema de errores de los que a priori se contemple (esto es escalable):

Y añadiremos un control de errores o un error handler (de manera más o menos ad-hoc) mediante el tipo Either que gestione estos errores.

De esta forma el código quedaría de la siguiente forma:

3. Generalizando el uso de Regex

Llegamos a la recta final. ¿Que vamos a hacer? Pues que dado cualquier entrada de fecha podamos dar una fecha estandarizada para nuestro proceso. Contemplaremos, además de los anteriores, los siguientes:
  • yyyy/mm, yyyy-mm, mm/yyyy, mm-yyyy: Con esto que te devuelva el día 1 del mes mm y del año yyyy.
  • yyyy: Con esto que devuelva el día 1 del mes 1 del año yyyy.

Para ello, definiremos una forma genérica de gestionar los regex sin tener que estar constantemente añadiendo casos a nuestra función normalizeDate con pattern matching.

Definamos un nuevo datatype que exprese esta idea de las expresiones regulares para fechas:

De esta forma, para cada expresión que queramos recoger, solo debemos construirla mediante su patrón pattern y su forma de separar las distintas partes cuando haga match. Entonces, todas las formas de dates como input que admitiremos serán de la siguiente forma:

Le hemos dado una vueltita más, y hemos parametrizado el separador, para que podamos construir reconocimientos de patrones en formato dd-mm-yyyy, dd/mm/yyyy, dd#mm#yyyy, dd&&mm&&yyyy etc.

Lo que comentaba, por ejemplo, para el caso FullDateLastYear (que es cuando consideramos el año en última posición), tenemos que definirle el pattern, que sería: «([0-9]{2,})([$sep])([0-9]{2,})([$sep])([0-9]{4,})”

y la función map que configura la forma en la que recoge el patron desde el string cuando haga match:

dónde regex es el Regex que se genera de manera automática cuando se define el pattern.

Entonces, ahora la función normalizeDate de nuestro smart constructor StandardDate, puede ser generalizado y redefinido de la siguiente forma:

Y por tanto, para la ejecución final, definimos nuestra lista implícita de regex.

Código Final

Nuestro código final quedaría de la siguiente forma:

Lo ideal sería tener modularizado todo en una jerarquía de carpetas/objetos y no en un script de scala. Pero eso ya es como uno se organice.

Resultado

Con esto ya podríamos ejecutar nuestra función principal showFrom con un amplio abanico de dates como inputs de manera que el programa se autogestione y sea transparente para nosotros, desde el lenguaje natural.

Conclusión

Desde el punto de vista del desarrollo, esta clase de patrones de diseño que hemos usado para gestionar el uso de patrones con regex es altamente escalable y homogéneo. Lo que facilita el modus operandi de añadir nuevas formas de aceptar los inputs.

Sin embargo, requiere un alto conocimiento de scala a la hora de debuggear. Al final es el problema de siempre, código de lectura fácil en un script línea a línea y poco escalable VS código altamente scalable y generalizado pero requiere conocimiento avanzado.

En mi opinión personal, siempre voy a apoyar un diseño óptimo y escalable frente a los mares de códigos que a veces uno se encuentra. Considero que el desarrollador debe enfrentarse a este nivel de retos y crecer como profesional.