Site WWW de Laurent Bloch
Slogan du site

ISSN 2271-3905
Cliquez ici si vous voulez visiter mon autre site, orienté vers des sujets moins techniques.

Pour recevoir (au plus une fois par semaine) les nouveautés de ce site, indiquez ici votre adresse électronique :

Corrigé de l’examen d’algorithmique de la biologie
Programmer avec des expressions régulières
Article mis en ligne le 19 février 2015
dernière modification le 1er mars 2015

par Laurent Bloch

Traitement des expressions régulières

On s’intéresse à l’écriture des nombres conformes à la syntaxe du calculateur standard des systèmes Unix, bc, et on veut créer des expressions régulières qui vérifient la syntaxe de tels nombres (on trouvera à la fin de cet article un petit rappel de la syntaxe des expressions régulières). Par exemple, si j’écris -1.23^-5 cela dénote le nombre $-1,23^{-5}$ (l’accent circonflexe est l’opérateur d’élévation à la puissance, et conformément à l’usage anglo-saxon c’est un point qui sépare la partie entière de la partie décimale du nombre).

Les expressions régulières peuvent être traitées par un programme Scheme, et les textes reconnus peuvent être décomposés en leurs parties élémentaires ;
ainsi, pour un nombre écrit selon la syntaxe de bc, il sera possible de séparer l’exposant de la mantisse (le nombre auquel est appliqué l’exposant) pour ensuite les utiliser dans des expressions Scheme ordinaires.

On rappelle qu’en mettant entre parenthèses une sous-expression d’une expression régulière, il est possible de distinguer les parties du texte reconnues par chaque sous-expression régulière. Ces parties de texte sont placées, par la procédure pregexp-match à la ligne 8 du programme ci-dessous, dans une liste, dans l’ordre des parenthèses ouvrantes auxquelles elles correspondent. Le premier élément de la liste (numéro 0) sera le texte entier reconnu par l’expression régulière, le second élément (numéro 1) sera la partie de texte reconnue par la première sous-expression entre parenthèses, et ainsi de suite. Par exemple, soit le programme suivant :

  1. (module number-parser
  2.    (main get-number))
  3. ;;
  4. (define (get-number args)
  5.    (let* ((number (cadr args))
  6.           (regexp
  7.              "^(\\+|-)?([0-9]*)(\\.([0-9]*))?(\\^((\\+|-)?[0-9]+))?$")
  8.           (number-elems (pregexp-match regexp number)))
  9.       (print number-elems)))

Télécharger

Son invocation avec un nombre tel que décrit ci-dessus donnera :

 Le premier élément de la liste est le nombre en entier : 123.8^-4 ;
 le second élément est #f : en effet le groupe de la première paire de parenthèses (\\+|-) reconnaît le signe du nombre, qui ne figure pas dans notre cas ; il faut une double barre oblique inverse devant le signe +, parce que nous voulons bien dénoter le signe + lui-même, et non pas l’opérateur + de la syntaxe des expressions régulières ;
 le troisème élément, 123, est la partie entière, reconnue par ([0-9]*) ;
 le quatrième élément reconnu par (\\.([0-9]*)) (attention aux doubles barres obliques inversées !) est la partie décimale, précédée du point, .8 ;
 le cinquième élément reconnu par ([0-9]*) est la partie décimale ;
 le sixième élément est l’exposant précédé de l’opérateur d’élévation à la puissance, soit ^-4, reconnu par (\\^((\\+|-)?[0-9]+)) ;
 le septième élément, reconnu par ((\\+|-)?[0-9]+), est l’exposant muni de son signe ;
 le dernier élément est le signe de l’exposant.

Question 1

Modifiez le programme ci-dessus pour afficher, non plus la liste des sous-chaînes reconnues par des sous-expressions, mais le texte d’une expression Scheme qui dénote le nombre.

Réponse :

  1. (module number-parser-2
  2.    (main get-number-string))
  3. ;;
  4. (define (get-number-string args)
  5.    (let ((number-string (cadr args)))
  6.       (print (parse2number number-string)) ))
  7. ;; On isole la procédure d'analyse proprement dite :
  8. (define (parse2number number-string)
  9.    (let* ((regexp
  10.              "^(\\+|-)?([0-9]*)(\\.([0-9]*))?(\\^((\\+|-)?[0-9]+))?$")
  11.           (number-elems (pregexp-match regexp number-string))
  12. ;; On localise les éléments qui seront utiles à la
  13. ;; construction du nombre et on leur donne des noms :
  14.           (sign-num (cadr number-elems))
  15.           (int-part (caddr number-elems))
  16.           (point-part (cadddr number-elems))
  17.           (exponent (caddr (cddddr number-elems)))
  18. ;; si un élément donne #f on le remplace par la chaîne vide
  19.           (mantissa (string-append
  20.                        (or sign-num "")
  21.                        int-part
  22.                        (or point-part ""))))
  23.       (if exponent
  24.           (list 'expt mantissa exponent)
  25.           mantissa)))

Télécharger

Question 2

Nous souhaitons maintenant que notre programme accepte sur la ligne de commande un opérateur arithmétique tel que +, /, ou encore expt, deux opérandes, et calcule le résultat de l’opération.

On rappelle que la procédure string->number permet de convertir une chaîne de caractères en nombre, que string->symbol permet de convertir une chaîne de caractères en symbole, et que si ce symbole est le nom d’une procédure, (eval <symbol>) en réalise une double évaluation, ce qui permet d’utiliser <symbol> pour invoquer la procédure dont il est le nom.

  1. (module number-parser-3
  2.    (main get-number-string))
  3. ;; Comme le programme précédent, mais un opérateur et deux
  4. ;; nombres sur la ligne de commande. Si l'opérateur est aussi
  5. ;; un opérateur du shell comme la multiplication, le mettre entre
  6. ;; quotes : '*'
  7. (define (get-number-string args)
  8.    (let ((op (eval (string->symbol (cadr args))))
  9.          (n1 (parse2number (caddr args)))
  10.          (n2 (parse2number (cadddr args))))
  11.       (print (op n1 n2)) ))
  12. ;;
  13. (define (parse2number number-string)
  14.    (let* ((regexp
  15.              "^(\\+|-)?([0-9]*)(\\.([0-9]*))?(\\^((\\+|-)?[0-9]+))?$")
  16.           (number-elems (pregexp-match regexp number-string))
  17. ;; on décompose chaque nombre en éléments simples
  18.           (sign-num (cadr number-elems))
  19.           (int-part (caddr number-elems))
  20.           (point-part (cadddr number-elems))
  21.           (exponent (caddr (cddddr number-elems)))
  22. ;; si un élément donne #f on le remplace par la chaîne vide
  23.           (mantissa (string-append
  24.                        (or sign-num "")
  25.                        int-part
  26.                        (or point-part ""))))
  27.       (if exponent
  28.           (expt (string->number mantissa)
  29.                 (string->number exponent))
  30.           (string->number mantissa))))

Télécharger

Pour être vraiment juste

Pour avoir un nombre variable d’opérandes, il suffit de mettre Scheme un peu plus
à contribution (une occasion de réviser map et apply, expliqués par exemple ici), en modifiant la procédure get-number-string ainsi :

  1. (define (get-number-string args)
  2.    (let ((op (string->symbol (cadr args)))
  3.          (Largs (map parse2number (cddr args))))
  4.       (print (apply (eval op) Largs)) ))

Télécharger

Mon collègue Emmanuel Lazard me fait remarquer que mon expression régulière « attrape » des formes qui, bien que certaines soient acceptées par bc, ne sont pas vraiment des nombres, telles que ^6, .^6 ou -^8. L’expression vraiment juste est :

Petit rappel de syntaxe des expressions régulières

 z? signifie que z doit exister en zéro ou un
exemplaire à cet endroit du texte examiné ; (xyz)? signifie que
xyz doit exister en zéro ou un exemplaire à cet endroit du texte
examiné (cette règle des parenthèses s’applique de la même façon aux
cas ci-dessous) ;
 z* signifie que z doit exister en un nombre
quelconque
d’exemplaires à cet endroit du texte examiné,
éventuellement zéro ;
 z+ signifie que z doit exister en au moins un
exemplaire à cet endroit du texte examiné ;
 z|w signifie qu’à cet endroit du texte examiné il doit y avoir
z ou bien w ;
 l’accent circonflexe ^, lorsqu’il apparaît en
début d’expression régulière
, dénote le début de ligne (ou de
chaîne de caractères), $, lorsqu’il apparaît en fin
d’expression régulière, la fin de ligne (ou de chaîne de
caractères) ;
 le point . dénote n’importe quel caractère ; l’expression régulière qui dénote le point est \.
 certains caractères, tels ? * + . ^ $ |, ont une signification particulière dans la syntaxe des expressions régulières ; lorsque l’on veut dénoter le caractère lui-même, en tant que tel, et non pas sa signification syntaxique, on le précède d’une barre oblique inverse, \ (backslash), ainsi \. signifie « ici je veux un point » ; si on veut une barre oblique inverse, on écrit \\ ; mais certains langages, tels Scheme ou Java, représentent les expressions régulières par des chaînes de caractères, où la barre oblique inverse possède aussi une signification syntaxique particulière, ce qui oblige à mettre deux barres obliques inverses là où il en faudrait une, et quatre là où il en faudrait deux (cf. les exemples ci-dessus).