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

Attribution
Source : Link , Question Author : Ruma Svenchko , Answer Author : Reinderien

Leave a Comment