Skip to content

Instantly share code, notes, and snippets.

@technillogue
Created June 28, 2013 18:53
Show Gist options
  • Save technillogue/5887092 to your computer and use it in GitHub Desktop.
Save technillogue/5887092 to your computer and use it in GitHub Desktop.
A more complicated recursive python calculator
from collections import OrderedDict
#reverse order of operations
#I didn't have to use an OrderedDict, but it's cute
operations = OrderedDict([
("+", 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)
])
symbols = operations.keys()
def lex(expr):
"""
seperates numbers from symbols, recursively nests parens
"""
tokens = []
while expr:
char, *expr = expr
#char is equal to the first charecter of the expression, expr is equal
#to the rest of it
if char == "#":
#the rest of the line is a comment
break
if char == "(":
try:
paren, expr = lex(expr)
tokens.append(paren)
#expr is what's after the end of the paren, we'll just continue
#lexing after that''
except ValueError:
raise Exception("paren mismatch")
elif char == ")":
return tokens, expr
#returns the tokens leading up to the to the paren and the rest of
#the expression after it
elif char.isdigit() or char == ".":
#number
try:
if tokens[-1] in symbols:
tokens.append(char) #start a new num
elif type(tokens[-1]) is list:
raise Exception("parens cannot be followed by numbers")
#no support for 5(1+1) yet
else:
tokens[-1] += char #add to last num
except IndexError:
#if tokens is empty
tokens.append(char) #start first num
elif char in symbols:
tokens.append(char)
elif char.isspace():
pass
else:
raise Exception("invalid charecter: " + char)
return tokens
def evaluate(tokens):
for symbol, func in operations.items():
#try to find an operation to eval in order
try:
pos = tokens.index(symbol)
#split the tokens by the operation and eval that
leftTerm = evaluate(tokens[:pos])
rightTerm = evaluate(tokens[pos + 1:])
return func(leftTerm, rightTerm)
#incidentially, return immediatly breaks all loops within the
# function
except ValueError:
pass
#index raises ValueError when it's not found
if len(tokens) is 1:
try:
#it must be a number
return float(tokens[0])
except TypeError:
#if it's not a number
return evaluate(tokens[0])
else:
raise Exception("bad expression: " + tokens)
def calc(expr):
return evaluate(lex(expr))
while 1:
print(calc(input("Input? ")))
$ python3 calc.py
Input? 1 + 1 #simple operations, comments
2.0
Input? 0.1-0.5#floats and negatives, irrelevant whitespace
-0.4
Input? 4 + 4 / 2 - 1 #order of operations is respected
5.0
Input? (4-4 + (3-2) * (((((((7)))))))) # complex parens
7.0
@along02
Copy link

along02 commented Jul 22, 2017

("^", lambda x, y: x ^ y) should be ("^", lambda x, y: x**y)

(Also if you enter a string into the input the code doesn't work.)

@OoLunar
Copy link

OoLunar commented Sep 19, 2019

Hello!
I was wondering if I have your permission to use your calculator in my project. All credit will go to you, along with a link to your GitHub. The file will be modified slightly to allow math equations similar to 4(8), and of course small spelling differences as well. Is this okay with you?

@technillogue
Copy link
Author

Uh, sure?

@OoLunar
Copy link

OoLunar commented Sep 19, 2019

Uh, sure?

Thank you very much!

@nachoregulardude
Copy link

Hello!
I was wondering if I have your permission to use your calculator in my project. All credit will go to you, along with a link to your GitHub. The file will be modified slightly to allow math equations similar to 4(8), and of course small spelling differences as well. Is this okay with you?

How did you get the 4(8+8) to work?

@technillogue
Copy link
Author

technillogue commented Jul 18, 2020 via email

@gitdev-bash
Copy link

Hello, i have fixed your project. There was a issue with exponential calculation:

Input? 2^2
Traceback (most recent call last):
  File "/home/kuflierl/Software/Test/py/recrusive-calc.py", line 89, in <module>
    print(calc(input("Input? ")))
  File "/home/kuflierl/Software/Test/py/recrusive-calc.py", line 86, in calc
    return evaluate(lex(expr))
  File "/home/kuflierl/Software/Test/py/recrusive-calc.py", line 69, in evaluate
    return func(leftTerm, rightTerm)
  File "/home/kuflierl/Software/Test/py/recrusive-calc.py", line 11, in <lambda>
    ("^", lambda x, y: x ^ y)
TypeError: unsupported operand type(s) for ^: 'float' and 'float'

In python we use ** for exponential calculations. This should fix it:

#!/bin/python3
from collections import OrderedDict

#reverse order of operations
#I didn't have to use an OrderedDict, but it's cute
operations = OrderedDict([
    ("+", 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)
])
    
symbols = operations.keys()

def lex(expr):
    """
    seperates numbers from symbols, recursively nests parens
    """
    tokens = []
    while expr:
        char, *expr = expr
        #char is equal to the first charecter of the expression, expr is equal
        #to the rest of it
        if char == "#":
            #the rest of the line is a comment
            break
        if char == "(":
            try:
                paren, expr = lex(expr)
                tokens.append(paren)
                #expr is what's after the end of the paren, we'll just continue
                #lexing after that''
            except ValueError:
                raise Exception("paren mismatch")
        elif char == ")":
            return tokens, expr
            #returns the tokens leading up to the to the paren and the rest of 
            #the expression after it
        elif char.isdigit() or char == ".":
            #number
            try:
                if tokens[-1] in symbols:
                    tokens.append(char) #start a new num
                elif type(tokens[-1]) is list:
                    raise Exception("parens cannot be followed by numbers")
                    #no support for 5(1+1) yet
                else:
                    tokens[-1] += char #add to last num
            except IndexError:
                #if tokens is empty
                tokens.append(char) #start first num
        elif char in symbols:
            tokens.append(char)
        elif char.isspace():
            pass
        else:
            raise Exception("invalid charecter: " + char)
    return tokens

def evaluate(tokens):
    for symbol, func in operations.items():
        #try to find an operation to eval in order
        try:
            pos = tokens.index(symbol)
            #split the tokens by the operation and eval that
            leftTerm = evaluate(tokens[:pos])
            rightTerm = evaluate(tokens[pos + 1:])
            return func(leftTerm, rightTerm)
            #incidentially, return immediatly breaks all loops within the
            # function
        except ValueError:
            pass
            #index raises ValueError when it's not found
    if len(tokens) is 1:
        try:
            #it must be a number
            return float(tokens[0])
        except TypeError:
            #if it's not a number
            return evaluate(tokens[0])
    else:
        raise Exception("bad expression: " + tokens)

while 1:
    print(evaluate(lex(input("Input? "))))

BTW i shortened your project a bit.

@thanhhung0112
Copy link

thanks for your code very much

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment