# 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 == "def":
define(line, line)
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 == "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.

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)
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)
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 `Optional`s:

``````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)
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
``````