calculator.py

Download
python 328 lines 8.3 KB
  1"""
  2Calculator Module
  3
  4Simple calculator implementation to demonstrate testing.
  5This module will be tested in test_calculator.py.
  6"""
  7
  8from typing import Union, List
  9import math
 10
 11
 12class Calculator:
 13    """
 14    Basic calculator with arithmetic operations.
 15    Demonstrates testable code design.
 16    """
 17
 18    def add(self, a: float, b: float) -> float:
 19        """Add two numbers"""
 20        return a + b
 21
 22    def subtract(self, a: float, b: float) -> float:
 23        """Subtract b from a"""
 24        return a - b
 25
 26    def multiply(self, a: float, b: float) -> float:
 27        """Multiply two numbers"""
 28        return a * b
 29
 30    def divide(self, a: float, b: float) -> float:
 31        """
 32        Divide a by b.
 33
 34        Raises:
 35            ValueError: If b is zero
 36        """
 37        if b == 0:
 38            raise ValueError("Cannot divide by zero")
 39        return a / b
 40
 41    def power(self, base: float, exponent: float) -> float:
 42        """Raise base to the power of exponent"""
 43        return base ** exponent
 44
 45    def square_root(self, x: float) -> float:
 46        """
 47        Calculate square root.
 48
 49        Raises:
 50            ValueError: If x is negative
 51        """
 52        if x < 0:
 53            raise ValueError("Cannot calculate square root of negative number")
 54        return math.sqrt(x)
 55
 56    def modulo(self, a: float, b: float) -> float:
 57        """
 58        Calculate a modulo b.
 59
 60        Raises:
 61            ValueError: If b is zero
 62        """
 63        if b == 0:
 64            raise ValueError("Cannot calculate modulo with zero divisor")
 65        return a % b
 66
 67
 68class ScientificCalculator(Calculator):
 69    """
 70    Extended calculator with scientific functions.
 71    Demonstrates inheritance testing.
 72    """
 73
 74    def sin(self, x: float) -> float:
 75        """Calculate sine (x in radians)"""
 76        return math.sin(x)
 77
 78    def cos(self, x: float) -> float:
 79        """Calculate cosine (x in radians)"""
 80        return math.cos(x)
 81
 82    def tan(self, x: float) -> float:
 83        """Calculate tangent (x in radians)"""
 84        return math.tan(x)
 85
 86    def log(self, x: float, base: float = math.e) -> float:
 87        """
 88        Calculate logarithm.
 89
 90        Args:
 91            x: Value to calculate log of
 92            base: Logarithm base (default: e for natural log)
 93
 94        Raises:
 95            ValueError: If x <= 0 or base <= 0 or base == 1
 96        """
 97        if x <= 0:
 98            raise ValueError("Logarithm input must be positive")
 99        if base <= 0 or base == 1:
100            raise ValueError("Logarithm base must be positive and not equal to 1")
101        return math.log(x, base)
102
103    def factorial(self, n: int) -> int:
104        """
105        Calculate factorial.
106
107        Raises:
108            ValueError: If n is negative
109            TypeError: If n is not an integer
110        """
111        if not isinstance(n, int):
112            raise TypeError("Factorial requires integer input")
113        if n < 0:
114            raise ValueError("Factorial not defined for negative numbers")
115        return math.factorial(n)
116
117
118class StatisticsCalculator:
119    """
120    Calculator for statistical operations.
121    Demonstrates testing with data structures.
122    """
123
124    def mean(self, numbers: List[float]) -> float:
125        """
126        Calculate arithmetic mean.
127
128        Raises:
129            ValueError: If list is empty
130        """
131        if not numbers:
132            raise ValueError("Cannot calculate mean of empty list")
133        return sum(numbers) / len(numbers)
134
135    def median(self, numbers: List[float]) -> float:
136        """
137        Calculate median.
138
139        Raises:
140            ValueError: If list is empty
141        """
142        if not numbers:
143            raise ValueError("Cannot calculate median of empty list")
144
145        sorted_numbers = sorted(numbers)
146        n = len(sorted_numbers)
147
148        if n % 2 == 0:
149            # Even number of elements - average of two middle values
150            return (sorted_numbers[n // 2 - 1] + sorted_numbers[n // 2]) / 2
151        else:
152            # Odd number of elements - middle value
153            return sorted_numbers[n // 2]
154
155    def mode(self, numbers: List[float]) -> float:
156        """
157        Calculate mode (most frequent value).
158
159        Raises:
160            ValueError: If list is empty or no unique mode
161        """
162        if not numbers:
163            raise ValueError("Cannot calculate mode of empty list")
164
165        frequency = {}
166        for num in numbers:
167            frequency[num] = frequency.get(num, 0) + 1
168
169        max_freq = max(frequency.values())
170        modes = [num for num, freq in frequency.items() if freq == max_freq]
171
172        if len(modes) > 1:
173            raise ValueError("No unique mode exists")
174
175        return modes[0]
176
177    def variance(self, numbers: List[float]) -> float:
178        """
179        Calculate variance.
180
181        Raises:
182            ValueError: If list is empty
183        """
184        if not numbers:
185            raise ValueError("Cannot calculate variance of empty list")
186
187        avg = self.mean(numbers)
188        return sum((x - avg) ** 2 for x in numbers) / len(numbers)
189
190    def standard_deviation(self, numbers: List[float]) -> float:
191        """
192        Calculate standard deviation.
193
194        Raises:
195            ValueError: If list is empty
196        """
197        return math.sqrt(self.variance(numbers))
198
199
200class CalculatorMemory:
201    """
202    Calculator with memory functionality.
203    Demonstrates stateful testing.
204    """
205
206    def __init__(self):
207        self._memory: float = 0.0
208        self._history: List[float] = []
209
210    def store(self, value: float) -> None:
211        """Store value in memory"""
212        self._memory = value
213        self._history.append(value)
214
215    def recall(self) -> float:
216        """Recall value from memory"""
217        return self._memory
218
219    def clear(self) -> None:
220        """Clear memory"""
221        self._memory = 0.0
222
223    def add_to_memory(self, value: float) -> None:
224        """Add value to current memory"""
225        self._memory += value
226        self._history.append(self._memory)
227
228    def get_history(self) -> List[float]:
229        """Get calculation history"""
230        return self._history.copy()
231
232    def clear_history(self) -> None:
233        """Clear calculation history"""
234        self._history.clear()
235
236
237# Helper functions for demonstrating pure function testing
238def is_even(n: int) -> bool:
239    """Check if number is even"""
240    return n % 2 == 0
241
242
243def is_prime(n: int) -> bool:
244    """
245    Check if number is prime.
246
247    Args:
248        n: Number to check
249
250    Returns:
251        True if prime, False otherwise
252    """
253    if n < 2:
254        return False
255    if n == 2:
256        return True
257    if n % 2 == 0:
258        return False
259
260    for i in range(3, int(math.sqrt(n)) + 1, 2):
261        if n % i == 0:
262            return False
263
264    return True
265
266
267def fibonacci(n: int) -> int:
268    """
269    Calculate nth Fibonacci number (0-indexed).
270
271    Raises:
272        ValueError: If n is negative
273    """
274    if n < 0:
275        raise ValueError("Fibonacci not defined for negative numbers")
276    if n <= 1:
277        return n
278
279    a, b = 0, 1
280    for _ in range(n - 1):
281        a, b = b, a + b
282    return b
283
284
285def gcd(a: int, b: int) -> int:
286    """
287    Calculate greatest common divisor using Euclidean algorithm.
288
289    Raises:
290        ValueError: If either number is negative
291    """
292    if a < 0 or b < 0:
293        raise ValueError("GCD not defined for negative numbers")
294
295    while b:
296        a, b = b, a % b
297    return a
298
299
300if __name__ == "__main__":
301    # Simple demonstration
302    calc = Calculator()
303    print("Calculator Demo:")
304    print(f"5 + 3 = {calc.add(5, 3)}")
305    print(f"10 - 4 = {calc.subtract(10, 4)}")
306    print(f"6 * 7 = {calc.multiply(6, 7)}")
307    print(f"15 / 3 = {calc.divide(15, 3)}")
308    print(f"2^8 = {calc.power(2, 8)}")
309    print(f"√16 = {calc.square_root(16)}")
310
311    sci_calc = ScientificCalculator()
312    print(f"\nScientific Calculator Demo:")
313    print(f"sin(π/2) = {sci_calc.sin(math.pi / 2):.4f}")
314    print(f"cos(0) = {sci_calc.cos(0):.4f}")
315    print(f"5! = {sci_calc.factorial(5)}")
316
317    stats_calc = StatisticsCalculator()
318    data = [1, 2, 3, 4, 5]
319    print(f"\nStatistics Demo:")
320    print(f"Mean of {data} = {stats_calc.mean(data)}")
321    print(f"Median of {data} = {stats_calc.median(data)}")
322    print(f"Std Dev of {data} = {stats_calc.standard_deviation(data):.4f}")
323
324    print(f"\nHelper Functions Demo:")
325    print(f"is_prime(17) = {is_prime(17)}")
326    print(f"fibonacci(10) = {fibonacci(10)}")
327    print(f"gcd(48, 18) = {gcd(48, 18)}")