You are here: Partenza > Dive Into Python > Test delle unità di codice > roman.py, fase 5 | << >> | ||||
Dive Into PythonPython per programmatori esperti |
Ora che fromRoman funziona correttamente con input validi, è tempo di far combaciare l'ultimo pezzo del puzzle: farla funzionare con input non validi. Senza troppi giri di parole cerchiamo di osservare una stringa e di determinare se è un numero romano valido. Questo è ancor più difficoltoso se paragonato agli input validi in toRoman, ma noi abbiamo a disposizione un potente strumento a disposizione: le espressioni regolari
Se non avete familiarità con le espressioni regolari e non avete letto il capitolo sulle Espressioni regolari, questo è il momento opportuno per farlo.
Come abbiamo visto all'inizio di questo capitolo, ci sono diverse semplici regole per comporre un numero romano. La prima di tali regole è che le migliaia, ammesso che ve ne siano, sono rappresentate da una serie di caratteri M.
>>> import re >>> pattern = '^M?M?M?$' >>> re.search(pattern, 'M') <SRE_Match object at 0106FB58> >>> re.search(pattern, 'MM') <SRE_Match object at 0106C290> >>> re.search(pattern, 'MMM') <SRE_Match object at 0106AA38> >>> re.search(pattern, 'MMMM') >>> re.search(pattern, '') <SRE_Match object at 0106F4A8>
Trattare le centinaia è più difficile che trattare le migliaia, perché ci sono diversi modi mutualmente esclusivi in cui possono essere espresse, a seconda del valore da considerare.
Dunque ci sono quattro possibili schemi:
Gli ultimi due schemi possone essere riassunti in uno:
>>> import re >>> pattern = '^M?M?M?(CM|CD|D?C?C?C?)$' >>> re.search(pattern, 'MCM') <SRE_Match object at 01070390> >>> re.search(pattern, 'MD') <SRE_Match object at 01073A50> >>> re.search(pattern, 'MMMCCC') <SRE_Match object at 010748A8> >>> re.search(pattern, 'MCMC') >>> re.search(pattern, '') <SRE_Match object at 01071D98>
Wow! Vedete quanto rapidamente un'espressione regolare può diventare maligna? Ed abbiamo solo coperto le migliaia e le centinaia. (Più tardi, in questa sezione, vedremo una sintassi leggermente diversa per scrivere le espressioni regolari; questa sintassi, pur essendo altrettanto complicata, perlomeno ci permette di documentare sul posto le differenti componenti delle espressioni.) Fortunatamente, se avete seguito fino ad ora, le decine e le unità sono facili, perché seguono esattamente lo stesso pattern.
Se non lo avete ancora fatto, potete scaricare questo ed altri esempi usati in questo libro.
"""Convert to and from Roman numerals""" import re #Define exceptions class RomanError(Exception): pass class OutOfRangeError(RomanError): pass class NotIntegerError(RomanError): pass class 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""" if not (0 < n < 4000): raise OutOfRangeError, "number out of range (must be 1..3999)" if int(n) <> n: raise NotIntegerError, "decimals 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""" if not 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
A questo punto, siete autorizzati ad essere scettici sul fatto che quella orribile espressione regolare possa filtrare tutti i tipi di numeri romani non validi. Ma non dovete prendere per buona la mia parola; osservate i risultati:
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
Una cosa che non ho detto a proposito delle espressioni regolari è che, di norma, trattano diversamente maiuscole e minuscole. Dato che la nostra espressione regolare romanNumeralPattern era espressa in caratteri maiuscoli, il controllo fatto con re.search rigetta ogni input che non sia completamente in lettere maiuscole. Quindi il nostro test sugli input in lettere maiuscole passa con succcesso. | |
Cosa ancora più importante, tutti i nostri test con valori non validi passano con successo. Per esempio, il test sull'ordine erroneo delle cifre controlla casi come MCMC. Come abbiamo visto, questa stringa non corrisponde alla nostra espressione regolare, per cui la funzione fromRoman solleva una eccezione InvalidRomanNumeralError, che è quello che il nostro test si aspetta, per cui il test ha successo. | |
In effetti, tutti i nostri test con input non validi hanno successo. Questa nostra espressione regolare individua tutti i casi a cui avevamo pensato quando abbiamo preparato i nostri test. | |
Il premio dell'anno per il termine più eccitante va alla parola “OK”, che è stampata dal modulo unittest quando tutti i test hanno successo. |
Quando tutti i test passano con successo, smettete di scrivere codice. |
<< roman.py, fase 4 |
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | |
Come gestire gli errori di programmazione >> |