From 29129df29960d4cd14be96f7850e28f89355800e Mon Sep 17 00:00:00 2001 From: Mark Pilgrim Date: Mon, 16 Feb 2009 14:31:42 -0500 Subject: [PATCH] added source files for unit testing chapter(s), fixed some bugs in regular expressions chapter --- .hgignore | 2 + regular-expressions.html | 18 +++--- roman1.py | 33 ++++++++++ roman2.py | 37 +++++++++++ roman3.py | 37 +++++++++++ roman4.py | 40 ++++++++++++ roman5.py | 46 ++++++++++++++ roman6.py | 64 +++++++++++++++++++ roman7.py | 66 ++++++++++++++++++++ roman8.py | 68 ++++++++++++++++++++ romantest1.py | 76 ++++++++++++++++++++++ romantest2.py | 81 ++++++++++++++++++++++++ romantest3.py | 89 ++++++++++++++++++++++++++ romantest4.py | 93 +++++++++++++++++++++++++++ romantest5.py | 107 +++++++++++++++++++++++++++++++ romantest6.py | 124 ++++++++++++++++++++++++++++++++++++ romantest7.py | 128 +++++++++++++++++++++++++++++++++++++ romantest8.py | 132 +++++++++++++++++++++++++++++++++++++++ 18 files changed, 1232 insertions(+), 9 deletions(-) create mode 100644 .hgignore create mode 100644 roman1.py create mode 100644 roman2.py create mode 100644 roman3.py create mode 100644 roman4.py create mode 100644 roman5.py create mode 100644 roman6.py create mode 100644 roman7.py create mode 100644 roman8.py create mode 100644 romantest1.py create mode 100644 romantest2.py create mode 100644 romantest3.py create mode 100644 romantest4.py create mode 100644 romantest5.py create mode 100644 romantest6.py create mode 100644 romantest7.py create mode 100644 romantest8.py diff --git a/.hgignore b/.hgignore new file mode 100644 index 0000000..9e32b48 --- /dev/null +++ b/.hgignore @@ -0,0 +1,2 @@ +syntax: glob +*.pyc diff --git a/regular-expressions.html b/regular-expressions.html index b720678..23aaa1d 100644 --- a/regular-expressions.html +++ b/regular-expressions.html @@ -209,7 +209,7 @@ characters. If you've used regular expressions in other languages (like Perl), t

Checking for tens and ones

Now let's expand the Roman numeral regular expression to cover the tens and ones place. This example shows the check for tens.

->>> pattern = '^M?M?M?M?(CM|CD|D?C?C?C?)(XC|XL|L?X?X?X?)$'
+>>> pattern = '^M?M?M?(CM|CD|D?C?C?C?)(XC|XL|L?X?X?X?)$'
 >>> re.search(pattern, 'MCMXL')     
 <_sre.SRE_Match object at 0x008EEB48>
 >>> re.search(pattern, 'MCML')      
@@ -229,10 +229,10 @@ characters. If you've used regular expressions in other languages (like Perl), t
 
 

The expression for the ones place follows the same pattern. I'll spare you the details and show you the end result.

->>> pattern = '^M?M?M?M?(CM|CD|D?C?C?C?)(XC|XL|L?X?X?X?)(IX|IV|V?I?I?I?)$'
+>>> pattern = '^M?M?M?(CM|CD|D?C?C?C?)(XC|XL|L?X?X?X?)(IX|IV|V?I?I?I?)$'
 

So what does that look like using this alternate {n,m} syntax? This example shows the new syntax.

->>> pattern = '^M{0,4}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$'
+>>> pattern = '^M{0,3}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$'
 >>> re.search(pattern, 'MDLV')              
 <_sre.SRE_Match object at 0x008EEB48>
 >>> re.search(pattern, 'MMDCLXVI')          
@@ -259,15 +259,15 @@ characters. If you've used regular expressions in other languages (like Perl), t
 

This will be more clear with an example. Let's revisit the compact regular expression you've been working with, and make it a verbose regular expression. This example shows how.

 >>> pattern = """
-    ^ # beginning of string
-    M{0,4}              # thousands - 0 to 4 M's
+    ^                   # beginning of string
+    M{0,3}              # thousands - 0 to 3 M's
     (CM|CD|D?C{0,3})    # hundreds - 900 (CM), 400 (CD), 0-300 (0 to 3 C's),
-      #            or 500-800 (D, followed by 0 to 3 C's)
+                        #            or 500-800 (D, followed by 0 to 3 C's)
     (XC|XL|L?X{0,3})    # tens - 90 (XC), 40 (XL), 0-30 (0 to 3 X's),
-      #        or 50-80 (L, followed by 0 to 3 X's)
+                        #        or 50-80 (L, followed by 0 to 3 X's)
     (IX|IV|V?I{0,3})    # ones - 9 (IX), 4 (IV), 0-3 (0 to 3 I's),
-      #        or 5-8 (V, followed by 0 to 3 I's)
-    $ # end of string
+                        #        or 5-8 (V, followed by 0 to 3 I's)
+    $                   # end of string
     """
 >>> re.search(pattern, 'M', re.VERBOSE)                 
 <_sre.SRE_Match object at 0x008EEB48>
diff --git a/roman1.py b/roman1.py
new file mode 100644
index 0000000..e051037
--- /dev/null
+++ b/roman1.py
@@ -0,0 +1,33 @@
+"""Convert to and from Roman numerals
+
+This program is part of "Dive Into Python 3", a free Python book for
+experienced programmers.  Visit http://diveintopython3.org/ for the
+latest version.
+"""
+
+roman_numeral_map = (('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 to_roman(n):
+    """convert integer to Roman numeral"""
+    result = ""
+    for numeral, integer in roman_numeral_map:
+        while n >= integer:
+            result += numeral
+            n -= integer
+    return result
+
+def from_roman(s):
+    """convert Roman numeral to integer"""
+    pass
diff --git a/roman2.py b/roman2.py
new file mode 100644
index 0000000..285abce
--- /dev/null
+++ b/roman2.py
@@ -0,0 +1,37 @@
+"""Convert to and from Roman numerals
+
+This program is part of "Dive Into Python 3", a free Python book for
+experienced programmers.  Visit http://diveintopython3.org/ for the
+latest version.
+"""
+class OutOfRangeError(ValueError): pass
+
+roman_numeral_map = (('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 to_roman(n):
+    """convert integer to Roman numeral"""
+    if n > 3999:
+        raise OutOfRangeError("number out of range (must be less than 3999)")
+
+    result = ""
+    for numeral, integer in roman_numeral_map:
+        while n >= integer:
+            result += numeral
+            n -= integer
+    return result
+
+def from_roman(s):
+    """convert Roman numeral to integer"""
+    pass
diff --git a/roman3.py b/roman3.py
new file mode 100644
index 0000000..832dfcf
--- /dev/null
+++ b/roman3.py
@@ -0,0 +1,37 @@
+"""Convert to and from Roman numerals
+
+This program is part of "Dive Into Python 3", a free Python book for
+experienced programmers.  Visit http://diveintopython3.org/ for the
+latest version.
+"""
+class OutOfRangeError(ValueError): pass
+
+roman_numeral_map = (('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 to_roman(n):
+    """convert integer to Roman numeral"""
+    if not (0 < n < 4000):
+        raise OutOfRangeError("number out of range (must be 0..3999)")
+
+    result = ""
+    for numeral, integer in roman_numeral_map:
+        while n >= integer:
+            result += numeral
+            n -= integer
+    return result
+
+def from_roman(s):
+    """convert Roman numeral to integer"""
+    pass
diff --git a/roman4.py b/roman4.py
new file mode 100644
index 0000000..e0ddc8b
--- /dev/null
+++ b/roman4.py
@@ -0,0 +1,40 @@
+"""Convert to and from Roman numerals
+
+This program is part of "Dive Into Python 3", a free Python book for
+experienced programmers.  Visit http://diveintopython3.org/ for the
+latest version.
+"""
+class OutOfRangeError(ValueError): pass
+class NotIntegerError(ValueError): pass
+
+roman_numeral_map = (('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 to_roman(n):
+    """convert integer to Roman numeral"""
+    if not (0 < n < 4000):
+        raise OutOfRangeError("number out of range (must be 0..3999)")
+    if not isinstance(n, int):
+        raise NotIntegerError("non-integers can not be converted")
+
+    result = ""
+    for numeral, integer in roman_numeral_map:
+        while n >= integer:
+            result += numeral
+            n -= integer
+    return result
+
+def from_roman(s):
+    """convert Roman numeral to integer"""
+    pass
diff --git a/roman5.py b/roman5.py
new file mode 100644
index 0000000..04e3046
--- /dev/null
+++ b/roman5.py
@@ -0,0 +1,46 @@
+"""Convert to and from Roman numerals
+
+This program is part of "Dive Into Python 3", a free Python book for
+experienced programmers.  Visit http://diveintopython3.org/ for the
+latest version.
+"""
+class OutOfRangeError(ValueError): pass
+class NotIntegerError(ValueError): pass
+
+roman_numeral_map = (('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 to_roman(n):
+    """convert integer to Roman numeral"""
+    if not (0 < n < 4000):
+        raise OutOfRangeError("number out of range (must be 0..3999)")
+    if not isinstance(n, int):
+        raise NotIntegerError("non-integers can not be converted")
+
+    result = ""
+    for numeral, integer in roman_numeral_map:
+        while n >= integer:
+            result += numeral
+            n -= integer
+    return result
+
+def from_roman(s):
+    """convert Roman numeral to integer"""
+    result = 0
+    index = 0
+    for numeral, integer in roman_numeral_map:
+        while s[index : index + len(numeral)] == numeral:
+            result += integer
+            index += len(numeral)
+    return result
diff --git a/roman6.py b/roman6.py
new file mode 100644
index 0000000..47f450c
--- /dev/null
+++ b/roman6.py
@@ -0,0 +1,64 @@
+"""Convert to and from Roman numerals
+
+This program is part of "Dive Into Python 3", a free Python book for
+experienced programmers.  Visit http://diveintopython3.org/ for the
+latest version.
+"""
+import re
+
+class OutOfRangeError(ValueError): pass
+class NotIntegerError(ValueError): pass
+class InvalidRomanNumeralError(ValueError): pass
+
+roman_numeral_map = (('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))
+
+roman_numeral_pattern = re.compile("""
+    ^                   # beginning of string
+    M{0,3}              # thousands - 0 to 3 M's
+    (CM|CD|D?C{0,3})    # hundreds - 900 (CM), 400 (CD), 0-300 (0 to 3 C's),
+                        #            or 500-800 (D, followed by 0 to 3 C's)
+    (XC|XL|L?X{0,3})    # tens - 90 (XC), 40 (XL), 0-30 (0 to 3 X's),
+                        #        or 50-80 (L, followed by 0 to 3 X's)
+    (IX|IV|V?I{0,3})    # ones - 9 (IX), 4 (IV), 0-3 (0 to 3 I's),
+                        #        or 5-8 (V, followed by 0 to 3 I's)
+    $                   # end of string
+    """, re.VERBOSE)
+
+def to_roman(n):
+    """convert integer to Roman numeral"""
+    if not (0 < n < 4000):
+        raise OutOfRangeError("number out of range (must be 0..3999)")
+    if not isinstance(n, int):
+        raise NotIntegerError("non-integers can not be converted")
+
+    result = ""
+    for numeral, integer in roman_numeral_map:
+        while n >= integer:
+            result += numeral
+            n -= integer
+    return result
+
+def from_roman(s):
+    """convert Roman numeral to integer"""
+    if not roman_numeral_pattern.search(s):
+        raise InvalidRomanNumeralError("Invalid Roman numeral: {0}".format(s))
+
+    result = 0
+    index = 0
+    for numeral, integer in roman_numeral_map:
+        while s[index : index + len(numeral)] == numeral:
+            result += integer
+            index += len(numeral)
+    return result
diff --git a/roman7.py b/roman7.py
new file mode 100644
index 0000000..e848e59
--- /dev/null
+++ b/roman7.py
@@ -0,0 +1,66 @@
+"""Convert to and from Roman numerals
+
+This program is part of "Dive Into Python 3", a free Python book for
+experienced programmers.  Visit http://diveintopython3.org/ for the
+latest version.
+"""
+import re
+
+class OutOfRangeError(ValueError): pass
+class NotIntegerError(ValueError): pass
+class InvalidRomanNumeralError(ValueError): pass
+
+roman_numeral_map = (('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))
+
+roman_numeral_pattern = re.compile("""
+    ^                   # beginning of string
+    M{0,3}              # thousands - 0 to 3 M's
+    (CM|CD|D?C{0,3})    # hundreds - 900 (CM), 400 (CD), 0-300 (0 to 3 C's),
+                        #            or 500-800 (D, followed by 0 to 3 C's)
+    (XC|XL|L?X{0,3})    # tens - 90 (XC), 40 (XL), 0-30 (0 to 3 X's),
+                        #        or 50-80 (L, followed by 0 to 3 X's)
+    (IX|IV|V?I{0,3})    # ones - 9 (IX), 4 (IV), 0-3 (0 to 3 I's),
+                        #        or 5-8 (V, followed by 0 to 3 I's)
+    $                   # end of string
+    """, re.VERBOSE)
+
+def to_roman(n):
+    """convert integer to Roman numeral"""
+    if not (0 < n < 4000):
+        raise OutOfRangeError("number out of range (must be 0..3999)")
+    if not isinstance(n, int):
+        raise NotIntegerError("non-integers can not be converted")
+
+    result = ""
+    for numeral, integer in roman_numeral_map:
+        while n >= integer:
+            result += numeral
+            n -= integer
+    return result
+
+def from_roman(s):
+    """convert Roman numeral to integer"""
+    if not s:
+        raise InvalidRomanNumeralError("Input can not be blank")
+    if not roman_numeral_pattern.search(s):
+        raise InvalidRomanNumeralError("Invalid Roman numeral: {0}".format(s))
+
+    result = 0
+    index = 0
+    for numeral, integer in roman_numeral_map:
+        while s[index : index + len(numeral)] == numeral:
+            result += integer
+            index += len(numeral)
+    return result
diff --git a/roman8.py b/roman8.py
new file mode 100644
index 0000000..124bbbc
--- /dev/null
+++ b/roman8.py
@@ -0,0 +1,68 @@
+"""Convert to and from Roman numerals
+
+This program is part of "Dive Into Python 3", a free Python book for
+experienced programmers.  Visit http://diveintopython3.org/ for the
+latest version.
+"""
+import re
+
+class OutOfRangeError(ValueError): pass
+class NotIntegerError(ValueError): pass
+class InvalidRomanNumeralError(ValueError): pass
+
+roman_numeral_map = (('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))
+
+roman_numeral_pattern = re.compile("""
+    ^                   # beginning of string
+    M{0,3}              # thousands - 0 to 3 M's
+    (CM|CD|D?C{0,3})    # hundreds - 900 (CM), 400 (CD), 0-300 (0 to 3 C's),
+                        #            or 500-800 (D, followed by 0 to 3 C's)
+    (XC|XL|L?X{0,3})    # tens - 90 (XC), 40 (XL), 0-30 (0 to 3 X's),
+                        #        or 50-80 (L, followed by 0 to 3 X's)
+    (IX|IV|V?I{0,3})    # ones - 9 (IX), 4 (IV), 0-3 (0 to 3 I's),
+                        #        or 5-8 (V, followed by 0 to 3 I's)
+    $                   # end of string
+    """, re.VERBOSE)
+
+def to_roman(n):
+    """convert integer to Roman numeral"""
+    if not (0 < n < 4000):
+        raise OutOfRangeError("number out of range (must be 0..3999)")
+    if not isinstance(n, int):
+        raise NotIntegerError("non-integers can not be converted")
+
+    result = ""
+    for numeral, integer in roman_numeral_map:
+        while n >= integer:
+            result += numeral
+            n -= integer
+    return result
+
+def from_roman(s):
+    """convert Roman numeral to integer"""
+    if not isinstance(s, str):
+        raise InvalidRomanNumeralError("Input must be a string")
+    if not s:
+        raise InvalidRomanNumeralError("Input can not be blank")
+    if not roman_numeral_pattern.search(s):
+        raise InvalidRomanNumeralError("Invalid Roman numeral: {0}".format(s))
+
+    result = 0
+    index = 0
+    for numeral, integer in roman_numeral_map:
+        while s[index : index + len(numeral)] == numeral:
+            result += integer
+            index += len(numeral)
+    return result
diff --git a/romantest1.py b/romantest1.py
new file mode 100644
index 0000000..bc75857
--- /dev/null
+++ b/romantest1.py
@@ -0,0 +1,76 @@
+"""Unit test for roman1.py
+
+This program is part of "Dive Into Python 3", a free Python book for
+experienced programmers.  Visit http://diveintopython3.org/ for the
+latest version.
+"""
+
+import roman1
+import unittest
+
+class KnownValues(unittest.TestCase):
+    known_values = ( (1, 'I'),
+                     (2, 'II'),
+                     (3, 'III'),
+                     (4, 'IV'),
+                     (5, 'V'),
+                     (6, 'VI'),
+                     (7, 'VII'),
+                     (8, 'VIII'),
+                     (9, 'IX'),
+                     (10, 'X'),
+                     (50, 'L'),
+                     (100, 'C'),
+                     (500, 'D'),
+                     (1000, 'M'),
+                     (31, 'XXXI'),
+                     (148, 'CXLVIII'),
+                     (294, 'CCXCIV'),
+                     (312, 'CCCXII'),
+                     (421, 'CDXXI'),
+                     (528, 'DXXVIII'),
+                     (621, 'DCXXI'),
+                     (782, 'DCCLXXXII'),
+                     (870, 'DCCCLXX'),
+                     (941, 'CMXLI'),
+                     (1043, 'MXLIII'),
+                     (1110, 'MCX'),
+                     (1226, 'MCCXXVI'),
+                     (1301, 'MCCCI'),
+                     (1485, 'MCDLXXXV'),
+                     (1509, 'MDIX'),
+                     (1607, 'MDCVII'),
+                     (1754, 'MDCCLIV'),
+                     (1832, 'MDCCCXXXII'),
+                     (1993, 'MCMXCIII'),
+                     (2074, 'MMLXXIV'),
+                     (2152, 'MMCLII'),
+                     (2212, 'MMCCXII'),
+                     (2343, 'MMCCCXLIII'),
+                     (2499, 'MMCDXCIX'),
+                     (2574, 'MMDLXXIV'),
+                     (2646, 'MMDCXLVI'),
+                     (2723, 'MMDCCXXIII'),
+                     (2892, 'MMDCCCXCII'),
+                     (2975, 'MMCMLXXV'),
+                     (3051, 'MMMLI'),
+                     (3185, 'MMMCLXXXV'),
+                     (3250, 'MMMCCL'),
+                     (3313, 'MMMCCCXIII'),
+                     (3408, 'MMMCDVIII'),
+                     (3501, 'MMMDI'),
+                     (3610, 'MMMDCX'),
+                     (3743, 'MMMDCCXLIII'),
+                     (3844, 'MMMDCCCXLIV'),
+                     (3888, 'MMMDCCCLXXXVIII'),
+                     (3940, 'MMMCMXL'),
+                     (3999, 'MMMCMXCIX'))
+
+    def test_to_roman_known_values(self):
+        """to_roman should give known result with known input"""
+        for integer, numeral in self.known_values:
+            result = roman1.to_roman(integer)
+            self.assertEqual(numeral, result)
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/romantest2.py b/romantest2.py
new file mode 100644
index 0000000..f111f1b
--- /dev/null
+++ b/romantest2.py
@@ -0,0 +1,81 @@
+"""Unit test for roman1.py
+
+This program is part of "Dive Into Python 3", a free Python book for
+experienced programmers.  Visit http://diveintopython3.org/ for the
+latest version.
+"""
+
+import roman2
+import unittest
+
+class KnownValues(unittest.TestCase):
+    known_values = ( (1, 'I'),
+                     (2, 'II'),
+                     (3, 'III'),
+                     (4, 'IV'),
+                     (5, 'V'),
+                     (6, 'VI'),
+                     (7, 'VII'),
+                     (8, 'VIII'),
+                     (9, 'IX'),
+                     (10, 'X'),
+                     (50, 'L'),
+                     (100, 'C'),
+                     (500, 'D'),
+                     (1000, 'M'),
+                     (31, 'XXXI'),
+                     (148, 'CXLVIII'),
+                     (294, 'CCXCIV'),
+                     (312, 'CCCXII'),
+                     (421, 'CDXXI'),
+                     (528, 'DXXVIII'),
+                     (621, 'DCXXI'),
+                     (782, 'DCCLXXXII'),
+                     (870, 'DCCCLXX'),
+                     (941, 'CMXLI'),
+                     (1043, 'MXLIII'),
+                     (1110, 'MCX'),
+                     (1226, 'MCCXXVI'),
+                     (1301, 'MCCCI'),
+                     (1485, 'MCDLXXXV'),
+                     (1509, 'MDIX'),
+                     (1607, 'MDCVII'),
+                     (1754, 'MDCCLIV'),
+                     (1832, 'MDCCCXXXII'),
+                     (1993, 'MCMXCIII'),
+                     (2074, 'MMLXXIV'),
+                     (2152, 'MMCLII'),
+                     (2212, 'MMCCXII'),
+                     (2343, 'MMCCCXLIII'),
+                     (2499, 'MMCDXCIX'),
+                     (2574, 'MMDLXXIV'),
+                     (2646, 'MMDCXLVI'),
+                     (2723, 'MMDCCXXIII'),
+                     (2892, 'MMDCCCXCII'),
+                     (2975, 'MMCMLXXV'),
+                     (3051, 'MMMLI'),
+                     (3185, 'MMMCLXXXV'),
+                     (3250, 'MMMCCL'),
+                     (3313, 'MMMCCCXIII'),
+                     (3408, 'MMMCDVIII'),
+                     (3501, 'MMMDI'),
+                     (3610, 'MMMDCX'),
+                     (3743, 'MMMDCCXLIII'),
+                     (3844, 'MMMDCCCXLIV'),
+                     (3888, 'MMMDCCCLXXXVIII'),
+                     (3940, 'MMMCMXL'),
+                     (3999, 'MMMCMXCIX'))
+
+    def test_to_roman_known_values(self):
+        """to_roman should give known result with known input"""
+        for integer, numeral in self.known_values:
+            result = roman2.to_roman(integer)
+            self.assertEqual(numeral, result)
+
+class ToRomanBadInput(unittest.TestCase):
+    def test_too_large(self):
+        """to_roman should fail with large input"""
+        self.assertRaises(roman2.OutOfRangeError, roman2.to_roman, 4000)
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/romantest3.py b/romantest3.py
new file mode 100644
index 0000000..b759806
--- /dev/null
+++ b/romantest3.py
@@ -0,0 +1,89 @@
+"""Unit test for roman1.py
+
+This program is part of "Dive Into Python 3", a free Python book for
+experienced programmers.  Visit http://diveintopython3.org/ for the
+latest version.
+"""
+
+import roman3
+import unittest
+
+class KnownValues(unittest.TestCase):
+    known_values = ( (1, 'I'),
+                     (2, 'II'),
+                     (3, 'III'),
+                     (4, 'IV'),
+                     (5, 'V'),
+                     (6, 'VI'),
+                     (7, 'VII'),
+                     (8, 'VIII'),
+                     (9, 'IX'),
+                     (10, 'X'),
+                     (50, 'L'),
+                     (100, 'C'),
+                     (500, 'D'),
+                     (1000, 'M'),
+                     (31, 'XXXI'),
+                     (148, 'CXLVIII'),
+                     (294, 'CCXCIV'),
+                     (312, 'CCCXII'),
+                     (421, 'CDXXI'),
+                     (528, 'DXXVIII'),
+                     (621, 'DCXXI'),
+                     (782, 'DCCLXXXII'),
+                     (870, 'DCCCLXX'),
+                     (941, 'CMXLI'),
+                     (1043, 'MXLIII'),
+                     (1110, 'MCX'),
+                     (1226, 'MCCXXVI'),
+                     (1301, 'MCCCI'),
+                     (1485, 'MCDLXXXV'),
+                     (1509, 'MDIX'),
+                     (1607, 'MDCVII'),
+                     (1754, 'MDCCLIV'),
+                     (1832, 'MDCCCXXXII'),
+                     (1993, 'MCMXCIII'),
+                     (2074, 'MMLXXIV'),
+                     (2152, 'MMCLII'),
+                     (2212, 'MMCCXII'),
+                     (2343, 'MMCCCXLIII'),
+                     (2499, 'MMCDXCIX'),
+                     (2574, 'MMDLXXIV'),
+                     (2646, 'MMDCXLVI'),
+                     (2723, 'MMDCCXXIII'),
+                     (2892, 'MMDCCCXCII'),
+                     (2975, 'MMCMLXXV'),
+                     (3051, 'MMMLI'),
+                     (3185, 'MMMCLXXXV'),
+                     (3250, 'MMMCCL'),
+                     (3313, 'MMMCCCXIII'),
+                     (3408, 'MMMCDVIII'),
+                     (3501, 'MMMDI'),
+                     (3610, 'MMMDCX'),
+                     (3743, 'MMMDCCXLIII'),
+                     (3844, 'MMMDCCCXLIV'),
+                     (3888, 'MMMDCCCLXXXVIII'),
+                     (3940, 'MMMCMXL'),
+                     (3999, 'MMMCMXCIX'))
+
+    def test_to_roman_known_values(self):
+        """to_roman should give known result with known input"""
+        for integer, numeral in self.known_values:
+            result = roman3.to_roman(integer)
+            self.assertEqual(numeral, result)
+
+class ToRomanBadInput(unittest.TestCase):
+    def test_too_large(self):
+        """to_roman should fail with large input"""
+        self.assertRaises(roman3.OutOfRangeError, roman3.to_roman, 4000)
+
+    def test_zero(self):
+        """to_roman should fail with 0 input"""
+        self.assertRaises(roman3.OutOfRangeError, roman3.to_roman, 0)
+
+    def test_negative(self):
+        """to_roman should fail with negative input"""
+        self.assertRaises(roman3.OutOfRangeError, roman3.to_roman, -1)
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/romantest4.py b/romantest4.py
new file mode 100644
index 0000000..8a9f37a
--- /dev/null
+++ b/romantest4.py
@@ -0,0 +1,93 @@
+"""Unit test for roman1.py
+
+This program is part of "Dive Into Python 3", a free Python book for
+experienced programmers.  Visit http://diveintopython3.org/ for the
+latest version.
+"""
+
+import roman4
+import unittest
+
+class KnownValues(unittest.TestCase):
+    known_values = ( (1, 'I'),
+                     (2, 'II'),
+                     (3, 'III'),
+                     (4, 'IV'),
+                     (5, 'V'),
+                     (6, 'VI'),
+                     (7, 'VII'),
+                     (8, 'VIII'),
+                     (9, 'IX'),
+                     (10, 'X'),
+                     (50, 'L'),
+                     (100, 'C'),
+                     (500, 'D'),
+                     (1000, 'M'),
+                     (31, 'XXXI'),
+                     (148, 'CXLVIII'),
+                     (294, 'CCXCIV'),
+                     (312, 'CCCXII'),
+                     (421, 'CDXXI'),
+                     (528, 'DXXVIII'),
+                     (621, 'DCXXI'),
+                     (782, 'DCCLXXXII'),
+                     (870, 'DCCCLXX'),
+                     (941, 'CMXLI'),
+                     (1043, 'MXLIII'),
+                     (1110, 'MCX'),
+                     (1226, 'MCCXXVI'),
+                     (1301, 'MCCCI'),
+                     (1485, 'MCDLXXXV'),
+                     (1509, 'MDIX'),
+                     (1607, 'MDCVII'),
+                     (1754, 'MDCCLIV'),
+                     (1832, 'MDCCCXXXII'),
+                     (1993, 'MCMXCIII'),
+                     (2074, 'MMLXXIV'),
+                     (2152, 'MMCLII'),
+                     (2212, 'MMCCXII'),
+                     (2343, 'MMCCCXLIII'),
+                     (2499, 'MMCDXCIX'),
+                     (2574, 'MMDLXXIV'),
+                     (2646, 'MMDCXLVI'),
+                     (2723, 'MMDCCXXIII'),
+                     (2892, 'MMDCCCXCII'),
+                     (2975, 'MMCMLXXV'),
+                     (3051, 'MMMLI'),
+                     (3185, 'MMMCLXXXV'),
+                     (3250, 'MMMCCL'),
+                     (3313, 'MMMCCCXIII'),
+                     (3408, 'MMMCDVIII'),
+                     (3501, 'MMMDI'),
+                     (3610, 'MMMDCX'),
+                     (3743, 'MMMDCCXLIII'),
+                     (3844, 'MMMDCCCXLIV'),
+                     (3888, 'MMMDCCCLXXXVIII'),
+                     (3940, 'MMMCMXL'),
+                     (3999, 'MMMCMXCIX'))
+
+    def test_to_roman_known_values(self):
+        """to_roman should give known result with known input"""
+        for integer, numeral in self.known_values:
+            result = roman4.to_roman(integer)
+            self.assertEqual(numeral, result)
+
+class ToRomanBadInput(unittest.TestCase):
+    def test_too_large(self):
+        """to_roman should fail with large input"""
+        self.assertRaises(roman4.OutOfRangeError, roman4.to_roman, 4000)
+
+    def test_zero(self):
+        """to_roman should fail with 0 input"""
+        self.assertRaises(roman4.OutOfRangeError, roman4.to_roman, 0)
+
+    def test_negative(self):
+        """to_roman should fail with negative input"""
+        self.assertRaises(roman4.OutOfRangeError, roman4.to_roman, -1)
+
+    def test_non_integer(self):
+        """to_roman should fail with non-integer input"""
+        self.assertRaises(roman4.NotIntegerError, roman4.to_roman, 0.5)
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/romantest5.py b/romantest5.py
new file mode 100644
index 0000000..d3f9d16
--- /dev/null
+++ b/romantest5.py
@@ -0,0 +1,107 @@
+"""Unit test for roman1.py
+
+This program is part of "Dive Into Python 3", a free Python book for
+experienced programmers.  Visit http://diveintopython3.org/ for the
+latest version.
+"""
+
+import roman5
+import unittest
+
+class KnownValues(unittest.TestCase):
+    known_values = ( (1, 'I'),
+                     (2, 'II'),
+                     (3, 'III'),
+                     (4, 'IV'),
+                     (5, 'V'),
+                     (6, 'VI'),
+                     (7, 'VII'),
+                     (8, 'VIII'),
+                     (9, 'IX'),
+                     (10, 'X'),
+                     (50, 'L'),
+                     (100, 'C'),
+                     (500, 'D'),
+                     (1000, 'M'),
+                     (31, 'XXXI'),
+                     (148, 'CXLVIII'),
+                     (294, 'CCXCIV'),
+                     (312, 'CCCXII'),
+                     (421, 'CDXXI'),
+                     (528, 'DXXVIII'),
+                     (621, 'DCXXI'),
+                     (782, 'DCCLXXXII'),
+                     (870, 'DCCCLXX'),
+                     (941, 'CMXLI'),
+                     (1043, 'MXLIII'),
+                     (1110, 'MCX'),
+                     (1226, 'MCCXXVI'),
+                     (1301, 'MCCCI'),
+                     (1485, 'MCDLXXXV'),
+                     (1509, 'MDIX'),
+                     (1607, 'MDCVII'),
+                     (1754, 'MDCCLIV'),
+                     (1832, 'MDCCCXXXII'),
+                     (1993, 'MCMXCIII'),
+                     (2074, 'MMLXXIV'),
+                     (2152, 'MMCLII'),
+                     (2212, 'MMCCXII'),
+                     (2343, 'MMCCCXLIII'),
+                     (2499, 'MMCDXCIX'),
+                     (2574, 'MMDLXXIV'),
+                     (2646, 'MMDCXLVI'),
+                     (2723, 'MMDCCXXIII'),
+                     (2892, 'MMDCCCXCII'),
+                     (2975, 'MMCMLXXV'),
+                     (3051, 'MMMLI'),
+                     (3185, 'MMMCLXXXV'),
+                     (3250, 'MMMCCL'),
+                     (3313, 'MMMCCCXIII'),
+                     (3408, 'MMMCDVIII'),
+                     (3501, 'MMMDI'),
+                     (3610, 'MMMDCX'),
+                     (3743, 'MMMDCCXLIII'),
+                     (3844, 'MMMDCCCXLIV'),
+                     (3888, 'MMMDCCCLXXXVIII'),
+                     (3940, 'MMMCMXL'),
+                     (3999, 'MMMCMXCIX'))
+
+    def test_to_roman_known_values(self):
+        """to_roman should give known result with known input"""
+        for integer, numeral in self.known_values:
+            result = roman5.to_roman(integer)
+            self.assertEqual(numeral, result)
+
+    def test_from_roman_known_values(self):
+        """from_roman should give known result with known input"""
+        for integer, numeral in self.known_values:
+            result = roman5.from_roman(numeral)
+            self.assertEqual(integer, result)
+
+class ToRomanBadInput(unittest.TestCase):
+    def test_too_large(self):
+        """to_roman should fail with large input"""
+        self.assertRaises(roman5.OutOfRangeError, roman5.to_roman, 4000)
+
+    def test_zero(self):
+        """to_roman should fail with 0 input"""
+        self.assertRaises(roman5.OutOfRangeError, roman5.to_roman, 0)
+
+    def test_negative(self):
+        """to_roman should fail with negative input"""
+        self.assertRaises(roman5.OutOfRangeError, roman5.to_roman, -1)
+
+    def test_non_integer(self):
+        """to_roman should fail with non-integer input"""
+        self.assertRaises(roman5.NotIntegerError, roman5.to_roman, 0.5)
+
+class SanityCheck(unittest.TestCase):
+    def testSanity(self):
+        """from_roman(to_roman(n))==n for all n"""
+        for integer in range(1, 4000):
+            numeral = roman5.to_roman(integer)
+            result = roman5.from_roman(numeral)
+            self.assertEqual(integer, result)
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/romantest6.py b/romantest6.py
new file mode 100644
index 0000000..ebf535b
--- /dev/null
+++ b/romantest6.py
@@ -0,0 +1,124 @@
+"""Unit test for roman1.py
+
+This program is part of "Dive Into Python 3", a free Python book for
+experienced programmers.  Visit http://diveintopython3.org/ for the
+latest version.
+"""
+
+import roman6
+import unittest
+
+class KnownValues(unittest.TestCase):
+    known_values = ( (1, 'I'),
+                     (2, 'II'),
+                     (3, 'III'),
+                     (4, 'IV'),
+                     (5, 'V'),
+                     (6, 'VI'),
+                     (7, 'VII'),
+                     (8, 'VIII'),
+                     (9, 'IX'),
+                     (10, 'X'),
+                     (50, 'L'),
+                     (100, 'C'),
+                     (500, 'D'),
+                     (1000, 'M'),
+                     (31, 'XXXI'),
+                     (148, 'CXLVIII'),
+                     (294, 'CCXCIV'),
+                     (312, 'CCCXII'),
+                     (421, 'CDXXI'),
+                     (528, 'DXXVIII'),
+                     (621, 'DCXXI'),
+                     (782, 'DCCLXXXII'),
+                     (870, 'DCCCLXX'),
+                     (941, 'CMXLI'),
+                     (1043, 'MXLIII'),
+                     (1110, 'MCX'),
+                     (1226, 'MCCXXVI'),
+                     (1301, 'MCCCI'),
+                     (1485, 'MCDLXXXV'),
+                     (1509, 'MDIX'),
+                     (1607, 'MDCVII'),
+                     (1754, 'MDCCLIV'),
+                     (1832, 'MDCCCXXXII'),
+                     (1993, 'MCMXCIII'),
+                     (2074, 'MMLXXIV'),
+                     (2152, 'MMCLII'),
+                     (2212, 'MMCCXII'),
+                     (2343, 'MMCCCXLIII'),
+                     (2499, 'MMCDXCIX'),
+                     (2574, 'MMDLXXIV'),
+                     (2646, 'MMDCXLVI'),
+                     (2723, 'MMDCCXXIII'),
+                     (2892, 'MMDCCCXCII'),
+                     (2975, 'MMCMLXXV'),
+                     (3051, 'MMMLI'),
+                     (3185, 'MMMCLXXXV'),
+                     (3250, 'MMMCCL'),
+                     (3313, 'MMMCCCXIII'),
+                     (3408, 'MMMCDVIII'),
+                     (3501, 'MMMDI'),
+                     (3610, 'MMMDCX'),
+                     (3743, 'MMMDCCXLIII'),
+                     (3844, 'MMMDCCCXLIV'),
+                     (3888, 'MMMDCCCLXXXVIII'),
+                     (3940, 'MMMCMXL'),
+                     (3999, 'MMMCMXCIX'))
+
+    def test_to_roman_known_values(self):
+        """to_roman should give known result with known input"""
+        for integer, numeral in self.known_values:
+            result = roman6.to_roman(integer)
+            self.assertEqual(numeral, result)
+
+    def test_from_roman_known_values(self):
+        """from_roman should give known result with known input"""
+        for integer, numeral in self.known_values:
+            result = roman6.from_roman(numeral)
+            self.assertEqual(integer, result)
+
+class ToRomanBadInput(unittest.TestCase):
+    def test_too_large(self):
+        """to_roman should fail with large input"""
+        self.assertRaises(roman6.OutOfRangeError, roman6.to_roman, 4000)
+
+    def test_zero(self):
+        """to_roman should fail with 0 input"""
+        self.assertRaises(roman6.OutOfRangeError, roman6.to_roman, 0)
+
+    def test_negative(self):
+        """to_roman should fail with negative input"""
+        self.assertRaises(roman6.OutOfRangeError, roman6.to_roman, -1)
+
+    def test_non_integer(self):
+        """to_roman should fail with non-integer input"""
+        self.assertRaises(roman6.NotIntegerError, roman6.to_roman, 0.5)
+
+class FromRomanBadInput(unittest.TestCase):
+    def test_too_many_repeated_numerals(self):
+        """from_roman should fail with too many repeated numerals"""
+        for s in ('MMMMM', 'DD', 'CCCC', 'LL', 'XXXX', 'VV', 'IIII'):
+            self.assertRaises(roman6.InvalidRomanNumeralError, roman6.from_roman, s)
+
+    def test_repeated_pairs(self):
+        """from_roman should fail with repeated pairs of numerals"""
+        for s in ('CMCM', 'CDCD', 'XCXC', 'XLXL', 'IXIX', 'IVIV'):
+            self.assertRaises(roman6.InvalidRomanNumeralError, roman6.from_roman, s)
+
+    def test_malformed_antecedents(self):
+        """from_roman should fail with malformed antecedents"""
+        for s in ('IIMXCC', 'VX', 'DCM', 'CMM', 'IXIV',
+                  'MCMC', 'XCX', 'IVI', 'LM', 'LD', 'LC'):
+            self.assertRaises(roman6.InvalidRomanNumeralError, roman6.from_roman, s)
+
+class SanityCheck(unittest.TestCase):
+    def testSanity(self):
+        """from_roman(to_roman(n))==n for all n"""
+        for integer in range(1, 4000):
+            numeral = roman6.to_roman(integer)
+            result = roman6.from_roman(numeral)
+            self.assertEqual(integer, result)
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/romantest7.py b/romantest7.py
new file mode 100644
index 0000000..3e4a699
--- /dev/null
+++ b/romantest7.py
@@ -0,0 +1,128 @@
+"""Unit test for roman1.py
+
+This program is part of "Dive Into Python 3", a free Python book for
+experienced programmers.  Visit http://diveintopython3.org/ for the
+latest version.
+"""
+
+import roman7
+import unittest
+
+class KnownValues(unittest.TestCase):
+    known_values = ( (1, 'I'),
+                     (2, 'II'),
+                     (3, 'III'),
+                     (4, 'IV'),
+                     (5, 'V'),
+                     (6, 'VI'),
+                     (7, 'VII'),
+                     (8, 'VIII'),
+                     (9, 'IX'),
+                     (10, 'X'),
+                     (50, 'L'),
+                     (100, 'C'),
+                     (500, 'D'),
+                     (1000, 'M'),
+                     (31, 'XXXI'),
+                     (148, 'CXLVIII'),
+                     (294, 'CCXCIV'),
+                     (312, 'CCCXII'),
+                     (421, 'CDXXI'),
+                     (528, 'DXXVIII'),
+                     (621, 'DCXXI'),
+                     (782, 'DCCLXXXII'),
+                     (870, 'DCCCLXX'),
+                     (941, 'CMXLI'),
+                     (1043, 'MXLIII'),
+                     (1110, 'MCX'),
+                     (1226, 'MCCXXVI'),
+                     (1301, 'MCCCI'),
+                     (1485, 'MCDLXXXV'),
+                     (1509, 'MDIX'),
+                     (1607, 'MDCVII'),
+                     (1754, 'MDCCLIV'),
+                     (1832, 'MDCCCXXXII'),
+                     (1993, 'MCMXCIII'),
+                     (2074, 'MMLXXIV'),
+                     (2152, 'MMCLII'),
+                     (2212, 'MMCCXII'),
+                     (2343, 'MMCCCXLIII'),
+                     (2499, 'MMCDXCIX'),
+                     (2574, 'MMDLXXIV'),
+                     (2646, 'MMDCXLVI'),
+                     (2723, 'MMDCCXXIII'),
+                     (2892, 'MMDCCCXCII'),
+                     (2975, 'MMCMLXXV'),
+                     (3051, 'MMMLI'),
+                     (3185, 'MMMCLXXXV'),
+                     (3250, 'MMMCCL'),
+                     (3313, 'MMMCCCXIII'),
+                     (3408, 'MMMCDVIII'),
+                     (3501, 'MMMDI'),
+                     (3610, 'MMMDCX'),
+                     (3743, 'MMMDCCXLIII'),
+                     (3844, 'MMMDCCCXLIV'),
+                     (3888, 'MMMDCCCLXXXVIII'),
+                     (3940, 'MMMCMXL'),
+                     (3999, 'MMMCMXCIX'))
+
+    def test_to_roman_known_values(self):
+        """to_roman should give known result with known input"""
+        for integer, numeral in self.known_values:
+            result = roman7.to_roman(integer)
+            self.assertEqual(numeral, result)
+
+    def test_from_roman_known_values(self):
+        """from_roman should give known result with known input"""
+        for integer, numeral in self.known_values:
+            result = roman7.from_roman(numeral)
+            self.assertEqual(integer, result)
+
+class ToRomanBadInput(unittest.TestCase):
+    def test_too_large(self):
+        """to_roman should fail with large input"""
+        self.assertRaises(roman7.OutOfRangeError, roman7.to_roman, 4000)
+
+    def test_zero(self):
+        """to_roman should fail with 0 input"""
+        self.assertRaises(roman7.OutOfRangeError, roman7.to_roman, 0)
+
+    def test_negative(self):
+        """to_roman should fail with negative input"""
+        self.assertRaises(roman7.OutOfRangeError, roman7.to_roman, -1)
+
+    def test_non_integer(self):
+        """to_roman should fail with non-integer input"""
+        self.assertRaises(roman7.NotIntegerError, roman7.to_roman, 0.5)
+
+class FromRomanBadInput(unittest.TestCase):
+    def test_too_many_repeated_numerals(self):
+        """from_roman should fail with too many repeated numerals"""
+        for s in ('MMMMM', 'DD', 'CCCC', 'LL', 'XXXX', 'VV', 'IIII'):
+            self.assertRaises(roman7.InvalidRomanNumeralError, roman7.from_roman, s)
+
+    def test_repeated_pairs(self):
+        """from_roman should fail with repeated pairs of numerals"""
+        for s in ('CMCM', 'CDCD', 'XCXC', 'XLXL', 'IXIX', 'IVIV'):
+            self.assertRaises(roman7.InvalidRomanNumeralError, roman7.from_roman, s)
+
+    def test_malformed_antecedents(self):
+        """from_roman should fail with malformed antecedents"""
+        for s in ('IIMXCC', 'VX', 'DCM', 'CMM', 'IXIV',
+                  'MCMC', 'XCX', 'IVI', 'LM', 'LD', 'LC'):
+            self.assertRaises(roman7.InvalidRomanNumeralError, roman7.from_roman, s)
+
+    def test_blank(self):
+        """from_roman should fail with blank string"""
+        self.assertRaises(roman7.InvalidRomanNumeralError, roman7.from_roman, "")
+
+class SanityCheck(unittest.TestCase):
+    def testSanity(self):
+        """from_roman(to_roman(n))==n for all n"""
+        for integer in range(1, 4000):
+            numeral = roman7.to_roman(integer)
+            result = roman7.from_roman(numeral)
+            self.assertEqual(integer, result)
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/romantest8.py b/romantest8.py
new file mode 100644
index 0000000..cf9c421
--- /dev/null
+++ b/romantest8.py
@@ -0,0 +1,132 @@
+"""Unit test for roman1.py
+
+This program is part of "Dive Into Python 3", a free Python book for
+experienced programmers.  Visit http://diveintopython3.org/ for the
+latest version.
+"""
+
+import roman8
+import unittest
+
+class KnownValues(unittest.TestCase):
+    known_values = ( (1, 'I'),
+                     (2, 'II'),
+                     (3, 'III'),
+                     (4, 'IV'),
+                     (5, 'V'),
+                     (6, 'VI'),
+                     (7, 'VII'),
+                     (8, 'VIII'),
+                     (9, 'IX'),
+                     (10, 'X'),
+                     (50, 'L'),
+                     (100, 'C'),
+                     (500, 'D'),
+                     (1000, 'M'),
+                     (31, 'XXXI'),
+                     (148, 'CXLVIII'),
+                     (294, 'CCXCIV'),
+                     (312, 'CCCXII'),
+                     (421, 'CDXXI'),
+                     (528, 'DXXVIII'),
+                     (621, 'DCXXI'),
+                     (782, 'DCCLXXXII'),
+                     (870, 'DCCCLXX'),
+                     (941, 'CMXLI'),
+                     (1043, 'MXLIII'),
+                     (1110, 'MCX'),
+                     (1226, 'MCCXXVI'),
+                     (1301, 'MCCCI'),
+                     (1485, 'MCDLXXXV'),
+                     (1509, 'MDIX'),
+                     (1607, 'MDCVII'),
+                     (1754, 'MDCCLIV'),
+                     (1832, 'MDCCCXXXII'),
+                     (1993, 'MCMXCIII'),
+                     (2074, 'MMLXXIV'),
+                     (2152, 'MMCLII'),
+                     (2212, 'MMCCXII'),
+                     (2343, 'MMCCCXLIII'),
+                     (2499, 'MMCDXCIX'),
+                     (2574, 'MMDLXXIV'),
+                     (2646, 'MMDCXLVI'),
+                     (2723, 'MMDCCXXIII'),
+                     (2892, 'MMDCCCXCII'),
+                     (2975, 'MMCMLXXV'),
+                     (3051, 'MMMLI'),
+                     (3185, 'MMMCLXXXV'),
+                     (3250, 'MMMCCL'),
+                     (3313, 'MMMCCCXIII'),
+                     (3408, 'MMMCDVIII'),
+                     (3501, 'MMMDI'),
+                     (3610, 'MMMDCX'),
+                     (3743, 'MMMDCCXLIII'),
+                     (3844, 'MMMDCCCXLIV'),
+                     (3888, 'MMMDCCCLXXXVIII'),
+                     (3940, 'MMMCMXL'),
+                     (3999, 'MMMCMXCIX'))
+
+    def test_to_roman_known_values(self):
+        """to_roman should give known result with known input"""
+        for integer, numeral in self.known_values:
+            result = roman8.to_roman(integer)
+            self.assertEqual(numeral, result)
+
+    def test_from_roman_known_values(self):
+        """from_roman should give known result with known input"""
+        for integer, numeral in self.known_values:
+            result = roman8.from_roman(numeral)
+            self.assertEqual(integer, result)
+
+class ToRomanBadInput(unittest.TestCase):
+    def test_too_large(self):
+        """to_roman should fail with large input"""
+        self.assertRaises(roman8.OutOfRangeError, roman8.to_roman, 4000)
+
+    def test_zero(self):
+        """to_roman should fail with 0 input"""
+        self.assertRaises(roman8.OutOfRangeError, roman8.to_roman, 0)
+
+    def test_negative(self):
+        """to_roman should fail with negative input"""
+        self.assertRaises(roman8.OutOfRangeError, roman8.to_roman, -1)
+
+    def test_non_integer(self):
+        """to_roman should fail with non-integer input"""
+        self.assertRaises(roman8.NotIntegerError, roman8.to_roman, 0.5)
+
+class FromRomanBadInput(unittest.TestCase):
+    def test_too_many_repeated_numerals(self):
+        """from_roman should fail with too many repeated numerals"""
+        for s in ('MMMMM', 'DD', 'CCCC', 'LL', 'XXXX', 'VV', 'IIII'):
+            self.assertRaises(roman8.InvalidRomanNumeralError, roman8.from_roman, s)
+
+    def test_repeated_pairs(self):
+        """from_roman should fail with repeated pairs of numerals"""
+        for s in ('CMCM', 'CDCD', 'XCXC', 'XLXL', 'IXIX', 'IVIV'):
+            self.assertRaises(roman8.InvalidRomanNumeralError, roman8.from_roman, s)
+
+    def test_malformed_antecedents(self):
+        """from_roman should fail with malformed antecedents"""
+        for s in ('IIMXCC', 'VX', 'DCM', 'CMM', 'IXIV',
+                  'MCMC', 'XCX', 'IVI', 'LM', 'LD', 'LC'):
+            self.assertRaises(roman8.InvalidRomanNumeralError, roman8.from_roman, s)
+
+    def test_blank(self):
+        """from_roman should fail with blank string"""
+        self.assertRaises(roman8.InvalidRomanNumeralError, roman8.from_roman, "")
+
+    def test_non_string(self):
+        """from_roman should fail with non-string input"""
+        self.assertRaises(roman8.InvalidRomanNumeralError, roman8.from_roman, 1)
+
+class SanityCheck(unittest.TestCase):
+    def testSanity(self):
+        """from_roman(to_roman(n))==n for all n"""
+        for integer in range(1, 4000):
+            numeral = roman8.to_roman(integer)
+            result = roman8.from_roman(numeral)
+            self.assertEqual(integer, result)
+
+if __name__ == "__main__":
+    unittest.main()