Base converter
Convert whole numbers of any length between bases 2 to 36 by long division on their digits, show a number in bases 2, 8, 10 and 16 at once, and turn numbers into Roman numerals and back - refusing a numeral that is not written the standard way and saying how it should be - from a text menu.
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
Converts whole numbers between any two bases from 2 to 36, shows a number in bases 2, 8, 10 and 16 at once, and turns numbers into Roman numerals and back. A text menu.
main.eml- the menu and its questions, with their checksbases.eml- digits and long division by handroman.eml- writing and reading Roman numerals, and the standard-form checktext.eml- trimming, capitals and whole numbers
A number is kept as its list of digit values, most significant first, and converted by long division done by hand: divide the whole list by the new base, keep the remainder as the next digit, and repeat until nothing is left. Every step uses small numbers only, so a number of up to 60 digits comes out exact - 123456789012345678901234567890 is BYW97UM9S91DLZ68TSI in base 36. Digits are 0-9 and then A-Z for 10 to 35, in either case; a minus sign in front is kept.
Roman numerals run from I to MMMCMXCIX (1 to 3999). Writing one walks a table from the largest value down, with the subtractive pairs (CM, CD, XC, XL, IX, IV) as entries of the table. Reading one adds each symbol's value, except that a symbol smaller than the one after it is subtracted. That rule alone would also accept IIII, IC or VV, so a numeral counts only if writing its value again gives exactly the same letters - and when it does not, the screen says how the value is written.
What is checked: a base is a whole number from 2 to 36; a number has only digits of its base (at most 60, with a minus sign allowed); a number for Roman numerals is from 1 to 3999; a numeral uses only I, V, X, L, C, D and M, stays in range and is written the standard way. Anything else asks again; nothing cancels.
Sessions: sessions/basic.in converts 255 to base 16, FF to base 2, a 30-digit number to base 36 and -42 to base 2, shows 2026 in the four common bases, writes 1994 and 3999 as Roman numerals and reads mcmxciv and XLII; sessions/bad-input.in types bases out of range or not numbers, digits that do not belong to the base, a point, a doubled or lone minus sign, -0, numbers outside 1 to 3999, and numerals written the wrong way (IIII, IC, VV), too large (MMMM) or with other letters.
Built on the verified corpus cases base-converter (digits by repeated division and remainder), base-conversion-roundtrip (writing a number in a base and reading it back), roman-numeral-converter (the table walk) and roman-to-integer (the add-or-subtract reading rule).
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
== Base converter ==
1) convert a number 2) in bases 2, 8, 10, 16 3) to Roman 4) from Roman 5) quit
choice> 9
Pick a number from 1 to 5.
== Base converter ==
1) convert a number 2) in bases 2, 8, 10, 16 3) to Roman 4) from Roman 5) quit
choice> 1
from base (2-36)> 1
A base is a whole number from 2 to 36, or nothing to cancel.
from base (2-36)> 37
A base is a whole number from 2 to 36, or nothing to cancel.
from base (2-36)> x
A base is a whole number from 2 to 36, or nothing to cancel.
from base (2-36)> 8
number> 129
9 is not a digit in base 8.
number> 8.5
8 is not a digit in base 8.
number> --5
- is not a digit; digits are 0-9, then A-Z for 10-35.
number> -
Type some digits.
number>
Cancelled.
== Base converter ==
1) convert a number 2) in bases 2, 8, 10, 16 3) to Roman 4) from Roman 5) quit
choice> 1
from base (2-36)> 2
number> -0
to base (2-36)> 10
0 (base 2) = 0 (base 10)
== Base converter ==
1) convert a number 2) in bases 2, 8, 10, 16 3) to Roman 4) from Roman 5) quit
choice> 3
number (1-3999)> 0
Type a whole number from 1 to 3999, or nothing to cancel.
number (1-3999)> 4000
Type a whole number from 1 to 3999, or nothing to cancel.
number (1-3999)> abc
Type a whole number from 1 to 3999, or nothing to cancel.
number (1-3999)> 2026
2026 = MMXXVI
== Base converter ==
1) convert a number 2) in bases 2, 8, 10, 16 3) to Roman 4) from Roman 5) quit
choice> 4
numeral> IIII
IIII is not written the standard way; 4 is IV.
numeral> IC
IC is not written the standard way; 99 is XCIX.
numeral> VV
VV is not written the standard way; 10 is X.
numeral> MMMM
MMMM is not a numeral from I to MMMCMXCIX.
numeral> ABC
Roman numerals use only I, V, X, L, C, D and M.
numeral> IV
IV = 4
== Base converter ==
1) convert a number 2) in bases 2, 8, 10, 16 3) to Roman 4) from Roman 5) quit
choice> 4
numeral>
Cancelled.
== Base converter ==
1) convert a number 2) in bases 2, 8, 10, 16 3) to Roman 4) from Roman 5) quit
choice> 5
Bye.
What was typed (30 lines)
9
1
1
37
x
8
129
8.5
--5
-
1
2
-0
10
3
0
4000
abc
2026
4
IIII
IC
VV
MMMM
ABC
IV
4
5
basic
interpreter: byte-equal
== Base converter ==
1) convert a number 2) in bases 2, 8, 10, 16 3) to Roman 4) from Roman 5) quit
choice> 1
from base (2-36)> 10
number> 255
to base (2-36)> 16
255 (base 10) = FF (base 16)
== Base converter ==
1) convert a number 2) in bases 2, 8, 10, 16 3) to Roman 4) from Roman 5) quit
choice> 1
from base (2-36)> 16
number> ff
to base (2-36)> 2
FF (base 16) = 11111111 (base 2)
== Base converter ==
1) convert a number 2) in bases 2, 8, 10, 16 3) to Roman 4) from Roman 5) quit
choice> 1
from base (2-36)> 10
number> 123456789012345678901234567890
to base (2-36)> 36
123456789012345678901234567890 (base 10) = BYW97UM9S91DLZ68TSI (base 36)
== Base converter ==
1) convert a number 2) in bases 2, 8, 10, 16 3) to Roman 4) from Roman 5) quit
choice> 1
from base (2-36)> 10
number> -42
to base (2-36)> 2
-42 (base 10) = -101010 (base 2)
== Base converter ==
1) convert a number 2) in bases 2, 8, 10, 16 3) to Roman 4) from Roman 5) quit
choice> 2
from base (2-36)> 10
number> 2026
The number 2026 (base 10) is:
base 2 11111101010
base 8 3752
base 10 2026
base 16 7EA
== Base converter ==
1) convert a number 2) in bases 2, 8, 10, 16 3) to Roman 4) from Roman 5) quit
choice> 3
number (1-3999)> 1994
1994 = MCMXCIV
== Base converter ==
1) convert a number 2) in bases 2, 8, 10, 16 3) to Roman 4) from Roman 5) quit
choice> 3
number (1-3999)> 3999
3999 = MMMCMXCIX
== Base converter ==
1) convert a number 2) in bases 2, 8, 10, 16 3) to Roman 4) from Roman 5) quit
choice> 4
numeral> mcmxciv
MCMXCIV = 1994
== Base converter ==
1) convert a number 2) in bases 2, 8, 10, 16 3) to Roman 4) from Roman 5) quit
choice> 4
numeral> XLII
XLII = 42
== Base converter ==
1) convert a number 2) in bases 2, 8, 10, 16 3) to Roman 4) from Roman 5) quit
choice> 5
Bye.
What was typed (28 lines)
1
10
255
16
1
16
ff
2
1
10
123456789012345678901234567890
36
1
10
-42
2
2
10
2026
3
1994
3
3999
4
mcmxciv
4
XLII
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# P021 base converter: whole numbers between bases 2 to 36, a number in the
# common bases at once, and Roman numerals both ways.
import bases
import roman
import text
60 => longest
def ask_base(prompt):
# A base from 2 to 36, asked again until one is typed; -1 cancels.
while True:
text.trim(input(prompt)) => answer
if answer == "":
return 0 - 1
text.number(answer) => b
if b >= 2 and b <= 36:
return b
"A base is a whole number from 2 to 36, or nothing to cancel." ^0
def parse_number(s, base):
# [negative, digits, ""] for a whole number written in base (a minus sign
# in front allowed), or [False, [], what is wrong].
text.upper(s) => s
False => negative
if len(s) > 0 and s[0] == "-":
True => negative
s[1:len(s)] => s
if s == "":
return [False, [], "Type some digits."]
if len(s) > longest:
return [False, [], "At most " + str(longest) + " digits."]
[] => digits
for c in s:
bases.digit_value(c) => v
if v == 0 - 1:
return [False, [], c + " is not a digit; digits are 0-9, then A-Z for 10-35."]
if v >= base:
return [False, [], c + " is not a digit in base " + str(base) + "."]
digits + [v] => digits
bases.strip(digits) => digits
if digits == [0]:
False => negative
return [negative, digits, ""]
def ask_number(base):
# A number in base, asked again until one is typed; None cancels.
while True:
text.trim(input("number> ")) => answer
if answer == "":
return None
parse_number(answer, base) => r
if r[2] == "":
return r
r[2] ^0
def shown(n, digits):
# A number with its sign, from its sign and digit values.
if n[0]:
return "-" + bases.written(digits)
return bases.written(digits)
True => running
while running:
"" ^0
"== Base converter ==" ^0
"1) convert a number 2) in bases 2, 8, 10, 16 3) to Roman 4) from Roman 5) quit" ^0
text.trim(input("choice> ")) => choice
if choice == "1" or choice == "2":
ask_base("from base (2-36)> ") => base
None => n
if base != 0 - 1:
ask_number(base) => n
0 - 1 => target
if n != None and choice == "1":
ask_base("to base (2-36)> ") => target
if n == None or (choice == "1" and target == 0 - 1):
"Cancelled." ^0
elif choice == "1":
(shown(n, n[1]) + " (base " + str(base) + ") = " + shown(n, bases.convert(n[1], base, target)) + " (base " + str(target) + ")") ^0
else:
("The number " + shown(n, n[1]) + " (base " + str(base) + ") is:") ^0
for b in [2, 8, 10, 16]:
(" " + ("%-9s" % ("base " + str(b))) + shown(n, bases.convert(n[1], base, b))) ^0
elif choice == "3":
while True:
text.trim(input("number (1-3999)> ")) => answer
text.number(answer) => v
if answer == "" or (v >= 1 and v <= 3999):
break
"Type a whole number from 1 to 3999, or nothing to cancel." ^0
if answer == "":
"Cancelled." ^0
else:
(str(v) + " = " + roman.to_roman(v)) ^0
elif choice == "4":
while True:
text.upper(text.trim(input("numeral> "))) => answer
if answer == "":
break
roman.from_roman(answer) => r
if r[1] == "":
break
r[1] ^0
if answer == "":
"Cancelled." ^0
else:
(answer + " = " + str(r[0])) ^0
elif choice == "5":
False => running
else:
"Pick a number from 1 to 5." ^0
"Bye." ^0
Python projection (main.py)
import bases
import roman
import text
longest = 60
def ask_base(prompt):
while True:
answer = text.trim(input(prompt))
if answer == "":
return 0 - 1
b = text.number(answer)
if b >= 2 and b <= 36:
return b
print("A base is a whole number from 2 to 36, or nothing to cancel.")
def parse_number(s, base):
s = text.upper(s)
negative = False
if len(s) > 0 and s[0] == "-":
negative = True
s = s[1:len(s)]
if s == "":
return [False, [], "Type some digits."]
if len(s) > longest:
return [False, [], "At most " + str(longest) + " digits."]
digits = []
for c in s:
v = bases.digit_value(c)
if v == 0 - 1:
return [False, [], c + " is not a digit; digits are 0-9, then A-Z for 10-35."]
if v >= base:
return [False, [], c + " is not a digit in base " + str(base) + "."]
digits = digits + [v]
digits = bases.strip(digits)
if digits == [0]:
negative = False
return [negative, digits, ""]
def ask_number(base):
while True:
answer = text.trim(input("number> "))
if answer == "":
return None
r = parse_number(answer, base)
if r[2] == "":
return r
print(r[2])
def shown(n, digits):
if n[0]:
return "-" + bases.written(digits)
return bases.written(digits)
running = True
while running:
print("")
print("== Base converter ==")
print("1) convert a number 2) in bases 2, 8, 10, 16 3) to Roman 4) from Roman 5) quit")
choice = text.trim(input("choice> "))
if choice == "1" or choice == "2":
base = ask_base("from base (2-36)> ")
n = None
if base != 0 - 1:
n = ask_number(base)
target = 0 - 1
if n != None and choice == "1":
target = ask_base("to base (2-36)> ")
if n == None or choice == "1" and target == 0 - 1:
print("Cancelled.")
elif choice == "1":
print(shown(n, n[1]) + " (base " + str(base) + ") = " + shown(n, bases.convert(n[1], base, target)) + " (base " + str(target) + ")")
else:
print("The number " + shown(n, n[1]) + " (base " + str(base) + ") is:")
for b in [2, 8, 10, 16]:
print(" " + "%-9s" % ("base " + str(b)) + shown(n, bases.convert(n[1], base, b)))
elif choice == "3":
while True:
answer = text.trim(input("number (1-3999)> "))
v = text.number(answer)
if answer == "" or v >= 1 and v <= 3999:
break
print("Type a whole number from 1 to 3999, or nothing to cancel.")
if answer == "":
print("Cancelled.")
else:
print(str(v) + " = " + roman.to_roman(v))
elif choice == "4":
while True:
answer = text.upper(text.trim(input("numeral> ")))
if answer == "":
break
r = roman.from_roman(answer)
if r[1] == "":
break
print(r[1])
if answer == "":
print("Cancelled.")
else:
print(answer + " = " + str(r[0]))
elif choice == "5":
running = False
else:
print("Pick a number from 1 to 5.")
print("Bye.")
bases.eml
eml# P021 base converter - whole numbers between bases 2 to 36. A number is kept
# as its list of digit values, most significant first, and converted by long
# division done by hand: divide the whole list by the new base, keep the
# remainder as the next digit, and repeat until nothing is left. Every step
# uses small numbers only, so numbers of any length come out exact.
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" => digit_chars
def digit_value(c):
# The value of digit c (0-9, A-Z), or -1.
for i in [0:35]:
if digit_chars[i] == c:
return i
return 0 - 1
def divide(digits, base, by):
# [quotient digits, remainder] of the number digits (in base) divided by
# the small number by; the quotient has no leading zeros.
[] => q
0 => carry
for d in digits:
carry * base + d => current
int((current - current % by) / by) => part
current % by => carry
if len(q) > 0 or part > 0:
q + [part] => q
return [q, carry]
def convert(digits, base, new_base):
# The digits of the same number in new_base.
if len(digits) == 0:
return [0]
digits => rest
[] => out
while len(rest) > 0:
divide(rest, base, new_base) => r
[r[1]] + out => out
r[0] => rest
return out
def strip(digits):
# digits without leading zeros (but at least one digit).
0 => i
while i < len(digits) - 1 and digits[i] == 0:
i + 1 => i
return digits[i:len(digits)]
def written(digits):
"" => out
for d in digits:
out + digit_chars[d] => out
return out
Python projection (bases.py)
digit_chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
def digit_value(c):
for i in range(0, 36):
if digit_chars[i] == c:
return i
return 0 - 1
def divide(digits, base, by):
q = []
carry = 0
for d in digits:
current = carry * base + d
part = int((current - current % by) / by)
carry = current % by
if len(q) > 0 or part > 0:
q = q + [part]
return [q, carry]
def convert(digits, base, new_base):
if len(digits) == 0:
return [0]
rest = digits
out = []
while len(rest) > 0:
r = divide(rest, base, new_base)
out = [r[1]] + out
rest = r[0]
return out
def strip(digits):
i = 0
while i < len(digits) - 1 and digits[i] == 0:
i = i + 1
return digits[i:len(digits)]
def written(digits):
out = ""
for d in digits:
out = out + digit_chars[d]
return out
roman.eml
eml# P021 base converter - Roman numerals from 1 to 3999. Writing one walks a
# table from the largest value down, the subtractive pairs (CM, CD, XC, XL,
# IX, IV) being entries of the table. Reading one adds each symbol's value,
# except that a symbol smaller than the one after it is subtracted. Reading
# alone would accept IIII or IC, so a numeral counts only if writing its value
# again gives exactly the same letters.
[[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"]] => table
def to_roman(n):
# The Roman numeral for 1 <= n <= 3999.
"" => out
for row in table:
while n >= row[0]:
out + row[1] => out
n - row[0] => n
return out
def symbol_value(c):
for row in table:
if row[1] == c:
return row[0]
return 0
def read_value(s):
# The value of s by the add-or-subtract rule, or -1 if a letter is not a
# Roman symbol.
0 => total
for i in [0:len(s) - 1]:
symbol_value(s[i]) => v
if v == 0:
return 0 - 1
if i + 1 < len(s) and v < symbol_value(s[i + 1]):
total - v => total
else:
total + v => total
return total
def from_roman(s):
# [value, ""] for a numeral written the standard way, otherwise
# [0, what is wrong].
read_value(s) => v
if v == 0 - 1:
return [0, "Roman numerals use only I, V, X, L, C, D and M."]
if v < 1 or v > 3999:
return [0, s + " is not a numeral from I to MMMCMXCIX."]
if to_roman(v) != s:
return [0, s + " is not written the standard way; " + str(v) + " is " + to_roman(v) + "."]
return [v, ""]
Python projection (roman.py)
table = [[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 = ""
for row in table:
while n >= row[0]:
out = out + row[1]
n = n - row[0]
return out
def symbol_value(c):
for row in table:
if row[1] == c:
return row[0]
return 0
def read_value(s):
total = 0
for i in range(0, len(s)):
v = symbol_value(s[i])
if v == 0:
return 0 - 1
if i + 1 < len(s) and v < symbol_value(s[i + 1]):
total = total - v
else:
total = total + v
return total
def from_roman(s):
v = read_value(s)
if v == 0 - 1:
return [0, "Roman numerals use only I, V, X, L, C, D and M."]
if v < 1 or v > 3999:
return [0, s + " is not a numeral from I to MMMCMXCIX."]
if to_roman(v) != s:
return [0, s + " is not written the standard way; " + str(v) + " is " + to_roman(v) + "."]
return [v, ""]
text.eml
eml# P021 base converter - reading what is typed. The interpreter that checks
# every session does not run string methods yet, so the text handling is
# written out here.
def trim(s):
# s without the spaces at either end.
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 upper(s):
# s with a-z turned into A-Z; everything else as it was.
"abcdefghijklmnopqrstuvwxyz" => lower_letters
"ABCDEFGHIJKLMNOPQRSTUVWXYZ" => upper_letters
"" => out
for c in s:
c => d
for i in [0:25]:
if lower_letters[i] == c:
upper_letters[i] => d
out + d => out
return out
def number(s):
# The value of s if it is digits only (at least one), otherwise -1.
if s == "":
return 0 - 1
0 => n
for c in s:
if not (c in "0123456789"):
return 0 - 1
n * 10 + int(c) => n
return n
Python projection (text.py)
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 upper(s):
lower_letters = "abcdefghijklmnopqrstuvwxyz"
upper_letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
out = ""
for c in s:
d = c
for i in range(0, 26):
if lower_letters[i] == c:
d = upper_letters[i]
out = out + d
return out
def number(s):
if s == "":
return 0 - 1
n = 0
for c in s:
if not c in "0123456789":
return 0 - 1
n = n * 10 + int(c)
return n