4
\$\begingroup\$

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))
            layout.add_widget(button)
            button.bind(on_press=self.press)

        box = BoxLayout(orientation='vertical')  # Set layout orientation to add widgets below the previously added
        box.add_widget(self.txt_in)
        box.add_widget(self.warning_label)
        box.add_widget(layout)

        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[0]:(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[0]
            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

                # Add to building number
                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.

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

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.