RegEx pour faire correspondre le modèle de date en Java

1. Introduction

Les expressions régulières sont un outil puissant pour faire correspondre différents types de modèles lorsqu'elles sont utilisées de manière appropriée.

Dans cet article, nous utiliserons le package java.util.regex pour déterminer si une chaîne donnée contient une date valide ou non.

Pour une introduction aux expressions régulières, reportez-vous à notre Guide de l'API Java Regular Expressions.

2. Présentation du format de date

Nous allons définir une date valide par rapport au calendrier grégorien international. Notre format suivra le modèle général: AAAA-MM-JJ.

Incluons également le concept d'une année bissextile qui est une année contenant un jour du 29 février. Selon le calendrier grégorien, nous appellerons un saut d' année si le nombre d'année peut être divisé également par 4 sauf pour ceux qui sont divisibles par 100 mais y compris ceux qui sont divisibles par 400 .

Dans tous les autres cas , nous appellerons une année régulière .

Exemples de dates valides:

  • 31/12/2017
  • 2020-02-29
  • 2400-02-29

Exemples de dates invalides:

  • 2017/12/31 : délimiteur de jeton incorrect
  • 2018-1-1 : zéros de tête manquants
  • 2018-04-31 : les mauvais jours comptent pour avril
  • 2100-02-29 : cette année n'est pas bissextile car la valeur divise par 100 , donc février est limité à 28 jours

3. Implémentation d'une solution

Puisque nous allons faire correspondre une date à l'aide d'expressions régulières, esquissons d'abord une interface DateMatcher , qui fournit une méthode de correspondance unique :

public interface DateMatcher { boolean matches(String date); }

Nous allons présenter la mise en œuvre étape par étape ci-dessous, en vue d'une solution complète à la fin.

3.1. Correspondance au format large

Nous allons commencer par créer un prototype très simple gérant les contraintes de format de notre matcher:

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(); } }

Ici, nous spécifions qu'une date valide doit être constituée de trois groupes d'entiers séparés par un tiret. Le premier groupe est composé de quatre entiers, les deux autres groupes ayant chacun deux entiers.

Dates de correspondance: 2017-12-31 , 2018-01-31 , 0000-00-00 , 1029-99-72

Dates non concordantes: 2018-01 , 2018-01-XX , 2020/02/29

3.2. Correspondance avec le format de date spécifique

Notre deuxième exemple accepte des plages de jetons de date ainsi que notre contrainte de mise en forme. Par souci de simplicité, nous avons limité notre intérêt aux années 1900 - 2999.

Maintenant que nous avons réussi à faire correspondre notre format de date général, nous devons le contraindre davantage - pour nous assurer que les dates sont réellement correctes:

^((19|2[0-9])[0-9]{2})-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])$

Ici, nous avons introduit trois groupes de plages d'entiers qui doivent correspondre:

  • (19|2[0-9])[0-9]{2}couvre une plage limitée d'années en faisant correspondre un nombre qui commence par 19 ou 2X suivi de quelques chiffres quelconques.
  • 0[1-9]|1[012]correspond à un numéro de mois compris entre 01 et 12
  • 0[1-9]|[12][0-9]|3[01]correspond à un numéro de jour compris entre 01 et 31

Dates correspondantes: 1900-01-01 , 2205-02-31 , 2999-12-31

Dates non concordantes: 1899-12-31 , 2018-05-35 , 2018-13-05 , 3000-01-01 , 2018-01-XX

3.3. Match du 29 février

Afin de faire correspondre correctement les années bissextiles, nous devons d'abord identifier quand nous avons rencontré une année bissextile , puis nous assurer que nous acceptons le 29 février comme date valide pour ces années.

Comme le nombre d'années bissextiles dans notre plage restreinte est suffisamment grand, nous devons utiliser les règles de divisibilité appropriées pour les filtrer:

  • Si le nombre formé par les deux derniers chiffres d'un nombre est divisible par 4, le nombre d'origine est divisible par 4
  • Si les deux derniers chiffres du nombre sont 00, le nombre est divisible par 100

Voici une solution:

^((2000|2400|2800|(19|2[0-9](0[48]|[2468][048]|[13579][26])))-02-29)$

Le motif se compose des parties suivantes:

  • 2000|2400|2800correspond à un ensemble d'années bissextiles avec un diviseur de 400 dans une plage restreinte de 1900 à 2999
  • 19|2[0-9](0[48]|[2468][048]|[13579][26]))correspond à toutes les combinaisons d'années de la liste blanche qui ont un diviseur de 4 et qui n'ont pas de diviseur de 100
  • -02-29matchs 2 février

Dates de mise en correspondance: 29/02/2020 , 29/02/2020 , 29h00-02-24

Dates non concordantes: 2019-02-29 , 2100-02-29 , 3200-02-29 , 2020/02/29

3.4. Matching General Days of Février

En plus de faire correspondre le 29 février dans les années bissextiles, nous devons également faire correspondre tous les autres jours de février (1 - 28) de toutes les années :

^(((19|2[0-9])[0-9]{2})-02-(0[1-9]|1[0-9]|2[0-8]))$

Dates de correspondance: 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

L'analyse d'expressions régulières complexes peut affecter considérablement les performances du flux d'exécution. L'objectif principal de cet article n'était pas d'apprendre un moyen efficace de tester une chaîne pour son appartenance à un ensemble de toutes les dates possibles.

Envisagez d'utiliser LocalDate.parse () fourni par Java8 si une approche fiable et rapide pour valider une date est nécessaire.

4. Conclusion

Dans cet article, nous avons appris à utiliser des expressions régulières pour faire correspondre la date strictement formatée du calendrier grégorien en fournissant également des règles de format, de plage et de longueur de mois.

Tout le code présenté dans cet article est disponible à l'adresse over sur Github. Il s'agit d'un projet basé sur Maven, il devrait donc être facile à importer et à exécuter tel quel.