Roman numerals
Numbers from 1 to 3999 to Roman numerals and back, each split into its parts; a check that a numeral is written the standard way - IIII and IC read, but are not - with the right way given; and adding and subtracting Roman numerals.
Every screen below was recorded under CPython. When this page was built, the EML interpreter replayed each session from the same input and printed the same bytes.
About
Numbers from 1 to 3999 to Roman numerals and back, each numeral split into its parts. A numeral typed in is checked for the standard way of writing it; when it reads but is not standard, the right way is given. Roman numerals can also be added and subtracted.
main.eml- the menu, reading numbers and numerals, and the messagesroman.eml- both directions, the standard-form check, and splitting a numeral into parts
How each part works:
- To a numeral: walk a table of values from 1000 (M) down to 1 (I) and take the largest that still fits, again and again. The subtractive pairs - CM, CD, XC, XL, IX, IV - are entries of the table, as in the corpus case
roman-numeral-converter, so no special case is needed. - To a number: add each letter's value, except subtract a letter smaller than the one after it, the reading rule of the corpus case
roman-to-integer. - That rule also reads numerals that are not written the standard way: IIII as 4, IC as 99, VX as 5. So a numeral counts as standard only when turning its value back into a numeral gives the same letters - the round trip the two corpus cases make between them. Lower case is read as upper case.
- Roman numerals have no zero and no negatives, and past 3999 they need a bar over the letters, which plain text cannot show. So sums and differences outside 1 to 3999 are given as numbers only.
What is checked: menu choices 1 to 5; numbers from 1 to 3999; numerals using only I V X L C D M, in the standard form, from I to MMMCMXCIX. An empty answer cancels.
Sessions: sessions/basic.in gives: - 1994, 2026 and 3999 to numerals, with their parts; - mmxxvi, in lower case, and XLII back to numbers; - XIV + XXVIII = XLII, and MMXXVI - MCMXCIV = XXXII.
sessions/bad-input.in gives: - menu choices 0 and x; - the numbers 0, 4000, -5 and abc; - the numerals IIII, IC, VX and IIX, which read but are not standard, MMMM (4000), and ABC; - MMM + M, which passes 3999; - V - V and X - L, which have no numeral.
Built on the verified corpus cases roman-numeral-converter (a greedy walk down a table that includes the subtractive pairs) and roman-to-integer (the reverse reading rule, checked as a round trip).
Recorded sessions
What the screen shows while someone uses the program. Each typed line appears after its prompt, the way a terminal shows it.
bad-input
interpreter: byte-equal== Roman numerals ==
Numbers from 1 to 3999 to Roman numerals and back, checked for the standard way of writing them.
1) to Roman 2) from Roman 3) add 4) subtract 5) quit
choice> 0
Pick a number from 1 to 5.
1) to Roman 2) from Roman 3) add 4) subtract 5) quit
choice> x
Pick a number from 1 to 5.
1) to Roman 2) from Roman 3) add 4) subtract 5) quit
choice> 1
number (1 to 3999)> 0
Type a whole number from 1 to 3999 - Roman numerals have no zero, and larger numbers need a bar over the letters.
number (1 to 3999)> 4000
Type a whole number from 1 to 3999 - Roman numerals have no zero, and larger numbers need a bar over the letters.
number (1 to 3999)> -5
Type a whole number from 1 to 3999 - Roman numerals have no zero, and larger numbers need a bar over the letters.
number (1 to 3999)> abc
Type a whole number from 1 to 3999 - Roman numerals have no zero, and larger numbers need a bar over the letters.
number (1 to 3999)>
Cancelled.
1) to Roman 2) from Roman 3) add 4) subtract 5) quit
choice> 2
Roman numeral> IIII
IIII reads as 4, but is not the standard way to write it: 4 is IV.
Roman numeral> IC
IC reads as 99, but is not the standard way to write it: 99 is XCIX.
Roman numeral> VX
VX reads as 5, but is not the standard way to write it: 5 is V.
Roman numeral> MMMM
MMMM reads as 4000, but Roman numerals here go from I (1) to MMMCMXCIX (3999).
Roman numeral> ABC
Use only the letters I V X L C D M.
Roman numeral> IIX
IIX reads as 10, but is not the standard way to write it: 10 is X.
Roman numeral>
Cancelled.
1) to Roman 2) from Roman 3) add 4) subtract 5) quit
choice> 3
first numeral> MMM
second numeral> M
MMM + M is 3000 + 1000 = 4000.
That is past MMMCMXCIX (3999), the largest numeral without a bar over the letters.
1) to Roman 2) from Roman 3) add 4) subtract 5) quit
choice> 4
first numeral> V
second numeral> V
V - V is 5 - 5 = 0.
There is no Roman numeral for that: they start at I, with no zero or negatives.
1) to Roman 2) from Roman 3) add 4) subtract 5) quit
choice> 4
first numeral> X
second numeral> L
X - L is 10 - 50 = -40.
There is no Roman numeral for that: they start at I, with no zero or negatives.
1) to Roman 2) from Roman 3) add 4) subtract 5) quit
choice> 5
Bye.
What was typed (26 lines)
0
x
1
0
4000
-5
abc
2
IIII
IC
VX
MMMM
ABC
IIX
3
MMM
M
4
V
V
4
X
L
5
basic
interpreter: byte-equal== Roman numerals ==
Numbers from 1 to 3999 to Roman numerals and back, checked for the standard way of writing them.
1) to Roman 2) from Roman 3) add 4) subtract 5) quit
choice> 1
number (1 to 3999)> 1994
1994 is MCMXCIV = M (1000) + CM (900) + XC (90) + IV (4).
1) to Roman 2) from Roman 3) add 4) subtract 5) quit
choice> 1
number (1 to 3999)> 2026
2026 is MMXXVI = M (1000) + M (1000) + X (10) + X (10) + V (5) + I (1).
1) to Roman 2) from Roman 3) add 4) subtract 5) quit
choice> 1
number (1 to 3999)> 3999
3999 is MMMCMXCIX = M (1000) + M (1000) + M (1000) + CM (900) + XC (90) + IX (9).
1) to Roman 2) from Roman 3) add 4) subtract 5) quit
choice> 2
Roman numeral> mmxxvi
MMXXVI is 2026 = M (1000) + M (1000) + X (10) + X (10) + V (5) + I (1).
1) to Roman 2) from Roman 3) add 4) subtract 5) quit
choice> 2
Roman numeral> XLII
XLII is 42 = XL (40) + I (1) + I (1).
1) to Roman 2) from Roman 3) add 4) subtract 5) quit
choice> 3
first numeral> XIV
second numeral> XXVIII
XIV + XXVIII is 14 + 28 = 42.
In Roman numerals: XLII.
1) to Roman 2) from Roman 3) add 4) subtract 5) quit
choice> 4
first numeral> MMXXVI
second numeral> MCMXCIV
MMXXVI - MCMXCIV is 2026 - 1994 = 32.
In Roman numerals: XXXII.
1) to Roman 2) from Roman 3) add 4) subtract 5) quit
choice> 5
Bye.
What was typed (17 lines)
1
1994
1
2026
1
3999
2
mmxxvi
2
XLII
3
XIV
XXVIII
4
MMXXVI
MCMXCIV
5
Modules
The program as written, entry module first. Each module transpiles to its own Python file, which is what eml project run executes.
main.eml(entry)
eml# P061 Roman numerals: numbers to Roman numerals and back, a check that a
# numeral is written the standard way (and the right way when it is not),
# and adding and subtracting Roman numerals.
import roman
def trim(s):
0 => i
len(s) => j
while i < j and s[i] == " ":
i + 1 => i
while j > i and s[j - 1] == " ":
j - 1 => j
return s[i:j]
def number(s):
if s == "" or len(s) > 6:
return -1
0 => n
for c in s:
if not (c >= "0" and c <= "9"):
return -1
n * 10 + int(c) => n
return n
def breakdown(s):
"" => out
for p in roman.parts(s):
if out != "":
out + " + " => out
out + p[1] + " (" + str(p[0]) + ")" => out
return out
def ask_numeral(prompt):
# A standard Roman numeral, as [text, value], or [] when the answer is
# empty. A numeral that reads but is not standard is explained and
# asked for again.
while True:
roman.upper(trim(input(prompt))) => s
if s == "":
return []
roman.value_of(s) => v
if v == -1:
"Use only the letters I V X L C D M." ^0
elif roman.standard(s):
return [s, v]
elif v < 1 or v > 3999:
(s + " reads as " + str(v) + ", but Roman numerals here go from I (1) to MMMCMXCIX (3999).") ^0
else:
(s + " reads as " + str(v) + ", but is not the standard way to write it: " + str(v) + " is " + roman.to_roman(v) + ".") ^0
def to_roman():
while True:
trim(input("number (1 to 3999)> ")) => answer
if answer == "":
"Cancelled." ^0
return
number(answer) => n
if n >= 1 and n <= 3999:
break
"Type a whole number from 1 to 3999 - Roman numerals have no zero, and larger numbers need a bar over the letters." ^0
roman.to_roman(n) => r
(str(n) + " is " + r + " = " + breakdown(r) + ".") ^0
def from_roman():
ask_numeral("Roman numeral> ") => r
if len(r) == 0:
"Cancelled." ^0
return
(r[0] + " is " + str(r[1]) + " = " + breakdown(r[0]) + ".") ^0
def arithmetic(subtract):
ask_numeral("first numeral> ") => a
if len(a) == 0:
"Cancelled." ^0
return
ask_numeral("second numeral> ") => b
if len(b) == 0:
"Cancelled." ^0
return
a[1] + b[1] => v
"+" => op
if subtract:
a[1] - b[1] => v
"-" => op
(a[0] + " " + op + " " + b[0] + " is " + str(a[1]) + " " + op + " " + str(b[1]) + " = " + str(v) + ".") ^0
if v < 1:
"There is no Roman numeral for that: they start at I, with no zero or negatives." ^0
elif v > 3999:
"That is past MMMCMXCIX (3999), the largest numeral without a bar over the letters." ^0
else:
("In Roman numerals: " + roman.to_roman(v) + ".") ^0
"== Roman numerals ==" ^0
"Numbers from 1 to 3999 to Roman numerals and back, checked for the standard way of writing them." ^0
True => running
while running:
"" ^0
"1) to Roman 2) from Roman 3) add 4) subtract 5) quit" ^0
trim(input("choice> ")) => choice
if choice == "1":
to_roman()
elif choice == "2":
from_roman()
elif choice == "3":
arithmetic(False)
elif choice == "4":
arithmetic(True)
elif choice == "5":
False => running
else:
"Pick a number from 1 to 5." ^0
"Bye." ^0
Python projection (main.py)
import roman
def trim(s):
i = 0
j = len(s)
while i < j and s[i] == " ":
i = i + 1
while j > i and s[j - 1] == " ":
j = j - 1
return s[i:j]
def number(s):
if s == "" or len(s) > 6:
return -1
n = 0
for c in s:
if not (c >= "0" and c <= "9"):
return -1
n = n * 10 + int(c)
return n
def breakdown(s):
out = ""
for p in roman.parts(s):
if out != "":
out = out + " + "
out = out + p[1] + " (" + str(p[0]) + ")"
return out
def ask_numeral(prompt):
while True:
s = roman.upper(trim(input(prompt)))
if s == "":
return []
v = roman.value_of(s)
if v == -1:
print("Use only the letters I V X L C D M.")
elif roman.standard(s):
return [s, v]
elif v < 1 or v > 3999:
print(s + " reads as " + str(v) + ", but Roman numerals here go from I (1) to MMMCMXCIX (3999).")
else:
print(s + " reads as " + str(v) + ", but is not the standard way to write it: " + str(v) + " is " + roman.to_roman(v) + ".")
def to_roman():
while True:
answer = trim(input("number (1 to 3999)> "))
if answer == "":
print("Cancelled.")
return
n = number(answer)
if n >= 1 and n <= 3999:
break
print("Type a whole number from 1 to 3999 - Roman numerals have no zero, and larger numbers need a bar over the letters.")
r = roman.to_roman(n)
print(str(n) + " is " + r + " = " + breakdown(r) + ".")
def from_roman():
r = ask_numeral("Roman numeral> ")
if len(r) == 0:
print("Cancelled.")
return
print(r[0] + " is " + str(r[1]) + " = " + breakdown(r[0]) + ".")
def arithmetic(subtract):
a = ask_numeral("first numeral> ")
if len(a) == 0:
print("Cancelled.")
return
b = ask_numeral("second numeral> ")
if len(b) == 0:
print("Cancelled.")
return
v = a[1] + b[1]
op = "+"
if subtract:
v = a[1] - b[1]
op = "-"
print(a[0] + " " + op + " " + b[0] + " is " + str(a[1]) + " " + op + " " + str(b[1]) + " = " + str(v) + ".")
if v < 1:
print("There is no Roman numeral for that: they start at I, with no zero or negatives.")
elif v > 3999:
print("That is past MMMCMXCIX (3999), the largest numeral without a bar over the letters.")
else:
print("In Roman numerals: " + roman.to_roman(v) + ".")
print("== Roman numerals ==")
print("Numbers from 1 to 3999 to Roman numerals and back, checked for the standard way of writing them.")
running = True
while running:
print("")
print("1) to Roman 2) from Roman 3) add 4) subtract 5) quit")
choice = trim(input("choice> "))
if choice == "1":
to_roman()
elif choice == "2":
from_roman()
elif choice == "3":
arithmetic(False)
elif choice == "4":
arithmetic(True)
elif choice == "5":
running = False
else:
print("Pick a number from 1 to 5.")
print("Bye.")
roman.eml
eml# P061 Roman numerals - the two directions and the check between them.
def table():
# The values and symbols from largest to smallest, the subtractive pairs
# (CM, CD, XC, XL, IX, IV) as entries of their own, as in the corpus case
# roman-numeral-converter.
return [[1000, "M"], [900, "CM"], [500, "D"], [400, "CD"], [100, "C"], [90, "XC"],
[50, "L"], [40, "XL"], [10, "X"], [9, "IX"], [5, "V"], [4, "IV"], [1, "I"]]
def to_roman(n):
# 1 to 3999 as a Roman numeral: take the largest value that still fits,
# again and again.
"" => out
n => left
for row in table():
while left >= row[0]:
out + row[1] => out
left - row[0] => left
return out
def letter_value(ch):
# The value of one letter, upper or lower case, or 0 for anything else.
"IVXLCDM" => big
"ivxlcdm" => small
[1, 5, 10, 50, 100, 500, 1000] => values
for k in [0:6]:
if ch == big[k] or ch == small[k]:
return values[k]
return 0
def value_of(s):
# The value by the reading rule of the corpus case roman-to-integer: add
# each letter, except subtract one that is smaller than the letter after
# it. Returns -1 when a character is not a Roman letter. The rule reads
# non-standard numerals too - IIII as 4, IC as 99 - so a value alone does
# not say the numeral was written right.
0 => total
for i in [0:len(s) - 1]:
letter_value(s[i]) => v
if v == 0:
return -1
0 => next_v
if i + 1 < len(s):
letter_value(s[i + 1]) => next_v
if v < next_v:
total - v => total
else:
total + v => total
return total
def upper(s):
"" => out
for ch in s:
ch => c
for k in [0:6]:
if ch == "ivxlcdm"[k]:
"IVXLCDM"[k] => c
out + c => out
return out
def standard(s):
# Written the standard way when converting its value back gives the same
# letters - the round trip the two corpus cases make between them.
value_of(s) => v
if v < 1 or v > 3999:
return False
return to_roman(v) == upper(s)
def parts(s):
# A standard numeral split into its table entries, largest first:
# MCMXCIV is M, CM, XC, IV. Two-letter entries are tried before one.
[] => out
upper(s) => t
0 => i
while i < len(t):
False => found
for row in table():
if not found and len(row[1]) == 2 and i + 1 < len(t) and t[i:i + 2] == row[1]:
out + [row] => out
i + 2 => i
True => found
if not found:
for row in table():
if not found and len(row[1]) == 1 and t[i] == row[1]:
out + [row] => out
i + 1 => i
True => found
return out
Python projection (roman.py)
def table():
return [[1000, "M"], [900, "CM"], [500, "D"], [400, "CD"], [100, "C"], [90, "XC"], [50, "L"], [40, "XL"], [10, "X"], [9, "IX"], [5, "V"], [4, "IV"], [1, "I"]]
def to_roman(n):
out = ""
left = n
for row in table():
while left >= row[0]:
out = out + row[1]
left = left - row[0]
return out
def letter_value(ch):
big = "IVXLCDM"
small = "ivxlcdm"
values = [1, 5, 10, 50, 100, 500, 1000]
for k in range(0, 7):
if ch == big[k] or ch == small[k]:
return values[k]
return 0
def value_of(s):
total = 0
for i in range(0, len(s)):
v = letter_value(s[i])
if v == 0:
return -1
next_v = 0
if i + 1 < len(s):
next_v = letter_value(s[i + 1])
if v < next_v:
total = total - v
else:
total = total + v
return total
def upper(s):
out = ""
for ch in s:
c = ch
for k in range(0, 7):
if ch == "ivxlcdm"[k]:
c = "IVXLCDM"[k]
out = out + c
return out
def standard(s):
v = value_of(s)
if v < 1 or v > 3999:
return False
return to_roman(v) == upper(s)
def parts(s):
out = []
t = upper(s)
i = 0
while i < len(t):
found = False
for row in table():
if not found and len(row[1]) == 2 and i + 1 < len(t) and t[i:i + 2] == row[1]:
out = out + [row]
i = i + 2
found = True
if not found:
for row in table():
if not found and len(row[1]) == 1 and t[i] == row[1]:
out = out + [row]
i = i + 1
found = True
return out