Maintenant que fromRoman fonctionne pour
des entrées correctes, nous devons mettre en place la dernière pièce
du puzzle : le faire fonctionner avec des entrées incorrectes. Cela
veut dire trouver une manière d’examiner une chaîne et de déterminer
si elle constitue un nombre en chiffres romains valide. C’est intrinsèquement plus
difficile que de valider une entrée
numérique dans toRoman, mais nous avons un
outil puissant à notre disposition : les expressions
régulières.
Si vous n’êtes pas familiarisé avec les expressions régulières et
que vous n’avez pas lu le Chapitre 7, Expressions régulières, il est sans doute
temps de le faire.
Comme nous l’avons vu au Section 7.3, «Exemple : chiffres romains», il y a plusieurs règles simples pour construire des nombres en chiffres romains à l’aide des lettres M, D, C, L, X, V et I. Récapitulons ces règles :
Les caractères sont additifs. I est
1, II est
2 et III est
3. VI est 6
(littéralement «5 et
1»), VII est
7 et VIII est
8.
Les caractères en un (I,
X, C, and
M) peuvent être répétés jusqu’à trois fois. A
4, vous devez soustraire du prochain caractère en
cinq. Vous ne pouvez pas représenter 4 par
IIII, au lieu de ça il est représenté par
IV («1 de moins que
5»). 40 s’écrit
XL («10 de moins que
50»), 41 s’écrit
XLI, 42XLII, 43XLIII et 44XLIV («10 de moins que
50, puis 1 de moins que
5»).
De manière similaire, à 9, vous devez
soustraire du prochain caractère en un : 8 est
VIII mais 9 est
IX («1 de moins que
10»), pas VIIII
(puisque le caractère I ne peut être répété
quatre fois). 90 est XC et
900CM.
Les caractères en cinq ne peuvent être répétés.
10 est toujours représenté par
X, jamais par VV.
100 est toujours C, jamais
LL.
Les chiffres romains sont toujours écrits du plus haut vers le plus bas et lus de gauche à droite, l’ordre des caractères
est donc très important. DC est 600, CD est un nombre totalement différent (400, «100 de moins que 500»). CI est 101, IC n’est même pas valide en chiffres romains (puisqu’on ne peut pas soustraire 1 directement de 100, il faudrait l’écrire XCIX, «10 de moins que 100, puis 1 de moins que 10»).
Exemple 14.12. roman5.py
Ce fichier est disponible dans le sous-répertoire py/roman/stage5/ du répertoire des exemples.
"""Convert to and from Roman numerals"""import re
#Define exceptionsclass RomanError(Exception): passclass OutOfRangeError(RomanError): passclass NotIntegerError(RomanError): passclass InvalidRomanNumeralError(RomanError): pass#Define digit mapping
romanNumeralMap = (('M', 1000),
('CM', 900),
('D', 500),
('CD', 400),
('C', 100),
('XC', 90),
('L', 50),
('XL', 40),
('X', 10),
('IX', 9),
('V', 5),
('IV', 4),
('I', 1))
def toRoman(n):
"""convert integer to Roman numeral"""ifnot (0 < n < 4000):
raise OutOfRangeError, "number out of range (must be 1..3999)"if int(n) <> n:
raise NotIntegerError, "non-integers can not be converted"
result = ""for numeral, integer in romanNumeralMap:
while n >= integer:
result += numeral
n -= integer
return result
#Define pattern to detect valid Roman numerals
romanNumeralPattern = '^M?M?M?(CM|CD|D?C?C?C?)(XC|XL|L?X?X?X?)(IX|IV|V?I?I?I?)$'def fromRoman(s):
"""convert Roman numeral to integer"""ifnot re.search(romanNumeralPattern, s): raise InvalidRomanNumeralError, 'Invalid Roman numeral: %s' % s
result = 0
index = 0
for numeral, integer in romanNumeralMap:
while s[index:index+len(numeral)] == numeral:
result += integer
index += len(numeral)
return result
C’est simplement l’extension du motif que nous avons vu au Section 7.3, «Exemple : chiffres romains». Les dizaines sont soit
XC (90), soit
XL (40), soit un
L optionnel suivi de 0 à 3 X
optionnels. Les unités sont soit IX
(9), soit IV
(4), soit un V optionnel
suivi de 0 à 3 I optionnels.
Une fois toute cette logique encodée dans notre expression
régulière, le code vérifiant la validité des nombres romain est
une formalité. Si re.search retourne un
objet, alors l’expression régulière à reconnu la chaîne et notre
entrée est valide, sinon notre entrée est invalide.
A ce stade, vous avez le droit d’être sceptique quant à la
capacité de cette expression régulière longue et disgracieuse
d’intercepter tous les types de nombres romains invalides. Mais vous
n’avez pas à me croire sur parole, observez plutôt les résultats
:
Exemple 14.13. Sortie de romantest5.py avec roman5.py
fromRoman should only accept uppercase input ... ok
toRoman should always return uppercase ... ok
fromRoman should fail with malformed antecedents ... ok
fromRoman should fail with repeated pairs of numerals ... ok
fromRoman should fail with too many repeated numerals ... ok
fromRoman should give known result with known input ... ok
toRoman should give known result with known input ... ok
fromRoman(toRoman(n))==n for all n ... ok
toRoman should fail with non-integer input ... ok
toRoman should fail with negative input ... ok
toRoman should fail with large input ... ok
toRoman should fail with 0 input ... ok
----------------------------------------------------------------------
Ran 12 tests in 2.864s
OK
Il y a une chose que je n’ai pas mentionné à propos des
expressions régulières, c’est que par défaut, elles sont sensibles
à la casse. Comme notre expression régulière
romanNumeralPattern est exprimée en majuscules,
notre vérification re.search rejettera toute
entrée qui n’est pas entièrement en majuscules. Donc notre test
d’entrée en majuscules uniquement passe.
Plus important encore, notre test d’entrée incorrecte passe.
Par exemple, le test d’antécédent mal formé vérifie les cas comme
MCMC. Comme nous l’avons vu, cela
ne correspond pas à notre expression régulière, donc
fromRoman déclenche une exception
InvalidRomanNumeralError, ce qui est ce que
le cas de test d’antécédent mal formé attend, donc le test
passe.
En fait, tous les tests d’entrées incorrectes passent. Cette
expression régulière intercepte tout ce que nous avons pu imaginer
quand nous avons écrit nos cas de test.
Et le prix du triomphe modeste est attribué au petit
«OK» qui est affiché par le module
unittest quand tous les
tests passent.
Quand tous vos tests passent, arrêtez d’écrire du code.