# Calculator using variable names

I've recently been assigned an assignment to create a word calculator, and given my knowledge in python is still quite lacking, I want to ask if anybody has any better ideas for any possible solutions.

The question is here:

Jimmy has invented a new kind of calculator that works with words
rather than numbers.

• Input is read from stdin and consists of up to 1000 commands, one per line.

• Each command is a definition, a calculation or clear.

• All tokens within a command are separated by single spaces.

• A definition has the format def x y where x is a variable name and y is an integer in the range [-1000, 1000].

• Existing definitions are replaced by new ones i.e. if x has been defined previously, defining x again erases its old definition.

• Variable names consist of 1-30 lowercase characters.

• No two variables are ever defined to have the same value at the same time.

• The clear command erases all existing variable definitions.

• A calculation command starts with the word calc, and is followed by one or more variable names separated by addition or subtraction operators.

• The end of a calculation command is an equals sign.

The goal is to write a program for Jimmy's calculator. Some rules are:

• The program should produce no output for definitions, but for calculations it should output the value of the calculation.
• Where there is no word for the result, or some word in a calculation has not been defined, then the output should be unknown. (The word unknown is never used as a variable name.)
• Your solution may only import content from the sys module.
• Your solution may not use the eval() function.

Here is a sample input below:

calc foo + bar =
def bar 7
def programming 10
calc foo + bar =
def is 4
def fun 8
calc programming - is + fun =
def fun 1
calc programming - is + fun =
clear


And the corresponding output:

foo + bar = unknown
foo + bar = programming
programming - is + fun = unknown
programming - is + fun = bar


My current solution is this:

import sys

d_define = {}
numsInDict = []
operators = ["+", "-", "="]

# IN : word to checked it is in the dictionary
# OUT : returns unknown or value for word
def lookup(word):
if word in d_define:
return d_define[word]
else:
return "unknown"

# IN : answer to check if there is a word assigned to it a dictionary
# OUT : returns unknown or word assigned to answer
for k, v in d_define.items():
return k
return "unknown"

# IN : All values to calc (includes operators)
# OUT : print unknown or word if in dict
def calc(args):
equation = 0
lastOperator = "+"
for word in args:
if word not in operators:
res = lookup(word)
if res == "unknown":
return "unknown"
else:
#print(res)
if lastOperator == "+":
equation += res
else:
equation -= res
else:
lastOperator = word
if equation in numsInDict:
return res
else:
return "unknown"

# IN : word to be added and its value
# OUT : updated dictionary
def define(word, num):
num = int(num)
if num in numsInDict or word in d_define:
# print(f'NEEDS REPLACE')
# print(f'same value -> {num in numsInDict}')
# print(f'same word -> {word in d_define}')
# print(f'same word -> {d_define}')
# print(f'same word -> {numsInDict}')
topop = ""
for k, v in d_define.items():
if k == word:
d_define[word] = num  # Update Word with new value
elif v == num:
topop = k  # Saves value to pop later
if topop != "":
d_define.pop(topop)
d_define[word] = num
else:
d_define[word] = num
numsInDict.append(num)
#print(f'{word} - {d_define[word]}')

for line in lines:
#print(f'-------------------------------------- LOOP START ------------------------------------')
line = line.rstrip().split()
#print(f'Line Split - {line}')
if len(line) == 3 and line[0] == "def":
define(line[1], line[2])
elif len(line) > 1 and line[len(line) - 1] == "=":
result = calc(line[1:])
print(f'{" ".join(line[1:]) + " " + result}')
elif len(line) == 1 and line[0] == "clear":
d_define = {}
wordsInDict = []
numsInDict = []
#print(f'Cleared d_define - {d_define} {wordsInDict}')
#print(d_define)
#print(f'--------------------------------------- LOOP END -------------------------------------')


It does feel quite clunky but it gets the job done. I am just wondering if anybody has any better ways in which it could be improved upon.

• There's at least one reference to this online, at Chegg. What is the original source? Commented Mar 11, 2022 at 23:25
• It feels a bit odd that an unparseable line doesn't throw an error Commented Mar 12, 2022 at 22:04

I think your solution does work, but it's very long-winded. Your IN:  and OUT:  comments should be moved to docstrings """ """ in the first line of the function body.

getAnswer should be get_answer by PEP8.

Your getAnswer doesn't need to loop if you maintain an inverse dictionary of values to names.

Consider using the built-in operator.add and .sub. This violates the letter (though perhaps not the spirit) of Your solution may only import content from the sys module; if that's actually a problem just define the add and sub functions yourself.

Consider writing unit tests to validate your output. This can be done by operating on the stdin and stdout streams passed as parameters, or yielding lines.

## Suggested

from operator import add, sub
from typing import Iterable, Iterator

def rewritten(in_stream: Iterable[str]) -> Iterator[str]:
OPS = {'+': add, '-': sub}
state, inverse_state = {}, {}

for line in in_stream:
command, *args = line.split()

if command == 'def':
name, val = args
val = int(val)
state[name] = val
inverse_state[val] = name

elif command == 'clear':
state.clear()
inverse_state.clear()

elif command == 'calc':
result, *values = (state.get(name) for name in args[::2])

if result is not None:
for op, value in zip(args[1:-1:2], values):
if value is None:
result = None
break
result = OPS[op](result, value)

prefix = line.rstrip().split(maxsplit=1)[1]
result = inverse_state.get(result, 'unknown')
yield f'{prefix} {result}'

def test() -> None:
sample_in = (
'''def foo 3
calc foo + bar =
def bar 7
def programming 10
calc foo + bar =
def is 4
def fun 8
calc programming - is + fun =
def fun 1
calc programming - is + fun =
clear'''
).splitlines()

actual = '\n'.join(rewritten(sample_in))

assert actual == (
'''foo + bar = unknown
foo + bar = programming
programming - is + fun = unknown
programming - is + fun = bar'''
)

if __name__ == '__main__':
test()


## Even more lookups

As @Stef suggests, it is possible to add a lookup for the command. It's awkward, because only one of the three functions actually produces a result. One way to express this is an unconditional outer yield from, and all functions as generators, only one being non-empty. I don't particularly recommend this; it's just a demonstration:

def rewritten(in_stream: Iterable[str]) -> Iterator[str]:
def clear() -> Iterator[str]:
state.clear()
inverse_state.clear()
return; yield

def define(name: str, val: str) -> Iterator[str]:
val = int(val)
state[name] = val
inverse_state[val] = name
return; yield

def calc(*args: str) -> Iterator[str]:
result, *values = (state.get(name) for name in args[::2])

if result is not None:
for op, value in zip(args[1:-1:2], values):
if value is None:
result = None
break
result = OPS[op](result, value)

prefix = line.rstrip().split(maxsplit=1)[1]
result = inverse_state.get(result, 'unknown')
yield f'{prefix} {result}'

OPS = {'+': add, '-': sub}
COMMANDS = {'clear': clear, 'def': define, 'calc': calc}
state, inverse_state = {}, {}

for line in in_stream:
parts = line.split()
command, *args = parts
yield from COMMANDS[command](*args)


Or just use Optionals:

def rewritten(in_stream: Iterable[str]) -> Iterator[str]:
def clear() -> Optional[str]:
state.clear()
inverse_state.clear()

def define(name: str, val: str) -> Optional[str]:
val = int(val)
state[name] = val
inverse_state[val] = name

def calc(*args: str) -> Optional[str]:
result, *values = (state.get(name) for name in args[::2])

if result is not None:
for op, value in zip(args[1:-1:2], values):
if value is None:
result = None
break
result = OPS[op](result, value)

prefix = line.rstrip().split(maxsplit=1)[1]
result = inverse_state.get(result, 'unknown')
return f'{prefix} {result}'

OPS = {'+': add, '-': sub}
COMMANDS = {'clear': clear, 'def': define, 'calc': calc}
state, inverse_state = {}, {}

for line in in_stream:
parts = line.split()
command, *args = parts
result = COMMANDS[command](*args)
if result is not None:
yield result

• You suggest using a dictionary to map operator symbols to the corresponding operations, OPS = {'+': add, '-': sub}. I think that's a great idea. You could also use a dictionary to map commands clear, def, calc to functions. This way, you could encapsulate the code of the commands in functions, and avoid bogging the main loop of function rewritten with a huge if/elif/else.
– Stef
Commented Mar 12, 2022 at 13:41
• @Stef I hesitate to think that a ~20-line if/elif is "huge". If it were larger I would agree. Commented Mar 12, 2022 at 16:52

The reason your solution is convoluted is that the problem hamstringing you by not allowing any modules (except sys) including the built in ones!

Secondly it seems you need to freshen up on your basics. For instance this

def lookup(word):
if word in d_define:
return d_define[word]
else:
return "unknown"


Can be written as

d_define.get(word, "unknown")


Secondly d_define is a bad name; as a general rule avoid abbreviations in variable and function names. Assume that everything is fair game, no limitations on external libraries. The crux is that eval is really dangerous, and you should never run it on user input. For instance an user could import the os library and then run commands to delete your whole file system. However, there are safe ways to use it, for instance

    SAFE_CHARS = set("0123456789+-*(). /")

for char in expr:
if char not in SAFE_CHARS:
value = "unknown"
break
else:
value = eval(expr)


If we detect any "malicious intent", e.g anything other that mathematical expressions we break and return "unknown". We first replace all the words with their values before evaluating. Some care is taken here for instance if we have "foobar=3" and "bar=2" then "foobar" must be replaced before "bar" otherwise we might end up with "foo2".

Note that this way of solving the problem is very slow with many definitions, but I really do not think speed matters.

import sys

SAFE_CHARS = set("0123456789+-*(). /")

class Calculator:
def __init__(self):
self.definitions = dict()
self.values = dict()

def definition(self, name: str, integer: float) -> None:
self.definitions[name] = integer
self.values[integer] = name

def clear(self):
self.definitions = dict()
self.values = dict()

def calculate(self, expr: str) -> float | str:
expr = "".join(expr.split())  # Remove all spaces
definitions = sorted(
self.definitions.items(), key=lambda x: len(x[0]), reverse=True
)

for (word, value) in definitions:
expr = expr.replace(word, str(value))

if all(c in SAFE_CHARS for c in char):
value = eval(expr)
else:
value = "unknown"

return self.values.get(value, "unknown")

if __name__ == "__main__":

calc = Calculator()
if not (line := line.strip()):
continue
if line.startswith("def"):
_, name, value = line.split()
calc.definition(name, float(value))
elif line.startswith("calc") and line.endswith("="):
_, expr = line[:-1].split("calc")
print(f"{line} {calc.calculate(expr)}")
elif line.startswith("clear"):
calc.clear()


Assuming that eval is off the table, I would probably rewrite it as follows. Due note that we use a recursive algorithm just for fun to evaluate the expressions. A lot of care is taken to correctly handle deeply nested expressions.

import sys

def is_number(s: str) -> bool:
try:
float(s)
return True
except ValueError:
return False

class Calculator:
def __init__(self):
self.definitions = dict()
self.values = dict()

def definition(self, name: str, integer: float) -> None:
self.definitions[name] = integer
self.values[integer] = name

def clear(self):
self.definitions = dict()
self.values = dict()

def calculate(self, expr: str) -> float | str:
expr = "".join(expr.split())  # Remove all spaces
try:
value = self._eval(expr)
except KeyError:
value = "unknown"
return self.values.get(value, "unknown")

def _eval(self, expr: str) -> float:
"""self.evaluates an expression consisting of binary expressions

>>> calculator._eval("123.5")
123.5
>>> calculator._eval("(5 + 3) + (1 + (2 + 6))")
17.0
>>> calculator._eval("((((5))+(2))+1+2)")
10.0
"""
if is_number(expr):
return float(expr)
if "(" in expr:
start, stop = inner_expression(expr)
new_expr = self._eval(expr[start + 1 : stop - 1])
expr = expr.replace(expr[start:stop], str(new_expr))
return self._eval(expr)
elif "+" in expr:
expr_1, expr_2 = expr.split("+", 1)
return self._eval(expr_1) + self._eval(expr_2)
elif "-" in expr:
expr_1, expr_2 = expr.split("-", 1)
return self._eval(expr_1) - self._eval(expr_2)
elif "/" in expr:
expr_1, expr_2 = expr.split("/", 1)
return self._eval(expr_1) / self._eval(expr_2)
elif "*" in expr:
expr_1, expr_2 = expr.split("*", 1)
return self._eval(expr_1) * self._eval(expr_2)
elif "^" in expr:
expr_1, expr_2 = expr.split("^", 1)
return self._eval(expr_1) ** self._eval(expr_2)
return self.definitions[expr]

MATCHING_BRACKET = {")": "(", "}": "{", "[": "]"}
OPEN_BRACKETS = set(["(", "{", "["])

def inner_expression(expr: str):
"""Returns the indices for the inner most expression

>>> inner_expression("(5}")
Traceback (most recent call last):
ValueError: Incompatible brackets '}' does not match '('!

>>> inner_expression("((5)")
Traceback (most recent call last):
ValueError: Incomplete brackets found, the brackets ['('] was never closed!

>>> a, b = inner_expression('((5+2)+(1+2))'); '((5+2)+(1+2))'[a:b]
'(5+2)'

>>> a, b = inner_expression('((((5))+(2))+1+2)'); '((((5))+(2))+1+2)'[a:b]
'(5)'
"""
stack = []
deepest_nesting = start = 0
indices = None
for i, char in enumerate(expr):
if char in MATCHING_BRACKET:
if stack[-1] != MATCHING_BRACKET[char]:
raise ValueError(
f"Incompatible brackets '{char}' does not match '{stack[-1]}'!"
)
if (nesting := len(stack)) > deepest_nesting:
deepest_nesting = nesting
indices = (start, i + 1)
stack.pop()
elif char in OPEN_BRACKETS:
stack.append(char)
start = i
if stack:
raise ValueError(
f"Incomplete brackets found, the brackets {stack} was never closed!"
)
return indices if indices is not None else (0, len(expr))

if __name__ == "__main__":
import doctest

doctest.testmod(extraglobs={"calculator": Calculator()})

calc = Calculator()
if not (line := line.strip()):
continue
if line.startswith("def"):
_, name, value = line.split()
calc.definition(name, float(value))
elif line.startswith("calc") and line.endswith("="):
_, expr = line[:-1].split("calc")
print(f"{line} {calc.calculate(expr)}")
elif line.startswith("clear"):
calc.clear()


EDIT: If we allow ourselves to use the full capabilities of Python, disregarding that our code must be beginner friendly we can do as shown below

• Reduce was introduced to reduce the number of recursive calls and offer a small speed improvement. Our original code reduced the expression as follows a + b + c + d + e = a + (b + c + d+ e) = a a + (b + (c + d + e)) usw.

• The operators were extracted into a seperate function reducing this

      elif "+" in expr:
expr_1, expr_2 = expr.split("+", 1)
return self._eval(expr_1) + self._eval(expr_2)
elif "-" in expr:
expr_1, expr_2 = expr.split("-", 1)
return self._eval(expr_1) - self._eval(expr_2)
elif "/" in expr:
expr_1, expr_2 = expr.split("/", 1)
return self._eval(expr_1) / self._eval(expr_2)
elif "*" in expr:
expr_1, expr_2 = expr.split("*", 1)
return self._eval(expr_1) * self._eval(expr_2)
elif "^" in expr:
expr_1, expr_2 = expr.split("^", 1)
return self._eval(expr_1) ** self._eval(expr_2)
return self.definitions[expr]


into this

  for operator, function in OPERATORS.items():
if operator not in expr:
continue
expressions = map(self._eval, expr.split(operator))
return reduce(function, expressions)

• Minor cleanup for the inner_expression as well

• A bit more typing hints and doctests

### Code without imports

import sys

def is_number(s: str) -> bool:
try:
float(s)
return True
except ValueError:
return False

OPERATORS = {
"+": lambda x, y: x + y,
"-": lambda x, y: x - y,
"/": lambda x, y: x / y,
"*": lambda x, y: x * y,
"^": lambda x, y: x**y,
}

def reduce(function, iterable, initializer=None):
"""Implements the reduce function without external modules

>>> reduce(lambda x, y: x + y, range(5))
10
>>> reduce(lambda x, y: x + y, range(5), initializer=100)
110
>>> reduce(lambda x, y: f"({x}) + {y}", range(5))
'((((0) + 1) + 2) + 3) + 4'
>>> reduce(lambda x, y: f"({x}) + {y}", range(5), initializer=100)
'(((((100) + 0) + 1) + 2) + 3) + 4'
"""
it = iter(iterable)
if initializer is None:
value = next(it)
else:
value = initializer
for element in it:
value = function(value, element)
return value

class Calculator:
def __init__(self) -> None:
self.definitions = dict()
self.values = dict()

def definition(self, name: str, integer: float) -> None:
self.definitions[name] = integer
self.values[integer] = name

def clear(self) -> None:
self.definitions = dict()
self.values = dict()

def calculate(self, expr: str) -> float | str:
expr = "".join(expr.split())  # Remove all spaces
try:
value = self._eval(expr)
except KeyError:
value = "unknown"
return self.values.get(value, "unknown")

def _eval(self, expr: str) -> float:
"""self.evaluates an expression consisting of binary expressions

>>> qalc._eval("123.5")
123.5
>>> qalc._eval("(5 + 3) + (1 + (2 + 6))")
17.0
>>> qalc._eval("((((5))+(2))+1+2)")
10.0
"""
if is_number(expr):
return float(expr)

if "(" in expr:
start, stop = inner_expression(expr)
new_expr = self._eval(expr[start + 1 : stop - 1])
expr = expr.replace(expr[start:stop], str(new_expr))
return self._eval(expr)

for operator, function in OPERATORS.items():
if operator not in expr:
continue
expressions = map(self._eval, expr.split(operator))
return reduce(function, expressions)

return self.definitions[expr]

MATCHING_BRACKET = {")": "(", "}": "{", "[": "]"}
OPEN_BRACKETS = set(["(", "{", "["])

def inner_expression(expr: str) -> tuple[int, int]:
"""Returns the indices for the inner most expression

>>> inner_expression("(5}")
Traceback (most recent call last):
ValueError: Incompatible brackets '}' does not match '('!
>>> inner_expression("((5)")
Traceback (most recent call last):
ValueError: Incomplete brackets found, the brackets ['('] was never closed!
>>> a, b = inner_expression('((5+2)+(1+2))'); '((5+2)+(1+2))'[a:b]
'(5+2)'
>>> a, b = inner_expression('((((5))+(2))+1+2)'); '((((5))+(2))+1+2)'[a:b]
'(5)'
"""
stack = []
deepest_nesting = start = 0
indices = None
for i, char in enumerate(expr):

if char in OPEN_BRACKETS:
stack.append(char)
start = i
continue
elif char not in MATCHING_BRACKET:
continue

matching_brackets = stack[-1] == MATCHING_BRACKET[char]

if not matching_brackets:
raise ValueError(
f"Incompatible brackets '{char}' does not match '{stack[-1]}'!"
)

if (nesting := len(stack)) > deepest_nesting:
deepest_nesting = nesting
indices = (start, i + 1)
stack.pop()

if stack:
raise ValueError(
f"Incomplete brackets found, the brackets {stack} was never closed!"
)

if indices is None:
return (0, len(expr))
return indices

def first_and_remaining(line: str) -> tuple[str, str | None]:
"""If lines = 'a b c' this returns ('a', 'b c')

>>> first_and_remaining('a b c')
('a', 'b c')
>>> first_and_remaining('')
('', None)
>>> first_and_remaining(' a ')
('a', None)
>>> first_and_remaining(' a b ')
('a', 'b')
"""
if " " not in (stripped := line.strip()):
return stripped, None
return tuple(x.strip() for x in stripped.split(maxsplit=1))

def calculator(lines, calc=None) -> None:
"""Feeds lines into a calculator

>>> commands = '''
... def foo 3
... calc foo + bar =
... def bar 7
... def programming 10
... calc foo + bar =
... def is 4
... def fun 8
... calc programming - is + fun =
... def fun 1
... calc programming - is + fun =
... clear'''
>>> calculator(commands.splitlines())
calc foo + bar = unknown
calc foo + bar = programming
calc programming - is + fun = unknown
calc programming - is + fun = bar
"""
if calc is None:
calc = Calculator()

for command, expression in map(first_and_remaining, lines):

if not command:
continue

if expression is None:
if command == "clear":
calc.clear()
continue

if command == "def":
name, value = expression.split()
calc.definition(name, int(value))

elif command == "calc" and expression.endswith("="):
value = calc.calculate(expression[:-1])
print(f"{command} {expression} {value}")

if __name__ == "__main__":
import doctest

doctest.testmod(extraglobs={"qalc": Calculator()})