diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..eb2c507 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,32 @@ +name: Run Tests + +on: + push: + branches: ["**"] + pull_request: + branches: ["**"] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install dependencies + run: pip install pytest pytest-cov + + - name: Run base calculator tests + run: pytest test_calculator.py -v + + - name: Run binary module tests + run: pytest tests/testbinary.py -v + + - name: Coverage report + run: pytest tests/testbinary.py --cov=modules --cov-report=term-missing \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9fb5e81 --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +# Virtual environment +.venv/ +env/ +venv/ + +# Python cache +__pycache__/ +*.pyc +*.pyo +*.pyd + +# Pytest cache +.pytest_cache/ +.coverage +htmlcov/ + +# IDE +.vscode/ +.idea/ + +# OS +.DS_Store +Thumbs.db \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..c586371 --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +# SE Calculator — Feature 5.6: Binary Number System + +Group 6 | Branch: FeatureA_6 | Process: Classical Waterfall + +## Features +- Binary ↔ Decimal conversion +- Binary arithmetic (add, sub, mul, div) +- 1's and 2's complement +- Input validation with custom exceptions + +## Running Tests + pytest tests/testbinary.py -v + +## Input Format +- Binary: B'1010 +- Decimal: D'10 diff --git a/calculator.py b/calculator.py index 5daac9a..e3b8c90 100644 --- a/calculator.py +++ b/calculator.py @@ -1,11 +1,57 @@ +import re +from modules.binary import ( + binary_to_decimal, decimal_to_binary, + binary_add, binary_subtract, + binary_multiply, binary_divide, + ones_complement, twos_complement, +) + + class Calculator: - def add(self, a, b): - return a + b - def subtract(self, a, b): - return a - b - def multiply(self, a, b): - return a * b + def add(self, a, b): return a + b + def subtract(self, a, b): return a - b + def multiply(self, a, b): return a * b def divide(self, a, b): if b == 0: raise ValueError("Division by zero") return a / b + + def evaluate(self, expression: str) -> str: + """ + Route string expressions to the correct module. + Examples: + "bin(10)" -> "B'1010" + "dec(B'1010)" -> "D'10" + "1s(B'1010)" -> "B'0101" + "2s(B'0111)" -> "B'1001" + "B'011 + B'010" -> "B'101" + """ + expr = expression.strip() + + if re.match(r"^bin\(", expr, re.I): + val = re.search(r"\((.+)\)", expr).group(1).strip() + return decimal_to_binary(f"D'{val}" if not val.upper().startswith("D'") else val) + + if re.match(r"^dec\(", expr, re.I): + val = re.search(r"\((.+)\)", expr).group(1).strip() + return binary_to_decimal(val) + + if re.match(r"^1s\(", expr, re.I): + val = re.search(r"\((.+)\)", expr).group(1).strip() + return ones_complement(val) + + if re.match(r"^2s\(", expr, re.I): + val = re.search(r"\((.+)\)", expr).group(1).strip() + return twos_complement(val) + + # Binary arithmetic: "B'011 + B'010" + m = re.match( + r"(B'[01]+)\s*([\+\-\*\/])\s*(B'[01]+)", expr, re.I + ) + if m: + a, op, b = m.group(1), m.group(2), m.group(3) + ops = {'+': binary_add, '-': binary_subtract, + '*': binary_multiply, '/': binary_divide} + return ops[op](a, b) + + raise ValueError(f"Unrecognised expression: {expression}") \ No newline at end of file diff --git a/modules/__init__.py b/modules/__init__.py new file mode 100644 index 0000000..eefee6b --- /dev/null +++ b/modules/__init__.py @@ -0,0 +1,10 @@ +from .binary import ( + binary_to_decimal, + decimal_to_binary, + binary_add, + binary_subtract, + binary_multiply, + binary_divide, + ones_complement, + twos_complement, +) \ No newline at end of file diff --git a/modules/binary.py b/modules/binary.py new file mode 100644 index 0000000..063df4e --- /dev/null +++ b/modules/binary.py @@ -0,0 +1,116 @@ +# -- Pritam 038 ------------------------------------------------------- + +from .exceptions import InvalidBinaryInputError, NegativeBinaryResultError + + +def _strip(b: str) -> str: + """Strip B' prefix and whitespace.""" + b = b.strip().upper() + if b.startswith("B'"): + b = b[2:] + return b + + +def _validate(b: str) -> None: + if not b or not all(c in '01' for c in b): + raise InvalidBinaryInputError(b) + + +def binary_to_decimal(b: str) -> str: + """ + Convert binary string to decimal. + e.g. binary_to_decimal("B'1010") -> "D'10" + """ + raw = _strip(b) + _validate(raw) + return f"D'{int(raw, 2)}" + + +def decimal_to_binary(d: str) -> str: + """ + Convert decimal string to binary. + e.g. decimal_to_binary("D'10") -> "B'1010" + """ + d = d.strip().upper() + if d.startswith("D'"): + d = d[2:] + n = int(d) + if n < 0: + raise ValueError("Negative decimal not supported in basic mode") + return f"B'{bin(n)[2:]}" + + + +# ── Saiyam 037: Arithmetic ────────────────────────────────────────────────────── + +def binary_add(a: str, b: str) -> str: + """ + Add two binary numbers. + e.g. binary_add("B'011", "B'010") -> "B'101" + """ + da = int(binary_to_decimal(a).split("'")[1]) + db = int(binary_to_decimal(b).split("'")[1]) + return decimal_to_binary(f"D'{da + db}") + + +def binary_subtract(a: str, b: str) -> str: + """ + Subtract two binary numbers. + e.g. binary_subtract("B'101", "B'010") -> "B'011" + """ + da = int(binary_to_decimal(a).split("'")[1]) + db = int(binary_to_decimal(b).split("'")[1]) + if da < db: + raise NegativeBinaryResultError() + bit_len = max(len(_strip(a)), len(_strip(b))) + result = bin(da - db)[2:].zfill(bit_len) + return f"B'{result}" + + +def binary_multiply(a: str, b: str) -> str: + """ + Multiply two binary numbers. + e.g. binary_multiply("B'011", "B'010") -> "B'110" + """ + da = int(binary_to_decimal(a).split("'")[1]) + db = int(binary_to_decimal(b).split("'")[1]) + return decimal_to_binary(f"D'{da * db}") + + +def binary_divide(a: str, b: str) -> str: + """ + Integer divide two binary numbers. + e.g. binary_divide("B'110", "B'010") -> "B'11" + """ + da = int(binary_to_decimal(a).split("'")[1]) + db = int(binary_to_decimal(b).split("'")[1]) + if db == 0: + raise ValueError("Binary division by zero") + return decimal_to_binary(f"D'{da // db}") + + + +#-----------suhani 034: complement------------------------------- + + +def ones_complement(b: str) -> str: + """ + Flip all bits. + e.g. ones_complement("B'1010") -> "B'0101" + """ + raw = _strip(b) + _validate(raw) + return "B'" + ''.join('1' if bit == '0' else '0' for bit in raw) + + +def twos_complement(b: str) -> str: + """ + Add 1 to the one's complement. + e.g. twos_complement("B'0111") -> "B'1001" + """ + raw = _strip(b) + _validate(raw) + ones = ones_complement(f"B'{raw}")[2:] + val = int(ones, 2) + 1 # no modulo + result = bin(val)[2:].zfill(len(ones)) # zfill won't truncate carry + return f"B'{result}" \ No newline at end of file diff --git a/modules/exceptions.py b/modules/exceptions.py new file mode 100644 index 0000000..56b13f2 --- /dev/null +++ b/modules/exceptions.py @@ -0,0 +1,9 @@ + +# ---- Harsh 035: Exceptions ---------- +class InvalidBinaryInputError(ValueError): + def __init__(self, value: str): + super().__init__(f"'{value}' is not a valid binary string") + +class NegativeBinaryResultError(ValueError): + def __init__(self): + super().__init__("Binary subtraction result cannot be negative") diff --git a/test_calculator.py b/test_calculator.py index 7cb79e9..5bb6c42 100644 --- a/test_calculator.py +++ b/test_calculator.py @@ -15,13 +15,13 @@ def test_sub(self): def test_multiply(self): self.assertEqual(self.calc.multiply(2, 3), 6) - def test_divide(self): + def test_divide_positive(self): self.assertEqual(self.calc.divide(2, 4), 0.5) - def test_divide(self): + def test_divide_negative(self): self.assertEqual(self.calc.divide(4, -2), -2) - - def test_divide_fail(self): # this will fail + + def test_divide_fail(self): self.assertNotEqual(self.calc.divide(4, -2), 2) def test_divide_by_zero(self): diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..9d76c9a --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +from .testbinary import * \ No newline at end of file diff --git a/tests/testbinary.py b/tests/testbinary.py new file mode 100644 index 0000000..702c3e5 --- /dev/null +++ b/tests/testbinary.py @@ -0,0 +1,85 @@ + +# ── deepjit pal (036): Exceptions───────────────────────────────────────────────────── + + +import unittest +from modules.binary import ( + binary_to_decimal, decimal_to_binary, + binary_add, binary_subtract, + binary_multiply, binary_divide, + ones_complement, twos_complement, +) +from modules.exceptions import InvalidBinaryInputError, NegativeBinaryResultError + + +class TestBinaryConversions(unittest.TestCase): + def test_bin_to_dec_basic(self): + self.assertEqual(binary_to_decimal("B'1010"), "D'10") + + def test_bin_to_dec_zero(self): + self.assertEqual(binary_to_decimal("B'0"), "D'0") + + def test_bin_to_dec_one(self): + self.assertEqual(binary_to_decimal("B'1"), "D'1") + + def test_dec_to_bin_basic(self): + self.assertEqual(decimal_to_binary("D'10"), "B'1010") + + def test_dec_to_bin_zero(self): + self.assertEqual(decimal_to_binary("D'0"), "B'0") + + +class TestBinaryArithmetic(unittest.TestCase): + def test_add(self): + self.assertEqual(binary_add("B'011", "B'010"), "B'101") + + def test_subtract(self): + self.assertEqual(binary_subtract("B'101", "B'010"), "B'011") + + def test_subtract_negative_raises(self): + with self.assertRaises(NegativeBinaryResultError): + binary_subtract("B'001", "B'010") + + def test_multiply(self): + self.assertEqual(binary_multiply("B'011", "B'010"), "B'110") + + def test_divide(self): + self.assertEqual(binary_divide("B'110", "B'010"), "B'11") + + def test_divide_by_zero(self): + with self.assertRaises(ValueError): + binary_divide("B'110", "B'0") + + +class TestComplements(unittest.TestCase): + def test_ones_complement(self): + self.assertEqual(ones_complement("B'1010"), "B'0101") + + def test_ones_all_zeros(self): + self.assertEqual(ones_complement("B'0000"), "B'1111") + + def test_twos_complement(self): + self.assertEqual(twos_complement("B'0111"), "B'1001") + + + def test_twos_complement_zero(self): + self.assertEqual(twos_complement("B'0000"), "B'10000") + + +class TestInvalidInput(unittest.TestCase): + def test_invalid_char(self): + with self.assertRaises(InvalidBinaryInputError): + binary_to_decimal("B'1021") + + def test_empty_string(self): + with self.assertRaises(InvalidBinaryInputError): + binary_to_decimal("B'") + + def test_missing_prefix(self): + with self.assertRaises(InvalidBinaryInputError): + binary_to_decimal("1021") + + +if __name__ == '__main__': + unittest.main() +