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 wordunknown
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 lines = sys.stdin.readlines() 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 def getAnswer(answer): for k, v in d_define.items(): if v == answer: 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: res = getAnswer(equation) 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.
Answer
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.
You should just delete your LOOP START and LOOP END comments.
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 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)[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
Attribution
Source : Link , Question Author : Ruma Svenchko , Answer Author : Reinderien