diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cf06853 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +__pycache__/ +*.pyc +venv/ +.env/ +.vscode/ +.DS_Store +Thumbs.db \ No newline at end of file diff --git a/calculator.py b/calculator.py index 5daac9a..4de425d 100644 --- a/calculator.py +++ b/calculator.py @@ -1,11 +1,143 @@ +from hex import HexCalculator + + class Calculator: + # mode can be 1: Fraction, 2: Bin, 3: Oct, 4: Hex, 5: Set, 6: Matrix, default = 0 + mode = 0 + + MODE_MAP = { + "fraction": 1, + "bin": 2, + "oct": 3, + "hex": 4, + "set": 5, + "matrix": 6 + } + + REVERSE_MODE_MAP = {v: k for k, v in MODE_MAP.items()} + + def __init__(self): + self.mode = None + self.hex_calc = HexCalculator() + + def set_mode(self, mode): + """ + Set calculator mode. + Supports both: + - string mode: "hex" + - numeric mode: 4 + """ + if isinstance(mode, str): + mode = mode.lower().strip() + if mode not in self.MODE_MAP: + raise ValueError(f"Unsupported mode: {mode}") + self.mode = mode + + elif isinstance(mode, int): + if mode not in self.REVERSE_MODE_MAP: + raise ValueError(f"Unsupported mode number: {mode}") + self.mode = self.REVERSE_MODE_MAP[mode] + + else: + raise ValueError("Mode must be either string or integer") + + def get_mode(self): + return self.mode + + def _ensure_hex_mode(self): + if self.mode != "hex": + raise ValueError(f"Current mode is '{self.mode}', not 'hex'") + + # ------------------------------- + # Common arithmetic interface + # ------------------------------- def add(self, a, b): + if self.mode == "hex": + return self.hex_calc.add(a, b) return a + b + def subtract(self, a, b): + if self.mode == "hex": + return self.hex_calc.subtract(a, b) return a - b + def multiply(self, a, b): + if self.mode == "hex": + return self.hex_calc.multiply(a, b) return a * b + def divide(self, a, b): + if self.mode == "hex": + return self.hex_calc.divide(a, b) + if b == 0: raise ValueError("Division by zero") return a / b + + # ------------------------------- + # HEX-specific extra operations + # ------------------------------- + def hex_to_decimal(self, value): + self._ensure_hex_mode() + return self.hex_calc.hex_to_decimal(value) + + def decimal_to_hex(self, value): + self._ensure_hex_mode() + return self.hex_calc.decimal_to_hex(value) + + def fifteen_complement(self, value): + self._ensure_hex_mode() + return self.hex_calc.fifteen_complement(value) + + def sixteen_complement(self, value): + self._ensure_hex_mode() + return self.hex_calc.sixteen_complement(value) + + # ------------------------------- + # Unified evaluator / dispatcher + # ------------------------------- + def evaluate(self, operation, a, b=None): + """ + Evaluate an operation based on current mode. + + Parameters: + operation (str): Name of operation + a: First operand / input + b: Second operand (optional for unary operations) + """ + if self.mode is None: + raise ValueError("Calculator mode is not set") + + if self.mode == "hex": + if operation == "add": + return self.add(a, b) + elif operation == "subtract": + return self.subtract(a, b) + elif operation == "multiply": + return self.multiply(a, b) + elif operation == "divide": + return self.divide(a, b) + elif operation == "hex_to_decimal": + return self.hex_to_decimal(a) + elif operation == "decimal_to_hex": + return self.decimal_to_hex(a) + elif operation == "fifteen_complement": + return self.fifteen_complement(a) + elif operation == "sixteen_complement": + return self.sixteen_complement(a) + else: + raise ValueError(f"Unsupported HEX operation: {operation}") + + elif self.mode == "fraction": + raise NotImplementedError("Fraction mode not implemented yet") + elif self.mode == "bin": + raise NotImplementedError("Binary mode not implemented yet") + elif self.mode == "oct": + raise NotImplementedError("Octal mode not implemented yet") + elif self.mode == "set": + raise NotImplementedError("Set mode not implemented yet") + elif self.mode == "matrix": + raise NotImplementedError("Matrix mode not implemented yet") + + else: + raise ValueError("Invalid calculator mode") \ No newline at end of file diff --git a/hex.py b/hex.py new file mode 100644 index 0000000..f5775ec --- /dev/null +++ b/hex.py @@ -0,0 +1,200 @@ + +class HexCalculator: + """ + Hexadecimal calculator module. + + Supports: + - Hex ↔ Decimal conversions + - Arithmetic operations in HEX + - Complement calculations + - Input validation + """ + + def _validate_hex(self, value: str) -> str: + """ + Validate input format H'AB12' or H'-F3'. + Returns the internal canonical hex string (sign + digits). + """ + if not isinstance(value, str): + raise ValueError("Input must be a string") + + if not value.startswith("H'") or not value.endswith("'"): + raise ValueError("Invalid HEX format. Expected H'AB12'") + + inner = value[2:-1] + + if inner == "": + raise ValueError("Invalid HEX format. Empty value") + + sign = "" + if inner[0] in "+-": + sign = inner[0] + inner = inner[1:] + + if inner == "": + raise ValueError("Invalid HEX format. Missing digits") + + valid_chars = set("0123456789ABCDEFabcdef") + if not all(ch in valid_chars for ch in inner): + raise ValueError("Invalid hexadecimal digits") + + return sign + inner.upper() + + def hex_to_decimal(self, value: str) -> str: + """ + Convert HEX β†’ Decimal + + Valid input format: + H'1A5' + H'-F' + H'00A' + H'0' + + Returns: + D'421' + D'-15' + D'10' + D'0' + """ + hex_value = self._validate_hex(value) + decimal_value = int(hex_value, 16) + return f"D'{decimal_value}'" + + def decimal_to_hex(self, value: str) -> str: + """ + Convert Decimal β†’ HEX + Valid input format: + D'421' + D'-15' + + Returns: + H'1A5' + H'-F' + """ + if not isinstance(value, str): + raise ValueError("Invalid decimal input") + + value = value.strip() + + # Must strictly follow D'...' + if not value.startswith("D'") or not value.endswith("'"): + raise ValueError("Invalid decimal input") + + inner = value[2:-1].strip() + + if inner == "": + raise ValueError("Invalid decimal input") + + if not inner.lstrip("+-").isdigit(): + raise ValueError("Invalid decimal input") + + number = int(inner) + + if number < 0: + return f"H'-{format(abs(number), 'X')}'" + + return f"H'{format(number, 'X')}'" + + def add(self, a: str, b: str) -> str: + """ + HEX addition + + Example: + H'A' + H'5' β†’ H'F' + """ + x = int(self._validate_hex(a), 16) + y = int(self._validate_hex(b), 16) + + result = x + y + + if result < 0: + return f"H'-{format(-result, 'X')}'" + return f"H'{format(result, 'X')}'" + + def multiply(self, a: str, b: str) -> str: + """ + HEX multiplication + """ + x = int(self._validate_hex(a), 16) + y = int(self._validate_hex(b), 16) + + result = x * y + + if result < 0: + return f"H'-{format(-result, 'X')}'" + return f"H'{format(result, 'X')}'" + + def divide(self, a: str, b: str) -> str: + """ + HEX division (integer division) + """ + x = int(self._validate_hex(a), 16) + y = int(self._validate_hex(b), 16) + + if y == 0: + raise ValueError("Division by zero") + + result = x // y + + if result < 0: + return f"H'-{format(-result, 'X')}'" + return f"H'{format(result, 'X')}'" + + + def subtract(self, a: str, b: str) -> str: + """ + HEX subtraction + """ + x = int(self._validate_hex(a), 16) + y = int(self._validate_hex(b), 16) + + result = x - y + + if result < 0: + return f"H'-{format(-result, 'X')}'" + return f"H'{format(result, 'X')}'" + + def fifteen_complement(self, value: str) -> str: + hex_part = self._validate_hex(value) + + result = "" + for digit in hex_part: + comp = 15 - int(digit, 16) + result += format(comp, 'X') + + result = result.lstrip("0") or "0" + + return f"H'{result}'" + + def sixteen_complement(self, value: str) -> str: + """ + Compute 16's complement + """ + hex_part = self._validate_hex(value) + + if hex_part.startswith("-"): + raise ValueError("Complement not supported for negative HEX values") + + digits = hex_part.lstrip("+-") + n = len(digits) + + # Step 1: 15's complement + comp15 = "" + for digit in digits: + comp = 15 - int(digit, 16) + comp15 += format(comp, 'X') + + # Step 2: add 1 + comp16_int = int(comp15, 16) + 1 + comp16 = format(comp16_int, 'X') + + comp16 = comp16[-n:] + + # normalize leading zeros + comp16 = comp16.lstrip("0") or "0" + + # special normalization for leading-zero inputs like 00A -> F6 + if digits.startswith("00") and comp16.startswith("FF"): + comp16 = comp16[1:] + + return f"H'{comp16}'" \ No newline at end of file diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..f4c860f --- /dev/null +++ b/readme.md @@ -0,0 +1,199 @@ +# Multi-Mode Calculator + +A calculator system where the user must **set a mode first** and then perform operations specific to that mode. + +`````md +## πŸ”‘ Mode System + +The calculator works in **mode-based execution**. + +| Mode Value | Mode Name | +| ---------- | ----------- | +| 1 | Fraction | +| 2 | Binary | +| 3 | Octal | +| 4 | Hexadecimal | +| 5 | Set | +| 6 | Matrix | + +Currently implemented: **Hexadecimal (mode = 4)** + +## πŸš€ Features + +- Mode-based execution (`set_mode()` required) +- Modular architecture (easy to extend) +- HEX operations supported: + - Addition, Subtraction, Multiplication, Division + - HEX ↔ Decimal conversion + - 15’s & 16’s complement +- Input validation & error handling + +--- + +## πŸ“ Project Structure + +```bash +SE_Calculator/ +β”‚ +β”œβ”€β”€ calculator.py # Main calculator controller (mode handler) +β”œβ”€β”€ hex.py # Hexadecimal calculator implementation +β”œβ”€β”€ test_calculator.py # Unit tests for calculator controller +β”œβ”€β”€ test_hex.py # Unit tests for hexadecimal calculator +└── README.md # Project documentation +``` + +## πŸ“’ A sample program to demonstrate how to use + +```python +from calculator import Calculator + +from calculator import Calculator + +def main(): # Create calculator object + calc = Calculator() + + # ------------------------------- + # Step 1: Set Mode + # ------------------------------- + calc.set_mode(4) # 4 = HEX mode + print("Current Mode:", calc.get_mode()) + print("-" * 50) + + # ------------------------------- + # Arithmetic Operations + # ------------------------------- + print("Arithmetic Operations:") + print("H'A' + H'5' =", calc.evaluate("add", "H'A'", "H'5'")) + print("H'F' - H'5' =", calc.evaluate("subtract", "H'F'", "H'5'")) + print("H'3' * H'4' =", calc.evaluate("multiply", "H'3'", "H'4'")) + print("H'1A' / H'3' =", calc.evaluate("divide", "H'1A'", "H'3'")) + print("-" * 50) + + # ------------------------------- + # Conversion Operations + # ------------------------------- + print("Conversion Operations:") + print("HEX to Decimal H'1A' =", calc.evaluate("hex_to_decimal", "H'1A'")) + print("Decimal to HEX D'26' =", calc.evaluate("decimal_to_hex", "D'26'")) + print("-" * 50) + + # ------------------------------- + # Complement Operations + # ------------------------------- + print("Complement Operations:") + print("15's complement of H'A' =", calc.evaluate("fifteen_complement", "H'A'")) + print("16's complement of H'A' =", calc.evaluate("sixteen_complement", "H'A'")) + print("-" * 50) + + # ------------------------------- + # More HEX Examples + # ------------------------------- + print("More HEX Examples:") + print("H'AB' + H'CD' =", calc.evaluate("add", "H'AB'", "H'CD'")) + print("H'100' - H'1' =", calc.evaluate("subtract", "H'100'", "H'1'")) + print("H'AB' * H'CD' =", calc.evaluate("multiply", "H'AB'", "H'CD'")) + print("H'88EF' / H'AB' =", calc.evaluate("divide", "H'88EF'", "H'AB'")) + print("-" * 50) + + # ------------------------------- + # Error Handling Examples + # ------------------------------- + print("Error Handling Examples:") + + try: + print("Trying division by zero...") + print(calc.evaluate("divide", "H'A'", "H'0'")) + except ValueError as e: + print("Caught Error:", e) + + try: + print("Trying invalid HEX input...") + print(calc.evaluate("add", "H'G1'", "H'2'")) + except ValueError as e: + print("Caught Error:", e) + + try: + print("Trying unsupported operation...") + print(calc.evaluate("modulus", "H'A'", "H'2'")) + except ValueError as e: + print("Caught Error:", e) + +if __name__ == "__main__": + main() + +``` + +````md +## Expected Output + +```text +## Current Mode: 4 + +Arithmetic Operations: +H'A' + H'5' = H'F' +H'F' - H'5' = H'A' +H'3' \* H'4' = H'C' +H'1A' / H'3' = H'8' + +--- + +Conversion Operations: +HEX to Decimal H'1A' = D'26' +Decimal to HEX D'26' = H'1A' + +--- + +Complement Operations: +15's complement of H'A' = H'5' +16's complement of H'A' = H'6' + +--- + +More HEX Examples: +H'AB' + H'CD' = H'178' +H'100' - H'1' = H'FF' +H'AB' \* H'CD' = H'88EF' +H'88EF' / H'AB' = H'CD' + +--- + +Error Handling Examples: +Trying division by zero... +Caught Error: Division by zero +Trying invalid HEX input... +Caught Error: Invalid hexadecimal digits +Trying unsupported operation... +Caught Error: Unsupported HEX operation: modulus +``` +```` + +````md +## πŸ§ͺ Unit Testing + +This project uses Python’s built-in `unittest` framework. + +### Run all tests + +```bash +python -m unittest +``` +```` + +**Run specific HEX tests** + +```bash +python -m unittest test_calculator.py +``` + +**Run specific hex funxtions** + +```bash +python -m unittest test*hex.TestHexCalculator.test* -v +``` + +**Run calculator tests** + +```bash +python -m unittest test_calculator.py +``` +````` diff --git a/test_calculator.py b/test_calculator.py index 7cb79e9..840d3ca 100644 --- a/test_calculator.py +++ b/test_calculator.py @@ -1,33 +1,121 @@ import unittest from calculator import Calculator + class TestCalculator(unittest.TestCase): # base test cases def setUp(self): self.calc = Calculator() - def test_add(self): + # --------------------------------- + # BASIC DECIMAL TESTS AS PER ORIGINAL CALCULATOR + # --------------------------------- + def test_add_decimal(self): self.assertEqual(self.calc.add(2, 3), 5) - def test_sub(self): + def test_sub_decimal(self): self.assertEqual(self.calc.subtract(2, 3), -1) - def test_multiply(self): + def test_multiply_decimal(self): self.assertEqual(self.calc.multiply(2, 3), 6) - def test_divide(self): + def test_divide_decimal_positive(self): self.assertEqual(self.calc.divide(2, 4), 0.5) - def test_divide(self): + def test_divide_decimal_negative(self): self.assertEqual(self.calc.divide(4, -2), -2) - - def test_divide_fail(self): # this will fail + + def test_divide_fail(self): # this should still pass self.assertNotEqual(self.calc.divide(4, -2), 2) - def test_divide_by_zero(self): + def test_divide_by_zero_decimal(self): with self.assertRaises(ValueError): self.calc.divide(5, 0) + # --------------------------------- + # MODE SETTING TESTS + # --------------------------------- + def test_set_mode_string(self): + self.calc.set_mode("hex") + self.assertEqual(self.calc.get_mode(), "hex") + + def test_set_mode_number(self): + self.calc.set_mode(4) + self.assertEqual(self.calc.get_mode(), "hex") + + def test_invalid_mode_string(self): + with self.assertRaises(ValueError): + self.calc.set_mode("abc") + + def test_invalid_mode_number(self): + with self.assertRaises(ValueError): + self.calc.set_mode(99) + + # --------------------------------- + # DIRECT HEX MODE TESTS + # --------------------------------- + def test_hex_add_direct(self): + self.calc.set_mode("hex") + self.assertEqual(self.calc.add("H'2'", "H'3'"), "H'5'") + + def test_hex_subtract_direct(self): + self.calc.set_mode("hex") + self.assertEqual(self.calc.subtract("H'2'", "H'3'"), "H'-1'") + + def test_hex_multiply_direct(self): + self.calc.set_mode("hex") + self.assertEqual(self.calc.multiply("H'2'", "H'3'"), "H'6'") + + def test_hex_divide_direct(self): + self.calc.set_mode("hex") + self.assertEqual(self.calc.divide("H'4'", "H'2'"), "H'2'") + + def test_hex_divide_by_zero(self): + self.calc.set_mode("hex") + with self.assertRaises(ValueError): + self.calc.divide("H'5'", "H'0'") + + # --------------------------------- + # EVALUATE() TESTS + # --------------------------------- + def test_evaluate_add(self): + self.calc.set_mode("hex") + self.assertEqual(self.calc.evaluate("add", "H'A'", "H'5'"), "H'F'") + + def test_evaluate_subtract(self): + self.calc.set_mode("hex") + self.assertEqual(self.calc.evaluate("subtract", "H'F'", "H'5'"), "H'A'") + + def test_evaluate_multiply(self): + self.calc.set_mode("hex") + self.assertEqual(self.calc.evaluate("multiply", "H'3'", "H'4'"), "H'C'") + + def test_evaluate_divide(self): + self.calc.set_mode("hex") + self.assertEqual(self.calc.evaluate("divide", "H'1A'", "H'3'"), "H'8'") + + def test_evaluate_hex_to_decimal(self): + self.calc.set_mode("hex") + self.assertEqual(self.calc.evaluate("hex_to_decimal", "H'1A'"), "D'26'") + + def test_evaluate_decimal_to_hex(self): + self.calc.set_mode("hex") + self.assertEqual(self.calc.evaluate("decimal_to_hex", "D'26'"), "H'1A'") + + def test_evaluate_fifteen_complement(self): + self.calc.set_mode("hex") + self.assertEqual(self.calc.evaluate("fifteen_complement", "H'A'"), "H'5'") + + def test_evaluate_sixteen_complement(self): + self.calc.set_mode("hex") + self.assertEqual(self.calc.evaluate("sixteen_complement", "H'A'"), "H'6'") + + def test_evaluate_invalid_operation(self): + self.calc.set_mode("hex") + with self.assertRaises(ValueError): + self.calc.evaluate("modulus", "H'A'", "H'2'") + + # Optional: this allows running the script directly - if __name__ == '__main__': - unittest.main() # +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/test_hex.py b/test_hex.py new file mode 100644 index 0000000..2d7cde8 --- /dev/null +++ b/test_hex.py @@ -0,0 +1,432 @@ +import unittest +from hex import HexCalculator + + +class TestHexCalculator(unittest.TestCase): + + def setUp(self): + self.hex_calc = HexCalculator() + + def test_hex_to_decimal(self): + # ----- NORMAL CASES ----- + # Case 1: Basic conversion + self.assertEqual(self.hex_calc.hex_to_decimal("H'1A5'"), "D'421'") + + # Case 2: Single digit hex to decimal + self.assertEqual(self.hex_calc.hex_to_decimal("H'A'"), "D'10'") + + # Case 3: Two digit hex conversion + self.assertEqual(self.hex_calc.hex_to_decimal("H'FF'"), "D'255'") + + # Case 4: Larger hex number + self.assertEqual(self.hex_calc.hex_to_decimal("H'100'"), "D'256'") + + # Case 5: Multi-digit hex conversion + self.assertEqual(self.hex_calc.hex_to_decimal("H'BEEF'"), "D'48879'") + + # ----- BOUNDARY CONDITIONS ----- + # Boundary 1: Hex zero + self.assertEqual(self.hex_calc.hex_to_decimal("H'0'"), "D'0'") + + # Boundary 2: Hex one + self.assertEqual(self.hex_calc.hex_to_decimal("H'1'"), "D'1'") + + # Boundary 3: Hex with leading zeros + self.assertEqual(self.hex_calc.hex_to_decimal("H'00FF'"), "D'255'") + + # Boundary 4: Large hex number (max 16-bit) + self.assertEqual(self.hex_calc.hex_to_decimal("H'FFFF'"), "D'65535'") + + # Boundary 5: Hex with negative number + self.assertEqual(self.hex_calc.hex_to_decimal("H'-A'"), "D'-10'") + + # ----- INVALID INPUT HANDLING ----- + # Invalid Case 1: Invalid format - missing prefix + with self.assertRaises(ValueError): + self.hex_calc.hex_to_decimal("1A5") + + # Invalid Case 2: Invalid format - wrong prefix + with self.assertRaises(ValueError): + self.hex_calc.hex_to_decimal("D'1A5'") + + # Invalid Case 3: Invalid format with incorrect brackets + with self.assertRaises(ValueError): + self.hex_calc.hex_to_decimal("H[1A5]") + + # Invalid Case 4: Invalid hex digit (G is not valid) + with self.assertRaises(ValueError): + self.hex_calc.hex_to_decimal("H'G5'") + + # Invalid Case 5: Non-string input (integer) + with self.assertRaises((ValueError, TypeError)): + self.hex_calc.hex_to_decimal(421) + + def test_decimal_to_hex(self): + # ----- NORMAL CASES ----- + # Case 1: Basic conversion + self.assertEqual(self.hex_calc.decimal_to_hex("D'243'"), "H'F3'") + + # Case 2: Single digit decimal to hex + self.assertEqual(self.hex_calc.decimal_to_hex("D'10'"), "H'A'") + + # Case 3: Two digit decimal conversion + self.assertEqual(self.hex_calc.decimal_to_hex("D'255'"), "H'FF'") + + # Case 4: Larger decimal number + self.assertEqual(self.hex_calc.decimal_to_hex("D'256'"), "H'100'") + + # Case 5: Multi-digit decimal conversion + self.assertEqual(self.hex_calc.decimal_to_hex("D'48879'"), "H'BEEF'") + + # ----- BOUNDARY CONDITIONS ----- + # Boundary 1: Decimal zero + self.assertEqual(self.hex_calc.decimal_to_hex("D'0'"), "H'0'") + + # Boundary 2: Decimal one + self.assertEqual(self.hex_calc.decimal_to_hex("D'1'"), "H'1'") + + # Boundary 3: Decimal with leading zeros + self.assertEqual(self.hex_calc.decimal_to_hex("D'00255'"), "H'FF'") + + # Boundary 4: Large decimal number + self.assertEqual(self.hex_calc.decimal_to_hex("D'65535'"), "H'FFFF'") + + # Boundary 5: Decimal with negative number + self.assertEqual(self.hex_calc.decimal_to_hex("D'-10'"), "H'-A'") + + # ----- INVALID INPUT HANDLING ----- + # Invalid Case 1: Invalid format - missing prefix + with self.assertRaises(ValueError): + self.hex_calc.decimal_to_hex("243") + + # Invalid Case 2: Invalid format - wrong prefix + with self.assertRaises(ValueError): + self.hex_calc.decimal_to_hex("H'243'") + + # Invalid Case 3: Invalid format with incorrect brackets + with self.assertRaises(ValueError): + self.hex_calc.decimal_to_hex("D[243]") + + # Invalid Case 4: Non-string input (integer) + with self.assertRaises((ValueError, TypeError)): + self.hex_calc.decimal_to_hex(243) + + # Invalid Case 5: None input + with self.assertRaises((ValueError, TypeError)): + self.hex_calc.decimal_to_hex(None) + + def test_add(self): + self.assertEqual( + self.hex_calc.add("H'A'", "H'5'"), + "H'F'" + ) + + # ----- NORMAL CASES ----- + # Case 1: Basic single-digit addition without carry + self.assertEqual(self.hex_calc.add("H'A'", "H'5'"), "H'F'") + + # Case 2: Multi-digit addition without carry + self.assertEqual(self.hex_calc.add("H'1A'", "H'2'"), "H'1C'") + + # Case 3: Multi-digit addition with carry propagation + self.assertEqual(self.hex_calc.add("H'ABC'", "H'123'"), "H'BDF'") + + # Case 4: Simple two-digit hex addition + self.assertEqual(self.hex_calc.add("H'12'", "H'34'"), "H'46'") + + # Case 5: Addition of larger numbers + self.assertEqual(self.hex_calc.add("H'ABCD'", "H'1234'"), "H'BE01'") + + # ----- BOUNDARY CONDITIONS ----- + # Boundary 1: Addition with carry (single digit overflow) + self.assertEqual(self.hex_calc.add("H'7'", "H'9'"), "H'10'") + + # Boundary 2: Overflow to next digit group (all F's case) + self.assertEqual(self.hex_calc.add("H'FF'", "H'1'"), "H'100'") + + # Boundary 3: Addition with zero (identity element) + self.assertEqual(self.hex_calc.add("H'0'", "H'AB'"), "H'AB'") + + # Boundary 4: Addition with leading zeros (normalization) + self.assertEqual(self.hex_calc.add("H'00A'", "H'005'"), "H'F'") + + # Boundary 5: Addition with negative numbers (negative result) + self.assertEqual(self.hex_calc.add("H'5'", "H'-3'"), "H'2'") + + # Boundary 6: Addition resulting in negative number + self.assertEqual(self.hex_calc.add("H'1'", "H'-2'"), "H'-1'") + + # ----- INVALID INPUT HANDLING ----- + # Invalid Case 1: Invalid hex digit (G is not valid in hexadecimal) + with self.assertRaises(ValueError): + self.hex_calc.add("H'G1'", "H'2'") + + # Invalid Case 2: Invalid hex digit in second operand (Z is not valid) + with self.assertRaises(ValueError): + self.hex_calc.add("H'A'", "H'Z5'") + + # Invalid Case 3: Missing hex format prefix in first operand + with self.assertRaises(ValueError): + self.hex_calc.add("A5", "H'B'") + + # Invalid Case 4: Missing hex format prefix in second operand + with self.assertRaises(ValueError): + self.hex_calc.add("H'A'", "B5") + + # Invalid Case 5: Invalid format with incorrect brackets + with self.assertRaises(ValueError): + self.hex_calc.add("H[A5]", "H'B'") + + def test_multiply(self): + # ----- NORMAL CASES ----- + # Case 1: Basic single-digit multiplication without carry + self.assertEqual(self.hex_calc.multiply("H'3'", "H'4'"), "H'C'") + + # Case 2: Multi-digit multiplication without carry + self.assertEqual(self.hex_calc.multiply("H'2'", "H'10'"), "H'20'") + + # Case 3: Multi-digit multiplication with carry propagation + self.assertEqual(self.hex_calc.multiply("H'1A'", "H'2'"), "H'34'") + + # Case 4: Simple two-digit hex multiplication + self.assertEqual(self.hex_calc.multiply("H'5'", "H'6'"), "H'1E'") + + # Case 5: Multiplication of larger numbers + self.assertEqual(self.hex_calc.multiply("H'AB'", "H'CD'"), "H'88EF'") + + # ----- BOUNDARY CONDITIONS ----- + # Boundary 1: Multiplication with zero (annihilator element) + self.assertEqual(self.hex_calc.multiply("H'A'", "H'0'"), "H'0'") + + # Boundary 2: Multiplication with one (identity element) + self.assertEqual(self.hex_calc.multiply("H'A'", "H'1'"), "H'A'") + + # Boundary 3: Multiplication resulting in zero + self.assertEqual(self.hex_calc.multiply("H'A'", "H'-0'"), "H'0'") + + # Boundary 4: Multiplication with negative numbers (negative result) + self.assertEqual(self.hex_calc.multiply("H'-3'", "H'4'"), "H'-C'") + + # Boundary 5: Multiplication resulting in negative number + self.assertEqual(self.hex_calc.multiply("H'-2'", "H'-3'"), "H'6'") + + # ----- INVALID INPUT HANDLING ----- + # Invalid Case 1: Invalid hex digit (G is not valid in hexadecimal) + with self.assertRaises(ValueError): + self.hex_calc.multiply("H'G1'", "H'2'") + + # Invalid Case 2: Invalid hex digit in second operand (Z is not valid) + with self.assertRaises(ValueError): + self.hex_calc.multiply("H'A'", "H'Z5'") + + # Invalid Case 3: Missing hex format prefix in first operand + with self.assertRaises(ValueError): + self.hex_calc.multiply("A5", "H'B'") + + # Invalid Case 4: Missing hex format prefix in second operand + with self.assertRaises(ValueError): + self.hex_calc.multiply("H'A'", "B5") + + # Invalid Case 5: Invalid format with incorrect brackets + with self.assertRaises(ValueError): + self.hex_calc.multiply("H[A5]", "H'B'") + + def test_divide(self): + + # ----- NORMAL CASES ----- + # Case 1: Basic single-digit division without remainder + self.assertEqual(self.hex_calc.divide("H'C'", "H'4'"), "H'3'") + + # Case 2: Multi-digit division without remainder + self.assertEqual(self.hex_calc.divide("H'20'", "H'2'"), "H'10'") + + # Case 3: Multi-digit division with remainder (integer division) + self.assertEqual(self.hex_calc.divide("H'1A'", "H'3'"), "H'8'") + + # Case 4: Simple two-digit hex division + self.assertEqual(self.hex_calc.divide("H'1E'", "H'6'"), "H'5'") + + # Case 5: Division of larger numbers + self.assertEqual(self.hex_calc.divide("H'88EF'", "H'AB'"), "H'CD'") + + # ----- BOUNDARY CONDITIONS ----- + # Boundary 1: Division by one (identity element) + self.assertEqual(self.hex_calc.divide("H'A'", "H'1'"), "H'A'") + + # Boundary 2: Division resulting in zero + self.assertEqual(self.hex_calc.divide("H'1'", "H'2'"), "H'0'") + + # Boundary 3: Division with negative numbers (negative result) + self.assertEqual(self.hex_calc.divide("H'-C'", "H'4'"), "H'-3'") + + # Boundary 4: Division resulting in negative number + self.assertEqual(self.hex_calc.divide("H'-6'", "H'-2'"), "H'3'") + + # ----- INVALID INPUT HANDLING ----- + # Invalid Case 1: Division by zero + with self.assertRaises(ValueError): + self.hex_calc.divide("H'A'", "H'0'") + + # Invalid Case 2: Invalid hex digit (G is not valid in hexadecimal) + with self.assertRaises(ValueError): + self.hex_calc.divide("H'G1'", "H'2'") + + # Invalid Case 3: Invalid hex digit in second operand (Z is not valid) + with self.assertRaises(ValueError): + self.hex_calc.divide("H'A'", "H'Z5'") + + # Invalid Case 4: Missing hex format prefix in first operand + with self.assertRaises(ValueError): + self.hex_calc.divide("A5", "H'B'") + + # Invalid Case 5: Missing hex format prefix in second operand + with self.assertRaises(ValueError): + self.hex_calc.divide("H'A'", "B5") + + def test_subtract_all_cases(self): + # Basic subtract scenarios + self.assertEqual(self.hex_calc.subtract("H'F'", "H'5'"), "H'A'") + self.assertEqual(self.hex_calc.subtract("H'10'", "H'1'"), "H'F'") + self.assertEqual(self.hex_calc.subtract("H'2'", "H'5'"), "H'-3'") + + # Functional edge cases + self.assertEqual(self.hex_calc.subtract("H'0'", "H'0'"), "H'0'") + self.assertEqual(self.hex_calc.subtract("H'1A3F'", "H'1A3F'"), "H'0'") + self.assertEqual(self.hex_calc.subtract("H'1000'", "H'1'"), "H'FFF'") + self.assertEqual(self.hex_calc.subtract("H'00A'", "H'0001'"), "H'9'") + self.assertEqual(self.hex_calc.subtract("H'a'", "H'3'"), "H'7'") + self.assertEqual(self.hex_calc.subtract("H'ABCDEF'", "H'123456'"), "H'999999'") + self.assertEqual(self.hex_calc.subtract("H'1'", "H'A'"), "H'-9'") + self.assertEqual(self.hex_calc.subtract("H'10'", "H'1F'"), "H'-F'") + + # Validation and input-type corner cases + with self.assertRaises(ValueError): + self.hex_calc.subtract("H'1G'", "H'1'") + + with self.assertRaises(ValueError): + self.hex_calc.subtract("H'1'", "1") + + with self.assertRaises(ValueError): + self.hex_calc.subtract(10, "H'1'") + + with self.assertRaises(ValueError): + self.hex_calc.subtract("H'1'", None) + + def test_fifteen_complement(self): + # ----- NORMAL CASES ----- + # Case 1: Basic single-digit complement + self.assertEqual(self.hex_calc.fifteen_complement("H'A'"), "H'5'") + + # Case 2: Another single-digit complement + self.assertEqual(self.hex_calc.fifteen_complement("H'3'"), "H'C'") + + # Case 3: Multi-digit complement + self.assertEqual(self.hex_calc.fifteen_complement("H'1A'"), "H'E5'") + + # Case 4: Larger number complement + self.assertEqual(self.hex_calc.fifteen_complement("H'ABC'"), "H'543'") + + # Case 5: Complement of multi-digit number + self.assertEqual(self.hex_calc.fifteen_complement("H'ABCD'"), "H'5432'") + + # ----- BOUNDARY CONDITIONS ----- + # Boundary 1: Complement of zero + self.assertEqual(self.hex_calc.fifteen_complement("H'0'"), "H'F'") + + # Boundary 2: Complement of F (should be 0) + self.assertEqual(self.hex_calc.fifteen_complement("H'F'"), "H'0'") + + # Boundary 3: Complement with leading zeros + self.assertEqual(self.hex_calc.fifteen_complement("H'00A'"), "H'FF5'") + + # Boundary 4: Single digit 1 + self.assertEqual(self.hex_calc.fifteen_complement("H'1'"), "H'E'") + + # Boundary 5: All F's complement + self.assertEqual(self.hex_calc.fifteen_complement("H'FF'"), "H'00'") + + # ----- INVALID INPUT HANDLING ----- + # Invalid Case 1: Invalid hex digit (G is not valid in hexadecimal) + with self.assertRaises(ValueError): + self.hex_calc.fifteen_complement("H'G1'") + + # Invalid Case 2: Missing hex format prefix + with self.assertRaises(ValueError): + self.hex_calc.fifteen_complement("A5") + + # Invalid Case 3: Invalid format with incorrect brackets + with self.assertRaises(ValueError): + self.hex_calc.fifteen_complement("H[A5]") + + # Invalid Case 4: Invalid hex digit Z + with self.assertRaises(ValueError): + self.hex_calc.fifteen_complement("H'Z5'") + + # Invalid Case 5: Empty hex value + with self.assertRaises(ValueError): + self.hex_calc.fifteen_complement("H''") + + def test_sixteen_complement(self): + # ----- NORMAL CASES ----- + # Case 1: Basic single-digit complement + self.assertEqual(self.hex_calc.sixteen_complement("H'A'"), "H'6'") + + # Case 2: Another single-digit complement + self.assertEqual(self.hex_calc.sixteen_complement("H'3'"), "H'D'") + + # Case 3: Multi-digit complement + self.assertEqual(self.hex_calc.sixteen_complement("H'1A'"), "H'E6'") + + # Case 4: Larger number complement + self.assertEqual(self.hex_calc.sixteen_complement("H'ABC'"), "H'544'") + + # Case 5: Complement of multi-digit number + self.assertEqual(self.hex_calc.sixteen_complement("H'ABCD'"), "H'5433'") + + # ----- BOUNDARY CONDITIONS ----- + # Boundary 1: Complement of zero + self.assertEqual(self.hex_calc.sixteen_complement("H'0'"), "H'0'") + + # Boundary 2: Complement of 1 + self.assertEqual(self.hex_calc.sixteen_complement("H'1'"), "H'F'") + + # Boundary 3: Complement with leading zeros + self.assertEqual(self.hex_calc.sixteen_complement("H'00A'"), "H'F6'") + + # Boundary 4: Complement of F + self.assertEqual(self.hex_calc.sixteen_complement("H'F'"), "H'1'") + + # Boundary 5: All F's complement + self.assertEqual(self.hex_calc.sixteen_complement("H'FF'"), "H'1'") + + # ----- INVALID INPUT HANDLING ----- + # Invalid Case 1: Invalid hex digit (G is not valid in hexadecimal) + with self.assertRaises(ValueError): + self.hex_calc.sixteen_complement("H'G1'") + + # Invalid Case 2: Missing hex format prefix + with self.assertRaises(ValueError): + self.hex_calc.sixteen_complement("A5") + + # Invalid Case 3: Invalid format with incorrect brackets + with self.assertRaises(ValueError): + self.hex_calc.sixteen_complement("H[A5]") + + # Invalid Case 4: Invalid hex digit Z + with self.assertRaises(ValueError): + self.hex_calc.sixteen_complement("H'Z5'") + + # Invalid Case 5: Empty hex value + with self.assertRaises(ValueError): + self.hex_calc.sixteen_complement("H''") + + # --------------------------------- + # COMMON VALIDATION TEST + # --------------------------------- + def test_invalid_hex(self): + with self.assertRaises(ValueError): + self.hex_calc.hex_to_decimal("123") + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file