RegEx per la corrispondenza del modello di data in Java

1. Introduzione

Le espressioni regolari sono un potente strumento per la corrispondenza di vari tipi di modelli se utilizzate in modo appropriato.

In questo articolo, utilizzeremo il pacchetto java.util.regex per determinare se una determinata stringa contiene una data valida o meno.

Per un'introduzione alle espressioni regolari, fare riferimento alla nostra Guida all'API Java Regular Expressions.

2. Panoramica del formato della data

Definiremo una data valida in relazione al calendario gregoriano internazionale. Il nostro formato seguirà lo schema generale: AAAA-MM-GG.

Includiamo anche il concetto di anno bisestile che è un anno contenente un giorno del 29 febbraio. Secondo il calendario gregoriano, chiameremo un anno bisestile se il numero dell'anno può essere diviso equamente per 4 ad eccezione di quelli che sono divisibili per 100 ma inclusi quelli che sono divisibili per 400 .

In tutti gli altri casi , chiameremo regolarmente un anno .

Esempi di date valide:

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

Esempi di date non valide:

  • 31/12/2017 : delimitatore di token non corretto
  • 2018-1-1 : zero iniziali mancanti
  • 2018-04-31 : i giorni sbagliati contano per aprile
  • 2100-02-29 : quest'anno non è un salto in quanto il valore divide per 100 , quindi febbraio è limitato a 28 giorni

3. Implementazione di una soluzione

Dal momento che abbineremo una data utilizzando espressioni regolari, prima abbozziamo un'interfaccia DateMatcher , che fornisce un unico metodo di corrispondenza :

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

Presenteremo l'implementazione passo dopo passo di seguito, costruendo per completare la soluzione alla fine.

3.1. Corrispondenza del grande formato

Inizieremo creando un prototipo molto semplice che gestisca i vincoli di formato del nostro 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(); } }

Qui stiamo specificando che una data valida deve essere composta da tre gruppi di numeri interi separati da un trattino. Il primo gruppo è composto da quattro numeri interi, mentre i restanti due gruppi hanno due numeri interi ciascuno.

Date corrispondenti: 2017-12-31 , 2018-01-31 , 0000-00-00 , 1029-99-72

Date non corrispondenti: 2018-01 , 2018-01-XX , 2020/02/29

3.2. Corrispondenza al formato data specifico

Il nostro secondo esempio accetta intervalli di token di data e il nostro vincolo di formattazione. Per semplicità, abbiamo limitato il nostro interesse agli anni 1900 - 2999.

Ora che abbiamo abbinato con successo il nostro formato di data generale, dobbiamo vincolarlo ulteriormente per assicurarci che le date siano effettivamente corrette:

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

Qui abbiamo introdotto tre gruppi di intervalli interi che devono corrispondere:

  • (19|2[0-9])[0-9]{2}copre un intervallo limitato di anni abbinando un numero che inizia con 19 o 2X seguito da un paio di cifre qualsiasi.
  • 0[1-9]|1[012]corrisponde a un numero di mese compreso tra 01 e 12
  • 0[1-9]|[12][0-9]|3[01]corrisponde a un numero di giorno compreso tra 01 e 31

Date corrispondenti: 1900-01-01 , 2205-02-31 , 2999-12-31

Date non corrispondenti: 1899-12-31 , 2018-05-35 , 2018-13-05 , 3000-01-01 , 2018-01-XX

3.3. Corrispondenza del 29 febbraio

Per abbinare correttamente gli anni bisestili, dobbiamo prima identificare quando abbiamo incontrato un anno bisestile , quindi assicurarci di accettare il 29 febbraio come data valida per quegli anni.

Poiché il numero di anni bisestili nel nostro intervallo limitato è abbastanza grande, dovremmo utilizzare le regole di divisibilità appropriate per filtrarli:

  • Se il numero formato dalle ultime due cifre di un numero è divisibile per 4, il numero originale è divisibile per 4
  • Se le ultime due cifre del numero sono 00, il numero è divisibile per 100

Ecco una soluzione:

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

Il modello è composto dalle seguenti parti:

  • 2000|2400|2800corrisponde a una serie di anni bisestili con un divisore di 400 in un intervallo limitato di 1900-2999
  • 19|2[0-9](0[48]|[2468][048]|[13579][26]))corrisponde a tutte le combinazioni di anni della lista bianca che hanno un divisore di 4 e non hanno un divisore di 100
  • -02-29partite il 2 febbraio

Date corrispondenti: 2020-02-29 , 2024-02-29 , 2400-02-29

Date non corrispondenti: 2019-02-29 , 2100-02-29 , 3200-02-29 , 2020/02/29

3.4. Giornate generali corrispondenti di febbraio

Oltre a far corrispondere il 29 febbraio negli anni bisestili, dobbiamo anche abbinare tutti gli altri giorni di febbraio (1 - 28) in tutti gli anni :

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

Date corrispondenti: 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'analisi di espressioni regolari complesse può influire in modo significativo sulle prestazioni del flusso di esecuzione. Lo scopo principale di questo articolo non era imparare un modo efficiente per testare una stringa per la sua appartenenza a un insieme di tutte le date possibili.

Considerare l'utilizzo di LocalDate.parse () fornito da Java8 se è necessario un approccio affidabile e veloce alla convalida di una data.

4. Conclusione

In questo articolo, abbiamo imparato come utilizzare le espressioni regolari per abbinare la data rigorosamente formattata del calendario gregoriano fornendo anche le regole del formato, dell'intervallo e della durata dei mesi.

Tutto il codice presentato in questo articolo è disponibile su Github. Questo è un progetto basato su Maven, quindi dovrebbe essere facile da importare ed eseguire così com'è.