From 5af472214730ffb4c0428492d8623ea4ab4ea433 Mon Sep 17 00:00:00 2001 From: Antigravity Agent Date: Sun, 25 Oct 2026 19:58:17 +0000 Subject: [PATCH] feat: Create a Python Calculator module with tests (closes #1) --- .gitignore | 3 + calc.py | 123 ++++++++++++++++++++++++++++++++++ test_calc.py | 185 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 311 insertions(+) create mode 100644 .gitignore create mode 100644 calc.py create mode 100644 test_calc.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6e4266f --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +__pycache__/ +*.py[cod] +*$py.class diff --git a/calc.py b/calc.py new file mode 100644 index 0000000..697b418 --- /dev/null +++ b/calc.py @@ -0,0 +1,123 @@ +"""calc.py - Calculator module providing basic arithmetic operations. + +Functions: + add(a, b): Returns the sum of a and b. + subtract(a, b): Returns the difference of a and b. + multiply(a, b): Returns the product of a and b. + divide(a, b): Returns the quotient of a divided by b. +""" + +from typing import Union + +Number = Union[int, float] + + +def _validate_numeric(a: Number, b: Number) -> None: + """Validate that both arguments are numeric types (int or float, not bool). + + Args: + a: First operand. + b: Second operand. + + Raises: + TypeError: If either operand is not an int or float, or is a bool. + """ + if isinstance(a, bool) or isinstance(b, bool): + raise TypeError("Operands must be numeric (int or float), not bool.") + if not isinstance(a, (int, float)) or not isinstance(b, (int, float)): + raise TypeError("Operands must be numeric (int or float).") + + +def add(a: Number, b: Number) -> Number: + """Return the sum of two numbers. + + Args: + a: The first number. + b: The second number. + + Returns: + The sum of a and b. + + Raises: + TypeError: If either argument is not a number. + """ + _validate_numeric(a, b) + return a + b + + +def subtract(a: Number, b: Number) -> Number: + """Return the difference of two numbers (a - b). + + Args: + a: The first number (minuend). + b: The second number (subtrahend). + + Returns: + The difference of a and b. + + Raises: + TypeError: If either argument is not a number. + """ + _validate_numeric(a, b) + return a - b + + +def multiply(a: Number, b: Number) -> Number: + """Return the product of two numbers. + + Args: + a: The first number. + b: The second number. + + Returns: + The product of a and b. + + Raises: + TypeError: If either argument is not a number. + """ + _validate_numeric(a, b) + return a * b + + +def divide(a: Number, b: Number) -> float: + """Return the quotient of two numbers (a / b). + + Args: + a: The numerator. + b: The denominator. + + Returns: + The quotient as a float. + + Raises: + ZeroDivisionError: If the denominator is zero. + TypeError: If either argument is not a number. + """ + _validate_numeric(a, b) + if b == 0: + raise ZeroDivisionError("Cannot divide by zero.") + return a / b + + +class Calculator: + """Calculator class providing static and instance access to basic arithmetic operations.""" + + @staticmethod + def add(a: Number, b: Number) -> Number: + """Return the sum of two numbers.""" + return add(a, b) + + @staticmethod + def subtract(a: Number, b: Number) -> Number: + """Return the difference of two numbers (a - b).""" + return subtract(a, b) + + @staticmethod + def multiply(a: Number, b: Number) -> Number: + """Return the product of two numbers.""" + return multiply(a, b) + + @staticmethod + def divide(a: Number, b: Number) -> float: + """Return the quotient of two numbers (a / b).""" + return divide(a, b) diff --git a/test_calc.py b/test_calc.py new file mode 100644 index 0000000..bcf8514 --- /dev/null +++ b/test_calc.py @@ -0,0 +1,185 @@ +"""test_calc.py - Unit tests for calc module.""" + +import unittest +import calc + + +class TestCalcOperations(unittest.TestCase): + """Test suite for basic arithmetic operations in calc module.""" + + # ------------------ ADD TESTS ------------------ + def test_add_integers(self): + """Test addition with standard integers.""" + self.assertEqual(calc.add(2, 3), 5) + self.assertEqual(calc.add(10, 5), 15) + + def test_add_negative_numbers(self): + """Test addition with negative numbers.""" + self.assertEqual(calc.add(-1, 1), 0) + self.assertEqual(calc.add(-1, -1), -2) + self.assertEqual(calc.add(-5, 10), 5) + + def test_add_zero(self): + """Test addition with zero.""" + self.assertEqual(calc.add(0, 0), 0) + self.assertEqual(calc.add(0, 5), 5) + self.assertEqual(calc.add(5, 0), 5) + self.assertEqual(calc.add(-5, 0), -5) + + def test_add_floats(self): + """Test addition with floating point numbers.""" + self.assertEqual(calc.add(1.5, 2.5), 4.0) + self.assertAlmostEqual(calc.add(0.1, 0.2), 0.3, places=7) + self.assertEqual(calc.add(-1.5, 1.5), 0.0) + + def test_add_large_numbers(self): + """Test addition with large numbers.""" + self.assertEqual(calc.add(10**12, 10**12), 2 * 10**12) + + def test_add_invalid_types(self): + """Test addition with invalid types raises TypeError.""" + with self.assertRaises(TypeError): + calc.add("a", 2) + with self.assertRaises(TypeError): + calc.add(2, "b") + with self.assertRaises(TypeError): + calc.add(None, 5) + with self.assertRaises(TypeError): + calc.add(True, 1) + with self.assertRaises(TypeError): + calc.add(1, False) + + # ------------------ SUBTRACT TESTS ------------------ + def test_subtract_integers(self): + """Test subtraction with standard integers.""" + self.assertEqual(calc.subtract(10, 5), 5) + self.assertEqual(calc.subtract(5, 10), -5) + + def test_subtract_negative_numbers(self): + """Test subtraction with negative numbers.""" + self.assertEqual(calc.subtract(-1, 1), -2) + self.assertEqual(calc.subtract(-1, -1), 0) + self.assertEqual(calc.subtract(5, -5), 10) + + def test_subtract_zero(self): + """Test subtraction with zero.""" + self.assertEqual(calc.subtract(0, 0), 0) + self.assertEqual(calc.subtract(5, 0), 5) + self.assertEqual(calc.subtract(0, 5), -5) + + def test_subtract_floats(self): + """Test subtraction with floating point numbers.""" + self.assertEqual(calc.subtract(5.5, 2.5), 3.0) + self.assertAlmostEqual(calc.subtract(0.3, 0.1), 0.2, places=7) + + def test_subtract_invalid_types(self): + """Test subtraction with invalid types raises TypeError.""" + with self.assertRaises(TypeError): + calc.subtract("5", 3) + with self.assertRaises(TypeError): + calc.subtract(5, [1]) + with self.assertRaises(TypeError): + calc.subtract(True, 3) + + # ------------------ MULTIPLY TESTS ------------------ + def test_multiply_integers(self): + """Test multiplication with standard integers.""" + self.assertEqual(calc.multiply(10, 5), 50) + self.assertEqual(calc.multiply(3, 7), 21) + + def test_multiply_negative_numbers(self): + """Test multiplication with negative numbers.""" + self.assertEqual(calc.multiply(-1, 1), -1) + self.assertEqual(calc.multiply(-1, -1), 1) + self.assertEqual(calc.multiply(-5, 6), -30) + + def test_multiply_zero(self): + """Test multiplication with zero.""" + self.assertEqual(calc.multiply(0, 0), 0) + self.assertEqual(calc.multiply(100, 0), 0) + self.assertEqual(calc.multiply(0, 100), 0) + + def test_multiply_floats(self): + """Test multiplication with floating point numbers.""" + self.assertEqual(calc.multiply(2.5, 4.0), 10.0) + self.assertAlmostEqual(calc.multiply(0.5, 0.5), 0.25, places=7) + + def test_multiply_invalid_types(self): + """Test multiplication with invalid types raises TypeError.""" + with self.assertRaises(TypeError): + calc.multiply("3", 4) + with self.assertRaises(TypeError): + calc.multiply(4, None) + with self.assertRaises(TypeError): + calc.multiply(False, 5) + + # ------------------ DIVIDE TESTS ------------------ + def test_divide_integers(self): + """Test division resulting in whole and fractional numbers.""" + self.assertEqual(calc.divide(10, 5), 2.0) + self.assertEqual(calc.divide(5, 2), 2.5) + + def test_divide_negative_numbers(self): + """Test division with negative numbers.""" + self.assertEqual(calc.divide(-1, 1), -1.0) + self.assertEqual(calc.divide(-1, -1), 1.0) + self.assertEqual(calc.divide(10, -2), -5.0) + + def test_divide_zero_numerator(self): + """Test division when numerator is zero.""" + self.assertEqual(calc.divide(0, 5), 0.0) + self.assertEqual(calc.divide(0, -5), 0.0) + + def test_divide_floats(self): + """Test division with floating point numbers.""" + self.assertAlmostEqual(calc.divide(1, 3), 1 / 3, places=7) + self.assertEqual(calc.divide(7.5, 2.5), 3.0) + + def test_divide_by_zero_raises_error(self): + """Test that dividing by zero raises ZeroDivisionError.""" + with self.assertRaises(ZeroDivisionError): + calc.divide(10, 0) + with self.assertRaises(ZeroDivisionError): + calc.divide(0, 0) + with self.assertRaises(ZeroDivisionError): + calc.divide(10, 0.0) + with self.assertRaises(ZeroDivisionError): + calc.divide(-5.5, 0) + + def test_divide_invalid_types(self): + """Test division with invalid types raises TypeError.""" + with self.assertRaises(TypeError): + calc.divide("10", 2) + with self.assertRaises(TypeError): + calc.divide(10, "2") + with self.assertRaises(TypeError): + calc.divide(10, None) + with self.assertRaises(TypeError): + calc.divide(True, 2) + + +class TestCalculatorClass(unittest.TestCase): + """Test suite verifying Calculator class interface.""" + + def test_calculator_static_methods(self): + """Test static methods on Calculator class.""" + self.assertEqual(calc.Calculator.add(3, 4), 7) + self.assertEqual(calc.Calculator.subtract(10, 4), 6) + self.assertEqual(calc.Calculator.multiply(3, 4), 12) + self.assertEqual(calc.Calculator.divide(10, 2), 5.0) + with self.assertRaises(ZeroDivisionError): + calc.Calculator.divide(10, 0) + + def test_calculator_instance_methods(self): + """Test instance methods on Calculator object.""" + calculator = calc.Calculator() + self.assertEqual(calculator.add(3, 4), 7) + self.assertEqual(calculator.subtract(10, 4), 6) + self.assertEqual(calculator.multiply(3, 4), 12) + self.assertEqual(calculator.divide(10, 2), 5.0) + with self.assertRaises(ZeroDivisionError): + calculator.divide(5, 0) + + +if __name__ == "__main__": + unittest.main()