diff --git a/README.md b/README.md index 6445ffa..3a1ec65 100644 --- a/README.md +++ b/README.md @@ -66,9 +66,25 @@ - Ln (natural logarithm) - ex (inverse natural logarithm) +### Custom Features +- Tip Calculator + allows users to calculate the total cost of a bill including gratuity. +- Temperature Converter + allows users to convert temperatures between Celsius and Fahrenheit. -### Custom Features +These custom features were selected bec they demonstrate the ability to extend the calculator beyond standard mathematical operations while applying real-world problem-solving. The Tip Calculator provides a practical financial calculation, while the Temperature Converter demonstrates the implementation of mathematical formulas and conditional logic. + +### User-Friendly Menu System +-The app provides an interactive menu that displays all available calculator operations. Users can simply enter the name of the desired operation rather than remembering commands. The menu also includes a HELP option to redisplay all available commands at any time, making the calculator easier to navigate and use. + +### Error Handling +-The calculator validates user input and displays meaningful error messages for invalid operations, including: + - Invalid numeric input + - Division by zero + - Invalid exponent operations + - Invalid mathematical operations +-The calculator prevents additional operations while an error is displayed until the user clears the error, ensuring consistent and predicatable behavior. In addition to the Core and Scientific features, you are required to create at least two of your own features for the calculator. They can be any two features that are not already covered and that you can implement as you see fit. These features must be properly tested. @@ -91,3 +107,4 @@ The following functions should take the displayed value (x) and updated it accor ## Submission Completed projects should be submitted by submitting a pull request against the [original repository](https://git.zipcode.rocks/Cohort4.2/ZCW-MacroLabs-OOP-ScientificCalculator). All work should be done in your own repository. + diff --git a/calctests.py b/calctests.py deleted file mode 100644 index 1964570..0000000 --- a/calctests.py +++ /dev/null @@ -1,25 +0,0 @@ -import unittest -from calculator import Calculator - - -class TestStringMethods(unittest.TestCase): - - def test_add(self): - c = Calculator() - self.assertEqual(c.add(3, 3), 6) - - def test_add2(self): - c = Calculator() - self.assertEqual(c.add(12, -10), 2) - - def test_add3(self): - c = Calculator() - self.assertEqual(c.add(5, 8), 13) - - def test_sub(self): - c = Calculator() - self.assertEqual(c.sub(9, 3), 6) - - -if __name__ == '__main__': - unittest.main() diff --git a/calculator.py b/calculator.py index 3c85ead..377923c 100644 --- a/calculator.py +++ b/calculator.py @@ -1,12 +1,310 @@ class Calculator: + # Error Messages + INVALID_INPUT_ERROR = "Invalid input" + DIVIDE_BY_ZERO_ERROR = "Cannot divide by zero" + OVERFLOW_ERROR = "Number too large" + INVALID_EXPONENT_ERROR = "Invalid exponent" + INVALID_OPERATION_ERROR = "Invalid operation" + ## ERROR = "Err"------- + +#status set here def __init__(self): - pass + self.display: int | float = 0 + self.error: str | None = None + self.memory: int | float = 0 + self.trig_mode: str = "Radians" + +#---------------- +#Display methods +#---------------- + def getDisplay(self): + """ + Return the error message when an error exists. + Otherwise, return the current numeric display. + """ + if self.hasError(): + return self.error + + return self.display + + def hasError(self): + """ + Return True when an error message has been stored. + """ + return self.error is not None + + def clear(self): + """ + Reset both the display and the error status. + """ + self.display = 0 + self.error = None + return self.display + + def setDisplay(self, value): + """ + Set the calculator display to a valid number. + The calculator must be cleared before continuing + after an error. + """ + if self.hasError(): + return self.error + + if not self.isValidNumber(value): + self.error = self.INVALID_INPUT_ERROR + return self.error + + self.display = value + return self.display + + def isValidNumber(self, value): + """ + Boolean values are excluded bc Python treats T / F as int + """ + + return( + isinstance(value, (int, float)) + and not isinstance(value, bool) + ) +#--------------- +#Memory methods +#--------------- + def memory_add(self): #M+ + """ + Add the current display value to memory. + Update both memory and the display. + """ + if self.hasError(): + return self.error + + # if not self.isValidNumber(self.display): + # self.error = self.INVALID_INPUT_ERROR + # return self.error + + self.memory += self.display + self.display = self.memory + return self.display + + def memory_clear(self): #MC + """ + Reset memory to zero. + Leave the current display unchanged. + """ + if self.hasError(): + return self.error + + self.memory = 0 + return self.display + + def memory_recall(self): # MRC + """ + Copy the stored memory value to the display. + """ + if self.hasError(): + return self.error + + self.display = self.memory + + return self.display + + def memory_store(self): + """ + Store the current display value in memory. + Leave the display unchanged. + """ + if self.hasError(): + return self.error + + if not self.isValidNumber(self.display): + self.error = self.INVALID_INPUT_ERROR + return self.error + + self.memory = self.display + return self.display + +#--------------- +#Simple math functions +#--------------- def add(self, x, y): - return x + y + if self.hasError(): + return self.error + + if not self.isValidNumber(x) or not self.isValidNumber(y): + self.error = self.INVALID_INPUT_ERROR + return self.error + try: + self.display = x + y + except OverflowError: + self.error = self.OVERFLOW_ERROR + + return self.getDisplay() + + def subtract(self, x, y): + if self.hasError(): + return self.error + + if not self.isValidNumber(x) or not self.isValidNumber(y): + self.error = self.INVALID_INPUT_ERROR + return self.error + + try: + self.display = x - y + except OverflowError: + self.error = self.OVERFLOW_ERROR + + return self.getDisplay() + + def multiply(self, x, y): + if self.hasError(): + return self.error + + if not self.isValidNumber(x) or not self.isValidNumber(y): + self.error = self.INVALID_INPUT_ERROR + return self.error + + try: + self.display = x * y + except OverflowError: + self.error = self.OVERFLOW_ERROR + + return self.getDisplay() + + def divide(self, x, y): + if self.hasError(): + return self.error + + if not self.isValidNumber(x) or not self.isValidNumber(y): + self.error = self.INVALID_INPUT_ERROR + return self.error + + if y == 0: + self.error = self.DIVIDE_BY_ZERO_ERROR + return self.error + + try: + self.display = x / y + except OverflowError: + self.error = self.OVERFLOW_ERROR + + return self.getDisplay() + + def inverse(self): + if self.hasError(): + return self.error + + if not self.isValidNumber(self.display): + self.error = self.INVALID_INPUT_ERROR + return self.error + + if self.display == 0: + self.error = self.DIVIDE_BY_ZERO_ERROR + return self.error + + try: + # ensure numeric division even if display is a numeric string; will raise on invalid types + self.display = 1 / float(self.display) + except OverflowError: + self.error = self.OVERFLOW_ERROR + + return self.getDisplay() + + def exponentiate(self, exponent): + if self.hasError(): + return self.error + + if not self.isValidNumber(self.display): + self.error = self.INVALID_INPUT_ERROR + return self.error + + if not self.isValidNumber(exponent): + self.error = self.INVALID_INPUT_ERROR + return self.error + + try: + result = self.display ** exponent + + if isinstance(result, complex): + self.error = self.INVALID_EXPONENT_ERROR + else: + self.display = result + + except ZeroDivisionError: + self.error = self.DIVIDE_BY_ZERO_ERROR + except OverflowError: + self.error = self.OVERFLOW_ERROR + except (TypeError, ValueError): + self.error = self.INVALID_EXPONENT_ERROR + + return self.getDisplay() + + def switch_sign(self): + if self.hasError(): + return self.error + + if not self.isValidNumber(self.display): + self.error = self.INVALID_INPUT_ERROR + return self.error + + self.display = -self.display + return self.display + + # added Sloanes code 7/12/26 8:07p + def square(self, x): + if self.hasError(): + return self.display + + if not self.isValidNumber(x): + self.error = self.INVALID_INPUT_ERROR + return self.error + + try: + self.display = x ** 2 + except OverflowError: + self.error = self.OVERFLOW_ERROR + return self.getDisplay() + + def squareroot(self, x): + if self.hasError(): + return self.error + + if not self.isValidNumber(x): + self.error = self.INVALID_INPUT_ERROR + return self.error + + if x < 0: + self.error = "Cannot calculate the square root of a negative number" + return self.error + + try: + self.display = x ** 0.5 + except OverflowError: + self.error = self.OVERFLOW_ERROR + return self.getDisplay() - def sub(self, x, y): - return 0 + +#--------------- +# Utility functions +#--------------- + def absolute_value(self): + if self.hasError(): + return self.error + + if not self.isValidNumber(self.display): + self.error = self.INVALID_INPUT_ERROR + return self.error + + self.display = abs(self.display) + return self.display + + def percentage(self): + if self.hasError(): + return self.error + + if not self.isValidNumber(self.display): + self.error = self.INVALID_INPUT_ERROR + return self.error -# add lots more methods to this calculator class. + self.display = self.display / 100 + return self.display \ No newline at end of file diff --git a/main-app.py b/main-app.py index a7cc4e2..6a2e627 100644 --- a/main-app.py +++ b/main-app.py @@ -1,31 +1,256 @@ -from calculator import Calculator +from sciCalc import SciCalc +import math +def getNumber(message): +#ask the user for one number. +#keep asking until the user enters a valid int or dec. + while True: + try: + return float(input(message)) + except ValueError: + print("Please enter a valid number.") def getTwoNumbers(): + while True: + try: + a = float(input("first number? ")) + b = float(input("second number? ")) + return a, b + + except ValueError: + print("Error: Please enter valid numbers") + print() + +def getOneNumber(): a = float(input("first number? ")) - b = float(input("second number? ")) - return a, b + return a +def displayResult(result): + print("Display:", result) + print() + +def displayMenu(): + print("-------------------------------") + print("WELCOME TO I.B.M CALCULATOR:") + print("Select a function") + print("-------------------------------") + print(" set - Put a number on the display") + print(" add - Add a number to the display") + print(" subtract - Subtract a number from the display") + print(" multiply - Multiply the display by a number") + print(" divide - Divide the display by a number") + print(" square - Raise the display to the second power") + print(" squareroot - Find the square root of the number on display") + print(" power - Raise the display to an exponent") + print(" inverse - Calculate 1 dividend by the display") + print(" sign - Switch the display between positive and negative") + # scientific functions + print(" mode - Switch trig units between Degrees and Radians") + print(" sine - Calculate the sine of the number on display") + print(" cosine - Calculate the cosine of the number on display") + print(" tangent - Calculate the tangent of the number on display") + print(" asine - Calculate the inverse sine of the number on display") + print(" acosine - Calculate the inverse cosine of the number on display") + print(" atangent - Calculate the inverse tangent of the number on display") + # custom functions + print(" absolute - Calculate the absolute value") + print(" percentage - Divide the display by 100") + print(" factorial - Calculate factorial") + print(" log - Base 10 logarithm") + print(" invlog - 10 raised to the display number") + print(" nl - Natural Logarithm") + print(" exp - e raised to the display number") + print(" ms - Store memory") + print(" m+ - Add display to memory") + print(" mc - Clear memory") + print(" mrc - Recall memory") + print(" tip - Calculate a tip") + print(" temp - Convert temperature") + print(" clear - Reset the display to 0") + print(" show - Show the current display") + print(" help - Show the available operations") + print(" q - Quit") + print() + +def performCalcLoop(calc): + displayMenu() -def displayResult(x: float): - print(x, "\n") + while True: + choice = input("What do you want to do, pick a function? ").strip().lower() + if choice == "q": + break # user types q to quit calulator. + + #when the calculator displays Err, only clear, show, help and quit allowed + if calc.hasError() and choice not in ( + "clear", "show", "help", "q" + ): + print("The calculator is displaying an Error") + print("Type 'clear' before performing another operation.") + print() + continue + + if choice == "set": + number = getNumber("Number? ") + displayResult(calc.setDisplay(number)) -def performCalcLoop(calc): - while True: - choice = input("Operation? ") - if choice == 'q': - break # user types q to quit calulator. elif choice == 'add': - a, b = getTwoNumbers() - displayResult(calc.add(a, b)) + x, y = getTwoNumbers() + displayResult(calc.add(x, y)) + + elif choice == "subtract": + x, y = getTwoNumbers() + displayResult(calc.subtract(x, y)) + + elif choice == "multiply": + x, y = getTwoNumbers() + displayResult(calc.multiply(x, y)) + + elif choice == "divide": + x, y = getTwoNumbers() + displayResult(calc.divide(x, y)) + + elif choice == "square": + x = getOneNumber() + displayResult(calc.square(x)) + + elif choice == "squareroot": + x = getOneNumber() + displayResult(calc.squareroot(x)) + + elif choice == "power": + base = getNumber("Base number? ") + exponent = getNumber("Exponent? ") + + calc.setDisplay(base) + displayResult(calc.exponentiate(exponent)) + + elif choice == "inverse": + number = getNumber("Number? ") + + calc.setDisplay(number) + displayResult(calc.inverse()) + + elif choice == "sign": + number = getNumber("Number? ") + + calc.setDisplay(number) + displayResult(calc.switch_sign()) + + elif choice == "absolute": + number = getNumber("Number? ") + + calc.setDisplay(number) + displayResult(calc.absolute_value()) + + elif choice == "percentage": + number = getNumber("Number? ") + + calc.setDisplay(number) + displayResult(calc.percentage()) + + # scientific functions + + elif choice == "mode": + new_mode = calc.switchUnitsMode() + print(f"Trig unit mode changed to: {new_mode}") + print() + + elif choice == "sine": + number = getNumber("Number? ") + calc.setDisplay(number) + displayResult(calc.sine()) + + elif choice == "cosine": + number = getNumber("Number? ") + calc.setDisplay(number) + displayResult(calc.cosine()) + + elif choice == "tangent": + number = getNumber("Number? ") + calc.setDisplay(number) + displayResult(calc.tangent()) + + elif choice == "asine": + number = getNumber("Number (between -1 and 1)? ") + calc.setDisplay(number) + displayResult(calc.inverse_sine()) + + elif choice == "acosine": + number = getNumber("Number (between -1 and 1)? ") + calc.setDisplay(number) + displayResult(calc.inverse_cosine()) + + elif choice == "atangent": + number = getNumber("Number (between -1 and 1)? ") + calc.setDisplay(number) + displayResult(calc.inverse_tangent()) +#----end of scientific + + elif choice == "factorial": + number = getNumber("Number? ") + calc.setDisplay(number) + displayResult(calc.factorial()) + + elif choice == "log": + number = getNumber("Number? ") + calc.setDisplay(number) + displayResult(calc.log()) + + elif choice == "invlog": + number = getNumber("Number? ") + calc.setDisplay(number) + displayResult(calc.inverse_log()) + + elif choice == "nl": + number = getNumber("Number? ") + calc.setDisplay(number) + displayResult(calc.natural_log()) + + elif choice == "exp": + number = getNumber("Number? ") + calc.setDisplay(number) + displayResult(calc.inverse_natural_log()) + + elif choice == "tip": + bill = getNumber("Bill Amount? ") + percent = getNumber("Tip Percentage? ") + displayResult(calc.tip_calculator(bill, percent)) + + elif choice == "temp": + temperature = getNumber("Temperature? ") + conversion = input("Convert to (C/F)? ") + displayResult(calc.temperature_converter(temperature, conversion)) + + elif choice == "ms": + displayResult(calc.memory_story()) + + elif choice == "m+": + displayResult(calc.memory_add()) + + elif choice == "mc": + displayResult(calc.memory_clear()) + + elif choice == "mrc": + displayResult(calc.memory_recall()) + + elif choice == "clear": + displayResult(calc.clear()) + + elif choice == "show": + displayResult(calc.getDisplay()) + + elif choice == "help": + displayMenu() + else: print("That is not a valid input.") - + print("Type 'help' to see the available operations.") + print() # main start def main(): - calc = Calculator() + calc = SciCalc() performCalcLoop(calc) print("Done Calculating.") diff --git a/sciCalc.py b/sciCalc.py new file mode 100644 index 0000000..b935881 --- /dev/null +++ b/sciCalc.py @@ -0,0 +1,211 @@ +import math +from calculator import Calculator + +class SciCalc(Calculator): + + def factorial(self): + if self.hasError(): + return self.error + + if not self.isValidNumber(self.display): + self.error = self.INVALID_INPUT_ERROR + return self.error + + if self.display < 0: + self.error = "Factorial requires a non-negative number" + return self.error + if self.display != int(self.display): + self.error = "Factorial requries a whole number" + return self.error + + self.display = math.factorial(int(self.display)) + return self.display + + def log(self): + if self.hasError(): + return self.error + + if self.display <= 0: + self.error = "Log undefined for zeror or negative numbers" + return self.error + + self.display = math.log10(self.display) + return self.display + + def inverse_log(self): + if self.hasError(): + return self.error + + self.display = 10 ** self.display + return self.display + + def natural_log(self): + if self.hasError(): + return self.error + + if self.display <= 0: + self.error = "Natural log undefined for zero or negative nubmers" + return self.error + + self.display = math.log(self.display) + return self.display + + def inverse_natural_log(self): + if self.hasError(): + return self.error + self.display = math.exp(self.display) + return self.display + +#---------------- +# Custom features +#---------------- + +# Tip Function + def tip_calculator(self, billAmount, tipPercent): + if self.hasError(): + return self.error + + if not self.isValidNumber(billAmount) or not self.isValidNumber(tipPercent): + self.error = self.INVALID_INPUT_ERROR + return self.error + + if billAmount < 0 or tipPercent < 0: + self.error = "Amounts cannot be negative" + return self.error + + tip = billAmount * (tipPercent /100) + self.display = billAmount + tip + + return self.display + + # Convert temp + def temperature_converter(self, temperature, conversion): + if self.hasError(): + return self.error + + if not self.isValidNumber(temperature): + self.error = self.INVALID_INPUT_ERROR + return self.error + + conversion = conversion.upper() + + if conversion == "C": + self.display = (temperature - 32) * 5 / 9 + + elif conversion == "F": + self.display = (temperature * 9 / 5) + 32 + + else: + self.error = "Use C or F" + + return self.getDisplay() +#------------------- +#Scientific Functions +#------------------- +# to switch between degrees and radians + def getTrigMode(self): + return self.trig_mode + + def switchUnitsMode(self, mode=None): + if self.hasError(): + return self.trig_mode + + if mode in {"Degrees", "Radians"}: + self.trig_mode = mode + else: + self.trig_mode = "Degrees" if self.trig_mode == "Radians" else "Radians" + + return self.trig_mode + + def sine(self): + if self.hasError(): + return self.display + try: #needs to be a float + val = float(self.display) + if self.trig_mode == "Degrees": + val = math.radians(val) + + result = math.sin(val) + + self.display = 0.0 if abs(result) < 1e-15 else result + except (TypeError, ValueError, OverflowError): + self.display = self.error + return self.display + + def cosine(self): + if self.hasError(): + return self.display + try: #needs to be a float + val = float(self.display) + if self.trig_mode == "Degrees": + val = math.radians(val) + + result = math.cos(val) + + self.display = 0.0 if abs(result) < 1e-15 else result + except (TypeError, ValueError, OverflowError): + self.display = self.error + return self.display + + def tangent(self): + if self.hasError(): + return self.display + try: #needs to be a float + val = float(self.display) + if self.trig_mode == "Degrees": + val = math.radians(val) + + result = math.tan(val) + + self.display = 0.0 if abs(result) < 1e-15 else result #decimal places + except (TypeError, ValueError, OverflowError): + self.display = self.error + return self.display + + def inverse_sine(self): + if self.hasError(): + return self.display + try: + val = float(self.display) + + result = math.asin(val) #value error if not between -1 and 1 + + if self.trig_mode == "Degrees": #convert radians back to degrees + result = math.degrees(result) + + self.display = 0.0 if abs(result) < 1e-15 else result + except (TypeError, ValueError, OverflowError): + self.display = self.error + return self.display + + def inverse_cosine(self): + if self.hasError(): + return self.display + try: + val = float(self.display) + + result = math.acos(val) #value error if not between -1 and 1 + + if self.trig_mode == "Degrees": #convert radians back to degrees + result = math.degrees(result) + + self.display = 0.0 if abs(result) < 1e-15 else result + except (TypeError, ValueError, OverflowError): + self.display = self.error + return self.display + + def inverse_tangent(self): + if self.hasError(): + return self.display + try: + val = float(self.display) + + result = math.atan(val) #value error if not between -1 and 1 + + if self.trig_mode == "Degrees": #convert radians back to degrees + result = math.degrees(result) + + self.display = 0.0 if abs(result) < 1e-15 else result + except (TypeError, ValueError, OverflowError): + self.display = self.error + return self.display \ No newline at end of file diff --git a/test_calculator.py b/test_calculator.py new file mode 100644 index 0000000..0103ab6 --- /dev/null +++ b/test_calculator.py @@ -0,0 +1,463 @@ +import unittest + +from calculator import Calculator + + +class TestCalculator(unittest.TestCase): + + def setUp(self): + """ + Create a new Calculator before every test. + + This keeps each test independent. + """ + self.calc = Calculator() + + # ------------------------------------------------- + # Initial state and display tests + # ------------------------------------------------- + + def test_default_display_is_zero(self): + self.assertEqual(self.calc.getDisplay(), 0) + + def test_default_error_is_none(self): + self.assertFalse(self.calc.hasError()) + + def test_default_memory_is_zero(self): + self.assertEqual(self.calc.memory, 0) + + def test_default_trig_mode_is_radians(self): + self.assertEqual(self.calc.trig_mode, "Radians") + + def test_set_display_updates_display(self): + result = self.calc.setDisplay(25) + + self.assertEqual(result, 25) + self.assertEqual(self.calc.getDisplay(), 25) + + def test_set_display_accepts_decimal_number(self): + result = self.calc.setDisplay(12.5) + + self.assertEqual(result, 12.5) + + def test_set_display_rejects_string(self): + result = self.calc.setDisplay("hello") + + self.assertEqual(result, self.calc.INVALID_INPUT_ERROR) + self.assertTrue(self.calc.hasError()) + + def test_set_display_rejects_boolean(self): + result = self.calc.setDisplay(True) + + self.assertEqual(result, self.calc.INVALID_INPUT_ERROR) + self.assertTrue(self.calc.hasError()) + + def test_set_display_is_blocked_when_error_exists(self): + self.calc.setDisplay("bad value") + + result = self.calc.setDisplay(10) + + self.assertEqual(result, self.calc.INVALID_INPUT_ERROR) + self.assertTrue(self.calc.hasError()) + + def test_clear_resets_display_and_error(self): + self.calc.setDisplay("bad value") + + result = self.calc.clear() + + self.assertEqual(result, 0) + self.assertEqual(self.calc.getDisplay(), 0) + self.assertFalse(self.calc.hasError()) + + # ------------------------------------------------- + # Number validation tests + # ------------------------------------------------- + + def test_is_valid_number_accepts_integer(self): + self.assertTrue(self.calc.isValidNumber(10)) + + def test_is_valid_number_accepts_float(self): + self.assertTrue(self.calc.isValidNumber(10.5)) + + def test_is_valid_number_rejects_string(self): + self.assertFalse(self.calc.isValidNumber("10")) + + def test_is_valid_number_rejects_boolean(self): + self.assertFalse(self.calc.isValidNumber(True)) + + # ------------------------------------------------- + # Memory tests + # ------------------------------------------------- + + def test_memory_add_adds_display_to_memory(self): + self.calc.setDisplay(10) + + result = self.calc.memory_add() + + self.assertEqual(result, 10) + self.assertEqual(self.calc.memory, 10) + self.assertEqual(self.calc.getDisplay(), 10) + + def test_memory_add_accumulates_values(self): + self.calc.setDisplay(10) + self.calc.memory_add() + + self.calc.setDisplay(5) + result = self.calc.memory_add() + + self.assertEqual(result, 15) + self.assertEqual(self.calc.memory, 15) + self.assertEqual(self.calc.getDisplay(), 15) + + def test_memory_clear_resets_memory_to_zero(self): + self.calc.setDisplay(10) + self.calc.memory_add() + + result = self.calc.memory_clear() + + self.assertEqual(self.calc.memory, 0) + self.assertEqual(result, 10) + + def test_memory_recall_copies_memory_to_display(self): + self.calc.setDisplay(12) + self.calc.memory_store() + + self.calc.setDisplay(0) + result = self.calc.memory_recall() + + self.assertEqual(result, 12) + self.assertEqual(self.calc.getDisplay(), 12) + + def test_memory_store_saves_current_display(self): + self.calc.setDisplay(22) + + result = self.calc.memory_store() + + self.assertEqual(result, 22) + self.assertEqual(self.calc.memory, 22) + + def test_memory_operation_is_blocked_when_error_exists(self): + self.calc.setDisplay("bad value") + + result = self.calc.memory_add() + + self.assertEqual(result, self.calc.INVALID_INPUT_ERROR) + + # ------------------------------------------------- + # Addition tests + # ------------------------------------------------- + + def test_add_two_positive_numbers(self): + result = self.calc.add(5, 3) + + self.assertEqual(result, 8) + self.assertEqual(self.calc.getDisplay(), 8) + + def test_add_positive_and_negative_numbers(self): + result = self.calc.add(10, -4) + + self.assertEqual(result, 6) + + def test_add_rejects_invalid_first_number(self): + result = self.calc.add("5", 3) + + self.assertEqual(result, self.calc.INVALID_INPUT_ERROR) + self.assertTrue(self.calc.hasError()) + + def test_add_rejects_invalid_second_number(self): + result = self.calc.add(5, "3") + + self.assertEqual(result, self.calc.INVALID_INPUT_ERROR) + + # ------------------------------------------------- + # Subtraction tests + # ------------------------------------------------- + + def test_subtract_two_numbers(self): + result = self.calc.subtract(10, 4) + + self.assertEqual(result, 6) + self.assertEqual(self.calc.getDisplay(), 6) + + def test_subtract_can_return_negative_result(self): + result = self.calc.subtract(4, 10) + + self.assertEqual(result, -6) + + def test_subtract_rejects_invalid_input(self): + result = self.calc.subtract(10, "4") + + self.assertEqual(result, self.calc.INVALID_INPUT_ERROR) + + # ------------------------------------------------- + # Multiplication tests + # ------------------------------------------------- + + def test_multiply_two_numbers(self): + result = self.calc.multiply(6, 7) + + self.assertEqual(result, 42) + self.assertEqual(self.calc.getDisplay(), 42) + + def test_multiply_by_zero(self): + result = self.calc.multiply(9, 0) + + self.assertEqual(result, 0) + + def test_multiply_rejects_invalid_input(self): + result = self.calc.multiply("6", 7) + + self.assertEqual(result, self.calc.INVALID_INPUT_ERROR) + + # ------------------------------------------------- + # Division tests + # ------------------------------------------------- + + def test_divide_two_numbers(self): + result = self.calc.divide(20, 4) + + self.assertEqual(result, 5) + self.assertEqual(self.calc.getDisplay(), 5) + + def test_divide_returns_decimal_result(self): + result = self.calc.divide(5, 2) + + self.assertEqual(result, 2.5) + + def test_divide_by_zero_sets_error(self): + result = self.calc.divide(10, 0) + + self.assertEqual(result, self.calc.DIVIDE_BY_ZERO_ERROR) + self.assertTrue(self.calc.hasError()) + + def test_divide_rejects_invalid_input(self): + result = self.calc.divide(10, "2") + + self.assertEqual(result, self.calc.INVALID_INPUT_ERROR) + + # ------------------------------------------------- + # Error-state tests + # ------------------------------------------------- + + def test_operation_is_blocked_when_error_exists(self): + self.calc.divide(10, 0) + + result = self.calc.add(2, 3) + + self.assertEqual(result, self.calc.DIVIDE_BY_ZERO_ERROR) + self.assertTrue(self.calc.hasError()) + + def test_clear_allows_operation_after_error(self): + self.calc.divide(10, 0) + self.calc.clear() + + result = self.calc.add(2, 3) + + self.assertEqual(result, 5) + self.assertFalse(self.calc.hasError()) + + # ------------------------------------------------- + # Inverse tests + # ------------------------------------------------- + + def test_inverse_of_positive_number(self): + self.calc.setDisplay(4) + + result = self.calc.inverse() + + self.assertEqual(result, 0.25) + + def test_inverse_of_negative_number(self): + self.calc.setDisplay(-4) + + result = self.calc.inverse() + + self.assertEqual(result, -0.25) + + def test_inverse_of_zero_sets_error(self): + self.calc.setDisplay(0) + + result = self.calc.inverse() + + self.assertEqual(result, self.calc.DIVIDE_BY_ZERO_ERROR) + self.assertTrue(self.calc.hasError()) + + # ------------------------------------------------- + # Exponentiation tests + # ------------------------------------------------- + + def test_exponentiate_positive_integer_exponent(self): + self.calc.setDisplay(2) + + result = self.calc.exponentiate(3) + + self.assertEqual(result, 8) + + def test_exponentiate_zero_exponent(self): + self.calc.setDisplay(5) + + result = self.calc.exponentiate(0) + + self.assertEqual(result, 1) + + def test_exponentiate_negative_exponent(self): + self.calc.setDisplay(2) + + result = self.calc.exponentiate(-2) + + self.assertEqual(result, 0.25) + + def test_exponentiate_rejects_invalid_exponent(self): + self.calc.setDisplay(2) + + result = self.calc.exponentiate("3") + + self.assertEqual(result, self.calc.INVALID_INPUT_ERROR) + + def test_exponentiate_negative_base_fractional_power_sets_error(self): + self.calc.setDisplay(-4) + + result = self.calc.exponentiate(0.5) + + self.assertEqual(result, self.calc.INVALID_EXPONENT_ERROR) + self.assertTrue(self.calc.hasError()) + + def test_zero_to_negative_exponent_sets_divide_by_zero_error(self): + self.calc.setDisplay(0) + + result = self.calc.exponentiate(-1) + + self.assertEqual(result, self.calc.DIVIDE_BY_ZERO_ERROR) + + # ------------------------------------------------- + # Switch-sign tests + # ------------------------------------------------- + + def test_switch_sign_changes_positive_to_negative(self): + self.calc.setDisplay(10) + + result = self.calc.switch_sign() + + self.assertEqual(result, -10) + + def test_switch_sign_changes_negative_to_positive(self): + self.calc.setDisplay(-10) + + result = self.calc.switch_sign() + + self.assertEqual(result, 10) + + def test_switch_sign_of_zero_remains_zero(self): + self.calc.setDisplay(0) + + result = self.calc.switch_sign() + + self.assertEqual(result, 0) + + # ------------------------------------------------- + # Square tests + # ------------------------------------------------- + + def test_square_positive_number(self): + result = self.calc.square(5) + + self.assertEqual(result, 25) + self.assertEqual(self.calc.getDisplay(), 25) + + def test_square_negative_number(self): + result = self.calc.square(-4) + + self.assertEqual(result, 16) + + def test_square_rejects_invalid_input(self): + result = self.calc.square("5") + + self.assertEqual(result, self.calc.INVALID_INPUT_ERROR) + + # ------------------------------------------------- + # Square-root tests + # ------------------------------------------------- + + def test_square_root_of_perfect_square(self): + result = self.calc.squareroot(81) + + self.assertEqual(result, 9) + self.assertEqual(self.calc.getDisplay(), 9) + + def test_square_root_of_zero(self): + result = self.calc.squareroot(0) + + self.assertEqual(result, 0) + + def test_square_root_of_decimal(self): + result = self.calc.squareroot(2.25) + + self.assertEqual(result, 1.5) + + def test_square_root_of_negative_number_sets_error(self): + result = self.calc.squareroot(-9) + + self.assertEqual( + result, + "Cannot calculate the square root of a negative number" + ) + self.assertTrue(self.calc.hasError()) + + def test_square_root_rejects_invalid_input(self): + result = self.calc.squareroot("81") + + self.assertEqual(result, self.calc.INVALID_INPUT_ERROR) + + # ------------------------------------------------- + # Absolute-value tests + # ------------------------------------------------- + + def test_absolute_value_changes_negative_to_positive(self): + self.calc.setDisplay(-15) + + result = self.calc.absolute_value() + + self.assertEqual(result, 15) + + def test_absolute_value_keeps_positive_number_positive(self): + self.calc.setDisplay(15) + + result = self.calc.absolute_value() + + self.assertEqual(result, 15) + + def test_absolute_value_of_zero_is_zero(self): + self.calc.setDisplay(0) + + result = self.calc.absolute_value() + + self.assertEqual(result, 0) + + # ------------------------------------------------- + # Percentage tests + # ------------------------------------------------- + + def test_percentage_divides_positive_number_by_one_hundred(self): + self.calc.setDisplay(25) + + result = self.calc.percentage() + + self.assertEqual(result, 0.25) + + def test_percentage_divides_negative_number_by_one_hundred(self): + self.calc.setDisplay(-25) + + result = self.calc.percentage() + + self.assertEqual(result, -0.25) + + def test_percentage_of_zero_is_zero(self): + self.calc.setDisplay(0) + + result = self.calc.percentage() + + self.assertEqual(result, 0) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/test_sciCalc.py b/test_sciCalc.py new file mode 100644 index 0000000..b194d6e --- /dev/null +++ b/test_sciCalc.py @@ -0,0 +1,487 @@ +import math +import unittest + +from sciCalc import SciCalc + + +class TestSciCalc(unittest.TestCase): + + def setUp(self): + """ + Create a new scientific calculator before every test. + + This ensures that every test is independent. + """ + self.calc = SciCalc() + + # ============================================================ + # Factorial tests + # ============================================================ + + def test_factorial_of_positive_whole_number(self): + self.calc.setDisplay(5) + + result = self.calc.factorial() + + self.assertEqual(result, 120) + self.assertEqual(self.calc.getDisplay(), 120) + + def test_factorial_of_zero_is_one(self): + self.calc.setDisplay(0) + + result = self.calc.factorial() + + self.assertEqual(result, 1) + + def test_factorial_of_one_is_one(self): + self.calc.setDisplay(1) + + result = self.calc.factorial() + + self.assertEqual(result, 1) + + def test_factorial_rejects_negative_number(self): + self.calc.setDisplay(-5) + + result = self.calc.factorial() + + self.assertEqual( + result, + "Factorial requires a non-negative number" + ) + self.assertTrue(self.calc.hasError()) + + def test_factorial_rejects_decimal_number(self): + self.calc.setDisplay(4.5) + + result = self.calc.factorial() + + self.assertEqual( + result, + "Factorial requries a whole number" + ) + self.assertTrue(self.calc.hasError()) + + def test_factorial_is_blocked_when_error_exists(self): + self.calc.setDisplay("invalid") + + result = self.calc.factorial() + + self.assertEqual(result, self.calc.INVALID_INPUT_ERROR) + + # ============================================================ + # Base-10 logarithm tests + # ============================================================ + + def test_log_of_one_is_zero(self): + self.calc.setDisplay(1) + + result = self.calc.log() + + self.assertEqual(result, 0) + + def test_log_of_one_hundred_is_two(self): + self.calc.setDisplay(100) + + result = self.calc.log() + + self.assertEqual(result, 2) + + def test_log_of_one_thousand_is_three(self): + self.calc.setDisplay(1000) + + result = self.calc.log() + + self.assertEqual(result, 3) + + def test_log_of_zero_sets_error(self): + self.calc.setDisplay(0) + + result = self.calc.log() + + self.assertEqual( + result, + "Log undefined for zeror or negative numbers" + ) + self.assertTrue(self.calc.hasError()) + + def test_log_of_negative_number_sets_error(self): + self.calc.setDisplay(-10) + + result = self.calc.log() + + self.assertEqual( + result, + "Log undefined for zeror or negative numbers" + ) + self.assertTrue(self.calc.hasError()) + + # ============================================================ + # Inverse-logarithm tests + # ============================================================ + + def test_inverse_log_of_two_is_one_hundred(self): + self.calc.setDisplay(2) + + result = self.calc.inverse_log() + + self.assertEqual(result, 100) + + def test_inverse_log_of_zero_is_one(self): + self.calc.setDisplay(0) + + result = self.calc.inverse_log() + + self.assertEqual(result, 1) + + def test_inverse_log_of_negative_one_is_point_one(self): + self.calc.setDisplay(-1) + + result = self.calc.inverse_log() + + self.assertAlmostEqual(result, 0.1) + + # ============================================================ + # Natural-logarithm tests + # ============================================================ + + def test_natural_log_of_e_is_one(self): + self.calc.setDisplay(math.e) + + result = self.calc.natural_log() + + self.assertAlmostEqual(result, 1.0, places=7) + + def test_natural_log_of_one_is_zero(self): + self.calc.setDisplay(1) + + result = self.calc.natural_log() + + self.assertEqual(result, 0) + + def test_natural_log_of_zero_sets_error(self): + self.calc.setDisplay(0) + + result = self.calc.natural_log() + + self.assertEqual( + result, + "Natural log undefined for zero or negative nubmers" + ) + self.assertTrue(self.calc.hasError()) + + def test_natural_log_of_negative_number_sets_error(self): + self.calc.setDisplay(-5) + + result = self.calc.natural_log() + + self.assertEqual( + result, + "Natural log undefined for zero or negative nubmers" + ) + self.assertTrue(self.calc.hasError()) + + # ============================================================ + # Inverse-natural-logarithm tests + # ============================================================ + + def test_inverse_natural_log_of_one_is_e(self): + self.calc.setDisplay(1) + + result = self.calc.inverse_natural_log() + + self.assertAlmostEqual(result, math.e, places=7) + + def test_inverse_natural_log_of_zero_is_one(self): + self.calc.setDisplay(0) + + result = self.calc.inverse_natural_log() + + self.assertEqual(result, 1) + + # ============================================================ + # Tip-calculator tests + # ============================================================ + + def test_tip_calculator_adds_twenty_percent_tip(self): + result = self.calc.tip_calculator(100, 20) + + self.assertEqual(result, 120) + self.assertEqual(self.calc.getDisplay(), 120) + + def test_tip_calculator_adds_fifteen_percent_tip(self): + result = self.calc.tip_calculator(50, 15) + + self.assertAlmostEqual(result, 57.5) + + def test_tip_calculator_allows_zero_tip(self): + result = self.calc.tip_calculator(75, 0) + + self.assertEqual(result, 75) + + def test_tip_calculator_rejects_negative_bill(self): + result = self.calc.tip_calculator(-100, 20) + + self.assertEqual(result, "Amounts cannot be negative") + self.assertTrue(self.calc.hasError()) + + def test_tip_calculator_rejects_negative_tip(self): + result = self.calc.tip_calculator(100, -20) + + self.assertEqual(result, "Amounts cannot be negative") + self.assertTrue(self.calc.hasError()) + + def test_tip_calculator_rejects_invalid_bill(self): + result = self.calc.tip_calculator("100", 20) + + self.assertEqual(result, self.calc.INVALID_INPUT_ERROR) + + def test_tip_calculator_rejects_invalid_tip(self): + result = self.calc.tip_calculator(100, "20") + + self.assertEqual(result, self.calc.INVALID_INPUT_ERROR) + + # ============================================================ + # Temperature-converter tests + # ============================================================ + + def test_temperature_converter_fahrenheit_to_celsius(self): + result = self.calc.temperature_converter(32, "C") + + self.assertAlmostEqual(result, 0.0, places=7) + + + def test_temperature_converter_celsius_to_fahrenheit(self): + result = self.calc.temperature_converter(0, "F") + + self.assertAlmostEqual(result, 32.0, places=7) + + def test_temperature_converter_accepts_lowercase_conversion(self): + result = self.calc.temperature_converter(32, "c") + + self.assertAlmostEqual(result, 0.0, places=7) + + def test_temperature_converter_rejects_invalid_temperature(self): + result = self.calc.temperature_converter("hot", "C") + + self.assertEqual(result, self.calc.INVALID_INPUT_ERROR) + + def test_temperature_converter_rejects_invalid_conversion(self): + result = self.calc.temperature_converter(100, "X") + + self.assertEqual(result, "Use C or F") + self.assertTrue(self.calc.hasError()) + + # ============================================================ + # Trigonometric-mode tests + # ============================================================ + + def test_default_trig_mode_is_radians(self): + self.assertEqual(self.calc.getTrigMode(), "Radians") + + def test_switch_units_mode_sets_degrees(self): + result = self.calc.switchUnitsMode("Degrees") + + self.assertEqual(result, "Degrees") + self.assertEqual(self.calc.getTrigMode(), "Degrees") + + def test_switch_units_mode_sets_radians(self): + self.calc.switchUnitsMode("Degrees") + + result = self.calc.switchUnitsMode("Radians") + + self.assertEqual(result, "Radians") + + def test_switch_units_mode_rotates_from_radians_to_degrees(self): + result = self.calc.switchUnitsMode() + + self.assertEqual(result, "Degrees") + + def test_switch_units_mode_rotates_from_degrees_to_radians(self): + self.calc.switchUnitsMode("Degrees") + + result = self.calc.switchUnitsMode() + + self.assertEqual(result, "Radians") + + # ============================================================ + # Sine tests + # ============================================================ + + def test_sine_of_zero_radians_is_zero(self): + self.calc.setDisplay(0) + + result = self.calc.sine() + + self.assertEqual(result, 0.0) + + def test_sine_of_pi_over_two_radians_is_one(self): + self.calc.setDisplay(math.pi / 2) + + result = self.calc.sine() + + self.assertAlmostEqual(result, 1.0, places=7) + + def test_sine_of_ninety_degrees_is_one(self): + self.calc.switchUnitsMode("Degrees") + self.calc.setDisplay(90) + + result = self.calc.sine() + + self.assertAlmostEqual(result, 1.0, places=7) + + # ============================================================ + # Cosine tests + # ============================================================ + + def test_cosine_of_zero_radians_is_one(self): + self.calc.setDisplay(0) + + result = self.calc.cosine() + + self.assertAlmostEqual(result, 1.0, places=7) + + def test_cosine_of_pi_radians_is_negative_one(self): + self.calc.setDisplay(math.pi) + + result = self.calc.cosine() + + self.assertAlmostEqual(result, -1.0, places=7) + + def test_cosine_of_sixty_degrees_is_point_five(self): + self.calc.switchUnitsMode("Degrees") + self.calc.setDisplay(60) + + result = self.calc.cosine() + + self.assertAlmostEqual(result, 0.5, places=7) + + def test_cosine_of_ninety_degrees_is_zero(self): + self.calc.switchUnitsMode("Degrees") + self.calc.setDisplay(90) + + result = self.calc.cosine() + + self.assertEqual(result, 0.0) + + # ============================================================ + # Tangent tests + # ============================================================ + + def test_tangent_of_zero_radians_is_zero(self): + self.calc.setDisplay(0) + + result = self.calc.tangent() + + self.assertEqual(result, 0.0) + + def test_tangent_of_pi_over_four_radians_is_one(self): + self.calc.setDisplay(math.pi / 4) + + result = self.calc.tangent() + + self.assertAlmostEqual(result, 1.0, places=7) + + def test_tangent_of_forty_five_degrees_is_one(self): + self.calc.switchUnitsMode("Degrees") + self.calc.setDisplay(45) + + result = self.calc.tangent() + + self.assertAlmostEqual(result, 1.0, places=7) + + # ============================================================ + # Inverse-sine tests + # ============================================================ + + def test_inverse_sine_of_one_in_radians(self): + self.calc.setDisplay(1) + + result = self.calc.inverse_sine() + + self.assertAlmostEqual(result, math.pi / 2, places=7) + + def test_inverse_sine_of_one_in_degrees(self): + self.calc.switchUnitsMode("Degrees") + self.calc.setDisplay(1) + + result = self.calc.inverse_sine() + + self.assertAlmostEqual(result, 90.0, places=7) + + def test_inverse_sine_of_zero_is_zero(self): + self.calc.setDisplay(0) + + result = self.calc.inverse_sine() + + self.assertEqual(result, 0.0) + + # ============================================================ + # Inverse-cosine tests + # ============================================================ + + def test_inverse_cosine_of_one_in_radians(self): + self.calc.setDisplay(1) + + result = self.calc.inverse_cosine() + + self.assertEqual(result, 0.0) + + def test_inverse_cosine_of_zero_in_degrees(self): + self.calc.switchUnitsMode("Degrees") + self.calc.setDisplay(0) + + result = self.calc.inverse_cosine() + + self.assertAlmostEqual(result, 90.0, places=7) + + # ============================================================ + # Inverse-tangent tests + # ============================================================ + + def test_inverse_tangent_of_one_in_radians(self): + self.calc.setDisplay(1) + + result = self.calc.inverse_tangent() + + self.assertAlmostEqual(result, math.pi / 4, places=7) + + def test_inverse_tangent_of_one_in_degrees(self): + self.calc.switchUnitsMode("Degrees") + self.calc.setDisplay(1) + + result = self.calc.inverse_tangent() + + self.assertAlmostEqual(result, 45.0, places=7) + + def test_inverse_tangent_of_zero_is_zero(self): + self.calc.setDisplay(0) + + result = self.calc.inverse_tangent() + + self.assertEqual(result, 0.0) + + # ============================================================ + # Inherited-error-state tests + # ============================================================ + + def test_scientific_operation_is_blocked_during_error(self): + self.calc.setDisplay("invalid") + + result = self.calc.factorial() + + self.assertEqual(result, self.calc.INVALID_INPUT_ERROR) + + def test_clear_allows_scientific_operation_after_error(self): + self.calc.setDisplay("invalid") + self.calc.clear() + self.calc.setDisplay(5) + + result = self.calc.factorial() + + self.assertEqual(result, 120) + self.assertFalse(self.calc.hasError()) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file