Ir al contenido
  1. Posts/

Fechas y expresiones regulares

·6 mins
Rafael Fernandez
Autor
Rafael Fernandez
Matemáticas, programación y cosas de la vida
Tabla de contenido
Fechas y expresiones regulares - Este artículo es parte de una serie.

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.

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).

Observación: Es cierto que existen otras técnicas como el uso de lentes; pero cuando lo que se pretende es algo muy concreto, para un atributo o dos como mucho, no veo la necesidad de usarlas.
Observació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.

Formato de fecha
#

Para entender cómo funcionan las expresiones regulares, es necesario entender un poco más sobre el formato de fecha.

La fecha se compone de tres partes: el día, el mes y el año. El mes es un número entre 1 y 12 y el año es cualquier número que tenga 4 dígitos.

La expresión regular que podemos usar para normalizar fechas en inglés es:

val dateRegex = "([0-9]{2})-([0-9]{2})-([0-9]{4})"

Esta expresión regular, es decir, la expresión que se encuentra entre paréntesis, tiene tres partes: el día, el mes y el año. Cada parte tiene un número de dígitos que debe tener, y que puede ser cualquier número.

Por ejemplo, el día 28 de febrero de 2022 sería: 28-02-2022.

También podemos usar un formato de fecha con otros separadores, por ejemplo, el formato dd/mm/aaaa, que sería: 28/02/2022.

val dateRegex = "([0-9]{2})/([0-9]{2})/([0-9]{4})"

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
#

import org.apache.spark.sql.functions.col
import org.apache.spark.sql.{DataFrame, SparkSession}

object RegexDate {

  val spark: SparkSession =
    SparkSession
      .builder()
      .master("local[*]")
      .getOrCreate()

  /** Format Date: YYYY-mm-dd */
  val data: DataFrame =
    spark.read.option("header", true)
      .csv("src/main/resources/historical-data/csv/stocks/AAPL.csv")

  def showFrom(date: String): Unit =
    data.where(col("Date") >= date).show(false)

  def main(args: Array[String]): Unit = {
    val date: String = "2021-07-10"
    showFrom(date)
  }

}

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:

Date,Open,High,Low,Close,Volume,Adj Close
2021-07-10,120.99,121.0,119.98,120.99,1000,120.99
2021-07-10,121.0,121.01,120.99,121.0,1000,121.0
2021-07-10,121.01,121.02,120.99,121.01,1000,121.01
2021-07-10,121.02,121.03,121.01,121.02,1000,121.02
2021-07-10,121.03,121.04,121.02,121.03,1000,121.03
2021-07-10,121.04,121.05,121.03,121.04,1000,121.04
2021-07-10,121.05,121.06,121.04,121.05,1000,121.05

Tabla
#

Date Open High Low Close Volume Adj Close
2021-07-10 120.99 121.0 119.98 120.99 1000 120.99
2021-07-10 121.0 121.01 120.99 121.0 1000 121.0
2021-07-10 121.01 121.02 120.99 121.01 1000 121.01
2021-07-10 121.02 121.03 121.01 121.02 1000 121.02
2021-07-10 121.03 121.04 121.02 121.03 1000 121.03
2021-07-10 121.04 121.05 121.03 121.04 1000 121.04

Usando precondiciones o require
#

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.

Observación: 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.

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:

 def showFrom(date: String): Unit = {
    require(date.contains("-") && date.split("-").length==3, "Date separator character must be '-'")
    require(date.split("-").head.length==4, "Date Format must be yyyy-mm-dd")
    data.where(col("Date") >= date).show(false)
  }

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

fechas-y-expresiones-regulares-img-39.png

Mientras que si usamos como input 07-10-2021, nos devuelve el siguiente resultado:

fechas-y-expresiones-regulares-img-40.png

Usando expresiones regulares
#

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 y 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}).

donde:

  • ([0-9]{4}) es un número de cuatro dígitos
  • ([0-9]{2}) es un número de dos dígitos
  • Cada parte está separada por /
Tip: Si queremos que el separador sea -, podemos hacerlo de la siguiente manera: ([0-9]{4})-([0-9]{2})-([0-9]{2}).

Ejemplo
#

  val noramlizeDateSep: String = "-"
  /* From yyyy/mm/dd date format */
  val dateRegex1: Regex = "([0-9]{4})/([0-9]{2})/([0-9]{2,})".r

  /* From dd/mm/yyyy date format */
  val dateRegex2: Regex = "([0-9]{2})-([0-9]{2})-([0-9]{4,})".r

  def normalizeDate(date: String): String = 
    date match {
      case dateRegex1(year, month, day) => Array(year, month, day).mkString(noramlizeDateSep)
      case dateRegex2(day, month, year) => Array(year, month, day).mkString(noramlizeDateSep)
    }

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.

Fechas y expresiones regulares - Este artículo es parte de una serie.