14.2. roman.py, étape 2

Maintenant que nous avons la structure de notre module roman en place, il est temps de commencer à écrire du code et à passer les cas de test.

Exemple 14.3. roman2.py

Ce fichier est disponible dans le sous-répertoire py/roman/stage2/ du répertoire des exemples.

Si vous ne l’avez pas déjà fait, vous pouvez télécharger cet exemple ainsi que les autres exemples du livre.

"""Convert to and from Roman numerals"""

#Define exceptions
class RomanError(Exception): pass
class OutOfRangeError(RomanError): pass
class NotIntegerError(RomanError): pass
class InvalidRomanNumeralError(RomanError): pass

#Define digit mapping
romanNumeralMap = (('M',  1000), 1
                   ('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:      2
            result += numeral
            n -= integer
    return result

def fromRoman(s):
    """convert Roman numeral to integer"""
    pass
1 romanNumeralMap est un tuple de tuples qui définit trois choses :
  1. 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 CMcent de moins que mille»). Cela rendra notre code pour toRoman plus simple.
  2. L’ordre des chiffres romains. Ils sont listés par ordre décroissant de valeur, de M jusqu’à I.
  3. La valeur de chaque chiffre romain. Chaque tuple est une paire de (romain, valeur).
2 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                  1
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       2
fromRoman(toRoman(n))==n for all n ... FAIL
toRoman should fail with non-integer input ... FAIL            3
toRoman should fail with negative input ... FAIL
toRoman should fail with large input ... FAIL
toRoman should fail with 0 input ... FAIL
1 toRoman retourne bien toujours des majuscules puisque notre romanNumeralMap définit les représentation en nombres romains en majuscules. Donc ce test passe.
2 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.
3 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)