Calculator
Type an expression with + - * / and parentheses; it is turned into postfix and worked out on a stack in exact fractions, so 0.1 + 0.2 is 0.3. Clear error messages, a history and ans for the last answer.
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
Type an expression with + - * /, parentheses and a minus in front, and it is worked out exactly: every number is a fraction, so 0.1 + 0.2 is 0.3 and 1 / 3 * 3 is 1. ans stands for the last answer, history lists what was worked out, clear forgets it, quit ends. A prompt rather than a menu, which is what a calculator is; the history lives while the program runs.
main.eml- the prompt, the commands and the historyexpr.eml- three steps from text to answer: cut the text into numbers, operators and parentheses; put them in postfix order with the shunting-yard method, checking the shape of the expression on the way (minus in front binds tightest, then* /, then+ -, all left to right); work the postfix out on a stackfrac.eml- exact fractions kept in lowest terms, and how an answer is shown: an integer as it is, a fraction whose decimal ends as that decimal (5/2 is 2.5), any other fraction asn/d, aboutits value to ten places, rounded half up
Integers have no size limit (123456789 * 987654321 is exact), but EML has no //, and a / b goes through a float that cannot hold a large quotient, so whole-number division is written out as long division by doubling.
What is reported instead of an answer: a number that is not one (1..2, .5), a character that is not part of an expression, a missing number or operator (3 4, *3, ()), an unclosed or unopened parenthesis, an expression that stops early (2 +), division by zero, and ans before any answer.
Sessions: sessions/basic.in works out integers, decimals that floats get wrong, fractions that do not end, ans, a minus in front, precedence, a big product and its exact quotient, and shows and clears the history; sessions/bad-input.in types every kind of error above, then a good expression.
Built on the verified corpus cases rpn-evaluator (working out an expression in postfix order on a stack) and simple-stack (a list used as a stack: push onto the end, pop from the end).
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-equalCalculator: + - * / and ( ), in exact fractions; ans is the last answer.
Commands: history, clear, quit.
calc> ans + 1
There is no answer yet for ans.
calc>
Type an expression, or quit.
calc> 2 +
The expression is incomplete.
calc> (1 + 2
A ( is not closed.
calc> 1 + 2)
There is no ( for this ).
calc> 3 4
An operator is missing before 4.
calc> 4 / (2 - 2)
Division by zero.
calc> 2 * x
Unexpected character: x
calc> 1..2
Not a number: 1..2
calc> .5 + 1
Not a number: .5
calc> *3
A number is missing before *.
calc> 7 / 0
Division by zero.
calc> ()
A number is missing before ).
calc> 2 * (3 + 4)
= 14
calc> quit
Bye.
What was typed (15 lines)
ans + 1
2 +
(1 + 2
1 + 2)
3 4
4 / (2 - 2)
2 * x
1..2
.5 + 1
*3
7 / 0
()
2 * (3 + 4)
quit
basic
interpreter: byte-equalCalculator: + - * / and ( ), in exact fractions; ans is the last answer.
Commands: history, clear, quit.
calc> 2 * (3 + 4)
= 14
calc> 0.1 + 0.2
= 0.3
calc> 10 / 4
= 2.5
calc> 1 / 3
= 1/3, about 0.3333333333
calc> ans * 3
= 1
calc> 2 / 3
= 2/3, about 0.6666666667
calc> -(2 + 3) * 4
= -20
calc> 2 * -3
= -6
calc> 1 - 2 / 3
= 1/3, about 0.3333333333
calc> 123456789 * 987654321
= 121932631112635269
calc> ans / 987654321
= 123456789
calc> 7 - 2 - 1
= 4
calc> history
1) 2 * (3 + 4) = 14
2) 0.1 + 0.2 = 0.3
3) 10 / 4 = 2.5
4) 1 / 3 = 1/3, about 0.3333333333
5) ans * 3 = 1
6) 2 / 3 = 2/3, about 0.6666666667
7) -(2 + 3) * 4 = -20
8) 2 * -3 = -6
9) 1 - 2 / 3 = 1/3, about 0.3333333333
10) 123456789 * 987654321 = 121932631112635269
11) ans / 987654321 = 123456789
12) 7 - 2 - 1 = 4
calc> clear
History and ans cleared.
calc> history
(nothing yet)
calc> quit
Bye.
What was typed (16 lines)
2 * (3 + 4)
0.1 + 0.2
10 / 4
1 / 3
ans * 3
2 / 3
-(2 + 3) * 4
2 * -3
1 - 2 / 3
123456789 * 987654321
ans / 987654321
7 - 2 - 1
history
clear
history
quit
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# P007 calculator: type an expression and get its value - exactly, because
# every number is a fraction. ans is the last answer; history lists what was
# worked out, and clear forgets it. The history lives while the program runs.
import expr
import frac
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]
"Calculator: + - * / and ( ), in exact fractions; ans is the last answer." ^0
"Commands: history, clear, quit." ^0
[] => history
None => ans
True => running
while running:
trim(input("calc> ")) => line
if line == "quit":
False => running
elif line == "history":
if len(history) == 0:
" (nothing yet)" ^0
0 => i
for h in history:
i + 1 => i
(" " + str(i) + ") " + h[0] + " = " + frac.shown(h[1])) ^0
elif line == "clear":
[] => history
None => ans
"History and ans cleared." ^0
elif line == "":
"Type an expression, or quit." ^0
else:
expr.run(line, ans) => r
if r[0]:
("= " + frac.shown(r[1])) ^0
history + [[line, r[1]]] => history
r[1] => ans
else:
r[1] ^0
"Bye." ^0
Python projection (main.py)
import expr
import frac
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]
print("Calculator: + - * / and ( ), in exact fractions; ans is the last answer.")
print("Commands: history, clear, quit.")
history = []
ans = None
running = True
while running:
line = trim(input("calc> "))
if line == "quit":
running = False
elif line == "history":
if len(history) == 0:
print(" (nothing yet)")
i = 0
for h in history:
i = i + 1
print(" " + str(i) + ") " + h[0] + " = " + frac.shown(h[1]))
elif line == "clear":
history = []
ans = None
print("History and ans cleared.")
elif line == "":
print("Type an expression, or quit.")
else:
r = expr.run(line, ans)
if r[0]:
print("= " + frac.shown(r[1]))
history = history + [[line, r[1]]]
ans = r[1]
else:
print(r[1])
print("Bye.")
expr.eml
eml# P007 calculator - from typed text to an answer, in three steps. tokens()
# cuts the text into numbers, operators and parentheses; postfix() puts them
# in postfix order with the shunting-yard method, checking the shape of the
# expression as it goes; evaluate() works the postfix out on a stack. Each
# step returns [True, result] or [False, what is wrong].
import frac
def tokens(s):
# [kind, value, text]: kind is "num" (value a fraction), "op" (+ - * /),
# "(", ")" or "ans".
[] => out
0 => i
len(s) => n
while i < n:
s[i] => c
if c == " ":
i + 1 => i
elif c in "0123456789.":
i => start
0 => points
while i < n and s[i] in "0123456789.":
if s[i] == ".":
points + 1 => points
i + 1 => i
s[start:i] => text
if points > 1 or text[0] == "." or text[len(text) - 1] == ".":
return [False, "Not a number: " + text]
out + [["num", frac.decimal(text), text]] => out
elif c in "+-*/":
out + [["op", c, c]] => out
i + 1 => i
elif c == "(" or c == ")":
out + [[c, c, c]] => out
i + 1 => i
elif s[i:i + 3] == "ans":
out + [["ans", "ans", "ans"]] => out
i + 3 => i
else:
return [False, "Unexpected character: " + c]
return [True, out]
def rank(t):
# How tightly an operator binds: minus-in-front ("neg") above * and /,
# above + and -.
if t[0] == "neg":
return 3
if t[1] == "*" or t[1] == "/":
return 2
return 1
def postfix(toks):
[] => out
[] => ops
True => want_number
for t in toks:
if want_number:
if t[0] == "num" or t[0] == "ans":
out + [t] => out
False => want_number
elif t[0] == "(":
ops + [t] => ops
elif t[0] == "op" and t[1] == "-":
ops + [["neg", "-", "-"]] => ops
else:
return [False, "A number is missing before " + t[2] + "."]
elif t[0] == "op":
# Left to right: first finish every operator on the stack that
# binds at least as tightly.
while len(ops) > 0 and ops[len(ops) - 1][0] != "(" and rank(ops[len(ops) - 1]) >= rank(t):
out + [ops[len(ops) - 1]] => out
ops[0:len(ops) - 1] => ops
ops + [t] => ops
True => want_number
elif t[0] == ")":
while len(ops) > 0 and ops[len(ops) - 1][0] != "(":
out + [ops[len(ops) - 1]] => out
ops[0:len(ops) - 1] => ops
if len(ops) == 0:
return [False, "There is no ( for this )."]
ops[0:len(ops) - 1] => ops
else:
return [False, "An operator is missing before " + t[2] + "."]
if want_number:
return [False, "The expression is incomplete."]
while len(ops) > 0:
if ops[len(ops) - 1][0] == "(":
return [False, "A ( is not closed."]
out + [ops[len(ops) - 1]] => out
ops[0:len(ops) - 1] => ops
return [True, out]
def evaluate(post, ans):
# Work the postfix out on a stack; ans is the last answer or None.
[] => stack
for t in post:
if t[0] == "num":
stack + [t[1]] => stack
elif t[0] == "ans":
if ans == None:
return [False, "There is no answer yet for ans."]
stack + [ans] => stack
elif t[0] == "neg":
stack[0:len(stack) - 1] + [frac.negated(stack[len(stack) - 1])] => stack
else:
stack[len(stack) - 2] => x
stack[len(stack) - 1] => y
stack[0:len(stack) - 2] => stack
if t[1] == "+":
frac.plus(x, y) => z
elif t[1] == "-":
frac.minus(x, y) => z
elif t[1] == "*":
frac.times(x, y) => z
else:
if y[0] == 0:
return [False, "Division by zero."]
frac.over(x, y) => z
stack + [z] => stack
return [True, stack[0]]
def run(text, ans):
tokens(text) => r
if r[0]:
postfix(r[1]) => r
if r[0]:
evaluate(r[1], ans) => r
return r
Python projection (expr.py)
import frac
def tokens(s):
out = []
i = 0
n = len(s)
while i < n:
c = s[i]
if c == " ":
i = i + 1
elif c in "0123456789.":
start = i
points = 0
while i < n and s[i] in "0123456789.":
if s[i] == ".":
points = points + 1
i = i + 1
text = s[start:i]
if points > 1 or text[0] == "." or text[len(text) - 1] == ".":
return [False, "Not a number: " + text]
out = out + [["num", frac.decimal(text), text]]
elif c in "+-*/":
out = out + [["op", c, c]]
i = i + 1
elif c == "(" or c == ")":
out = out + [[c, c, c]]
i = i + 1
elif s[i:i + 3] == "ans":
out = out + [["ans", "ans", "ans"]]
i = i + 3
else:
return [False, "Unexpected character: " + c]
return [True, out]
def rank(t):
if t[0] == "neg":
return 3
if t[1] == "*" or t[1] == "/":
return 2
return 1
def postfix(toks):
out = []
ops = []
want_number = True
for t in toks:
if want_number:
if t[0] == "num" or t[0] == "ans":
out = out + [t]
want_number = False
elif t[0] == "(":
ops = ops + [t]
elif t[0] == "op" and t[1] == "-":
ops = ops + [["neg", "-", "-"]]
else:
return [False, "A number is missing before " + t[2] + "."]
elif t[0] == "op":
while len(ops) > 0 and ops[len(ops) - 1][0] != "(" and rank(ops[len(ops) - 1]) >= rank(t):
out = out + [ops[len(ops) - 1]]
ops = ops[0:len(ops) - 1]
ops = ops + [t]
want_number = True
elif t[0] == ")":
while len(ops) > 0 and ops[len(ops) - 1][0] != "(":
out = out + [ops[len(ops) - 1]]
ops = ops[0:len(ops) - 1]
if len(ops) == 0:
return [False, "There is no ( for this )."]
ops = ops[0:len(ops) - 1]
else:
return [False, "An operator is missing before " + t[2] + "."]
if want_number:
return [False, "The expression is incomplete."]
while len(ops) > 0:
if ops[len(ops) - 1][0] == "(":
return [False, "A ( is not closed."]
out = out + [ops[len(ops) - 1]]
ops = ops[0:len(ops) - 1]
return [True, out]
def evaluate(post, ans):
stack = []
for t in post:
if t[0] == "num":
stack = stack + [t[1]]
elif t[0] == "ans":
if ans == None:
return [False, "There is no answer yet for ans."]
stack = stack + [ans]
elif t[0] == "neg":
stack = stack[0:len(stack) - 1] + [frac.negated(stack[len(stack) - 1])]
else:
x = stack[len(stack) - 2]
y = stack[len(stack) - 1]
stack = stack[0:len(stack) - 2]
if t[1] == "+":
z = frac.plus(x, y)
elif t[1] == "-":
z = frac.minus(x, y)
elif t[1] == "*":
z = frac.times(x, y)
else:
if y[0] == 0:
return [False, "Division by zero."]
z = frac.over(x, y)
stack = stack + [z]
return [True, stack[0]]
def run(text, ans):
r = tokens(text)
if r[0]:
r = postfix(r[1])
if r[0]:
r = evaluate(r[1], ans)
return r
frac.eml
eml# P007 calculator - exact fractions. A number is a list [n, d]: n / d in
# lowest terms with d > 0, so 0.1 is [1, 10] and 0.1 + 0.2 is exactly [3, 10].
# Integers have no size limit, but EML has no //, and a / b goes through a
# float that cannot hold a large quotient exactly, so whole-number division
# is written out below.
def quotient(a, b):
# a // b for whole numbers a >= 0 and b > 0, exact at any size. Long
# division by doubling: take away the largest b * 2^k that still fits.
0 => q
while a >= b:
b => m
1 => k
while m + m <= a:
m + m => m
k + k => k
a - m => a
q + k => q
return q
def gcd(a, b):
# Greatest common divisor of whole numbers a, b >= 0 (Euclid).
while b != 0:
a % b => r
b => a
r => b
return a
def make(n, d):
# n / d in lowest terms with a positive denominator (d != 0).
if d < 0:
0 - n => n
0 - d => d
abs(n) => a
gcd(a, d) => g
if n < 0:
return [0 - quotient(a, g), quotient(d, g)]
return [quotient(a, g), quotient(d, g)]
def plus(x, y):
return make(x[0] * y[1] + y[0] * x[1], x[1] * y[1])
def minus(x, y):
return make(x[0] * y[1] - y[0] * x[1], x[1] * y[1])
def times(x, y):
return make(x[0] * y[0], x[1] * y[1])
def over(x, y):
# x / y; the caller has checked that y is not zero.
return make(x[0] * y[1], x[1] * y[0])
def negated(x):
return [0 - x[0], x[1]]
def decimal(text):
# A number typed as digits with at most one point ("12", "0.25") as a
# fraction, exactly: 12.345 is 12345 / 1000.
"" => digits
1 => scale
False => point
for c in text:
if c == ".":
True => point
else:
digits + c => digits
if point:
scale * 10 => scale
return make(int(digits), scale)
def with_point(m, k):
# The whole number m >= 0 written with k digits after a point.
str(m) => s
while len(s) <= k:
"0" + s => s
if k == 0:
return s
return s[0:len(s) - k] + "." + s[len(s) - k:len(s)]
def shown(x):
# An integer as it is; a fraction whose decimal ends, as that decimal
# (5/2 is 2.5); any other fraction as n/d with its value to ten places,
# rounded half up ("1/3, about 0.3333333333").
"" => sign
if x[0] < 0:
"-" => sign
negated(x) => x
if x[1] == 1:
return sign + str(x[0])
# The decimal ends if the denominator divides a power of ten.
1 => p
0 => k
while p % x[1] != 0 and k < 60:
p * 10 => p
k + 1 => k
if p % x[1] == 0:
return sign + with_point(x[0] * quotient(p, x[1]), k)
10000000000 => ten10
quotient(2 * x[0] * ten10 + x[1], 2 * x[1]) => t
return sign + str(x[0]) + "/" + str(x[1]) + ", about " + sign + with_point(t, 10)
Python projection (frac.py)
def quotient(a, b):
q = 0
while a >= b:
m = b
k = 1
while m + m <= a:
m = m + m
k = k + k
a = a - m
q = q + k
return q
def gcd(a, b):
while b != 0:
r = a % b
a = b
b = r
return a
def make(n, d):
if d < 0:
n = 0 - n
d = 0 - d
a = abs(n)
g = gcd(a, d)
if n < 0:
return [0 - quotient(a, g), quotient(d, g)]
return [quotient(a, g), quotient(d, g)]
def plus(x, y):
return make(x[0] * y[1] + y[0] * x[1], x[1] * y[1])
def minus(x, y):
return make(x[0] * y[1] - y[0] * x[1], x[1] * y[1])
def times(x, y):
return make(x[0] * y[0], x[1] * y[1])
def over(x, y):
return make(x[0] * y[1], x[1] * y[0])
def negated(x):
return [0 - x[0], x[1]]
def decimal(text):
digits = ""
scale = 1
point = False
for c in text:
if c == ".":
point = True
else:
digits = digits + c
if point:
scale = scale * 10
return make(int(digits), scale)
def with_point(m, k):
s = str(m)
while len(s) <= k:
s = "0" + s
if k == 0:
return s
return s[0:len(s) - k] + "." + s[len(s) - k:len(s)]
def shown(x):
sign = ""
if x[0] < 0:
sign = "-"
x = negated(x)
if x[1] == 1:
return sign + str(x[0])
p = 1
k = 0
while p % x[1] != 0 and k < 60:
p = p * 10
k = k + 1
if p % x[1] == 0:
return sign + with_point(x[0] * quotient(p, x[1]), k)
ten10 = 10000000000
t = quotient(2 * x[0] * ten10 + x[1], 2 * x[1])
return sign + str(x[0]) + "/" + str(x[1]) + ", about " + sign + with_point(t, 10)