"""Convert to and from Roman numerals"""#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"""
result = ""for numeral, integer in romanNumeralMap:
while n >= integer:
result += numeral
n -= integer
return result
def fromRoman(s):
"""convert Roman numeral to integer"""pass
romanNumeralMap est un tuple de tuples
qui définit trois choses :
La représentation en caractères des chiffres romains les
plus élémentaires. Notez qu’il ne s’agit pas seulement des
chiffres romains à un seul caractère mais que nous définissons
également des paires comme CM («cent
de moins que mille»). Cela rendra notre code pour
toRoman plus simple.
L’ordre des chiffres romains. Ils sont listés par ordre
décroissant de valeur, de M jusqu’à
I.
La valeur de chaque chiffre romain. Chaque tuple est une
paire de (romain,
valeur).
C’est ici que nous bénéficions de notre structure de données
élaborée, nous n’avons pas besoin de logique particulière pour
prendre en charge la règle de soustraction. Pour convertir en
chiffres romains, nous parcourons simplement
romanNumeralMap à la recherche de la plus
grande valeur entière inférieure ou égale à notre entrée. Une fois
que nous l’avons trouvée, nous ajoutons sa représentation en
chiffres romains à la fin de la sortie, soustrayons la valeur de l’entrée et répétons l’opération.
Exemple 14.4. Comment toRoman fonctionne
Si vous n’êtes pas sûr de comprendre comment fonctionne
toRoman, ajoutez une instruction print
à la fin de la boucle while :
while n >= integer:
result += numeral
n -= integer
print'subtracting', integer, 'from input, adding', numeral, 'to output'
>>> import roman2>>> roman2.toRoman(1424)subtracting 1000 from input, adding M to output
subtracting 400 from input, adding CD to output
subtracting 10 from input, adding X to output
subtracting 10 from input, adding X to output
subtracting 4 from input, adding IV to output
'MCDXXIV'
toRoman a donc l’air de marcher, du moins
pour notre petit test manuel. Mais passera-t-il l test unitaire ? Et
bien non, pas complètement.
Exemple 14.5. Sortie de romantest2.py avec roman2.py
Rappelez-vous d’exécuter romantest2.py avec l’option de ligne de commande -v pour obtenir une sortie détaillée.
fromRoman should only accept uppercase input ... FAIL
toRoman should always return uppercase ... ok
fromRoman should fail with malformed antecedents ... FAIL
fromRoman should fail with repeated pairs of numerals ... FAIL
fromRoman should fail with too many repeated numerals ... FAIL
fromRoman should give known result with known input ... FAIL
toRoman should give known result with known input ... ok
fromRoman(toRoman(n))==n for all n ... FAIL
toRoman should fail with non-integer input ... FAIL
toRoman should fail with negative input ... FAIL
toRoman should fail with large input ... FAIL
toRoman should fail with 0 input ... FAIL
toRoman retourne bien toujours des
majuscules puisque notre romanNumeralMap
définit les représentation en nombres romains en majuscules. Donc
ce test passe.
Voici la grande nouvelle : cette version de la fonction
toRoman passe le test des valeurs
connues. Rappelez-vous, elle n’est pas exhaustive mais elle
teste largement la fonction avec un ensemble d’entrées correctes,
y compris les entrées pour chaque nombre romain d’un caractère,
l’entrée la plus grande possible (3999) et
l’entrée produisant le nombre romain le plus long
(3888). Arrivé là, on peut être raisonnablement
confiant que la fonction marche pour toute valeur correcte qu’il
lui est soumise.
Par contre, la fonction ne «marche» pas pour
les valeurs incorrectes, elle échoue pour tous les tests de valeurs
incorrectes. Cela semble logique puisque nous n’avons pas
écrit de vérification d’entrée. Ces cas de test attendent le
déclenchement d’exceptions spécifiques (à l’aide de
assertRaises) et nous ne les déclenchons
jamais. Nous le ferons à l’étape suivante.
Voici le reste de la sortie du test unitaire, détaillant tous
les échecs. Nous n’en sommes plus qu’à 10.
======================================================================
FAIL: fromRoman should only accept uppercase input
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\docbook\dip\py\roman\stage2\romantest2.py", line 156, in testFromRomanCase
roman2.fromRoman, numeral.lower())
File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
raise self.failureException, excName
AssertionError: InvalidRomanNumeralError
======================================================================
FAIL: fromRoman should fail with malformed antecedents
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\docbook\dip\py\roman\stage2\romantest2.py", line 133, in testMalformedAntecedent
self.assertRaises(roman2.InvalidRomanNumeralError, roman2.fromRoman, s)
File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
raise self.failureException, excName
AssertionError: InvalidRomanNumeralError
======================================================================
FAIL: fromRoman should fail with repeated pairs of numerals
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\docbook\dip\py\roman\stage2\romantest2.py", line 127, in testRepeatedPairs
self.assertRaises(roman2.InvalidRomanNumeralError, roman2.fromRoman, s)
File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
raise self.failureException, excName
AssertionError: InvalidRomanNumeralError
======================================================================
FAIL: fromRoman should fail with too many repeated numerals
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\docbook\dip\py\roman\stage2\romantest2.py", line 122, in testTooManyRepeatedNumerals
self.assertRaises(roman2.InvalidRomanNumeralError, roman2.fromRoman, s)
File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
raise self.failureException, excName
AssertionError: InvalidRomanNumeralError
======================================================================
FAIL: fromRoman should give known result with known input
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\docbook\dip\py\roman\stage2\romantest2.py", line 99, in testFromRomanKnownValues
self.assertEqual(integer, result)
File "c:\python21\lib\unittest.py", line 273, in failUnlessEqual
raise self.failureException, (msg or '%s != %s' % (first, second))
AssertionError: 1 != None
======================================================================
FAIL: fromRoman(toRoman(n))==n for all n
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\docbook\dip\py\roman\stage2\romantest2.py", line 141, in testSanity
self.assertEqual(integer, result)
File "c:\python21\lib\unittest.py", line 273, in failUnlessEqual
raise self.failureException, (msg or '%s != %s' % (first, second))
AssertionError: 1 != None
======================================================================
FAIL: toRoman should fail with non-integer input
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\docbook\dip\py\roman\stage2\romantest2.py", line 116, in testNonInteger
self.assertRaises(roman2.NotIntegerError, roman2.toRoman, 0.5)
File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
raise self.failureException, excName
AssertionError: NotIntegerError
======================================================================
FAIL: toRoman should fail with negative input
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\docbook\dip\py\roman\stage2\romantest2.py", line 112, in testNegative
self.assertRaises(roman2.OutOfRangeError, roman2.toRoman, -1)
File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
raise self.failureException, excName
AssertionError: OutOfRangeError
======================================================================
FAIL: toRoman should fail with large input
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\docbook\dip\py\roman\stage2\romantest2.py", line 104, in testTooLarge
self.assertRaises(roman2.OutOfRangeError, roman2.toRoman, 4000)
File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
raise self.failureException, excName
AssertionError: OutOfRangeError
======================================================================
FAIL: toRoman should fail with 0 input
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\docbook\dip\py\roman\stage2\romantest2.py", line 108, in testZero
self.assertRaises(roman2.OutOfRangeError, roman2.toRoman, 0)
File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
raise self.failureException, excName
AssertionError: OutOfRangeError
----------------------------------------------------------------------
Ran 12 tests in 0.320s
FAILED (failures=10)