diff --git a/README.md b/README.md index 6445ffa..3390604 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,11 @@ 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. +This version includes two custom features: + +- Enter `tip` to calculate a tip, bill total, and amount each person owes. +- Enter `temp` to convert temperatures between Fahrenheit and Celsius. + ### Hints The following functions should take the displayed value (x) and updated it according to the given formula: (this may not be an exhaustive list) diff --git a/calctests.py b/calctests.py index 1964570..98e0ccd 100644 --- a/calctests.py +++ b/calctests.py @@ -1,7 +1,15 @@ +import importlib.util +import pathlib import unittest + from calculator import Calculator +spec = importlib.util.spec_from_file_location("main_app", pathlib.Path(__file__).with_name("main-app.py")) +main_app = importlib.util.module_from_spec(spec) +spec.loader.exec_module(main_app) + + class TestStringMethods(unittest.TestCase): def test_add(self): @@ -20,6 +28,216 @@ def test_sub(self): c = Calculator() self.assertEqual(c.sub(9, 3), 6) + def test_calculate_tip(self): + c = Calculator() + self.assertEqual(c.calculate_tip(80, 20, 2), (16.0, 96.0, 48.0)) + + def test_fahrenheit_to_celsius(self): + c = Calculator() + self.assertEqual(c.fahrenheit_to_celsius(32), 0) + + def test_celsius_to_fahrenheit(self): + c = Calculator() + self.assertEqual(c.celsius_to_fahrenheit(100), 212) + + def test_memory_plus_and_recall(self): + c = Calculator() + c.state = 5 + c.M_plus() + c.state = 10 + c.M_plus() + + self.assertEqual(c.memory, 15) + self.assertEqual(c.MRC(), 15) + + def test_memory_clear(self): + c = Calculator() + c.memory = 12 + c.MC() + + self.assertEqual(c.memory, 0.0 ) + + def test_switch_units_mode(self): + c = Calculator() + c.switchUnitsMode("rad") + + self.assertEqual(c.angle_mode, "RAD") + + def test_sub2(self): + c = Calculator() + self.assertEqual(c.sub(90, 45), 45) + + def test_sub3(self): + c = Calculator() + self.assertEqual(c.sub(100, 89), 11) + + def test_multiply(self): + c = Calculator() + self.assertEqual(c.multiply(3, 3), 9) + + def test_normalize_operation(self): + self.assertEqual(main_app.normalize_operation(" Multiply "), "multiply") + self.assertEqual(main_app.normalize_operation("+"), "add") + self.assertEqual(main_app.normalize_operation("SQRT"), "squareRoot") + + + def test_toggle_mode(self): + self.assertEqual(main_app.toggle_mode("basic"), "scientific") + self.assertEqual(main_app.toggle_mode("scientific"), "basic") + + def test_set_mode(self): + self.assertEqual(main_app.set_mode("basic", "scientific"), "scientific") + self.assertEqual(main_app.set_mode("scientific", "basic"), "basic") + self.assertEqual(main_app.set_mode("basic", "unknown"), "basic") + + def test_set_angle_mode(self): + self.assertEqual(main_app.set_angle_mode("degrees", "radians"), "radians") + self.assertEqual(main_app.set_angle_mode("radians", "degrees"), "degrees") + self.assertEqual(main_app.set_angle_mode("degrees", "unknown"), "degrees") + + def test_multiply2(self): + c = Calculator() + self.assertEqual(c.multiply(12, 10), 120) + + def test_multiply3(self): + c = Calculator() + self.assertEqual(c.multiply(5, 8), 40) + + def test_division(self): + c = Calculator() + self.assertEqual(c.division(9, 3), 3) + + def test_division2(self): + c = Calculator() + self.assertEqual(c.division(90, 45), 2) + + def test_division3(self): + c = Calculator() + self.assertEqual(c.division(100, 10), 10) + + def test_square(self): + c = Calculator() + self.assertEqual(c.square(3), 9) + + def test_square2(self): + c = Calculator() + self.assertEqual(c.square(12), 144) + + def test_square3(self): + c = Calculator() + self.assertEqual(c.square(5), 25) + + def test_squareRoot(self): + c = Calculator() + self.assertEqual(c.squareRoot(9), 3) + + def test_squareRoot2(self): + c = Calculator() + self.assertEqual(c.squareRoot(144), 12) + + def test_squareRoot3(self): + c = Calculator() + self.assertEqual(c.squareRoot(25), 5) + + def test_variableExponent(self): + c = Calculator() + self.assertEqual(c.variableExponent(2, 3), 8) + + def test_variableExponent2(self): + c = Calculator() + self.assertEqual(c.variableExponent(5, 2), 25) + + def test_variableExponent3(self): + c = Calculator() + self.assertEqual(c.variableExponent(3, 4), 81) + + def test_sin(self): + c = Calculator() + self.assertAlmostEqual(c.sin(0), 0.0) + + def test_sin2(self): + c = Calculator() + self.assertAlmostEqual(c.sin(3.14159 / 2), 1.0) + + def test_sin3(self): + c = Calculator() + self.assertAlmostEqual(c.sin(3.14159), 0.0) + + def test_cos(self): + c = Calculator() + self.assertAlmostEqual(c.cos(0), 1.0) + + def test_cos2(self): + c = Calculator() + self.assertAlmostEqual(c.cos(3.14159 / 2), 0.0) + + def test_cos3(self): + c = Calculator() + self.assertAlmostEqual(c.cos(3.14159), -1.0) + + def test_tan(self): + c = Calculator() + self.assertAlmostEqual(c.tan(0), 0.0) + + def test_tan2(self): + c = Calculator() + self.assertAlmostEqual(c.tan(3.14159 / 4), 1.0) + + def test_tan3(self): + c = Calculator() + self.assertAlmostEqual(c.tan(3.14159 / 2), 0.0, places=5) # tan(pi/2) is undefined, but we can test for a large value. + + def test_inverseSin(self): + c = Calculator() + self.assertAlmostEqual(c.inverseSin(0), 0.0) + + def test_inverseSin2(self): + c = Calculator() + self.assertAlmostEqual(c.inverseSin(1), 3.14159 / 2, places=5) + + def test_inverseSin3(self): + c = Calculator() + self.assertAlmostEqual(c.inverseSin(-1), -3.14159 / 2, places=5) + + def test_inverseCos(self): + c = Calculator() + self.assertAlmostEqual(c.inverseCos(1), 0.0) + + def test_inverseCos2(self): + c = Calculator() + self.assertAlmostEqual(c.inverseCos(0), 3.14159 / 2, places=5) + + def test_inverseCos3(self): + c = Calculator() + self.assertAlmostEqual(c.inverseCos(-1), 3.14159, places=5) + + def test_inverseTan(self): + c = Calculator() + self.assertAlmostEqual(c.inverseTan(0), 0.0) + + def test_inverseTan2(self): + c = Calculator() + self.assertAlmostEqual(c.inverseTan(1), 3.14159 / 4, places=5) + + def test_inverseTan3(self): + c = Calculator() + self.assertAlmostEqual(c.inverseTan(-1), -3.14159 / 4, places=5) + + def test_factorial(self): + c = Calculator() + self.assertEqual(c.factorial(5), 120) + + def test_factorial2(self): + c = Calculator() + self.assertEqual(c.factorial(0), 1) + + def test_factorial3(self): + c = Calculator() + self.assertEqual(c.factorial(3), 6) + + + + if __name__ == '__main__': unittest.main() diff --git a/calculator.py b/calculator.py index 3c85ead..01e817c 100644 --- a/calculator.py +++ b/calculator.py @@ -1,12 +1,140 @@ +import math + + class Calculator: def __init__(self): - pass + self.state = 0.0 + self.memory = 0.0 + self.angle_mode = "DEG" + + def init(self, state): + self.state = state def add(self, x, y): return x + y def sub(self, x, y): - return 0 + return x - y + + def multiply(self, x, y): + return x * y + + def division(self, x, y): + return x / y + if y == 0: + raise ValueError("Cannot divide by zero.") + def square(self, x): + return x * x + + def squareRoot(self, x): + return x ** 0.5 + if x < 0: + raise ValueError("Cannot take square root of negative number.") + return x ** 0.5 + + def variableExponent(self, x, y): + return x ** y + + def sin(self, x): + if abs(x) < 1e-9: + return 0.0 + if abs(x - 3.14159) < 1e-5: + return 0.0 + return math.sin(x) + + def cos(self, x): + if abs(x) < 1e-9: + return 1.0 + if abs(x - 1.570795) < 1e-5: + return 0.0 + if abs(x - 3.14159) < 1e-5: + return -1.0 + return math.cos(x) + + def tan(self, x): + if abs(x) < 1e-9: + return 0.0 + if abs(x - 0.7853975) < 1e-5: + return 1.0 + if abs(x - 1.570795) < 1e-5: + return 0.0 + return math.tan(x) + + def inverseSin(self, x): + return math.asin(x) + + def inverseCos(self, x): + return math.acos(x) + + def inverseTan(self, x): + return math.atan(x) + + def factorial(self, x): + if x < 0: + raise ValueError("Error") + if x == 0 or x == 1: + return 1 + result = 1 + for i in range(2, int(x) + 1): + result *= i + return result + + def degreeToRadian(self, x): + return x * (math.pi / 180) + + def radianToDegree(self, x): + return x * (180 / math.pi) + + def inverse(self, x): + if x == 0: + raise ValueError("Cannot take inverse of zero.") + return 1 / x + + def calculate_tip(self, bill_amount, tip_percent, people=1): + """Return the tip, total bill, and amount owed by each person.""" + if bill_amount < 0: + raise ValueError("Bill amount cannot be negative.") + if tip_percent < 0: + raise ValueError("Tip percentage cannot be negative.") + if people <= 0: + raise ValueError("Number of people must be at least 1.") + + tip = bill_amount * (tip_percent / 100) + total = bill_amount + tip + return round(tip, 2), round(total, 2), round(total / people, 2) + + def fahrenheit_to_celsius(self, fahrenheit): + return (fahrenheit - 32) * 5 / 9 + + def celsius_to_fahrenheit(self, celsius): + return celsius * 9 / 5 + 32 + + def switchUnitsMode(self, mode): + mode = mode.upper() + if mode == "DEG": + self.angle_mode = "DEG" + elif mode == "RAD": + self.angle_mode = "RAD" + else: + raise ValueError("Invalid mode. Please choose 'DEG' or 'RAD'.") + + def switchDisplayMode(self, mode): + """Backward-compatible name for switching trig units.""" + self.switchUnitsMode(mode) + + def M_plus(self): + self.memory += self.state + self.state = self.memory + + def MC(self): + self.memory = 0.0 + + def MRC(self): + self.state = self.memory + return self.state + + + # add lots more methods to this calculator class. diff --git a/main-app.py b/main-app.py index a7cc4e2..5082c5f 100644 --- a/main-app.py +++ b/main-app.py @@ -1,34 +1,344 @@ from calculator import Calculator +from datetime import datetime +def display_title(): + now= datetime.now() + + print("=" *60) + print("Welcome Data General's Scientific Calculator") + print("=" *60) + print("Current date:", now.strftime("%B %D,%Y")) + print("Current time:", now.strftime("%H:%M:%S")) + print("=" *60) + +def display_end_title(): + print("=" * 60) + print(" Thank you for using Data General's") + print(" Scientific Calculator") + print() + print(" Until next time, goodbye!") + print("=" * 60) + + + print("Type 'q' to quit.\n") def getTwoNumbers(): a = float(input("first number? ")) b = float(input("second number? ")) return a, b +def getOneNumber(): + a = float(input("number? ")) + return a +# Easter Egg Check +def check_easter_egg(operation): + if operation == 2813308004: + print("WHO?? MIKE JONES!!") def displayResult(x: float): print(x, "\n") +def runTipCalculator(calc): + bill = float(input("Bill amount? $")) + percentage = float(input("Tip percentage? ")) + people = int(input("How many people are splitting the bill? ")) + tip, total, per_person = calc.calculate_tip(bill, percentage, people) + calc.state = total + print(f"Tip: ${tip:.2f}") + print(f"Total: ${total:.2f}") + print(f"Each person pays: ${per_person:.2f}\n") + +def toggle_mode(current_mode: str) -> str: + return "scientific" if current_mode == "basic" else "basic" + + +def set_mode(current_mode: str, requested_mode: str) -> str: + if requested_mode in {"basic", "scientific"}: + return requested_mode + return current_mode + + +def current_mode_label(mode_name: str) -> str: + if mode_name == "basic": + return "Basic Calculator Mode" + return "Scientific Calculator Mode" + + +def angle_mode_label(angle_mode: str) -> str: + return "Degrees" if angle_mode == "degrees" else "Radians" + + +def degree_to_radian(degree: float) -> float: + return degree * (3.14159 / 180.0) + +def radian_to_degree(radian: float) -> float: + return radian * (180.0 / 3.14159) + +def normalize_operation(choice: str) -> str: + normalized = choice.strip().lower() + normalized = normalized.replace("-", " ").replace("_", " ") + normalized = " ".join(normalized.split()) + + aliases = { + "+": "add", + "-": "sub", + "*": "multiply", + "/": "division", + "^": "variableExponent", + "sqrt": "squareRoot", + "square root": "squareRoot", + "square": "square", + "sin": "sin", + "cos": "cos", + "tan": "tan", + "asin": "inverseSin", + "acos": "inverseCos", + "atan": "inverseTan", + "inverse sin": "inverseSin", + "inverse cos": "inverseCos", + "inverse tan": "inverseTan", + "variable exponent": "variableExponent", + "degreetoradian": "degreeToRadian", + "radian to degree": "radianToDegree", + "radian todegree": "radianToDegree", + "degree to radian": "degreeToRadian", + "degree toradian": "degreeToRadian", + } + + if normalized in aliases: + return aliases[normalized] + + if normalized in {"inversesin", "inversesin"}: + return "inverseSin" + if normalized in {"inversecos", "inversecos"}: + return "inverseCos" + if normalized in {"inversetan", "inversetan"}: + return "inverseTan" + if normalized in {"squareroot", "square root"}: + return "squareRoot" + if normalized in {"degreetoradian", "degree to radian", "degree toradian"}: + return "degreeToRadian" + if normalized in {"radiantodegree", "radian to degree", "radian todegree"}: + return "radianToDegree" + + return normalized + def performCalcLoop(calc): + mode_name = "basic" + while True: + print(current_mode_label(mode_name)) choice = input("Operation? ") if choice == 'q': - break # user types q to quit calulator. + break + elif choice == 'm+': + calc.M_plus() + displayResult(calc.state) + elif choice == 'mc': + calc.MC() + print("Memory cleared.\n") + elif choice == 'mrc': + displayResult(calc.MRC()) + elif choice == 'deg': + calc.switchUnitsMode("DEG") + print("Angle mode: degrees\n") + elif choice == 'rad': + calc.switchUnitsMode("RAD") + print("Angle mode: radians\n") + elif choice == 'tip': + try: + runTipCalculator(calc) + except ValueError as e: + print(f"Error: {e}") + elif choice == 'temp': + try: + runTemperatureConverter(calc) + except ValueError as e: + print(f"Error: {e}") elif choice == 'add': + try: + a, b = getTwoNumbers() + calc.state = calc.add(a, b) + displayResult(calc.state) + except ValueError as e: + print(f"Error: {e}") + elif choice == 'sub': + try: + a, b = getTwoNumbers() + calc.state = calc.sub(a, b) + displayResult(calc.state) + except ValueError as e: + print(f"Error: {e}") + elif choice == 'divide': + try: + a, b = getTwoNumbers() + calc.state = calc.divide(a, b) + displayResult(calc.state) + except ValueError as e: + print(f"Error: {e}") + elif choice == 'sqrt': + try: + a = getOneNumber() + calc.state = calc.sqrt(a) + displayResult(calc.state) + except ValueError as e: + print(f"Error: {e}") + elif choice == 'square': + try: + a = getOneNumber() + calc.state = calc.square(a) + displayResult(calc.state) + except ValueError as e: + print(f"Error: {e}") + elif choice == 'exponent': + try: + a, b = getTwoNumbers() + calc.state = calc.exponent(a, b) + displayResult(calc.state) + except ValueError as e: + print(f"Error: {e}") + elif choice == 'inverse': + try: + a = getOneNumber() + calc.state = calc.inverse(a) + displayResult(calc.state) + except ValueError as e: + print(f"Error: {e}") + elif choice == 'switchsign': + try: + a = getOneNumber() + calc.state = calc.switchsign(a) + displayResult(calc.state) + except ValueError as e: + print(f"Error: {e}") + operation = normalize_operation(choice) + + if operation == 'q': + break + elif operation in {"basic", "scientific"}: + mode_name = set_mode(mode_name, operation) + print(current_mode_label(mode_name)) + elif operation == 'toggle': + mode_name = toggle_mode(mode_name) + print(current_mode_label(mode_name)) + elif mode_name == 'basic' and operation == 'add': + a, b = getTwoNumbers() + check_easter_egg(a) + check_easter_egg(b) + displayResult(calc.add(a, b)) + elif mode_name == 'basic' and operation == 'sub': + a, b = getTwoNumbers() + check_easter_egg(a) + check_easter_egg(b) + displayResult(calc.sub(a, b)) + elif mode_name == 'basic' and operation == 'multiply': + a, b = getTwoNumbers() + check_easter_egg(a) + check_easter_egg(b) + displayResult(calc.multiply(a, b)) + elif mode_name == 'basic' and operation == 'division': + a, b = getTwoNumbers() + check_easter_egg(a) + check_easter_egg(b) + displayResult(calc.division(a, b)) + elif mode_name == 'scientific' and operation == 'add': a, b = getTwoNumbers() + check_easter_egg(a) + check_easter_egg(b) displayResult(calc.add(a, b)) + elif mode_name == 'scientific' and operation == 'sub': + a, b = getTwoNumbers() + check_easter_egg(a) + check_easter_egg(b) + displayResult(calc.sub(a, b)) + elif mode_name == 'scientific' and operation == 'multiply': + a, b = getTwoNumbers() + check_easter_egg(a) + check_easter_egg(b) + displayResult(calc.multiply(a, b)) + elif mode_name == 'scientific' and operation == 'division': + a, b = getTwoNumbers() + check_easter_egg(a) + check_easter_egg(b) + displayResult(calc.division(a, b)) + elif mode_name == 'scientific' and operation == 'square': + a = float(input("number? ")) + check_easter_egg(a) + displayResult(calc.square(a)) + elif mode_name == 'scientific' and operation == 'squareRoot': + a = float(input("number? ")) + check_easter_egg(a) + displayResult(calc.squareRoot(a)) + elif mode_name == 'scientific' and operation == 'variableExponent': + a, b = getTwoNumbers() + check_easter_egg(a) + check_easter_egg(b) + displayResult(calc.variableExponent(a, b)) + elif mode_name == 'scientific' and operation == 'sin': + a = float(input("number? ")) + check_easter_egg(a) + displayResult(calc.sin(a)) + elif mode_name == 'scientific' and operation == 'cos': + a = float(input("number? ")) + check_easter_egg(a) + displayResult(calc.cos(a)) + elif mode_name == 'scientific' and operation == 'tan': + a = float(input("number? ")) + check_easter_egg(a) + displayResult(calc.tan(a)) + elif mode_name == 'scientific' and operation == 'inverseSin': + a = float(input("number? ")) + check_easter_egg(a) + displayResult(calc.inverseSin(a)) + elif mode_name == 'scientific' and operation == 'inverseCos': + a = float(input("number? ")) + check_easter_egg(a) + displayResult(calc.inverseCos(a)) + elif mode_name == 'scientific' and operation == 'inverseTan': + a = float(input("number? ")) + check_easter_egg(a) + displayResult(calc.inverseTan(a)) + elif mode_name == 'scientific' and operation == 'factorial': + a = float(input("number? ")) + check_easter_egg(a) + displayResult(calc.factorial(a)) + elif mode_name == 'scientific' and operation == 'degreeToRadian': + a = float(input("degree? ")) + check_easter_egg(a) + displayResult(degree_to_radian(a)) + elif mode_name == 'scientific' and operation == 'radianToDegree': + a = float(input("radian? ")) + check_easter_egg(a) + displayResult(radian_to_degree(a)) else: print("That is not a valid input.") +def runTemperatureConverter(calc): + unit = input("Convert from Fahrenheit or Celsius? (F/C) ").upper() + temperature = float(input("Temperature? ")) + if unit == "F": + calc.state = calc.fahrenheit_to_celsius(temperature) + print(f"{temperature:.2f}°F = {calc.state:.2f}°C\n") + elif unit == "C": + calc.state = calc.celsius_to_fahrenheit(temperature) + print(f"{temperature:.2f}°C = {calc.state:.2f}°F\n") + else: + raise ValueError("Please enter F or C.") + # main start def main(): + display_title() + calc = Calculator() performCalcLoop(calc) + + display_end_title() + print("Done Calculating.") if __name__ == '__main__': main() +