1. Introducción
Las expresiones regulares son una herramienta poderosa para hacer coincidir varios tipos de patrones cuando se usan de manera apropiada.
En este artículo, usaremos el paquete java.util.regex para determinar si una cadena determinada contiene una fecha válida o no.
Para obtener una introducción a las expresiones regulares, consulte nuestra Guía de la API de expresiones regulares de Java.
2. Descripción general del formato de fecha
Vamos a definir una fecha válida en relación con el calendario gregoriano internacional. Nuestro formato seguirá el patrón general: AAAA-MM-DD.
Incluyamos también el concepto de año bisiesto que es un año que contiene un día del 29 de febrero. De acuerdo con el calendario gregoriano, llamaremos un año bisiesto si el número del año se puede dividir uniformemente entre 4, excepto aquellos que son divisibles por 100 pero que incluyen aquellos que son divisibles por 400 .
En todos los demás casos , llamaremos un año regular .
Ejemplos de fechas válidas:
- 2017-12-31
- 2020-02-29
- 2400-02-29
Ejemplos de fechas inválidas:
- 2017/12/31 : delimitador de token incorrecto
- 2018-1-1 : faltan ceros iniciales
- 2018-04-31 : los días incorrectos cuentan para abril
- 2100-02-29 : este año no es bisiesto ya que el valor se divide por 100 , por lo que febrero está limitado a 28 días
3. Implementación de una solución
Como vamos a hacer coincidir una fecha usando expresiones regulares, primero esbocemos una interfaz DateMatcher , que proporciona un método de coincidencias único :
public interface DateMatcher { boolean matches(String date); }
Vamos a presentar la implementación paso a paso a continuación, construyendo hacia la solución completa al final.
3.1. Coincidencia del formato amplio
Comenzaremos creando un prototipo muy simple que maneje las restricciones de formato de nuestro comparador:
class FormattedDateMatcher implements DateMatcher { private static Pattern DATE_PATTERN = Pattern.compile( "^\\d{4}-\\d{2}-\\d{2}$"); @Override public boolean matches(String date) { return DATE_PATTERN.matcher(date).matches(); } }
Aquí especificamos que una fecha válida debe constar de tres grupos de enteros separados por un guión. El primer grupo se compone de cuatro números enteros, y los dos grupos restantes tienen dos números enteros cada uno.
Fechas coincidentes: 2017-12-31 , 2018-01-31 , 0000-00-00 , 1029-99-72
Fechas no coincidentes: 2018-01 , 2018-01-XX , 2020/02/29
3.2. Coincidencia con el formato de fecha específico
Nuestro segundo ejemplo acepta rangos de tokens de fecha, así como nuestra restricción de formato. Para simplificar, hemos restringido nuestro interés a los años 1900-2999.
Ahora que coincidimos con éxito con nuestro formato de fecha general, debemos restringirlo aún más, para asegurarnos de que las fechas sean realmente correctas:
^((19|2[0-9])[0-9]{2})-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])$
Aquí hemos introducido tres grupos de rangos de números enteros que deben coincidir:
(19|2[0-9])[0-9]{2}
cubre un rango restringido de años al hacer coincidir un número que comienza con 19 o 2X seguido de un par de dígitos.0[1-9]|1[012]
coincide con un número de mes en un rango de 01-120[1-9]|[12][0-9]|3[01]
coincide con un número de día en un rango de 01-31
Fechas de coincidencia: 1900-01-01 , 2205-02-31 , 2999-12-31
Fechas no coincidentes: 1899-12-31 , 2018-05-35 , 2018-13-05 , 3000-01-01 , 2018-01-XX
3.3. Coincidiendo con el 29 de febrero
Para hacer coincidir los años bisiestos correctamente, primero debemos identificar cuándo nos encontramos con un año bisiesto y luego asegurarnos de que aceptamos el 29 de febrero como una fecha válida para esos años.
Como el número de años bisiestos en nuestro rango restringido es lo suficientemente grande, debemos usar las reglas de divisibilidad adecuadas para filtrarlos:
- Si el número formado por los dos últimos dígitos de un número es divisible por 4, el número original es divisible por 4
- Si los dos últimos dígitos del número son 00, el número es divisible por 100
He aquí una solución:
^((2000|2400|2800|(19|2[0-9](0[48]|[2468][048]|[13579][26])))-02-29)$
El patrón consta de las siguientes partes:
2000|2400|2800
coincide con un conjunto de años bisiestos con un divisor de 400 en un rango restringido de 1900-299919|2[0-9](0[48]|[2468][048]|[13579][26]))
coincide con todas las combinaciones de años de la lista blanca que tienen un divisor de 4 y no tienen un divisor de 100-02-29
partidos 2 de febrero
Fechas coincidentes: 2020-02-29 , 2024-02-29 , 2400-02-29
Fechas no coincidentes: 2019-02-29 , 2100-02-29 , 3200-02-29 , 2020/02/29
3.4. Días generales coincidentes de febrero
Además de hacer coincidir el 29 de febrero en los años bisiestos, también debemos hacer coincidir todos los demás días de febrero (1 al 28) en todos los años :
^(((19|2[0-9])[0-9]{2})-02-(0[1-9]|1[0-9]|2[0-8]))$
Fechas de coincidencia: 2018-02-01 , 2019-02-13 , 2020-02-25
Non-matching dates: 2000-02-30, 2400-02-62, 2018/02/28
3.5. Matching 31-Day Months
The months January, March, May, July, August, October, and December should match for between 1 and 31 days:
^(((19|2[0-9])[0-9]{2})-(0[13578]|10|12)-(0[1-9]|[12][0-9]|3[01]))$
Matching dates: 2018-01-31, 2021-07-31, 2022-08-31
Non-matching dates: 2018-01-32, 2019-03-64, 2018/01/31
3.6. Matching 30-Day Months
The months April, June, September, and November should match for between 1 and 30 days:
^(((19|2[0-9])[0-9]{2})-(0[469]|11)-(0[1-9]|[12][0-9]|30))$
Matching dates: 2018-04-30, 2019-06-30, 2020-09-30
Non-matching dates: 2018-04-31, 2019-06-31, 2018/04/30
3.7. Gregorian Date Matcher
Now we can combine all of the patterns above into a single matcher to have a complete GregorianDateMatcher satisfying all of the constraints:
class GregorianDateMatcher implements DateMatcher { private static Pattern DATE_PATTERN = Pattern.compile( "^((2000|2400|2800|(19|2[0-9](0[48]|[2468][048]|[13579][26])))-02-29)$" + "|^(((19|2[0-9])[0-9]{2})-02-(0[1-9]|1[0-9]|2[0-8]))$" + "|^(((19|2[0-9])[0-9]{2})-(0[13578]|10|12)-(0[1-9]|[12][0-9]|3[01]))$" + "|^(((19|2[0-9])[0-9]{2})-(0[469]|11)-(0[1-9]|[12][0-9]|30))$"); @Override public boolean matches(String date) { return DATE_PATTERN.matcher(date).matches(); } }
We've used an alternation character “|” to match at least one of the four branches. Thus, the valid date of February either matches the first branch of February 29th of a leap year either the second branch of any day from 1 to 28. The dates of remaining months match third and fourth branches.
Since we haven't optimized this pattern in favor of a better readability, feel free to experiment with a length of it.
At this moment we have satisfied all the constraints, we introduced in the beginning.
3.8. Note on Performance
El análisis de expresiones regulares complejas puede afectar significativamente el rendimiento del flujo de ejecución. El propósito principal de este artículo no fue aprender una forma eficiente de probar una cadena para determinar su pertenencia a un conjunto de todas las fechas posibles.
Considere usar LocalDate.parse () proporcionado por Java8 si se necesita un enfoque confiable y rápido para validar una fecha.
4. Conclusión
En este artículo, hemos aprendido cómo usar expresiones regulares para hacer coincidir la fecha estrictamente formateada del calendario gregoriano al proporcionar reglas sobre el formato, el rango y la duración de los meses.
Todo el código presentado en este artículo está disponible en Github. Este es un proyecto basado en Maven, por lo que debería ser fácil de importar y ejecutar como está.