Pour construire des modèles généraux, gérer la répétition d'un caractère est parfois insuffisant. On peut alors utiliser les parenthèses ouvrante et fermante pour délimiter des sous-chaînes ou des groupes de caractères.

Nous avons vu un peu plus haut comment tester la validité dune date au format JJ/MM/AAAA avec l'expression régulière :
/^\d{2}\/\d{2}\/\d{4}$/

Dans cette expression on voit que le modèle \d{2}\/ est répété deux fois (pour les jours et les mois). On peut donc entourer ce modèle avec des parenthèses, et lui appliquer un quantificateur :

(\d{2}\/){2}

L'expression complète devient :

/^(\d{2}\/){2}\d{4}$/

Le raccourci est d'autant plus interessant que nous pouvons facilement modifier l'expression pour tester les dates avec un séparateur autre que le slash (/), comme le tiret (-), le point (.) ou l'espace :
/^(\d{2}[\/\-\. ]){2}\d{4}$/

Ainsi avec une seule expression on peut valider le format d'un date comme 01/02/2003 ou 01-02-2003 ou 01.02.2003 ou 01 02 2003.

Nous pouvons aller plus loin dans le contrôle du format, et la facilité de saisie laissée à l'utilisateur, en spécifiant que le jour et le mois peuvent avoir 1 ou 2 chiffres, et que l'année peut avoir 2 ou 4 chiffres :
/^(\d{1,2}[\/\-\. ]){2}\d{2,4}$/

Ainsi 1/2/03 est tout aussi valide que 1-02-2003 ou 1 2 03.

Remarque : l'expression régulière contrôle simplement que la date saisie correspond à un format particulier, mais ne contrôle pas la validité de la date elle même (ie 32/13/2345 est un format correcte, mais la date n'est pas valide)

Exemple : répétitions de sous-chaînes
<?php require('bib_fonctions.php'); htmlDebut('Répétitions de sous-chaînes'); htmlDebut('Tester un format de date'); $exp = '/^(\d{1,2}[\/\-\. ]){2}\d{2,4}$/'; testerExp($exp, '1/2/2003'); testerExp($exp, '01 02 03'); testerExp($exp, '1-02-03'); testerExp($exp, '1 2 2003'); testerExp($exp, '1.2.03'); htmlFin(); ?>

Vous vous souvenez de l'exemple de vérification du format d'un nombre (-####.##), pour lequel l'expression régulière est un peu longue si nous voulons gérer aussi la saisie d'un entier sans que l'utilisateur soit obligé de taper le point (.) décimal :
/^-{0,1}\d{1,4}$|^-{0,1}\d{1,4}\.\d{0,2}$/

Avec le groupement de caractère nous pouvons supprimer le ou (|). Commençons par nous occuper du début de la chaîne qui peut commencer éventuellement par le signe moins (-) :
/^(-?)\d{0,4}\.\d{0,2}$/

Pour spécifier que l'utilisateur n'a a saisir le point décimal que si il y a des décimales, il faut grouper la fin de l'expression :
/^(-?)\d{0,4}(\.\d{0,2})?$/

Et voilà !

Exemple : répétitions de sous-chaînes
<?php require('bib_fonctions.php'); htmlDebut('Répétitions de sous-chaînes'); htmlInfo('Un nombre à 2 décimales entre -9999.99 et 9999.99'); $exp = '/^(-?)\d{0,4}(\.\d{0,2})?$/'; testerExp($exp, 123.45); testerExp($exp, 1234.5); testerExp($exp, -12); testerExp($exp, -.12); testerExp($exp, .34); htmlFin(); ?>

### Exercice : vérifier le format d'une adresse e-mail

Ecrivez l'expression régulière qui permet de vérifier une adresse e-mail répondant à la suite de caractères suivante :

- un caractère alphabétique
- éventuellement d'autres "caractères acceptables"
- le caractère @
- un caractère alphabétique
- éventuellement d'autres "caractères acceptables"
- le caractère .
- de deux à six caractères alphabétiques.

La chaîne ne doit contenir aucun espace et il ne peut y avoir qu'un caractère @.

Les "caractères acceptables" sont :

- les caractères alphabétiques (a à z), en minuscule ou majuscule, caractères accentués exclus.
- les chiffres de 0 à 9
- l'underscore (_)
- le tiret (-)
- le point (.)

Remarque : le test de validité est volontairement simplifié. Vous trouverez dans la solution une explication complète sur le format des adresses e-mail conformes à la norme RFC2822.

Exemples à tester :

- piat@univ-fcomte.frfr
- piat@lifc.univ-fcomte.fr
- piatunifc-fcomte.fr
- piat@univfcomte-fr

Une solution possible

Exercice : vérifier une adresse e-mail
<?php require('bib_fonctions.php'); htmlDebut('Vérifier une adresse e-mail'); /* Vérifier une adresse e-mail répondant à la suite de caractères suivante : - un caractère alphabétique ^[a-zA-Z] - éventuellement d'autres "caractères acceptables" [a-zA-Z0-9_\.\-]* - le caractère @ @ - un caractère alphabétique [a-zA-Z] - éventuellement d'autres "caractères acceptables" [a-zA-Z0-9_\.\-]* - le caractère . \. - de deux à six caractères alphabétiques. [a-zA-Z]{2,6}$ En regroupant on obtient : ^[a-zA-Z][a-zA-Z0-9_\.\-]*@[a-zA-Z][a-zA-Z0-9_\.\-]*\.[a-zA-Z]{2,6}$ Une autre expression possible : ^[a-zA-Z][\w\-\.]*@[a-zA-Z][\w\-\.]*\.[a-zA-Z]{2,6}$ */ $e = '/^[a-zA-Z][a-zA-Z0-9_\.\-]*@[a-zA-Z][a-zA-Z0-9_\.\-]*\.[a-zA-Z]{2,6}$/'; $mail = 'piat@univ-fcomte.frfr'; testerExp($e, $mail); $mail = 'piat@lifc.univ-fcomte.fr'; testerExp($e, $mail); $mail = 'piatunifc-fcomte.fr'; testerExp($e, $mail); $mail = 'piat@univfcomte-fr'; testerExp($e, $mail); htmlFin(); ?>

Dans cet exercice nous avons vérifié une adresse e-mail tel qu'on le fait le plus souvent, et comme vous en trouverez de nombreux exemple sur le Web. Pourtant ces vérifications ne sont pas bonnes, voire fantaisistes, et s'appuient sur ce que l'on croit que peut être une adresse email valide.
Par exemple, on peut tomber sur des validateurs qui estiment qu'un nom de grand domaine ne peut pas faire plus de 3 caractères. C'est vrai pour les noms de grands domaines géographiques (.fr, .de, .ru), pour le .com, mais faux pour les nouveaux noms comme .info, .name, .museum ou .aero.
Le nom de domaine est toujours considéré comme une chaîne de caractère, mais une adresse IP comme 123.456.78.90 est tout à fait valable.
D'autres exemples d'adresses valables :
"Dark Vador"@zeforce.com
yoda@[123.124.233.4]
+1~1+@un-un.info
{o_o}@oeil.fr
"[[ piat ]]"@ok.com
"C'est vraiment le meilleur" <fp@piat.info>
Surprenant non ?

Le format des adresses e-mail est défini par l'IETF (Internet Engineering Task Force) dans la norme RFC2822 (http://www.ietf.org/rfc/rfc2822.txt ) qui décrit plus généralement le "format des messages Internet". La RFC2822 spécifie ce qui est permis et ce qui ne l'est pas dans une adresse e-mail, et partage une adresse en deux parties séparées par le signe @ : partie_locale@domaine.

La partie_locale peut avoir de 1 à 64 caractères et peut être composée de 2 façons.

Elle peut être une suite de caractères parmi la sélection suivante :
- a à z et A à Z
- 0 à 9
- les caractères !, #, $, %, &, ', *, +, -, /, =, ?, ^, _, `, {, |, }, ~
- le point (.) peut être utilisé, sauf à la première place.

La deuxième façon de composer la partie locale est d'utiliser n'importe quelle suite de caractères (sauf \) et de les entourer avec des guillemets doubles.

La partie_locale doit être traitée comme respectant les différences majuscules et minuscules, même si la norme "décourage" le mélange.

La partie domaine de l'adresse e-mail peut elle aussi être composées de plusieurs façons, mais ne doit jamais excéder 255 caractères au total. La forme la plus courante du domaine est une série de chaînes de caractères, séparées par un point (.). Une chaîne peut être composée de 1 à 63 caractères parmi la sélection suivante :
- a à z,
- 0 à 9,
- trait d'union.
Officiellement la chaîne doit commencer par une lettre, mais dans la pratique des noms de domaines commençant par un chiffre ont été enregistrés.

Généralement la partie domaine est composée de deux chaines, la seconde représentant le nom de grand domaine (.fr par exemple).

La partie domaine ne fait pas de différence entre les majuscules et les minuscules.

La partie domaine peut être composée sous la forme d'une adresse IP, et dans ce cas elle peut être entourée avec des guillements doubles.

L'adresse e-mail peut être précédée d'un commentaire, entouré de guillemets. L'adresse doit alors être elle même entourée par les caractères < et >.

La vérification d'une adresse e-mail, si l'on veut se conformer à la RFC 2822, est donc beaucoup plus complexe qu'on ne le pense généralement, et ne peut être faite correctement avec une seule expression régulière.

Exercice : vérifier une adresse e-mail
<?php require('bib_fonctions.php'); htmlDebut('Vérifier une adresse e-mail'); testerMail('piat@univ-fcomte.frfr'); testerMail('piat@lifc.univ-fcomte.fr'); testerMail('piatunifc-fcomte.fr'); testerMail('piat@univfcomte-fr'); testerMail('"François Piat"@univ-fcomte.fr'); testerMail('piat@194.57.91.223'); htmlFin(); //====================================================== /** Vérifier une adresse e-mail en conformité avec la norme RFC 2822 (ou a peu près (:-)) * @param string $mail Adresse à tester **/ function testerMail($mail) { /* Nous commençons par vérifier que l'adresse contient bien un @ (et un seul), et que les deux parties sont de longueur correcte. Il doit y avoir de 1 à 64 caractères, @ exclus [^@]{1,64} suivis de @ et de 1 à 255 caractères, @ exclus @[^@]{1,255} Comme la chaîne ne doit rien avoir d'autre, on l'entoure avec ^ (commence) et $ (fini) */ $exp = '/^[^@]{1,64}@[^@]{1,255}$/'; if (! preg_match($exp, $mail)) { echo "<hr>$mail n'est pas valide (longueur ou @)"; return; } // On découpe l'adresse en 2 parties : locale et domaine // Les tests seront plus faciles à faire. $parties = explode('@', $mail); /* La partie locale doit être conforme à une chaîne de caractères prèdéfinis et elle ne doit pas commencer par un point (.) ^[A-Za-z0-9!#$%&'*+-\/=?^_`{|}~] On peut ensuite avoir de 0 à 62 autres caractères prédéfinis [A-Za-z0-9!#$%&'*+-\/=?^_`{|}~\.]{0,62}$ Ce qui donne : ^[A-Za-z0-9!#$%&'*+-\/=?^_`{|}~][A-Za-z0-9!#$%&'*+-\/=?^_`{|}~\.]{0,62}$ La chaîne peute aussi être composée de n'importe quels caractères, sauf \, entourés de guillemets : ^"[^(\\|")]{1,62}"$ Dans cette dernière expression, la sous-expression entre crochets signifie que l'on exclu (caractère ^) le caractère \ (protégé ici par un \) et le caractère " Les deux expressions sont reliées avec un ou (|). Pour ne pas provoquer de collision avec les guillemets simples ou doubles contenus dans les expressions, nous construisons la variable par plusieurs concaténations. */ $exp = '/'; $exp .= "^[A-Za-z0-9!#$%&'*+-\/=?^_`{|}~][A-Za-z0-9!#$%&'*+-\/=?^_`{|}~\.]{0,62}$"; $exp .= '|'; $exp .= '^"[^(\\|")]{1,62}"$'; $exp .= '/'; if (! preg_match($exp, $parties[0])) { echo "<hr>$mail n'est valide (partie locale)"; return; } // On peut maintenant s'occuper de la partie domaine. // On commence par récupèrer les parties séparées par // des points (.) $domaines = explode('.', $parties[1]); /* Si la première partie est composée de 1 à 3 chiffres, on considère que le domaine est une adresse IP de la forme 123.123.12.1 soit ^(\d{1,3}\.){3}\d{1,3}$ Cette vérification d'adresse IP est un peu faible ... */ if (preg_match('/^\d{1,3}$/', $domaines[0])) { if (preg_match('/^(\d{1,3}\.){3}\d{1,3}$/', $parties[1])) { echo "<hr>$mail est une adress valide"; } else { echo "<hr>$mail n'est pas valide (IP)"; } return; } /* Le doamine peut aussi être une adresse IP de la forme [123.123.12.1] soit l'expression ^\[(\d{1,3}\.){3}\d{1,3}\]$ */ if (preg_match('/^\[\d{1,3}$/', $domaines[0])) { if (preg_match('/^\[(\d{1,3}\.){3}\d{1,3}\]$/', $domaines)) { echo "<hr>$mail est une adress valide"; } else { echo "<hr>$mail n'est pas valide (IP [])"; } return; } /* Si on passe ici c'est que le domaine n'est pas une adresse IP. On considère que le domaine doit être au moins composé de 2 parties comme monsite.com, bien que dans la norme rien ne l'oblige. */ if (count($domaines) < 2) { echo "<hr>$mail n'est pas valide (domaine 2)"; return; } /* Chacune des parties du domaine doit être conforme à : une lettre ou un chiffre, suivi de 0 à 61 lettres, chiffres ou tiret, suivi de une lettre ou un chiffre ^[A-Za-z0-9][A-Za-z0-9-]{0,61}[A-Za-z0-9]$ ou à l'expression : ^[A-Za-z0-9]+$ */ $exp = '/^[A-Za-z0-9][A-Za-z0-9-]{0,61}[A-Za-z0-9]$|^[A-Za-z0-9]+$/'; foreach ($domaines as $d) { if (! preg_match($exp, $d)) { echo "<hr>$mail n'est pas valide (partie domaine)"; return; } } echo "<hr>$mail est une adress valide"; } ?>