7.7. roman.py, fase 2

Ora che abbiamo delineato l'infrastruttura del modulo roman, è tempo di cominciare a scrivere il codice e passare con successo qualche test.

Esempio 7.9. roman2.py

Se non lo avete ancora fatto, potete scaricare questo ed altri esempi usati in questo libro.

"""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 La variabile romanNumeralMap è una tupla che definisce tre cose:
  1. La rappresentazione in caratteri dei numeri romani di base. Si noti che non si tratta solo dei numeri romani a singolo carattere; sono state anche incluse coppie di caratteri come CM (“cento in meno di mille”); come vedremo in seguito, questo rende il codice della funzione toRoman più semplice.
  2. L'ordine dei numeri romani. I numeri romani vengono elencati per valore discendente, da M in poi fino ad I.
  3. Il valore di ciascuno dei numeri romani elencati. Ciascuna delle tuple interne è una coppia del tipo (numero romano, valore corrispondente).
2 Qui è dove la nostra dettagliata struttura di dati mostra la sua utilità, dato che grazie ad essa non abbiamo bisogno di alcuna logica per gestire la “regola della sottrazione”. Per convertire in numeri romani, iteriamo semplicemente su romanNumeralMap, alla ricerca del più grande valore minore od uguale al nostro input. Una volta trovato, aggiungiamo la corrispondente rappresentazione in numero romano all'output, sottraiamo il valore trovato dall'input e ricominciamo da capo.

Esempio 7.10. Come funziona toRoman

Se non vi è chiaro come funziona toRoman, potete aggiungere una istruzione di stampa (print) alla fine del ciclo 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'

Quindi toRoman sembra funzionare, almeno nel nostro piccolo controllo fatto a mano. Ma passerà i test delle unità di codice? Beh, no, non completamente.

Esempio 7.11. Output di romantest2.py a fronte di roman2.py

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 La funzione toRoman restituisce effettivamente un numero romano in lettere maiuscole, perché la variabile romanNumeralMap definisce le rappresentazioni dei numeri romani di base in lettere maiuscole. Perciò questo test è già superato con successo.
2 Questa è la grande novità: questa versione di toRoman supera con successo il test dei valori noti. Va ricordato che non è un test comprensivo di tutti i valori, anche se esercita il codice della funzione con una varietà di input validi, includendo input che producono tutti i numeri romani di un solo carattere, il più grande numero che è possibile convertire (3999), ed infine il numero arabo che produce il più lungo numero romano (3888). A questo punto, siamo ragionevolmente sicuri che la funzione lavora correttamente per qualunque numero valido che gli possiamo passare.
3 Tuttavia, la funzione non “lavora” correttamente per valori di input non validi; fallisce infatti tutti i test in tal senso. Questo è spiegabile, dato che la funzione non include alcun controllo per valori di input non validi. I test di input non validi si aspettano che vengano sollevate specifiche eccezioni (controllandole con assertRaises) e non c'è niente che le possa sollevare. Questo sarà fatto nella prossima fase.

Qui c'è il resto dell'output del test delle unità di codice, che elenca i dettagli di tutti i test falliti. Siamo sotto di 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 testDecimal
    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)