Fechas, gramáticas y lenguaje natural

[English version]Este enlace se abrirá en una ventana nueva

Los datos son críticos en cualquier organización y, como siempre ha pasado, cuanto más estructurados se encuentren mejor para el procesamiento de los mismos. Debemos tener en cuenta, además, que un adecuado almacenamiento de estos nos permitira bien a nosotros, o bien a una máquina, saber de qué habla el dato, cual es su objetivo, para que puede servir. En definitiva, proporcionarnos información suficiente como para poder emplearse en sistemas de búsqueda, indexación o cualquier otro objetivo afín.

A mi modo de ver uno de los tipos de datos primtiivos más complejos es el correspondiente a las fechas, en si mismo es un dato agregado, que embebe información como el año, el mes, el día de la semana, etc. Toda esta información puede ser, desde el punto de vista analítico, muy imporante, y nos puede ayudar a resolver preguntas como: ¿cuándo se consume más? ¿qué tendencia tenemos?, etc.

Pero más allá de su complejidad intrínseca, cuando manejamos fechas. casi con total seguridad, tenemos que hacer frente al problema de su periodicidad. ¿Es algo que ocurre una única ve?, ¿tiene una serie de reglas de repetición? Si queremos emplear fechas periódicas y utilizarlas de forma eficaz, necesitamos expandirlas que nos cuenten su historia, que nos hablen del futuro, y quizá del pasado, permitendo, así, que nuestros usuarios puedan utilizarlas, buscar sobre ellas o emplearlas para otros usos inicialmente no previstos.

Si analizamos el concepto de periodicidad, tenemos que atender a dos puntos de vista. Por una parte, y si sólo atendemos a lo que el usuario final piensa, podríamos hablar de un evento que ocurre todos los lunes, o quizá todos los años o todos los meses. Si nos ponemos la gorra de desarrollador o administrador de sistemas, podemos pensar en escenarios más complejos en los que una expresión crontab sería más que necesaria. Pero, siempre hay peros, hay expresiones que cuando tenemos requerimientos complejos no son fácilmente expresables o no están disponibles en todas las implementaciones. Imaginémonos los siguientes escenarios, que pese a lo que se pueda pensar, no son tan atípicos.

  • Un evento que ocurre el fin de semana más próximo al 15 de agosto
  • Un evento que ocurre el lunes después del lunes de Pascua (Lunes de Aguas)
  • Un evento que se produce el domingo después del Corpus.
  • Un evento que ocurre el último domingo de abril si no es el domingo de Pascua y en ese caso sería el siguiente.

portadaportada

El problema

A decir verdad, crear alguna de esas expresiones requeriría que introdujéramos expresiones en el cron cuanto menos complejas, haciendo uso de expresiones eval o similares. Incluso podría ser necesario que tuviéramos que desarrollar bien código a medida, bien copiado de ChatGPT. Sea como fuere, el problema no es simple y fuera del alcance del usuario medio. Me podéis decir que esas expresiones están inventadas y que no se usan en la realidad. En realidad, son expresiones reales de algunas festividades patronales de las que abundan en nuestro país.

Así pues, si nos encontramos en una tesitura como esta, manejando este tipo de periodicidades, debemos proporcionar algún tipo de mecanismo que nos garantice que el usuario puede utilizar la información de alguna forma, que puede explotarla. Hay soluciones sencillas, aunque todas ellas tienen sus limitaciones.

  • Podríamos dejar al usuario final que adivine la fecha de la que estamos hablando. Lamentablemente, eso imposibilitaría realizar sistemas de búsqueda adecuados y, en definitiva, le podría impedir realizar una planificación a largo plazo.
  • Quizá podríamos pensar en mandar recordatorios amistosos a los administradores, para que actualicen la información para los años venideros. Pero esta tarea es tan aburrida, repetitiva y propensa a errores que, de seguro, en algunos años la información no se encontrará actualizada.

Así pués, ¿qué deberíamos hacer? ¡Automatizarlo! A lo largo de este artículo, voy a intentar mostraros como lo estamos hacienda en Divisa iT y que solución aplicamos en aquellos de nuestros proyectos que lo necesitan.

Analizando el problema

Como es por todos conocido, cuando desarrollamos soluciones estas deben estar orientadas al usuario, tanto el final como el administrador de los sistemas. En este caso sabemos muy bien lo que el usuario final busca, que las fechas se entiendan que no tenga que buscar en el calendario cuando cae una fiesta. Pero, si pensamos en el administrador, ¿qué necesita?

  • Poder introducir la expresión de una forma normal, natural, sin tener que introducir un hechizo en un lenguaje esotérico.
  • La expresión debería ser tratada por el sistema por sus propios medios, cuanto menos tenga que trabajar quien administra la información, mejor para todos.

Abordar la solución a este problema de forma eficiente requiere que tengamos en cuenta varias consideraciones. Quizá deberíamos soportar lenguaje natural, y puesto que estamos en 2024 podríamos estar tentados de utilizar una herramienta de IA. La verdad, es que a veces, necesitamos que las respuestas sean predecibles, determinadas, y éste es uno de esos casos. Además, no está de más recordar que hace tiempo que hay soluciones sencillas que nos podrían permitir satisfacer este objetivo de forma eficaz. Estoy hablando de gramáticas.

Como imagino sabéis, una gramática es un conjunto de reglas que define la estructura de un lenguaje, su sintaxis y morfología. En informática, cuando hacemos referencia a gramáticas hablamos de una serie de reglas léxicas y semánticas que nos permiten transformar la expresión en algo manejable, código que funcione.

Así pues, la solución al problema requiere que definamos la gramática, sepamos como procesarla para, finalmente, concluir con lo que buscábamos desde el principio: fechas reales.

Por otra parte, lo que tenemos es un problema de software, así pués necesitamos tener claros los requerimientos, aquellos que nos van a permitir definir adecuadamente la gramática y el modelo subyacente. Pero, además, siendo un problema de software no queremos acoplamiento, sino que queremos que ambos elementos estén aislados y eso exige un mecanismo de comunicación entre ambos extremos.

Requerimientos

En el lenguaje cotidiano cuando hablamos de tiempo, nos encontramos, fundamentalmente, con dos modificadores que nos permiten actuar sobre él.

  • Adverbios de tiempo: antes, a las, el, después, cerca de.
  • Elementos que permiten concretar que nos permiten hablar de un día concreto, de una día de la semana, un mes, una semana, una quincena, lo que sea.

Así pues, nuestra gramática y nuestro modelado debería ser capaz de trabajar con estas expresiones. Gráficamente, podríamos imaginar nuestra fecha como un objeto que se comporta tal que así:

date-schemadate-schema


En resumen, nuestra fecha se transforma por los adverbios y los elementos de concreción generando una nueva fecha, que podría utilizarse de forma iterativa hasta que la expresión se resuelva de forma satisfactoria.

La gramática

Para la definición de la gramática hemos empleado las capacidades de ANTLR, esta herramienta incluye tanto un analizador léxico como semántico que permite la generación de un árbol (Árbol de sintaxis abstracta – AST). Tal y como comenté antes, lo que queremos es expresar lenguaje natural, eso, lamentablemente, impone una limitación y es que debemos definir un analizador léxico-semántico para cada idioma que queramos soportar.

A modo de ejemplo la siguiente imagen os muestra un ejemplo del analizador de gramática empleado para nuestro caso de uso.

grammargrammar

Como podéis observar, estamos utilizando la aproximación iterativa que os comentaba con anterioridad, tenemos una expresión de fecha principal – el atom – que se transforma por una expresión at (en un momento determinado) y que puede ser utilizado posteriormente por expresiones de tipo before (antes), after (después) o near (próximo a). Todas ellas se embeben en niveles superiores, permitiendo distintos niveles de recurso como se muestra en el siguiente ejemplo:

primer lunes del domingo de pascua al primer domingo de junio

grammar-samplegrammar-sample


A decir, verdad se trata de una expresión un tanto forzada, pero podemos modelar sin dificultad expresiones más sencillas y a la vez normales.

Tres domingos después del Corpus Christi

grammar-sample2grammar-sample2

Incluso, condiciones lógicas (si, si no)

def christmas: 25 de diciembre

def eve: 31 de diciembre

si christmas es igual ultimo sábado de diciembre entonces eve si no 1 día después de eve

grammar-sample3grammar-sample3

El sistema software subyacente

Tal y como comentaba antes, cuando desarrollamos software queremos evitar acoplamientos, queremos facilitar la capacidad de usos no previstos, programáticos, y la facilidad de probar, de aislar y reemplazar. Así pues, en este caso, aislar la gramática del código es bastante importante. Considerando este punto de vista, podríamos imaginar nuestro sistema como un simple elemento con diferentes métodos de entrada.

modelomodelo

Como podéis observar, sigue la estructura recursiva que os comentaba antes, proporcioanando además una serie de métodos que nos permiten procesar la expresión de fecha a partir del texto en lenguaje natural. Lógicamente además de la expresión necesitamos

  • El idioma del usuario, para saber si la semana empieza en lunes o en domingo.
  • La zona horaria
  • Si las semanas se computan completas (de lunes a domingo) o pueden ser "parciales", es decir empezar en cualquier día de la semana.

Juntando las piezas

Unificar todo esto no es complicado, si tenemos en cuenta que hay una aproximación muy simple y eficaz, utilizar una máquina de pila, en la que todos los datos que se van leyendo se van almacenando en la pila y son extractados cuando se necesita. Claro que, debemos tener en cuenta, que cada regla de la gramática debe ser tratada como una función independiente, es decir con una pila propia y que el resultado de la misma sea almacenado en la "pila padre", a fin de cuentas, una solución simple a un problema muy conocido.

stack-samplestack-sample

Usando así el sistema, podríamos encontrar soluciones a expresiones como las manifestadas en el siguiente ejemplo.

date-resolutiondate-resolution

Para saber más

Si tienes más interés y quieres saber cómo esto está realmente implementado, podrías consultar mi repo en githubEste enlace se abrirá en una ventana nueva con el código detallado, ten en cuenta que esta, además, publicando en Maven Central.