EXPRESSIONS RÉGULIÈRES AVEC DELPHI

Utilisation des expressions régulières avec l'unité RegularExpressions

Roland Chastain

En lisant cet article vous apprendrez à vous servir des expressions régulières avec l'unité RegularExpressions de Delphi, unité apparue avec la version XE. Nous commencerons par quelques rappels généraux sur les expressions régulières. À la fin de chaque partie, des exercices corrigés vous seront proposés.

Commentez

- Titre : EXPRESSIONS RÉGULIÈRES AVEC DELPHI

- Auteur : Roland Chastain

- Parution : 16 octobre 2015

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2015 Roland Chastain. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Introduction

- I-A. Classes de caractères

- I-B. Classes prédéfinies

- I-C. Quantificateurs

- I-D. Autres opérateurs

- I-E. Exercices

- II. Vérifier qu'une chaîne appartient à un ensemble

- II-A. La fonction IsMatch

- II-B. Exercice

- III. Extraire d'une chaîne des groupes de caractères

- III-A. Exemple

- III-B. Groupes nommés

- III-C. Parenthèses non capturantes

- III-D. Exercice

- IV. Détecter des correspondances multiples

- IV-A. La fonction NextMatch

- IV-B. Le type TMatchCollection

- IV-C. Exercice

- V. Éclatement d'une chaîne

- V-A. Exemple

- V-B. Exercice

- VI. Remplacement de groupes de caractères

- VI-A. Remplacement par une chaîne constante

- VI-B. Remplacement par une chaîne composée

- VI-C. Remplacement par le résultat d'une fonction

- VI-D. Exercice

- VII. Expressions alternatives

- VII-A. Exemples

- VII-B. Exercice

- VIII. Unicode

- IX. Conclusion

I - Introduction

On appelle expression régulière une chaîne de caractères représentant un ensemble de chaînes de caractères.

Ainsi la chaîne 'abc', considérée comme expression régulière, représente un ensemble contenant un seul élément, la chaîne 'abc'. Chacun des trois caractères se représente lui-même.

I-A - Classes de caractères

Plus intéressante, l'expression régulière '[abc]' représente l'ensemble des chaînes constituées d'un seul caractère appartenant à l'ensemble ['a', 'b', 'c']. Ajoutons que les trois caractères 'a', 'b' et 'c' étant consécutifs dans l'alphabet, notre expression régulière aurait pu s'écrire '[a-c]', de même qu'en Pascal, on pourrait écrire ['a'..'c'] au lieu de ['a', 'b', 'c'].

À la différence des lettres de l'alphabet qui se représentent simplement elles-mêmes, les crochets et le trait d'union sont donc des caractères affectés d'une signification spéciale, que nous venons de voir. Pour que ces caractères spéciaux (qu'on appelle aussi métacaractères) soient traités comme des caractères ordinaires, c'est-à-dire interprétés littéralement, il faut les « échapper », comme dit l'anglais, en les faisant précéder d'une barre oblique inversée : '\[', '\]', '\-'. La barre oblique inversée est donc elle aussi un métacaractère.

Un autre caractère spécial dont il faut parler à cet endroit, c'est l'accent circonflexe. Lorsqu'il est placé immédiatement après un crochet ouvrant, il signifie la négation ou l'exclusion. Par exemple la classe '[^0-9]' contient tous les caractères qui ne sont pas des chiffres.

I-B - Classes prédéfinies

La barre oblique inversée sert aussi à donner une signification spéciale à des caractères qui par défaut sont interprétés littéralement.

Par exemple la lettre 'd', précédée d'une barre oblique inversée, représente la classe des chiffres (digits en anglais). Les trois expressions suivantes sont donc synonymes : '[0123456789]', '[0-9]', '\d'.

Un utilisateur averti des expressions régulières en Perl me faisait remarquer que dans les versions récentes de Perl, ces trois expressions ne sont plus synonymes, car la classe '\d' comprend désormais les chiffres de tous les alphabets, et non pas seulement ceux de l'alphabet latin.

Voici un tableau contenant toutes les classes prédéfinies.

Notation

Sens

Notation équivalente

\a

Cloche

\x07

\d

Chiffre

[0-9]

\e

Échappement

\x1B

\f

Nouvelle feuille

\x0C

\n

Nouvelle ligne

\0A

\r

Retour chariot

\0D

\s

Espace (au sens large)

[ \t]

\t

Tabulation

\x09

\v

Tabulation verticale

\x0B

\w

Caractère alphanumérique (y compris '_')

[A-Za-z0-9_]

.

Tous les caractères (sauf #13 et #10)

[^\r\n]

Notons que si nous remplaçons 'a', 'd', 'e' ou l'une des autres lettres par une majuscule, l'expression désigne alors l'ensemble des caractères n'étant pas la cloche, un chiffre, etc. Ainsi la classe '\D' est équivalente à la classe '[^0-9]' que nous avons vue tout à l'heure.

Le point est une classe de caractères à utiliser avec précaution : elle contient tous les caractères, à l'exception des caractères Retour chariot (#13) et Nouvelle ligne (#10).

La barre oblique inversée sert encore à former l'expression '\b' qui désigne, non pas une classe de caractères, mais le début ou la fin d'un mot, entendu comme séquence de caractères alphanumériques. C'est ce qu'on appelle une ancre.

I-C - Quantificateurs

Puisque nous savons maintenant comment désigner des ensembles de caractères, voyons comment noter le nombre de caractères attendus. Nous avons vu, sans le remarquer jusqu'ici, que par défaut c'est le nombre un.

Pour indiquer un autre nombre, ou même un intervalle, on peut se servir des accolades, mais aussi des caractères '+', '*', et '?'. Les accolades permettent d'indiquer un nombre exact de caractères, ou un intervalle exact. Le symbole '+' veut dire « une fois ou plus ». L'étoile veut dire « zéro fois ou plus ». Le point d'interrogation veut dire « zéro ou une fois ».

Par exemple, l'expression '\d{2}' veut dire « deux chiffres » ; '\d{2,4}' veut dire « de deux à quatre chiffres » ; '\d{2,}' veut dire « deux chiffres ou plus ». L'expression '\d+' veut dire « un chiffre ou plus » ; '\d*' veut dire « zéro chiffre ou plus » ; '\d?' veut dire « zéro chiffre ou un ».

Par défaut, les opérateurs '+', '*', et '?' sont gourmands (greedy en anglais), c'est-à-dire que la recherche renvoie non pas la première correspondance trouvée, mais la plus longue. Cela a souvent son importance. Pour changer ce comportement, autrement dit pour rendre les opérateurs paresseux (lazy), il faut les faire suivre d'un point d'interrogation : '+?', '*?', '??'.

I-D - Autres opérateurs

Les parenthèses sont également des caractères spéciaux : elles servent à délimiter des groupes de caractères, par exemple pour y appliquer des quantificateurs. Ainsi l'expression '(abc)?def' désigne un ensemble contenant deux éléments, les chaînes 'abcdef' et 'def'. Le point d'interrogation rend le groupe 'abc' facultatif.

Les parenthèses peuvent aussi être utilisées en combinaison avec la barre verticale, qui signifie « ou ». Par exemple l'expression 'a(b|c)d' désigne un ensemble contenant les chaînes 'abd' et 'acd'.

Enfin, les caractères '^' et '$' signifient respectivement le début et la fin de la chaîne. Nous allons voir dans un instant quelle est l'utilité de ces caractères. Mais oui, vous avez raison, cela fait deux significations différentes pour l'accent circonflexe. Suivant l'endroit où on le place, il ne sera pas interprété de la même façon, et cela vaut aussi pour les autres caractères.

I-E - Exercices

On veut s'assurer qu'une chaîne de caractères donnée représente une année comprise entre '1900' et '2999'. Proposez une expression régulière appropriée.

On veut s'assurer qu'une chaîne de caractères donnée est un nombre à six chiffres hexadécimaux précédé du symbole '$'. Proposez une expression régulière appropriée.

On veut extraire d'une chaîne tout ce qui précède la première virgule. Proposez une expression régulière appropriée.

'^(19|2\d)\d{2}$'

'^\$[0-9A-F]{6}$'

'(^[^,]+),'

II - Vérifier qu'une chaîne appartient à un ensemble

Il est temps à présent de faire connaissance avec l'unité RegularExpressions. Cette unité, apparue avec Delphi XE, va nous permettre d'effectuer diverses opérations sur des chaînes de caractères.

La plus simple de ces opérations est celle qui consiste à déterminer si telle chaîne appartient à tel ensemble.

II-A - La fonction IsMatch

La fonction dont nous avons besoin pour cela est la fonction IsMatch. Elle peut être appelée directement, avec deux chaînes de caractères comme arguments : le sujet (la chaîne à étudier) et l'expression régulière.

program IsMatch1;

{$APPTYPE CONSOLE}

uses
RegularExpressions;

begin
WriteLn(TRegEx.IsMatch('bonjour', '\w'));       // un caractère alphanumérique
WriteLn(TRegEx.IsMatch('bonjour', '\w+'));      // un ou plusieurs caractères alphanumériques
WriteLn(TRegEx.IsMatch('bonjour', '\w*'));      // zéro ou plus
WriteLn(TRegEx.IsMatch('bonjour', '\w{7}'));    // sept
WriteLn(TRegEx.IsMatch('bonjour', '[a-z]{7}')); // sept minuscules
WriteLn(not TRegEx.IsMatch('bonjour', '\d'));   // un chiffre
WriteLn(not TRegEx.IsMatch('bonjour', '\s'));   // un espace au sens large (équivalent à '[0-9]')
WriteLn(TRegEx.IsMatch('bonjour', '\D'));       // un caractère qui n'est pas un chiffre
ReadLn;
end.

Comme vous l'avez remarqué si vous avez regardé de près le code ci-dessus, la fonction IsMatch vérifie seulement qu'une partie au moins de la chaîne passée comme premier argument appartient à l'ensemble représenté par l'expression régulière.

Si l'on veut s'assurer que la chaîne entière appartient à l'ensemble, il faut utiliser les opérateurs '^' et '$', qui signifient respectivement le début de chaîne et sa fin.

WriteLn(TRegEx.IsMatch('bonjour', '\w'));       // TRUE
WriteLn(not TRegEx.IsMatch('bonjour', '^\w$')); // TRUE

Dans 'bonjour', il y a plusieurs caractères alphanumériques, mais il n'y a pas qu'un seul caractère entre le début et la fin de la chaîne.

II-B - Exercice

Écrire trois programmes vérifiant vos réponses aux trois exercices de la première partie.

program Solution_II_B_1;

{$APPTYPE CONSOLE}

uses
RegularExpressions;

{ Expression régulière représentant une année comprise entre 1900 et 2199. }

begin
WriteLn(TRegEx.IsMatch('2015', '(19|2\d)\d{2}'));
// 1, 9 et deux chiffres quelconques
// ou
// 2 et trois chiffres quelconques
WriteLn(TRegEx.IsMatch('2015', '^(19|2\d)\d{2}$'));
// la même chose plus les ancres de début et de fin de chaîne
ReadLn;
end.

program Solution_II_B_2;

{$APPTYPE CONSOLE}

uses
RegularExpressions;

{ Expression régulière représentant un nombre à six chiffres hexadécimaux précédé du symbole '$'. }

begin
WriteLn(TRegEx.IsMatch('$FF0000',
'^' +            // ancre début de chaîne
'\$' +           // symbole $
'[0-9A-F]{6}' +  // chiffre hexadécimal
'$'              // ancre fin de chaîne
));
ReadLn;
end.

program Solution_II_B_3;

{$APPTYPE CONSOLE}

uses
SysUtils, RegularExpressions;

{ On demande d'extraire tout ce qui précède la première virgule. }

const
PATTERN1 = '(^.+),';    // mode gourmand
PATTERN2 = '(^.+?),';   // mode paresseux
PATTERN3 = '(^[^,]+),'; // solution alternative

var
match: TMatch;

begin
match := TRegEx.Match('A13456,B23456,_34567,!45678', PATTERN3);

if match.Success then
WriteLn(match.Value);

{
Résultat avec PATTERN1 : "A13456,B23456,_34567,"
Résultat avec PATTERN2 : "A13456,"
Résultat avec PATTERN3 : "A13456,"
}

ReadLn;
end.

III - Extraire d'une chaîne des groupes de caractères

III-A - Exemple

Imaginons que nous voulions, non seulement vérifier qu'une chaîne donnée appartient à un ensemble, mais aussi extraire de cette chaîne certains groupes de caractères. Disons par exemple que nous voulons extraire les groupes de chiffres d'une chaîne contenant une date, comme '26/09/2015'.

program Group1;

{$APPTYPE CONSOLE}

uses
SysUtils, RegularExpressions;

const
SUBJECT = '26/09/2015';
PATTERN = '(\d{2})/(\d{2})/(\d{4})';

var
expr: TRegEx;
match: TMatch;
group: TGroup;

begin
expr := TRegEx.Create(PATTERN);

match := expr.Match(SUBJECT);

if match.Success then
for group in match.Groups do
WriteLn(Format('TGroup.Index=%d TGroup.Value="%s"', [group.Index, group.Value]));

ReadLn;
end.

Nous avons utilisé dans notre expression régulière des parenthèses pour délimiter les groupes de caractères à extraire, puis nous avons utilisé la fonction Match. Soit dit en passant, nous n'avons pas vérifié que les chiffres extraits formaient en effet une date valide. L'expression utilisée ne tient pas compte du fait qu'il n'y a que 31 jours au plus dans le mois, 12 mois dans l'année, etc. Une vérification complète devrait même tenir compte des années bissextiles, mais il est peu probable que cela soit faisable par les expressions régulières !

III-B - Groupes nommés

Nous pouvons également nommer les groupes. Pour nommer un groupe, il faut insérer juste après la parenthèse ouvrante les caractères '?<nom>'.

program Group2;

{$APPTYPE CONSOLE}

uses
SysUtils, RegularExpressions;

const
SUBJECT = '26/09/2015';
PATTERN = '(' + '?<day>' + '\d{2})/(' + '?<month>' + '\d{2})/(' + '?<year>' + '\d{4})';

var
group: TGroup;
match: TMatch;
regEx: TRegEx;

begin
regEx := TRegEx.Create(PATTERN, []);

match := regEx.Match(SUBJECT);

if match.Success then
begin
group := match.Groups['year'];
WriteLn(group.Value);
end;

ReadLn;
end.

III-C - Parenthèses non capturantes

Quelquefois on a besoin de délimiter un groupe de caractères, mais on n'a pas besoin de capturer les caractères correspondants. Dans ce cas, pour ne pas donner de travail inutile au programme, on aura recours à des parenthèses non capturantes.

Imaginons par exemple qu'on veuille détecter dans un code source en Pascal les Write et les WriteLn, sans faire de différence entre les deux. On pourrait utiliser l'expression 'Write(Ln)?'. Mais de cette façon on capturerait à chaque fois ou une chaîne vide ou la chaîne 'Ln'. Cependant nous avons supposé qu'on ne voulait pas tenir compte de la différence entre Write et WriteLn. On utilisera donc l'expression suivante :

'Write(?:Ln)?'

Les caractères '?:' ont été ajoutés immédiatement après la parenthèse ouvrante pour rendre les parenthèses non capturantes.

III-D - Exercice

Écrire un programme qui extrait les parties numériques d'une heure donnée sous la forme hh:mm, par exemple '10:29'. Le programme devra vérifier de la façon la plus stricte la validité de l'heure.

program Solution_III_D;

{$APPTYPE CONSOLE}

uses
SysUtils, RegularExpressions;

const
SUBJECT: array[0..2] of string = (
'10:39',
'23:30',
'25:30'
);
//PATTERN = '(\d{2}):(\d{2})';
PATTERN = '([01]\d|2[0-3]):([0-5]\d)';

var
expr: TRegEx;
match: TMatch;
group: TGroup;
s: string;

begin
expr := TRegEx.Create(PATTERN);

for s in SUBJECT do
begin
match := expr.Match(s);

if match.Success then
for group in match.Groups do
WriteLn(Format('TGroup.Value="%s"', [group.Value]))
else
WriteLn('ALERTE ! "', s, '" n''est pas une heure valide.');
end;

ReadLn;
end.

IV - Détecter des correspondances multiples

Il est possible de détecter les multiples groupes de caractères qui, dans une chaîne donnée, appartiennent à un même ensemble. Pour cela nous aurons besoin des fonctions Match, Success et NextMatch.

IV-A - La fonction NextMatch

L'exemple suivant détecte les groupes de caractères représentant des nombres.

program Match1a;

{$APPTYPE CONSOLE}

uses
SysUtils, RegularExpressions;

var
match: TMatch;

begin
match := TRegEx.Match('10 +10 0.5 .5', '\s*[-+]?[0-9]*\.?[0-9]+\s*');

while match.Success do
begin
WriteLn(match.Value);

match := match.NextMatch;
end;

ReadLn;
end.

IV-B - Le type TMatchCollection

La même opération peut être effectuée d'une autre façon : au moyen de la fonction Matches, laquelle renvoie un résultat de type TMatchCollection.

program MatchCollection1;

{$APPTYPE CONSOLE}

uses
SysUtils,
RegularExpressions;

var
expr: TRegEx;
collection: TMatchCollection;
i: Integer;

begin
expr.Create('\w');

collection := expr.Matches('abc');

for i := 0 to collection.Count - 1 do
with collection[i] do
WriteLn(Format('%d %d %d %s', [i, Index, Length, Value]));

ReadLn;
end.

IV-C - Exercice

Écrire deux versions d'un programme qui extrait tous les chemins de la variable d'environnement PATH. La première version du programme utilisera la méthode NextMatch, la seconde version utilisera la méthode Matches.

program Solution_IV_C_1;

{$APPTYPE CONSOLE}

uses
SysUtils, RegularExpressions;

var
expr: TRegEx;
match: TMatch;

begin
expr := TRegEx.Create('[A-Z]:\\[^;]+;?');
match := expr.match(GetEnvironmentVariable('PATH'));

while match.Success do
begin
WriteLn(match.Value);
match := match.NextMatch;
end;

ReadLn;
end.

program Solution_IV_C_2;

{$APPTYPE CONSOLE}

uses
SysUtils, RegularExpressions;

var
expr: TRegEx;
collection: TMatchCollection;
index: Integer;

begin
expr := TRegEx.Create('[A-Z]:\\[^;]+;?');
collection := expr.Matches(GetEnvironmentVariable('PATH'));

for index := 0 to collection.Count - 1 do
WriteLn(collection[index].Value);

ReadLn;
end.

V - Éclatement d'une chaîne

Voyons à présent comment éclater une chaîne au moyen d'une expression régulière, c'est-à-dire la découper selon une certaine règle et disposer les morceaux découpés dans un tableau.

V-A - Exemple

L'expression représente un groupe de caractères à traiter comme délimiteurs. Dans le cas le plus simple, c'est un seul caractère :

program Split1;

{$APPTYPE CONSOLE}

uses
SysUtils, RegularExpressions;

var
a: TArray<string>;
s: string;
i: integer;

begin
a := TRegEx.Split(GetEnvironmentVariable('PATH'), ';');

for s in a do
WriteLn(s);

a := TRegEx.Split('a b,c-d', '[ ,-]');

for i := 0 to High(a) do
WriteLn(a[i]);

ReadLn;
end.

V-B - Exercice

Écrire un programme qui découpe le contenu de la variable d'environnement PATH en traitant comme délimiteurs les caractères ';' et '\'.

program Solution_V_B;

{$APPTYPE CONSOLE}

uses
SysUtils, RegularExpressions;

var
a: TArray<string>;
s: string;

begin
a := TRegEx.Split(GetEnvironmentVariable('PATH'), '[;\\]');

for s in a do
WriteLn(s);

ReadLn;
end.

VI - Remplacement de groupes de caractères

L'opération suivante ressemble un peu à la précédente : au lieu de traiter certains groupes de caractères comme délimiteurs, nous les remplacerons, au moyen de l'expression Replace.

VI-A - Remplacement par une chaîne constante

program Replace1;

{$APPTYPE CONSOLE}

uses
RegularExpressions;

begin
WriteLn(TRegEx.Replace('WRITELN writeln', 'writeln', 'WriteLn', [roIgnoreCase]));
ReadLn;
end.

Comme vous le voyez, la fonction Replace admet quatre paramètres : la chaîne à traiter, l'expression régulière représentant les groupes à remplacer, la chaîne par laquelle ces groupes seront remplacés et enfin un ensemble d'options. Dans l'exemple ci-dessus, on a utilisé (sans aucune utilité d'ailleurs) l'option permettant d'obtenir un remplacement qui ne tienne pas compte de la casse des groupes à remplacer.

VI-B - Remplacement par une chaîne composée

Au lieu de remplacer les groupes par une chaîne constante, on peut les remplacer par une chaîne variable, composée d'éléments des groupes détectés.

program Replace2;

{$APPTYPE CONSOLE}

uses
RegularExpressions;

var
expr: TRegEx;

begin
expr := TRegEx.Create('(\w+)\s(\w+)');
WriteLn(expr.Replace('abc def', '\2 \1'));
ReadLn;
end.

Dans la chaîne '\2 \1', la barre oblique inversée suivie d'un chiffre représente l'élément capturé qui devra être inséré à cette place dans la chaîne de remplacement. Ainsi, '\1' signifie « le premier élément capturé ».

Pratique, non ? Mais ce n'est pas tout. La chaîne de remplacement peut même être le résultat d'une fonction recevant comme arguments les éléments capturés.

VI-C - Remplacement par le résultat d'une fonction

Supposons que nous voulions valider une chaîne représentant une position donnée d'une partie d'échecs. La chaîne devra être conforme à la notation standard, à savoir la notation FEN (Forsyth-Edwards Notation). Voici la chaîne FEN correspondant à la position initiale d'une partie de jeu d'échecs :

'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1'

Comme on le voit, l'occupation des lignes de l'échiquier est représentée par des groupes de caractères contenant des lettres (les pièces) et des chiffres (le nombre de cases vides consécutives). Nous devons nous assurer que le nombre total de cases pour chaque ligne de l'échiquier est bien égal à huit, et pour ce faire nous décidons de remplacer les chiffres par des caractères répétés autant de fois qu'il y a de cases vides.

type
TFENValidator = class
function ReplaceWith(const aMatch: TMatch): string;
function ExpandRow(const aRow: string): string;
function IsFEN(const aStr: string): boolean;
end;

function TFENValidator.ReplaceWith(const aMatch: TMatch): string;
const
EMPTY_SQUARE_SYMBOL = '-';
begin
result := StringOfChar(EMPTY_SQUARE_SYMBOL, StrToInt(aMatch.Groups.Item[1].Value));
end;

function TFENValidator.ExpandRow(const aRow: string): string;
begin
with TRegEx.Create('([1-8])') do
result := Replace(aRow, ReplaceWith);
end;

function TFENValidator.IsFEN(const aStr: string): boolean;
var
a,
b: TStringList;
expr: TRegEx;
i: integer;
s: string;
begin
a := TStringList.Create;
b := TStringList.Create;

b.Delimiter := '/';
b.StrictDelimiter := TRUE;

a.DelimitedText := aStr;
result := (a.Count = 6);

if not result then
Exit;

b.DelimitedText := a[0];
result := result and (b.Count = 8);

if not result then
Exit;

expr.Create('^[1-8BKNPQRbknpqr]+$');

for i := 0 to b.Count - 1 do
begin
s := ExpandRow(b[i]);
{WriteLn(s);}

result := result and expr.IsMatch(b[i]) and (Length(s) = 8);
end;

result := result and TRegEx.IsMatch(a[1], '^(w|b)$');
result := result and TRegEx.IsMatch(a[2], '^([KQkq]+|\-)$');
result := result and TRegEx.IsMatch(a[3], '^([a-h][36]|\-)$');

expr.Create('^\d+$');

result := result and expr.IsMatch(a[4]) and (StrToInt(a[4]) >= 0);
result := result and expr.IsMatch(a[5]) and (StrToInt(a[5]) >= 1);
end;

Voici un autre exemple de remplacement par fonction. C'est un programme qui cherche dans une chaîne de caractères des variables à remplacer par leur valeur. Les variables sont notées suivant la syntaxe utilisée pour les variables d'environnement comme %date% ou %username%. La fonction renvoyant la chaîne à substituer tirera ses résultats d'un dictionnaire.

program Replace3b;

{$APPTYPE CONSOLE}

uses
System.SysUtils,
System.Classes,
System.RegularExpressions;

type
TExpander = class
fDictionary: TStrings;
constructor Create(aDictionary: TStrings);
function ReplaceWith(const aMatch: TMatch): string;
function Expand(const s: string): string;
end;

constructor TExpander.Create(aDictionary: TStrings);
begin
fDictionary := aDictionary;
end;

function TExpander.ReplaceWith(const aMatch: TMatch): string;
begin
result := fDictionary.Values[aMatch.Groups.Item[1].Value];
end;

function TExpander.Expand(const s: string): string;
var
expr: TRegEx;
begin
expr.Create('%(.+)%');
result := expr.Replace(s, ReplaceWith);
end;

var
dictionary: TStrings;
expander: TExpander;

begin
dictionary := TStringList.Create;
expander := TExpander.Create(dictionary);

dictionary.Values['REPERTOIRE_PROJET'] := ExtractFileDir(ParamStr(0));

WriteLn(expander.Expand('%REPERTOIRE_PROJET%'#13#10'%REPERTOIRE_PROJET%'));

dictionary.Free;
expander.Free;

ReadLn;
end.

VI-D - Exercice

Écrire un programme qui remplace les multiples occurrences d'une date sous la forme jj/mm/aaaa par une date de la forme mm/jj/aaaa.

program Solution_VI_D;

{$APPTYPE CONSOLE}

uses
RegularExpressions;

var
expr: TRegEx;

begin
expr := TRegEx.Create('(\d{2})/(\d{2})/(\d{4})');
WriteLn(expr.Replace('04/10/2015 05/10/2015', '\2/\1/\3'));
ReadLn;
end.

VII - Expressions alternatives

Rassurez-vous, nous avons pratiquement fini notre tour des fonctions de l'unité RegularExpressions. Si vous avez encore une minute, je vous propose un dernier exemple montrant comment on peut utiliser des expressions alternatives, pour savoir si une chaîne donnée appartient à tel ensemble, ou à un tel autre, ou à un troisième, etc.

VII-A - Exemples

Dans le langage des expressions régulières, c'est le caractère '|' qui symbolise cette alternative.

program Alternative1;

{$APPTYPE CONSOLE}

uses
SysUtils, RegularExpressions;

const
SAMPLE: array[0..10] of string = (
'*ABORG-001360',
'?CASTELO 1419-002370',
'*EX/MINOU1424-412380',
'*RUBAN-003001',
'1234578011230',
'3760162052329',
'1456441001360',
'AAAAAAA',
'*AAAAA-001',
'*AAAAAA001360',
'123121212'
);

const
PATTERN1 = '^[*?]([^-]{1,15})-(\d{3})(\d{3})$';
PATTERN2 = '^(123\d{10})$';
PATTERN3 = '^(\d{13})$';
PATTERN = PATTERN1 + '|' + PATTERN2 + '|' + PATTERN3;

var
group: TGroup;
expr: TRegEx;
match: TMatch;
i: integer;

begin
expr := TRegEx.Create(PATTERN);

for i := Low(SAMPLE) to High(SAMPLE) do
begin
match := expr.Match(SAMPLE[i]);
if match.Success then
begin
Write(i);
for group in match.Groups do
Write(' "', group.Value, '"');
WriteLn;
end;
end;

ReadLn;
end.

Le code ci-dessus affiche tous les résultats sans les analyser. Si l'on veut savoir à laquelle des expressions régulières correspond tel groupe de caractères, on pourra procéder de la façon suivante :

procedure ShowResult(aGroupName: string);
begin
WriteLn(Format('  %s = "%s"', [aGroupName, match.Groups[aGroupName].Value]));
end;

begin
expr := TRegEx.Create(PATTERN);

for i := Low(SAMPLE) to High(SAMPLE) do
begin
WriteLn('SAMPLE[', i, ']');
match := expr.Match(SAMPLE[i]);
if match.Success then
begin
case match.Groups.Count of
4: begin
ShowResult('nom1');
ShowResult('nom2');
ShowResult('nom3');
end;
5: ShowResult('nom4');
6: ShowResult('nom5');
end;
end;
end;

ReadLn;
end.

On remarque dans l'exemple ci-dessus que lorsque la chaîne étudiée correspond à la troisième expression alternative, la variable match.Groups.Count vaut 6, parce que le groupe capturé correspond à la sixième sous-expression entre parenthèses.

Ce comportement peut être modifié, de telle sorte que le comptage des groupes soit fait séparément pour chacune des expressions alternatives, au lieu d'être un comptage global. Pour cela, il faudra utiliser l'expression suivante :

PATTERN = '(?|' + PATTERN1 + '|' + PATTERN2 + '|' + PATTERN3 + ')';

Attention, le comptage pour chaque expression alternative commencera à deux, à cause des parenthèses que nous avons ajoutées. Ces parenthèses produiront une capture supplémentaire (et inutile).

VII-B - Exercice

Écrire un programme qui utilise des expressions alternatives pour savoir si une chaîne donnée est une date (de la forme jj/mm/aaaa), une heure (de la forme hh:mm) ou autre chose.

program Solution_VII_B;

{$APPTYPE CONSOLE}

uses
SysUtils, RegularExpressions;

const
SAMPLE: array[0..2] of string = (
'04/10/2015',
'13:43',
'???'
);

const
PATTERN1 = '^\d{2}/\d{2}/\d{4}$';
PATTERN2 = '^\d{2}:\d{2}$';
PATTERN = PATTERN1 + '|' + PATTERN2;

var
group: TGroup;
expr: TRegEx;
match: TMatch;
i: integer;

begin
expr := TRegEx.Create(PATTERN);

for i := Low(SAMPLE) to High(SAMPLE) do
begin
match := expr.Match(SAMPLE[i]);
if match.Success then
case match.Length of
Length('00/00/0000'): WriteLn('"', SAMPLE[i], '" est une date.');
Length('00:00'): WriteLn('"', SAMPLE[i], '" est une heure.');
end
else
WriteLn('"', SAMPLE[i], '" n''est ni une date ni une heure.');
end;

ReadLn;
end.

VIII - Unicode

L'unité RegularExpressions supporte l'Unicode. Voici une expression régulière représentant les lettres capitales de l'alphabet grec :

program Unicode6;

{$APPTYPE CONSOLE}

uses
System.RegularExpressions,
Vcl.Dialogs;

var
s: UnicodeString;
match: TMatch;

begin
s := 'ΙΧΘΥΣ';

match := TRegEx.Match(s, '[Α-Ω]{5}');

ShowMessage(match.Value);
end.

IX - Conclusion

L'unité RegularExpressions est basée sur l'unité RegularExpressionsCore qui, sauf le nom, est identique à l'unité PerlRegEx de Jan Goyvaerts.

uses
{$IF CompilerVersion >= 22.0}
RegularExpressionsCore;
{$ELSE}
PerlRegEx; (* http://www.regular-expressions.info/download/TPerlRegEx.zip *)
{$IFEND}

L'unité PerlRegEx est, elle, basée sur la bibliothèque PCRE (Perl Compatible Regular Expressions) de Philip Hazel.

Si vous souhaitez en savoir plus sur la syntaxe des expressions régulières, vous pouvez consulter le tutoriel de Jan Goyvaerts : Regular Expressions. Vous pouvez télécharger les exemples commentés dans cet article (et quelques autres) : Exemples.

La solution des exercices se trouve dans le dossier Solutions. Le dossier RegularExpressionsCore contient des exemples d'utilisation de l'unité RegularExpressionsCore ou PerlRegEx.

Les exemples ont été testés avec Delphi XE2.

Je remercie les membres du forum qui m'ont permis de reproduire leurs exemples, ainsi que les relecteurs de cet article : gvasseur58, Alcatîz, Lolo78, ClaudeLELOUP, pierruel et f-leb.