PR: Create a Python Calculator module with tests (Closes #1) #2
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
123
calc.py
Normal file
123
calc.py
Normal file
@ -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)
|
||||
185
test_calc.py
Normal file
185
test_calc.py
Normal file
@ -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()
|
||||
Loading…
Reference in New Issue
Block a user