# Calculator - Kivy Order of Operations

I decided to make a simple calculator, but then I decided I want to make an advanced calculator. Before I achieve that though I settled for a calculator capable of order of operations.

#!/usr/bin/env python3

"""

Brandon Dunbar
Start Date: 11/20/17
v1.0 End Date: 12/04/17
Calculator

"""

# Can this be optimized?
from kivy.app import App
from kivy.core.window import Window

from kivy.uix.gridlayout import GridLayout
from kivy.uix.boxlayout import BoxLayout

from kivy.uix.button import Button
from kivy.uix.alignedtextinput import AlignedTextInput
from kivy.uix.label import Label

import copy
import numpy

class Calculator(App):

Window.size = (400, 500)
operations = ["+", "-", "*", "/"]
# Creating widgets outside of method for class-wide access, can we put this in build()?
txt_in = AlignedTextInput(size_hint_y=.1, multiline=False, focus=True, background_color=[0, 0, 0, 1],
cursor_color=[0, 0, 0, 1], foreground_color=[1, 0, 1, 1], halign="center",
valign="bottom")  # AlignedTextInput for centered text
warning_label = Label(text='', size_hint_y=.1, color=[1, 0, 0, 1])  # A label for warnings

def build(self):
"""Creates calculator GUI"""
displays = ["(", ")", "C", "<-", "7", "8", "9", " / ", "4", "5", "6",
" * ", "1", "2", "3", " - ", ".", "0", "=", " + "]  # For loop creating the gui
layout = GridLayout(cols=4)

for txt in displays:
button = Button(text=txt, color=(1, 0, 1, 1), background_color=(0, 0, 0, 1), outline_color=(1, 0, 1, 1))
button.bind(on_press=self.press)

box = BoxLayout(orientation='vertical')  # Set layout orientation to add widgets below the previously added

return box

def press(self, button, txt_display=txt_in):
"""Handles button presses.

Keyword arguments:
button -- The object of the button pressed
txt_display -- Where to display result of equation(default: txt_in)"""

self.warning_label.text = ''  # Clear warning on button press

# **----- Operators -----**
if button.text.strip() in self.operations:  # Is button an operator?
equation = txt_display.text.split()
last = len(equation) - 1
sec_last = len(equation) - 2

# Is the last button an operator? Could this be a negative?
if len(equation) > 1 and equation[last] in self.operations and button.text.strip() != "-":
# If the last two buttons were operators, replace both with new operator
if equation[sec_last] in self.operations:
equation[sec_last] = button.text
del equation[last]
# If only the last one is an operator, replace it with new operator
else:
equation[last] = button.text

# Reconstruct the modified equation
new_equation = ""
for x in equation:
new_equation += str(x)
# And display it
txt_display.text = new_equation

# Allowing negatives
elif button.text == ' - ':  # Unique in case of negatives
curr_disp = txt_display.text
curr_disp_l = curr_disp.split()

# If the last obj is an operator, pass a negative
if len(curr_disp_l) < 1 or curr_disp_l[-1] in self.operations and curr_disp[-1] != '-':
txt_display.insert_text("-")

# If the last obj is a negative, do nothing
elif curr_disp[-1] == '-':
pass

# If this isn't a negative, add the minus sign
else:
txt_display.insert_text(button.text)

# If it's not a special case, just add the operator
else:
txt_display.insert_text(button.text)

# **----- Solve -----**
elif button.text == '=':
# Take out all spaces
equation = []
for char in txt_display.text.replace(" ", ""):
equation.append(char)
# Process the equation and display result
txt_display.text = str(self.process(equation))

# **----- Clear -----**
elif button.text == 'C':
txt_display.text = ''

# **----- Backspace -----**
elif button.text == '<-':
while True:  # Loop to delete spaces as well as object without ruining format
try:  # In case of backspace with nothing to delete
txt_display.text = (str(txt_display.text)[0:-1])
if str(txt_display.text)[-1] == ' ':
continue
else:
break
except IndexError:
break

# **----- Normal Buttons -----**
else:
txt_display.insert_text(button.text)

def process(self, equation):
"""Solves mathematical equations

Keyword argument:
equation -- Equation to solve."""

# Check for errors
temp_equation = self.format_equation(equation)
if type(temp_equation) == str:  # Errors return as strings, no errors returns lists
self.warning_label.text = temp_equation
concat_str = ''
for obj in equation:
concat_str += str(obj)
return concat_str
else:
equation = temp_equation

# Loop until there's a result
while len(equation) > 1:

# Are parentheses, multiplication or division, exponents or square roots in equation?
parens, has_md, expo, sqrt_active = False, False, False, False

# See what is in equation
for item in equation:
if "(" in str(item):
parens = True
elif "^" in str(item):
expo = True
elif "*" in str(item) or "/" in str(item):
has_md = True

# Variables for parentheses
index = 0
# In parentheses? Equation in parentheses. Parentheses location. Indices of items in parentheses.
par_active, paren_equation, paren_index, paren_indices = False, [], 0, []

# Solve equation
for active in equation:
active = str(active)

# **----- Parentheses -----**
if parens is True:

if "(" in active and par_active is False and sqrt_active is False:  # Trigger parentheses
if active == "sqrt(":
sqrt_active = True
elif active == "(":
par_active = True
paren_indices.append(index)
paren_index = index

elif ")" in active:
# If active is last object or there's not another end parenthesis
if index == len(equation) - 1 or equation[index + 1] != ")":
paren_indices.append(index)
del equation[paren_indices:(paren_indices[-1] + 1)]

if par_active is True:
equation.insert(paren_index, self.process(paren_equation))

elif sqrt_active is True:
equation.insert(paren_index, numpy.sqrt(int(self.process(paren_equation))))

del paren_equation[:]
index -= 1
break
else:
paren_equation.append(active)  # Add it to the parentheses' equation
paren_indices.append(index)

elif par_active is True or sqrt_active is True:  # If we are working within parentheses
paren_equation.append(active)  # Add it to the parentheses equation
paren_indices.append(index)
# continue

# **----- Exponents ------**
elif expo is True:
if active == "^":
if len(equation) <= index + 2 and equation[index + 2] == "^":
continue
else:
processed = float(equation[index - 1]) ** float(equation[index + 1])
equation[index] = processed
equation.remove(equation[index - 1])
equation.remove(equation[index])

# **----- Multiplication and Division -----**
elif has_md is True:
if active == "*":
processed = float(equation[index - 1]) * float(equation[index + 1])
equation[index] = processed  # Replace operator with product
equation.remove(equation[index - 1])  # Removes previous number, indices decrement
equation.remove(equation[index])  # Removes number after operator
elif active == "/":
if float(equation[index+1]) == 0.0:
error = "Error: Can't divide by zero"
self.warning_label.text = error
concat_str = ''
for obj in equation:
concat_str += str(obj)
return concat_str
else:
processed = float(equation[index - 1]) / float(equation[index + 1])
equation[index] = processed
equation.remove(equation[index - 1])
equation.remove(equation[index])

# **----- Addition and Subtraction -----**
else:
if active == "+":  # PEMDAS
processed = float(equation[index - 1]) + float(equation[index + 1])
equation[index] = processed
equation.remove(equation[index - 1])
equation.remove(equation[index])
elif active == "-":
processed = float(equation[index - 1]) - float(equation[index + 1])
equation[index] = processed
equation.remove(equation[index - 1])
equation.remove(equation[index])

index += 1

# If it's an integer, return it as one
try:
result = equation
if result == int(result):
result = int(result)
return result
except ValueError:
return equation

def format_equation(self, equation):
"""Turns equation into something easily processed by combining numbers into single list objects

Keyword argument:
equation -- The equation to format"""
index = 0
while index < len(equation):

temp_num = ""
# If a number, decimal or negative
if equation[index].isdigit() or equation[index] == "." or equation[index] == "-":
# And if the preceding is not an operator
if index != 0 and equation[index - 1] not in self.operations:
index += 1  # Move on
continue

if index < len(equation):
temp_num += equation.pop(index)
temp_ind = copy.copy(index)
while True:

try:
next_char = equation[temp_ind]

# Add number or decimal to building number
if next_char.isdigit() or next_char == "." and "." not in temp_num:
temp_num += next_char  # Pop
del equation[temp_ind]

# Number is finished if next character is not digit or decimal
else:
equation.insert(index, temp_num)
temp_num = ""
break

except IndexError:  # When loop hits end of equation, just add number and return
equation.insert(index, temp_num)
break

elif equation[index] == "s":
i = 1
sqrt_list = ['s', 'q', 'r', 't', '(']
while i <= 4:
if equation[index + i] == sqrt_list[i]:
equation[index] += equation[index + i]
i += 1
del equation[index + 1:index + 5]

index += 1

# Allows 22(7)
i = 0
while i < len(equation) - 1:
if equation[i].isdigit() and equation[i + 1] == "(":
equation.insert(i + 1, "*")
i += 1

equation = self.check_equation(equation)
return equation

def check_equation(self, equation):
"""Checks equation for potential syntactical errors

Keyword argument:
equation -- The equation to check"""
index = 0  # Iterating var
open_par = 0  # For counting parentheses
close_par = 0

while index < len(equation):
obj = equation[index]

# Check if string is number
is_num = False
try:
obj = float(obj)
is_num = True
except ValueError:
pass

# Count parentheses
if obj == '(':
open_par += 1
elif obj == ')':
close_par += 1

# **----- Proper Operator Use -----**
# Is number after operator?
elif obj in self.operations and index == len(equation) - 1:
error = ("Error: %s is not succeeded by a number." % obj)
return error
# Is number before operator?
elif obj in self.operations and obj != "-":
if index == 0:
error = "Error: %s can not be first item in equation." % obj
return error
elif is_num:
print(equation[index - 1].isdecimal())
error = "Error: %s must be preceded by a number." % obj
return error

# **----- Proper Decimal Use -----**
elif obj == ".":
error = "Error: Improper decimal use."
return error
index += 1

# Check if parentheses match up
if open_par != close_par:
if open_par > close_par:
error = ("Error: Missing %s ')'s" % (open_par - close_par))
return error
elif open_par < close_par:
error = ("Error: Missing %s '('s" % (close_par - open_par))
return error

return equation

Calculator().run()


My goal is to build my github portfolio over time, so please point out any and all errors as I want to improve. I'm slowly learning PEP standards so please point out any violations you see.

• Exponent and Square Root are supported but don't have buttons yet, plan to implement in the next version – Goat_Reaper Dec 5 '17 at 5:59