Cosa sono le regex (1)? Sono espressioni regolari composte da:
- costanti e operatori che denotano insiemi di stringhe e
- operazioni tra questi insiemi.
Uno sviluppatore spesso usa le regex per fare validazioni o per il parsing di stringhe di dati.
Avendo avuto la necessità di dover validare una data in formato stringa ISO, mi sono chiesto: si riuscirà a farlo con le espressioni regolari? Ho iniziato quindi a fare esperimenti, più che altro come studio per approfondire l’argomento, e sono arrivato a delle interessanti conclusioni.
Note
Nel paragrafo Riferimenti sono presenti, tra gli altri, link a due siti web:
- regular expressions 101 (6), ben fatto, utile per provare le regex e
- REGEXPER (7) che genera un’ottima rappresentazione grafica di una regex fornita come input.
Per arrivare al risultato finale, ho preferito suddividere la spiegazione in tre fasi: time zone, ora, data (ordine di semplicità). Gli esempi con il codice delle espressioni regolari sono scritti in Javascript (4) utilizzando Node.js versione 14 (5).
Regex per validare la time zone
I formati validi sono +/-HH:mm
e Z
.
I limiti per il primo caso sono da 00:00
a 24:00
.
const timezoneRegex = /^(?:[+-][0-1]\d:[0-5]\d|[+-][2][0-3]:[0-5]\d|[+-]24:00|Z)$/; timezoneRegex.test('+01:00'); // true timezoneRegex.test('-01:00'); // true timezoneRegex.test('Z'); // true timezoneRegex.test('*01:00'); // false (errore di formato) timezoneRegex.test('+24:01'); // false (errore di limite) timezoneRegex.test('z'); // false (errore di formato)
^
indica dall’inizio stringa.[+-][0-1]\d:[0-5]\d
verifica che ci sia un+
o un-
e se ore e minuti vanno da00:00
a19:59
.[+-][2][0-3]:[0-5]\d
verifica che ci sia un+
o un-
e se ore e minuti vanno da20:00
a23:59
.[+-]24:00|Z
verifica che ci sia un+
o un-
e se ore e minuti sono24:00
.Z
verifica che ci sia laZ
(UTC).$
per il fine stringa.
Le operazioni 2, 3 e 4 sono raggruppate in un gruppo che le mette in or.
Regex per validare il formato dell’ora
I formati validi sono HH:mm:ss
e HH:mm:ss.SSS
.
L’orario va da 00:00:00
a 23:59:59
.
const timeRegex = /^(?:[0-1]\d|[2][0-3]):[0-5]\d:[0-5]\d(?:\.\d{3})?$/; timeRegex.test('10:24:59.100'); // true timeRegex.test('14:45:34'); // true timeRegex.test('1:45:34'); // false (errore di formato) timeRegex.test('01:60:34'); // false (errore di limite) timeRegex.test('04:56:23.1000'); // false (errore di formato) timeRegex.test('04:56:23.aaa'); // false (errore di formato)
^
indica dall’inizio stringa.(?:[0-1]\d|[2][0-3])
verifica che le ore siano tra00
e23
.[0-5]\d
verifica che i minuti siano tra00
e59
.[0-5]\d
verifica che i secondi siano tra00
e59
.(?:\.\d{3})?'
verifica che i millisecondi siano tre cifre numeriche ma può essere opzionale.$
indica il fine stringa.
Regex per validare la data
E qui arriviamo al punto critico. Le casistiche di controllo sono diverse, in particolare sul numero dei giorni del mese condizionati dal mese e dall’anno. Attenzione per il mese di Febbraio, per il quale bisogna tener conto del discorso dell’anno bisestile (2).
I formati validi sono YYYY-MM-DD
.
La data va da 0000-01-01
a 9999-12-28/29/30/31
.
dateRegex = /^(?:(?=[02468][048]00|[13579][26]00|\d{2}0[48]|\d{2}[2468] [048]|\d{2}[13579][26])\d{4})-(?:(?:01|03|05|07|08|10|12)-(?:[0-2]\d|3[0-1])| (?:04|06|09|11)-(?:[0-2]\d|30)|02-[0-2]\d)|(?:(?![02468][048]00|[13579] [26]00|\d{2}0[48]|\d{2}[2468][048]|\d{2}[13579][26])\d{4})-(?: (?:01|03|05|07|08|10|12)-(?:[0-2]\d|3[0-1])|(?:04|06|09|11)-(?:[0-2]\d|30)|02-(?: [0-1]\d|2[0-8]))$/; dateRegex.test('2020-10-10'); // true dateRegex.test('2020-02-29'); // true dateRegex.test('2019-02-28'); // true dateRegex.test('2019-02-29'); // false (errore di limite non è bisestile) dateRegex.test('2020-01-32'); // false (errore di limite) dateRegex.test('2020-aa-31'); // false (errore di formato) dateRegex.test('2020/01/31'); // false (errore di formato)
Prima di tutto bisogna esattamente dividere a metà la regex perché la seconda parte è semplicemente la negazione della prima sulla gestione dell’anno bisestile. La prima parte gestisce i 29 giorni e la seconda i 28.
Prima parte (per bisestile):
/(?:(?=[02468][048]00|[13579][26]00|\d{2}0[48]|\d{2}[2468][048]|\d{2}[13579] [26])\d{4})-(?:(?:01|03|05|07|08|10|12)-(?:[0-2]\d|3[0-1])|(?:04|06|09|11)-(?:[0- 2]\d|30)|02-(?:[0-1]\d|2[0-8]))/
Seconda parte (per non bisestile):
/(?:(?![02468][048]00|[13579][26]00|\d{2}0[48]|\d{2}[2468][048]|\d{2}[13579] [26])\d{4})-(?:(?:01|03|05|07|08|10|12)-(?:[0-2]\d|3[0-1])|(?:04|06|09|11)-(?:[0- 2]\d|30)|02-(?:[0-1]\d|2[0-8]))/
^
indica dall’inizio stringa.(?:(?=[02468][048]00|[13579][26]00|\d{2}0[48]|\d{2}[2468][048]|\d{2}[13579][26])\d{4})
verifica se è un anno bisestile.(?:(?:01|03|05|07|08|10|12)-(?:[0-2]\d|3[0-1]))
verifica i mesi a 31 giorni.(?:04|06|09|11)-(?:[0-2]\d|30)
verifica i mesi a 30 giorni.02-[0-2]\d
o02-(?:[0-1]\d|2[0-8])
verifica Febbraio rispettivamente per l’anno bisestile e non bisestile.$
indica a fine stringa.
Regole per la definizione dell’anno bisestile:
- gli anni secolari il cui numero è divisibile per 400;
- gli anni non secolari il cui numero è divisibile per 4.
Dettagli riguardo la verifica del bisestile:
[02468][048]00
,[13579][26]00
per verificare i divisibili per 400. E’ suddiviso in due per non avere falsi positivi sugli anni secolari dei numeri non divisibili per 400.\d{2}0[48]
,\d{2}[2468][048]
,\d{2}[13579][26]
prende i divisibili per 4. E’ suddiviso in tre per non coprire gli anni secolari.
Uniamo le tre regex per validare data e ora in formato ISO
Siamo arrivati alla fine e questa fase sarà solo un’unione delle tre regex viste in precedenza.
I formati validi sono YYYY-MM-DDTHH:mm:ss.SSSZ
, YYYY-MM-DDTHH:mm:ss.SSS+HH:mm
, YYYY-MM-DDTHH:mm:ssZ
, YYYY-MM-DDTHH:mm:ss+HH:mm
.
const isoRegex = /^(?:(?:(?=[02468][048]00|[13579][26]00|\d{2}0[48]|\d{2}[2468] [048]|\d{2}[13579][26])\d{4})-(?:(?:01|03|05|07|08|10|12)-(?:[0-2]\d|3[0-1])| (?:04|06|09|11)-(?:[0-2]\d|30)|02-[0-2]\d)|(?:(?![02468][048]00|[13579] [26]00|\d{2}0[48]|\d{2}[2468][048]|\d{2}[13579][26])\d{4})-(?: (?:01|03|05|07|08|10|12)-(?:[0-2]\d|3[0-1])|(?:04|06|09|11)-(?:[0-2]\d|30)|02-(?: [0-1]\d|2[0-8])))T(?:[0-1]\d|[2][0-3]):[0-5]\d:[0-5]\d(?:\.\d{3})?(?:[+-][0-1]\d: [0-5]\d|[+-][2][0-3]:[0-5]\d|[+-]24:00|Z)$/; isoRegex.test('2020-10-10T22:10:14.043Z'); // true isoRegex.test('2020-02-29T10:15:16+01:00'); // true isoRegex.test('2019-02-28T11:14:34.765+01:00'); // true isoRegex.test('2019-02-29T10:00:00Z'); // false (errore di limite non è bisestile) isoRegex.test('2019-02-28T10:60:00Z'); // false (errore di limite) isoRegex.test('2020-12-31'); // false (errore di formato) isoRegex.test('2020/01/31T14:45:28.546+01:00'); // false (errore di formato)
Conclusioni
Forse le regex non sono la migliore soluzione da utilizzare in particolare per la gestione dell’anno bisestile ma alla fine l’obiettivo che ci si era prefissati è stato raggiunto.
Un ultima considerazione, la regex finale non prevede la validazione del minuto di 61 secondi che succede ogni tanto e che serve per sincronizzare gli orologi atomici (3).