Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
51d8fd8
init structuring
prtm-bg Mar 16, 2026
13e6362
add readme file
SaiyamJana Mar 16, 2026
da50bbb
project structure creation
prtm-bg Mar 20, 2026
8527b63
feature: add binary arithmetic operations
SaiyamJana Mar 20, 2026
bd1f65b
Delete binop.py
prtm-bg Mar 20, 2026
b1ef391
Update binary.py
prtm-bg Mar 20, 2026
7e9cb82
Merge pull request #1 from prtm-bg/FeatureA_6
prtm-bg Mar 20, 2026
777c85c
Merge branch 'main' into FeatureA_6_arith
prtm-bg Mar 20, 2026
a8615e1
Merge pull request #2 from prtm-bg/FeatureA_6_arith
prtm-bg Mar 20, 2026
19f793b
feature: add binary complement operations
SuhaniBharti Mar 21, 2026
09edc93
Added invalid Check by Harsh
harshkr212 Mar 22, 2026
d7cb7e3
Merge pull request #3 from harshkr212/main
harshkr212 Mar 22, 2026
44d9aa2
Update binary.py by deepjit
deepjitpalmain-dot Mar 22, 2026
cf66032
Merge pull request #4 from deepjitpalmain-dot/main
deepjitpalmain-dot Mar 22, 2026
1cbdbe7
Revert "Update binary.py by deepjit"
prtm-bg Mar 22, 2026
d345fcc
Merge pull request #5 from prtm-bg/revert-4-main
prtm-bg Mar 22, 2026
4b59734
Revert "Add input validation for binary strings"
prtm-bg Mar 22, 2026
3933476
Merge pull request #6 from prtm-bg/revert-3-main
prtm-bg Mar 22, 2026
e91eefe
feature : validations
harshkr212 Mar 22, 2026
8ff6efb
Merge pull request #7 from harshkr212/FeatureA_6_validation
prtm-bg Mar 22, 2026
499ce2e
Merge pull request #10 from prtm-bg/Feature_A_6_comp
prtm-bg Mar 22, 2026
7739903
Delete modules/exceptions.py
deepjitpalmain-dot Mar 22, 2026
e6bc45e
Create test_binary.py
deepjitpalmain-dot Mar 22, 2026
0550227
Update binary.py
deepjitpalmain-dot Mar 22, 2026
6ecad9e
Create exceptions.py
deepjitpalmain-dot Mar 22, 2026
73f3e21
updated calculator.py and init.py
prtm-bg Mar 22, 2026
0c8c641
Update __init__.py
prtm-bg Mar 22, 2026
7a2dcc2
Delete modules/test_binary.py
deepjitpalmain-dot Mar 22, 2026
c89fc19
Update testbinary.py
deepjitpalmain-dot Mar 22, 2026
4936221
Merge branch 'main' into FeatureA_6_tests
prtm-bg Mar 22, 2026
ff7f6fc
Merge pull request #13 from deepjitpalmain-dot/FeatureA_6_tests
prtm-bg Mar 22, 2026
2a65dfb
Merge pull request #12 from prtm-bg/FeatureA_6
prtm-bg Mar 22, 2026
76dac91
Update test_calculator.py
prtm-bg Mar 22, 2026
594be35
Update README.md
prtm-bg Mar 22, 2026
fc35335
Create tests.yml
prtm-bg Mar 22, 2026
a300249
Create .gitignore
prtm-bg Mar 22, 2026
0a848aa
Update binary.py
prtm-bg Mar 22, 2026
68ad377
Update __init__.py
prtm-bg Mar 22, 2026
283e97b
Fix typo in README project structure
prtm-bg Mar 22, 2026
b71c849
Update README to remove project structure details
prtm-bg Mar 22, 2026
1ee3594
Update testbinary.py
prtm-bg Mar 22, 2026
4577c53
Merge branch 'main' of https://github.com/prtm-bg/SE_Calculator
prtm-bg Mar 22, 2026
81e499e
Update binary.py
prtm-bg Mar 22, 2026
b7b62ba
Update binary.py
prtm-bg Mar 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -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
23 changes: 23 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -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
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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
58 changes: 52 additions & 6 deletions calculator.py
Original file line number Diff line number Diff line change
@@ -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
Comment on lines +11 to +13
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These one-line method definitions reduce readability and make future edits (e.g., adding type hints/docstrings/logging) harder. Consider expanding them to the standard multi-line form to keep the class consistent with typical Python formatting.

Suggested change
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

Copilot uses AI. Check for mistakes.
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()
Comment on lines +32 to +44
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

evaluate() uses re.search(...).group(1) without checking the search matched. Inputs like bin() / bin(10 (missing closing paren) will raise AttributeError instead of a controlled ValueError. Consider switching to a single re.fullmatch() (or validating the search result) and raising a user-facing parse error when the expression is malformed.

Suggested change
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()
m = re.search(r"\((.+)\)", expr)
if not m:
raise ValueError(f"Malformed expression: {expression}")
val = m.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):
m = re.search(r"\((.+)\)", expr)
if not m:
raise ValueError(f"Malformed expression: {expression}")
val = m.group(1).strip()
return binary_to_decimal(val)
if re.match(r"^1s\(", expr, re.I):
m = re.search(r"\((.+)\)", expr)
if not m:
raise ValueError(f"Malformed expression: {expression}")
val = m.group(1).strip()
return ones_complement(val)
if re.match(r"^2s\(", expr, re.I):
m = re.search(r"\((.+)\)", expr)
if not m:
raise ValueError(f"Malformed expression: {expression}")
val = m.group(1).strip()

Copilot uses AI. Check for mistakes.
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)
Comment on lines +48 to +55
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The binary arithmetic regex matches only a prefix of the expression (no ^...$/fullmatch), so trailing junk like B'1 + B'1 foo would be accepted and partially evaluated. Anchor the pattern to the full string (or use re.fullmatch) so invalid expressions are rejected consistently.

Copilot uses AI. Check for mistakes.

raise ValueError(f"Unrecognised expression: {expression}")
Comment on lines +19 to +57
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

evaluate() is new public behavior but there are no tests exercising its supported expression forms (bin/dec/1s/2s/arithmetic) or its error handling for invalid expressions. Add unit tests (likely alongside test_calculator.py) to lock in the routing/parsing behavior and prevent regressions.

Copilot uses AI. Check for mistakes.
10 changes: 10 additions & 0 deletions modules/__init__.py
Original file line number Diff line number Diff line change
@@ -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,
)
116 changes: 116 additions & 0 deletions modules/binary.py
Original file line number Diff line number Diff line change
@@ -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
Comment on lines +108 to +115
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

twos_complement() can increase the bit-length when a carry occurs (e.g. input B'0000 returns B'10000). This is inconsistent with ones_complement() (which preserves width) and with typical two's-complement semantics (usually modulo 2^n). Consider masking/truncating to the original width (or explicitly documenting that overflow expands the width) so callers get predictable output.

Suggested change
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
Add 1 to the one's complement (modulo 2^n, preserving bit-width).
e.g. twos_complement("B'0111") -> "B'1001"
"""
raw = _strip(b)
_validate(raw)
bit_len = len(raw)
ones = ones_complement(b)[2:]
val = (int(ones, 2) + 1) & ((1 << bit_len) - 1)
result = format(val, f"0{bit_len}b")

Copilot uses AI. Check for mistakes.
return f"B'{result}"
9 changes: 9 additions & 0 deletions modules/exceptions.py
Original file line number Diff line number Diff line change
@@ -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")
8 changes: 4 additions & 4 deletions test_calculator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
1 change: 1 addition & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .testbinary import *
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tests/__init__.py re-exports tests via from .testbinary import *, which can introduce import side effects and can confuse test discovery/duplicate collection in some runners. If you don't need tests to act as a public package, prefer an empty __init__.py (or remove it entirely) and let pytest discover test modules directly.

Suggested change
from .testbinary import *

Copilot uses AI. Check for mistakes.
85 changes: 85 additions & 0 deletions tests/testbinary.py
Original file line number Diff line number Diff line change
@@ -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")
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test codifies twos_complement("B'0000") == "B'10000", which implies the implementation expands width on overflow. If the intent is fixed-width two's complement (mod 2^n), the expected value should stay the same width (e.g. B'0000). Please clarify the intended semantics and align the test with that definition.

Suggested change
self.assertEqual(twos_complement("B'0000"), "B'10000")
self.assertEqual(twos_complement("B'0000"), "B'0000")

Copilot uses AI. Check for mistakes.


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()

Loading