From fde502c866e9e6065a60d18611f7c87fee4eea5f Mon Sep 17 00:00:00 2001 From: markon Date: Thu, 4 Dec 2025 19:08:33 +0200 Subject: [PATCH 1/5] commit 1 --- calculator.py | 93 +++++++++++++++++++++------------------------------ 1 file changed, 38 insertions(+), 55 deletions(-) diff --git a/calculator.py b/calculator.py index 9b94170..97132b4 100644 --- a/calculator.py +++ b/calculator.py @@ -6,9 +6,10 @@ #This stament allows us to use any mathmatical functions in our code to calculate results. import math #The Thread statement is a part of the threading module.This provides a convenient way to create and manage threads in our program. -from threading import Thread +# from threading import Thread #This is used to present decimal numbers as this is important in certain calculations. from decimal import Decimal +import re #This code uses Tkinter to define a Calculator class for a basic calculator app. The background colour and title of the main window are set in the constructor (__init__ method). #The Tkinter window reference is kept in the self.root variable, and light blue is set as the background colour. @@ -114,22 +115,16 @@ def change_bg_color(self, color): self.logo_label.configure(bg=color) # Retrieves current content in the data field and stores it in the varriable 'current' - # Checks wether the current content of the data field contains a valid numeric value - # Either current is a positive integer or a negative integer def insert_number(self, number): current = self.entry.get() - if current and (current[0] == '-' and current[1:].isdigit() or current.isdigit()): + if current == "Error": # if there is an error, clear before typing self.entry.delete(0, tk.END) - self.entry.insert(0, str(current) + str(number)) - else: - self.entry.insert(tk.END, str(number)) - # The insert_decimal method is designed to triggered the tkinker decimal point function. - #This code adds a decimal point to the end of the entry field's content if certain conditions are met. + self.entry.insert(tk.END, str(number)) # just insert the new digit at the end + + # The insert_decimal method inserts a decimal point at the end of the current input. def insert_decimal(self): - current = self.entry.get() - if current and '.' not in current: - self.entry.insert(tk.END, '.') + self.entry.insert(tk.END, '.') #the binary_to_decimal method is designed to convert a binary number into its decimal equivalent. #It handles both successful conversions and cases where the input is not a valid binary number. @@ -148,7 +143,7 @@ def decimal_to_binary(self): decimal_input = self.entry.get() try: decimal_input = int(decimal_input) - binary_output = bin(decimal_input) + binary_output = bin(decimal_input)[2:] self.entry.delete(0, tk.END) self.entry.insert(0, str(binary_output)) except ValueError: @@ -177,65 +172,50 @@ def power_of(self): - #This adds the percentage function + # This adds the percentage operator to the current expression def percentage(self): - try: - value = float(self.entry.get()) - result = value / 100 #Calculate the percentage of the obtained value by dividing it by 100. - self.entry.delete(0, tk.END) - self.entry.insert(0, str(result)) - except ValueError: - self.entry.delete(0, tk.END) - self.entry.insert(0, "Error")#Clear the entry field and insert the string "Error" to indicate that the operation couldn't be performed due to invalid input. + text = self.entry.get() + if text == "" or text == "Error": + return + self.entry.insert(tk.END, '%') - #This adds the multiplication function + + # This adds the multiplication operator to the current expression def multiplication(self): - try: - value = float(self.entry.get()) - self.entry.delete(0, tk.END) - self.entry.insert(0, str(value) + '*') - except ValueError: - self.entry.delete(0, tk.END) - self.entry.insert(0, "Error")#Clear the entry field and insert the string "Error" to indicate that the operation couldn't be performed due to invalid input. + text = self.entry.get() + if text == "" or text == "Error": + return + self.entry.insert(tk.END, '*') - #This adds the additin function + # This adds the addition operator to the current expression def addition(self): - try: - value = float(self.entry.get()) # This Retrieve the value from the Tkinter entry widget and convert it to a floating-point number. - self.entry.delete(0, tk.END) - self.entry.insert(0, str(value) + '+') - except ValueError: - self.entry.delete(0, tk.END) - self.entry.insert(0, "Error")#Clear the entry field and insert the string "Error" to indicate that the operation couldn't be performed due to invalid input. + text = self.entry.get() + if text == "" or text == "Error": + return + self.entry.insert(tk.END, '+') - #This adds the subtraction function + # This adds the subtraction operator to the current expression def subtraction(self): - try: - value = float(self.entry.get())#Retrieve the value from the Tkinter entry widget and convert it to a floating-point number. - self.entry.delete(0, tk.END) - self.entry.insert(0, str(value) + '-') - except ValueError: - self.entry.delete(0, tk.END) - self.entry.insert(0, "Error")#Clear the entry field and insert the string "Error" to indicate that the operation couldn't be performed due to invalid input. + text = self.entry.get() + self.entry.insert(tk.END, '-') # allow leading negative number - #This adds the division function + # This adds the division operator to the current expression def division(self): - try: - value = float(self.entry.get())#This Retrieve the value from the Tkinter entry widget and convert it to a floating-point number. - self.entry.delete(0, tk.END) - self.entry.insert(0, str(value) + '/') - except ValueError: - self.entry.delete(0, tk.END) - self.entry.insert(0, "Error")#Clear the entry field and insert the string "Error" to indicate that the operation couldn't be performed due to invalid input. + text = self.entry.get() + if text == "" or text == "Error": + return + self.entry.insert(tk.END, '/') #retrieves the current content of the entry field associated with the class instance and stores it in the variable expression. def evaluate_expression(self): expression = self.entry.get() try: + expression = re.sub(r'(\d+(\.\d+)?)%', r'(\1/100)', expression) # percentage conversion + result = eval(expression) self.entry.delete(0, tk.END) self.entry.insert(0, str(result)) - except (ValueError, SyntaxError): + except (ValueError, SyntaxError, ZeroDivisionError, NameError, TypeError): self.entry.delete(0, tk.END) self.entry.insert(0, "Error")#Clear the entry field and insert the string "Error" to indicate that the operation couldn't be performed due to invalid input. @@ -259,6 +239,8 @@ def calculate_factorial(self): self.entry.insert(0, str(e)) #Clear the entry field and insert the error message as a string to indicate that the operation couldn't be performed due to invalid input. + + """ def calculate_factorial_threaded(self, value): result = math.factorial(value)#uses the math function to calculate the factorial of the given value self.root.after(0, lambda: self.update_gui_with_factorial(result))#prevents potential delays in responsiveness. @@ -266,6 +248,7 @@ def calculate_factorial_threaded(self, value): def update_gui_with_factorial(self, result): self.entry.delete(0, tk.END) self.entry.insert(0, str(result)) + """ # unused and unnecessary: no real threading is implemented #This runs the calculator def run_calculator():#Create the main Tkinter window From 3ce9cf7ca015eaebec2fa997ec2440631d691437 Mon Sep 17 00:00:00 2001 From: markon Date: Sat, 6 Dec 2025 18:07:04 +0200 Subject: [PATCH 2/5] commit 2 --- calculator.py | 234 +++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 193 insertions(+), 41 deletions(-) diff --git a/calculator.py b/calculator.py index 97132b4..6dd400c 100644 --- a/calculator.py +++ b/calculator.py @@ -6,7 +6,7 @@ #This stament allows us to use any mathmatical functions in our code to calculate results. import math #The Thread statement is a part of the threading module.This provides a convenient way to create and manage threads in our program. -# from threading import Thread +from threading import Thread #This is used to present decimal numbers as this is important in certain calculations. from decimal import Decimal import re @@ -33,31 +33,38 @@ def __init__(self, root): # Entry field for displaying and inputting numbers self.entry = tk.Entry(root, font=custom_font) - self.entry.grid(row=1, column=0, rowspan=2,columnspan=5, padx=10, pady=10, sticky="nsew") # Add sticky option + self.entry.grid(row=1, column=0, rowspan=2,columnspan=3, padx=10, pady=10, sticky="nsew") # Add sticky option + # this makes entry key trigger expression evaluation + self.entry.bind("", lambda event: self.evaluate_expression()) # These are just Basic operation buttons operations = [ - ('Square Root', self.square_root), - ('Power Of', self.power_of), + ('√', self.square_root), + ('^', self.power_of), ('%', self.percentage), ('*', self.multiplication), ('+', self.addition), ('-', self.subtraction), - ('/', self.division) + ('/', self.division), + ('round', self.round), + ('abs', self.abs), + ('sin', self.sin), + ('cos', self.cos), + ('tan', self.tan) ] #Here we created the operation buttons #for loop which iterates over each item in the operations list for i, (text, command) in enumerate(operations): operation_button = tk.Button(root, text=text, padx=40, pady=20, bg='light yellow', bd=0, command=command, width=2) - operation_button.grid(row=i // 2 + 3, column=i % 2 + 3, padx=5, pady=5) + operation_button.grid(row=i // 3 + 3, column=i % 3 + 3, padx=5, pady=5) # Number buttons # Uses a lambda function to capture the current value of number and passes it to the self.insert_number method. buttons = [] for number in range(10): - button = tk.Button(root, text=str(number), padx=40, pady=20, bg='light yellow', bd=0, command=lambda num=number: self.insert_number(num), width=2) + button = tk.Button(root, text=str(number), padx=40, pady=20, bg='white', bd=0, command=lambda num=number: self.insert_number(num), width=2) buttons.append(button) # Place number buttons (0 to 9) @@ -68,28 +75,66 @@ def __init__(self, root): for pos, button in zip(positions, buttons[:13]): button.grid(row=pos[0], column=pos[1], padx=5, pady=5) + # create a frame to hold both log and ln buttons + log_frame = tk.Frame(root, bg=self.bg_color, bd=0) + log_frame.grid(row=2, column=5, padx=5, pady=5, sticky="nsew") + log10_button = tk.Button(log_frame, text="log₁₀", padx=20, pady=5, bg='light yellow', command=self.log_10, width=4) + log10_button.pack(side="top", fill="both", expand=True) + separator = tk.Frame(log_frame, bg="gray70", height=1) + separator.pack(fill="x") + ln_button = tk.Button(log_frame, text="ln", padx=20, pady=5, bg='light yellow', command=self.ln, width=4) + ln_button.pack(side="bottom", fill="both", expand=True) + # Add Clear button clear_button = tk.Button(root, text="Clear", padx=40, pady=20, bg='#E8C1C5', command=self.clear, width=2) - clear_button.grid(row=7, column=1, columnspan=1, padx=5, pady=5) # Set row and column for clear button + clear_button.grid(row=3, column=7, columnspan=1, padx=5, pady=5) # Set row and column for clear button + + # Add backspace button + backspace_button = tk.Button(root, text="⌫", padx=40, pady=20, bg='#E8C1C5', command=self.backspace, width=2) + backspace_button.grid(row=2, column=7, columnspan=1, padx=5, pady=5) # Set row and column for backspace button + + # create a frame to hold both ( and ) buttons + paren_frame = tk.Frame(root, bg=self.bg_color, bd=0) + paren_frame.grid(row=6, column=2, padx=5, pady=5, sticky="nsew") + # Add open parenthesis button + open_paren_button = tk.Button(paren_frame, text="(", padx=20, pady=5, bg='light yellow', command=self.insert_open_paren, width=2) + open_paren_button.pack(side="top", fill="both", expand=True) + separator = tk.Frame(paren_frame, bg="gray70", height=1) + separator.pack(fill="x") + # Add close parenthesis button + close_paren_button = tk.Button(paren_frame, text=")", padx=20, pady=5, bg='light yellow', command=self.insert_close_paren, width=2) + close_paren_button.pack(side="bottom", fill="both", expand=True) + + # create a frame to hold both e and π buttons + number_frame = tk.Frame(root, bg=self.bg_color, bd=0) + number_frame.grid(row=6, column=0, padx=5, pady=5, sticky="nsew") + # Add euler's number button + e_button = tk.Button(number_frame, text="e", padx=20, pady=5, bg='light yellow', command=self.e, width=2) + e_button.pack(side="top", fill="both", expand=True) + separator = tk.Frame(number_frame, bg="gray70", height=1) + separator.pack(fill="x") + # Add pi number button + pi_button = tk.Button(number_frame, text="π", padx=20, pady=5, bg='light yellow', command=self.pi, width=2) + pi_button.pack(side="bottom", fill="both", expand=True) # Decimal point button decimal_button = tk.Button(root, text=".", padx=40, pady=20, bg='light yellow', bd=0, command=lambda: self.insert_decimal()) - decimal_button.grid(row=7, column=2) + decimal_button.grid(row=2, column=3) # Equals button equals_button = tk.Button(root, text="=", padx=40, pady=20, bg='light yellow', bd=0, command=self.evaluate_expression) - equals_button.grid(row=7, column=3, padx=5, pady=5) + equals_button.grid(row=6, column=7, padx=5, pady=5) # Conversion buttons convert_to_binary_btn = tk.Button(root, text="To Binary", padx=40, pady=20, bg='light yellow', command=self.decimal_to_binary, width=2) - convert_to_binary_btn.grid(row=6, column=0, padx=5, pady=5) + convert_to_binary_btn.grid(row=4, column=7, padx=5, pady=5) convert_to_decimal_btn = tk.Button(root, text="To Decimal", padx=40, pady=20, bg='light yellow', command=self.binary_to_decimal, width=2) - convert_to_decimal_btn.grid(row=6, column=2, padx=5, pady=5) + convert_to_decimal_btn.grid(row=5, column=7, padx=5, pady=5) # Factorial button - factorial_button = tk.Button(root, text="Factorial", padx=40, pady=20, bg='light yellow', command=self.calculate_factorial, width=2) - factorial_button.grid(row=6, column=4, padx=5, pady=5) + factorial_button = tk.Button(root, text="!", padx=40, pady=20, bg='light yellow', command=self.factorial, width=2) + factorial_button.grid(row=2, column=4, padx=5, pady=5) # Adding a button that allows the user to change the background color color_btn1 = tk.Button(root, text="", padx=10, pady=5, bg='lightblue', command=lambda: self.change_bg_color('lightblue')) @@ -122,6 +167,18 @@ def insert_number(self, number): self.entry.insert(tk.END, str(number)) # just insert the new digit at the end + # functions inserting open / close parenthesis + def insert_close_paren(self): + text = self.entry.get() + if text == "" or text == "Error": + return + self.entry.insert(tk.END, ')') + def insert_open_paren(self): + text = self.entry.get() + if text == "Error": + return + self.entry.insert(tk.END, '(') + # The insert_decimal method inserts a decimal point at the end of the current input. def insert_decimal(self): self.entry.insert(tk.END, '.') @@ -150,27 +207,54 @@ def decimal_to_binary(self): self.entry.delete(0, tk.END) self.entry.insert(0, "Error") - # This adds the square foot function - def square_root(self): - try: - value = float(self.entry.get()) - result = math.sqrt(value) - self.entry.delete(0, tk.END) - self.entry.insert(0, str(result)) - except ValueError: - self.entry.delete(0, tk.END) - self.entry.insert(0, "Error")#Clear the entry field and insert the string "Error" to indicate that the operation couldn't be performed due to invalid input. + # This adds the e number + def e(self): + text = self.entry.get() + if text == "Error": + return + self.entry.insert(tk.END, 'e') + # This adds the pi number + def pi(self): + text = self.entry.get() + if text == "Error": + return + self.entry.insert(tk.END, 'π') + + # This adds the log operator + def log_10(self): + text = self.entry.get() + if text == "Error": + return + self.entry.insert(tk.END, 'log(') + + # This adds the ln operator + def ln(self): + text = self.entry.get() + if text == "Error": + return + self.entry.insert(tk.END, 'ln(') + + # This adds the square root operator + def square_root(self): + text = self.entry.get() + if text == "Error": + return + self.entry.insert(tk.END, 'sqrt(') - #This adds the power of function + #This adds the power operator def power_of(self): - try: - self.entry.insert(tk.END, '**') # Insert '**' into the entry box - except ValueError: - self.entry.delete(0, tk.END) - self.entry.insert(0, "Error")#Clear the entry field and insert the string "Error" to indicate that the operation couldn't be performed due to invalid input. - + text = self.entry.get() + if text == "" or text == "Error": + return + self.entry.insert(tk.END, '^') + # this adds the factorial operator to the current expression + def factorial(self): + text = self.entry.get() + if text == "" or text == "Error": + return + self.entry.insert(tk.END, '!') # This adds the percentage operator to the current expression def percentage(self): @@ -196,7 +280,6 @@ def addition(self): # This adds the subtraction operator to the current expression def subtraction(self): - text = self.entry.get() self.entry.insert(tk.END, '-') # allow leading negative number # This adds the division operator to the current expression @@ -206,13 +289,70 @@ def division(self): return self.entry.insert(tk.END, '/') + # This adds the round operator to the current expression + def round(self): + text = self.entry.get() + if text == "Error": + return + self.entry.insert(tk.END, 'round(') + + # This adds the abs operator to the current expression + def abs(self): + text = self.entry.get() + if text == "Error": + return + self.entry.insert(tk.END, 'abs(') + + # This adds the sin operator to the current expression + def sin(self): + text = self.entry.get() + if text == "Error": + return + self.entry.insert(tk.END, 'sin(') + + # This adds the cos operator to the current expression + def cos(self): + text = self.entry.get() + if text == "Error": + return + self.entry.insert(tk.END, 'cos(') + + # This adds the tan operator to the current expression + def tan(self): + text = self.entry.get() + if text == "Error": + return + self.entry.insert(tk.END, 'tan(') + #retrieves the current content of the entry field associated with the class instance and stores it in the variable expression. def evaluate_expression(self): expression = self.entry.get() try: expression = re.sub(r'(\d+(\.\d+)?)%', r'(\1/100)', expression) # percentage conversion + expression = re.sub(r'(\d+)!', r'self.calculate_factorial(\1)', expression) # factorial conversion + expression = re.sub(r'\bsqrt\b', r'math.sqrt', expression) # square root conversion + expression = re.sub(r'\^', r'**', expression) # power conversion + expression = re.sub(r'\blog\b', r'math.log10', expression) # log conversion + expression = re.sub(r'\bln\b', r'math.log', expression) # ln conversion + expression = re.sub(r'π', r'math.pi', expression) # pi conversion + expression = re.sub(r'\be\b', r'math.e', expression) # e conversion + expression = re.sub(r'\bsin\b', r'math.sin(math.radians(', expression) # sin conversion (rounded on 10th digit) + expression = re.sub(r'\bcos\b', r'math.cos(math.radians(', expression) # cos conversion + expression = re.sub(r'\btan\b', r'math.tan(math.radians(', expression) # tan conversion + expression = re.sub(r'\babs\b', r'abs', expression) # abs conversion + expression = re.sub(r'\bround\b', r'round', expression) # round conversion + + # auto fix unmatched parentheses + if expression.count("(") > expression.count(")"): + expression += ")" * (expression.count("(") - expression.count(")")) + elif expression.count("(") < expression.count(")"): + expression = "(" * (expression.count(")") - expression.count("(")) + expression result = eval(expression) + # fix floating point issues for trig functions + if "math.sin" in expression or "math.cos" in expression or "math.tan" in expression: + result = round(result, 10) + self.entry.delete(0, tk.END) self.entry.insert(0, str(result)) except (ValueError, SyntaxError, ZeroDivisionError, NameError, TypeError): @@ -223,23 +363,35 @@ def evaluate_expression(self): def clear(self): self.entry.delete(0, tk.END) - def calculate_factorial(self): - value = self.entry.get() + # This creates the backspace function + def backspace(self): + current = self.entry.get() + if current and current != "Error": + self.entry.delete(len(current) - 1, tk.END) + + # this calculates the factorial for expression evaluation + def calculate_factorial(self, value): try: value = int(value)#Convert the input value to an integer. if value < 0: raise ValueError("Factorial is defined only for non-negative integers.")#Check if the converted value is a non-negative integer. If not, raise a ValueError with an appropriate error message. - result = Decimal(1) - for i in range(2, value + 1): - result *= Decimal(i) - self.entry.delete(0, tk.END) - self.entry.insert(0, str(result)) + + if value < 100000: + result = Decimal(1) + for i in range(2, value + 1): + result *= Decimal(i) + return result + else: + # for very large n: show as e^(ln(n!)) using log-gamma + # this is an approximation of the magnitude of n! + lnn_fact = math.lgamma(value + 1) + return f"e^{lnn_fact:.6f}" + except ValueError as e: self.entry.delete(0, tk.END) self.entry.insert(0, str(e)) #Clear the entry field and insert the error message as a string to indicate that the operation couldn't be performed due to invalid input. - """ def calculate_factorial_threaded(self, value): result = math.factorial(value)#uses the math function to calculate the factorial of the given value @@ -248,7 +400,7 @@ def calculate_factorial_threaded(self, value): def update_gui_with_factorial(self, result): self.entry.delete(0, tk.END) self.entry.insert(0, str(result)) - """ # unused and unnecessary: no real threading is implemented + """ # unused and unnecessary #This runs the calculator def run_calculator():#Create the main Tkinter window From c9086f3659f289de1952716ae3afe5f69a3ed83b Mon Sep 17 00:00:00 2001 From: markon Date: Wed, 10 Dec 2025 14:19:34 +0200 Subject: [PATCH 3/5] commit 3 --- README.md | 41 ++++++++++++++++++++++++++++------------- calculator.py | 27 +++++++++++++++------------ 2 files changed, 43 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index f5a76ce..0596e6c 100644 --- a/README.md +++ b/README.md @@ -1,50 +1,65 @@ # HEM Calculator A **Graphical User Interface (GUI)** calculator application built with Python's `tkinter` library. -This calculator supports both basic arithmetic operations and advanced functions like square root, power, percentage, and binary/decimal conversions. -This was for a group coursework academic and educational project. +Originally developed for an academic group coursework project. +This upgraded version supports a wide range of basic and advanced mathematical functions, +error handling, and additional tools such as binary / decimal conversion. -## Tech Stack +## 🚀 Tech Stack ![Build Status](https://img.shields.io/badge/build-passing-brightgreen) ![License](https://img.shields.io/badge/license-MIT-blue) ![Python](https://img.shields.io/badge/Python-3.9+-blue) ![Tkinter](https://img.shields.io/badge/Tkinter-8.6+-blue) -## Features +## ✨ Features -- **Basic Operations:** +### Basic Operations: - Addition (`+`) - Subtraction (`-`) - Multiplication (`*`) - Division (`/`) + - Decimal support -- **Advanced Functions:** - - Square Root (`√`) +### Advanced Functions: + - Square Root (`sqrt()`) - Exponentiation (`x^y`) - Percentage (`%`) - - Binary to Decimal & Decimal to Binary Conversion + - Factorial (`!`) + - Trigonometric functions (degrees): + - sin(x) (`sin()`) + - cos(x) (`cos()`) + - tan(x) (`tan()`) + - Logarithmic functions: + - log₁₀(x) (`log()`) + - ln(x) (`ln()`) + - Absolute value (`abs()`) + - Rounding (`round()`) -## Installation +### Conversion tools +- Binary → Decimal +- Decimal → Binary + +## 📥 Installation 1. Clone the repository: ```bash git clone https://github.com/morganmdx/randcoursework.git ``` -## Usage +## 🖥️ Usage Once you run the program, a GUI window will appear. You can: Perform basic arithmetic operations by clicking the respective buttons. Use the advanced features (square root, exponentiation, percentage, and conversions) by clicking on the respective buttons. -## Dependencies +## 📦 Dependencies - Python 3.x - tkinter (standard Python library for GUI development) -## Contributing +## 🤝 Contributing Contributions are welcome! If you have suggestions or improvements, feel free to open an issue or submit a pull request. -## License +## 📜 License This project is licensed under the MIT License. License subject to change. See the LICENSE file for details. diff --git a/calculator.py b/calculator.py index 6dd400c..d82f358 100644 --- a/calculator.py +++ b/calculator.py @@ -176,7 +176,7 @@ def insert_close_paren(self): def insert_open_paren(self): text = self.entry.get() if text == "Error": - return + self.entry.delete(0, tk.END) self.entry.insert(tk.END, '(') # The insert_decimal method inserts a decimal point at the end of the current input. @@ -211,35 +211,35 @@ def decimal_to_binary(self): def e(self): text = self.entry.get() if text == "Error": - return + self.entry.delete(0, tk.END) self.entry.insert(tk.END, 'e') # This adds the pi number def pi(self): text = self.entry.get() if text == "Error": - return + self.entry.delete(0, tk.END) self.entry.insert(tk.END, 'π') # This adds the log operator def log_10(self): text = self.entry.get() if text == "Error": - return + self.entry.delete(0, tk.END) self.entry.insert(tk.END, 'log(') # This adds the ln operator def ln(self): text = self.entry.get() if text == "Error": - return + self.entry.delete(0, tk.END) self.entry.insert(tk.END, 'ln(') # This adds the square root operator def square_root(self): text = self.entry.get() if text == "Error": - return + self.entry.delete(0, tk.END) self.entry.insert(tk.END, 'sqrt(') #This adds the power operator @@ -275,11 +275,14 @@ def multiplication(self): def addition(self): text = self.entry.get() if text == "" or text == "Error": - return + self.entry.delete(0, tk.END) self.entry.insert(tk.END, '+') # This adds the subtraction operator to the current expression def subtraction(self): + text = self.entry.get() + if text == "Error": + self.entry.delete(0, tk.END) self.entry.insert(tk.END, '-') # allow leading negative number # This adds the division operator to the current expression @@ -293,35 +296,35 @@ def division(self): def round(self): text = self.entry.get() if text == "Error": - return + self.entry.delete(0, tk.END) self.entry.insert(tk.END, 'round(') # This adds the abs operator to the current expression def abs(self): text = self.entry.get() if text == "Error": - return + self.entry.delete(0, tk.END) self.entry.insert(tk.END, 'abs(') # This adds the sin operator to the current expression def sin(self): text = self.entry.get() if text == "Error": - return + self.entry.delete(0, tk.END) self.entry.insert(tk.END, 'sin(') # This adds the cos operator to the current expression def cos(self): text = self.entry.get() if text == "Error": - return + self.entry.delete(0, tk.END) self.entry.insert(tk.END, 'cos(') # This adds the tan operator to the current expression def tan(self): text = self.entry.get() if text == "Error": - return + self.entry.delete(0, tk.END) self.entry.insert(tk.END, 'tan(') #retrieves the current content of the entry field associated with the class instance and stores it in the variable expression. From 27e30440eea8d5834bb33f557847eb60edff16e6 Mon Sep 17 00:00:00 2001 From: markon Date: Wed, 10 Dec 2025 14:24:58 +0200 Subject: [PATCH 4/5] upgrade: improve calculator features, fix minor issues, update README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 0596e6c..31e068b 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ error handling, and additional tools such as binary / decimal conversion. - ln(x) (`ln()`) - Absolute value (`abs()`) - Rounding (`round()`) + - Constants π, e ### Conversion tools - Binary → Decimal From 2858235f22f199825bbeeec6bcb648903637dbbb Mon Sep 17 00:00:00 2001 From: markon Date: Tue, 16 Dec 2025 17:21:22 +0200 Subject: [PATCH 5/5] added test cases, fixed remaining bugs --- calculator.py | 60 ++++++++++++++++++++----------------- test_calculator.py | 74 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+), 27 deletions(-) create mode 100644 test_calculator.py diff --git a/calculator.py b/calculator.py index d82f358..b734133 100644 --- a/calculator.py +++ b/calculator.py @@ -331,31 +331,8 @@ def tan(self): def evaluate_expression(self): expression = self.entry.get() try: - expression = re.sub(r'(\d+(\.\d+)?)%', r'(\1/100)', expression) # percentage conversion - expression = re.sub(r'(\d+)!', r'self.calculate_factorial(\1)', expression) # factorial conversion - expression = re.sub(r'\bsqrt\b', r'math.sqrt', expression) # square root conversion - expression = re.sub(r'\^', r'**', expression) # power conversion - expression = re.sub(r'\blog\b', r'math.log10', expression) # log conversion - expression = re.sub(r'\bln\b', r'math.log', expression) # ln conversion - expression = re.sub(r'π', r'math.pi', expression) # pi conversion - expression = re.sub(r'\be\b', r'math.e', expression) # e conversion - expression = re.sub(r'\bsin\b', r'math.sin(math.radians(', expression) # sin conversion (rounded on 10th digit) - expression = re.sub(r'\bcos\b', r'math.cos(math.radians(', expression) # cos conversion - expression = re.sub(r'\btan\b', r'math.tan(math.radians(', expression) # tan conversion - expression = re.sub(r'\babs\b', r'abs', expression) # abs conversion - expression = re.sub(r'\bround\b', r'round', expression) # round conversion - - # auto fix unmatched parentheses - if expression.count("(") > expression.count(")"): - expression += ")" * (expression.count("(") - expression.count(")")) - elif expression.count("(") < expression.count(")"): - expression = "(" * (expression.count(")") - expression.count("(")) + expression - - result = eval(expression) - # fix floating point issues for trig functions - if "math.sin" in expression or "math.cos" in expression or "math.tan" in expression: - result = round(result, 10) - + # call core logic + result = self.parse_and_calculate(expression) self.entry.delete(0, tk.END) self.entry.insert(0, str(result)) except (ValueError, SyntaxError, ZeroDivisionError, NameError, TypeError): @@ -380,9 +357,9 @@ def calculate_factorial(self, value): raise ValueError("Factorial is defined only for non-negative integers.")#Check if the converted value is a non-negative integer. If not, raise a ValueError with an appropriate error message. if value < 100000: - result = Decimal(1) + result = 1 for i in range(2, value + 1): - result *= Decimal(i) + result *= i return result else: # for very large n: show as e^(ln(n!)) using log-gamma @@ -405,6 +382,35 @@ def update_gui_with_factorial(self, result): self.entry.insert(0, str(result)) """ # unused and unnecessary + # separates the calculation logic from the UI + def parse_and_calculate (self, expression): + + # auto fix unmatched parentheses + if expression.count("(") > expression.count(")"): + expression += ")" * (expression.count("(") - expression.count(")")) + elif expression.count("(") < expression.count(")"): + expression = "(" * (expression.count(")") - expression.count("(")) + expression + + expression = re.sub(r'(\d+(\.\d+)?)%', r'(\1/100)', expression) # percentage conversion + expression = re.sub(r'(\d+)!', r'self.calculate_factorial(\1)', expression) # factorial conversion + expression = re.sub(r'\bsqrt\b', r'math.sqrt', expression) # square root conversion + expression = re.sub(r'\^', r'**', expression) # power conversion + expression = re.sub(r'\blog\b', r'math.log10', expression) # log conversion + expression = re.sub(r'\bln\b', r'math.log', expression) # ln conversion + expression = re.sub(r'π', r'math.pi', expression) # pi conversion + expression = re.sub(r'\be\b', r'math.e', expression) # e conversion + expression = re.sub(r'\bsin\(([^)]+)\)', r'math.sin(math.radians(\1))', expression) # sin conversion + expression = re.sub(r'\bcos\(([^)]+)\)', r'math.cos(math.radians(\1))', expression) # cos conversion (radians) + expression = re.sub(r'\btan\(([^)]+)\)', r'math.tan(math.radians(\1))', expression) # tan conversion + expression = re.sub(r'\babs\b', r'abs', expression) # abs conversion + expression = re.sub(r'\bround\b', r'round', expression) # round conversion + + result = eval(expression) + # fix floating point issues for trig functions + if "math.sin" in expression or "math.cos" in expression or "math.tan" in expression: + result = round(result, 10) + return result + #This runs the calculator def run_calculator():#Create the main Tkinter window root = tk.Tk() diff --git a/test_calculator.py b/test_calculator.py new file mode 100644 index 0000000..95d8335 --- /dev/null +++ b/test_calculator.py @@ -0,0 +1,74 @@ +import unittest +import tkinter as tk +import math +from calculator import Calculator + +class TestComplexExpressions(unittest.TestCase): + + # this runs before each test creating a hidden tkinter window so the calculator class can initialize + def setUp(self): + self.root = tk.Tk() + self.root.withdraw() + self.calc = Calculator(self.root) + + # this runs after each test destroying the window to clean up memory + def tearDown(self): + self.root.destroy() + + # order of operations check + def test_order_of_operations(self): + result = self.calc.parse_and_calculate("10+2*5") + self.assertEqual(result, 20) + + # power operator conversion check + def test_power_conversion(self): + result = self.calc.parse_and_calculate("2^3+4^2") + self.assertEqual(result, 24) + + # nested functions check + def test_nested_functions(self): + # simple nesting + result = self.calc.parse_and_calculate("sqrt(16)+16") + self.assertEqual(result, 20.0) + + # complex nesting + result = self.calc.parse_and_calculate("sqrt(3^2+4^2)") + self.assertEqual(result, 5.0) + + # math function inside math function + result = self.calc.parse_and_calculate("sqrt(log(10000))") + self.assertEqual(result, 2.0) + # decimals and logarithms + result = self.calc.parse_and_calculate("log(sqrt(10))") + self.assertEqual(result, 0.5) + + # trig inside math function + result = self.calc.parse_and_calculate("sqrt(sin(90))") + self.assertEqual(result, 1.0) + # mixing trig, power and decimal results + result = self.calc.parse_and_calculate("sin(30)^2") + self.assertEqual(result, 0.25) + + # this checks if the string replacement for degrees/radian breaks the syntax of surrounding math + def test_trig_arithmetic_mix(self): + # (internal logic converts degrees to radians) + result = self.calc.parse_and_calculate("10*sin(90)") + self.assertEqual(result, 10.0) + + # custom operator stability + def test_factorial_and_percent(self): + result = self.calc.parse_and_calculate("3!+10") + self.assertEqual(result, 16) + + result = self.calc.parse_and_calculate("200*50%") + self.assertEqual(result, 100.0) + # mixing factorial with percent + result = self.calc.parse_and_calculate("0!+50%") + self.assertEqual(result, 1.5) + + # parentheses, factorial, percent + result = self.calc.parse_and_calculate("(4!+6)*10%") + self.assertEqual(result, 3.0) + +if __name__ == '__main__': + unittest.main() \ No newline at end of file