1import math
2import sys
3
4
5# -------------------------------------------------------------
6# 1. Custom Exception Hierarchy
7# -------------------------------------------------------------
8class CalculatorError(Exception):
9 """Base exception for all calculator-related runtime failures."""
10 pass
11
12
13class InvalidOperatorError(CalculatorError):
14 """Raised when an unrecognized arithmetic operator is supplied."""
15 def __init__(self, operator):
16 super().__init__(f"Operator '{operator}' is not supported. Use (+, -, *, /, %, ^, sqrt, log).")
17 self.operator = operator
18
19
20class DivisionByZeroCalcError(CalculatorError):
21 """Raised when a calculation attempts division or modulo by zero."""
22 def __init__(self):
23 super().__init__("Mathematical error: Division or modulo by zero is undefined.")
24
25
26class MathDomainError(CalculatorError):
27 """Raised when operands violate mathematical domain constraints (e.g. sqrt(-1))."""
28 pass
29
30
31class OperandCountError(CalculatorError):
32 """Raised when expression tokens do not match expected operand counts."""
33 pass
34
35
36# -------------------------------------------------------------
37# 2. Calculator Engine
38# -------------------------------------------------------------
39class CalculatorEngine:
40 """Core mathematical execution engine with defensive exception handling."""
41
42 def __init__(self):
43 self.history = []
44
45 def execute_operation(self, operator: str, *operands: float) -> float:
46 """Executes the calculation and returns the float result."""
47 operator = operator.lower()
48
49 # Unary operations (1 operand)
50 if operator in ("sqrt", "log"):
51 if len(operands) != 1:
52 raise OperandCountError(f"Operation '{operator}' expects exactly 1 operand, received {len(operands)}.")
53 val = operands[0]
54
55 if operator == "sqrt":
56 if val < 0:
57 raise MathDomainError("Cannot compute square root of a negative number in real domain.")
58 return math.sqrt(val)
59
60 if operator == "log":
61 if val <= 0:
62 raise MathDomainError("Logarithm domain error: Input must be strictly greater than zero.")
63 return math.log(val)
64
65 # Binary operations (2 operands)
66 elif operator in ("+", "-", "*", "/", "%", "^", "**"):
67 if len(operands) != 2:
68 raise OperandCountError(f"Operation '{operator}' expects exactly 2 operands, received {len(operands)}.")
69 a, b = operands
70
71 if operator == "+":
72 return a + b
73 elif operator == "-":
74 return a - b
75 elif operator == "*":
76 return a * b
77 elif operator == "/":
78 if b == 0:
79 raise DivisionByZeroCalcError()
80 return a / b
81 elif operator == "%":
82 if b == 0:
83 raise DivisionByZeroCalcError()
84 return a % b
85 elif operator in ("^", "**"):
86 try:
87 return math.pow(a, b)
88 except OverflowError:
89 raise MathDomainError("Result exceeds maximum floating-point representation (Overflow).")
90 else:
91 raise InvalidOperatorError(operator)
92
93 def parse_and_calculate(self, raw_expression: str) -> float:
94 """Parses a user string, validates tokens, and dispatches calculation."""
95 tokens = raw_expression.strip().split()
96 if not tokens:
97 raise CalculatorError("Expression cannot be empty.")
98
99 # Scenario A: Unary functions like "sqrt 16" or "log 100"
100 if tokens[0].lower() in ("sqrt", "log"):
101 op = tokens[0].lower()
102 if len(tokens) != 2:
103 raise OperandCountError(f"Format for '{op}' must be: {op} <number>")
104 try:
105 val = float(tokens[1])
106 except ValueError:
107 raise ValueError(f"Operand '{tokens[1]}' is not a valid numeric value.")
108
109 result = self.execute_operation(op, val)
110 self._log_history(f"{op} {val}", result)
111 return result
112
113 # Scenario B: Infix binary operations like "15 / 3" or "2 ^ 8"
114 if len(tokens) == 3:
115 raw_a, op, raw_b = tokens
116 try:
117 a = float(raw_a)
118 except ValueError:
119 raise ValueError(f"Left operand '{raw_a}' is not a valid number.")
120
121 try:
122 b = float(raw_b)
123 except ValueError:
124 raise ValueError(f"Right operand '{raw_b}' is not a valid number.")
125
126 result = self.execute_operation(op, a, b)
127 self._log_history(f"{a} {op} {b}", result)
128 return result
129
130 raise OperandCountError("Invalid format. Use 'operand operator operand' (e.g. 5 * 10) or 'op val' (e.g. sqrt 25).")
131
132 def _log_history(self, expr: str, res: float):
133 self.history.append((expr, res))
134
135 def print_history(self):
136 if not self.history:
137 print("\nNo calculations recorded in this session.")
138 return
139 print("\n--- Session Calculation History ---")
140 for idx, (expr, res) in enumerate(self.history, start=1):
141 print(f"{idx}. {expr} = {res}")
142 print("-----------------------------------")
143
144
145# -------------------------------------------------------------
146# 3. Interactive CLI Interface
147# -------------------------------------------------------------
148def run_calculator():
149 calc = CalculatorEngine()
150 print("=" * 60)
151 print(" WELCOME TO THE ERROR-HANDLED CALCULATOR")
152 print(" Supported: +, -, *, /, %, ^, sqrt, log | 'history' | 'exit'")
153 print(" Example queries: '15 / 4', '2 ^ 10', 'sqrt 49', 'log 2.718'")
154 print("=" * 60)
155
156 while True:
157 try:
158 user_input = input("\ncalc > ").strip()
159
160 if not user_input:
161 continue
162
163 if user_input.lower() in ("exit", "quit", "q"):
164 print("Thank you for using the Calculator. Goodbye!")
165 break
166
167 if user_input.lower() == "history":
168 calc.print_history()
169 continue
170
171 # Execute calculation with defensive multi-layer error handling
172 result = calc.parse_and_calculate(user_input)
173
174 except KeyboardInterrupt:
175 # Intercept Ctrl+C gracefully without crash dump
176 print("\n[Notice] Operation interrupted by user (Ctrl+C). Type 'exit' to quit.")
177 except EOFError:
178 print("\nExiting calculator session.")
179 break
180 except CalculatorError as calc_err:
181 # Catches all domain-specific errors (InvalidOperator, DivisionByZero, etc.)
182 print(f"[Calculator Error] {calc_err}")
183 except ValueError as val_err:
184 # Catches invalid floating point conversions
185 print(f"[Input Format Error] {val_err}")
186 except Exception as unexpected:
187 # Fallback for unforeseen exceptions
188 print(f"[Unexpected Failure] {type(unexpected).__name__}: {unexpected}")
189 else:
190 # Runs only when calculation completes successfully
191 print(f"=> Result: {result:,.4f}")
192 finally:
193 # Housekeeping that executes every single cycle
194 pass
195
196
197if __name__ == "__main__":
198 run_calculator()