forked from UWPCE-PythonCert/ProgrammingInPython
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathroman17.py
More file actions
274 lines (229 loc) · 7.4 KB
/
Copy pathroman17.py
File metadata and controls
274 lines (229 loc) · 7.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
"""
roman.py
A Roman numeral to arabic numeral (and back!) converter
complete with tests
tests are expected to be able to be run with the pytest system
"""
import pytest
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 ValueError("number out of range (must be 1..3999)")
if int(n) != n:
raise ValueError("Only integers can be converted to Roman numerals")
result = ''
for numeral, integer in roman_numeral_map:
while n >= integer:
result += numeral
n -= integer
return result
def is_valid_roman_numeral(s):
"""
parse a Roman numeral as a human would: left to right,
looking for valid characters and removing them to determine
if this is, indeed, a valid Roman numeral
"""
# first look for the thousands -- up to three Ms
for _ in range(3):
if s[:1] == "M":
s = s[1:]
# then look for the hundreds:
# there can be only one of CM, CD, or D:
if s[:2] == "CM": # 900
s = s[2:]
elif s[:2] == "CD": # 400
s = s[2:]
else:
if s[:1] == "D": # 500
s = s[1:]
# there can be from 1 to 3 Cs
for _ in range(3):
if s[:1] == "C":
s = s[1:]
# now the tens
# There can be one of either XC, XL or L
if s[:2] == "XC": # 90
s = s[2:]
elif s[:2] == "XL": # 40
s = s[2:]
else:
if s[:1] == "L": # 50
s = s[1:]
# there can be up to three Xs
for _ in range(3):
if s[:1] == "X":
s = s[1:]
# and the ones
# There can be one of IX, IV or V
if s[:2] == "IX": # 9
s = s[2:]
elif s[:2] == "IV": # 4
s = s[2:]
else:
if s[:1] == "V": # 5
s = s[1:]
# There can be up to three Is
for _ in range(3):
if s[:1] == "I": # 1
s = s[1:]
# if there is anything left, it's not a valid Roman numeral
if s:
return False
else:
return True
def from_roman(s):
"""convert Roman numeral to integer"""
if not is_valid_roman_numeral(s):
raise ValueError(f"{s} is not a valid Roman numeral")
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
#####################################
# Tests for roman numeral conversion
#####################################
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():
"""
to_roman should give known result with known input
"""
for integer, numeral in KNOWN_VALUES:
result = to_roman(integer)
assert numeral == result
def test_too_large():
"""
to_roman should raise a ValueError when passed
values over 3999
"""
with pytest.raises(ValueError):
to_roman(4000)
def test_zero():
"""to_roman should raise a ValueError with 0 input"""
with pytest.raises(ValueError):
to_roman(0)
def test_negative():
"""to_roman should raise a ValueError with negative input"""
with pytest.raises(ValueError):
to_roman(-1)
def test_non_integer():
"""to_roman should raise a ValueError with non-integer input"""
with pytest.raises(ValueError):
to_roman(0.5)
def test_float_with_integer_value():
"""to_roman should work for floats with integer values"""
assert to_roman(3.0) == "III"
# ####################
# Tests for from_roman
# ####################
def test_from_roman_known_values():
"""from_roman should give known result with known input"""
for integer, numeral in KNOWN_VALUES:
result = from_roman(numeral)
assert integer == result
def test_roundtrip():
"""from_roman(to_roman(n))==n for all n"""
for integer in range(1, 4000):
numeral = to_roman(integer)
result = from_roman(numeral)
assert integer == result
# The following to test for valid roman numerals
def test_invalid_character():
"""
Roman numerals can only use these characters:
M, D, C, L, X, V, I
This tests that other characters will cause a failure
"""
for s in ['Z', 'XXIIIQ', 'QXXIII', 'XXYIII']:
with pytest.raises(ValueError):
from_roman(s)
def test_too_many_repeated_numerals():
'''from_roman should fail with too many repeated numerals'''
for s in ('MMMM', 'DD', 'CCCC', 'LL', 'XXXX', 'VV', 'IIII'):
with pytest.raises(ValueError):
print(f"trying: {s}")
from_roman(s)
def test_repeated_pairs():
'''from_roman should fail with repeated pairs of numerals'''
for s in ('CMCM', 'CDCD', 'XCXC', 'XLXL', 'IXIX', 'IVIV'):
with pytest.raises(ValueError):
print(f"trying: {s}")
from_roman(s)
def test_malformed_antecedents():
'''from_roman should fail with malformed antecedents'''
for s in ('IIMXCC', 'VX', 'DCM', 'CMM', 'IXIV',
'MCMC', 'XCX', 'IVI', 'LM', 'LD', 'LC'):
with pytest.raises(ValueError):
print(f"trying: {s}")
from_roman(s)