From 77cac6695ab2333afe45051e2d6310c023451072 Mon Sep 17 00:00:00 2001 From: CodingWithAishik Date: Sun, 22 Mar 2026 14:01:46 +0530 Subject: [PATCH 1/5] Team dev standards readme --- README.md | 247 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 247 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..0ac505e --- /dev/null +++ b/README.md @@ -0,0 +1,247 @@ +# Octal Calculator - Team Development Standards + +This document outlines coding standards and workflows for the 5-member development team to ensure seamless collaboration with minimal merge conflicts. + +--- + +## 1. Module Ownership & File Structure + +Each team member owns a specific module with a dedicated file. **Do not edit files outside your assigned module** except during integration review. + +| Member | Role | Module | File | Responsibility | +|--------|------|--------|------|-----------------| +| Member 1 | Parser | Input Parsing + Base Tag Handling | `parser.py` | Parse `O'247`, `D'123` format; extract mode prefix; normalize input | +| Member 2 | Validator | Input Validation Engine | `validator.py` | Validate octal/decimal characters; check format; error reporting | +| Member 3 | Converter | Octal ↔ Decimal Conversion | `converter.py` | Convert `O'247` ↔ `D'167`; preserve output format | +| Member 4 | Arithmetic | Octal Arithmetic + Complements | `arithmetic.py` | Add/subtract/multiply/divide in octal; 7's & 8's complements | +| Member 5 | Integration | Integration + E2E Testing | `calculator.py`, `test_calculator.py` | Wire all modules; orchestrate workflow; end-to-end testing | + +--- + +## 2. Branch & Commit Naming Conventions + +**Branch naming:** +``` +feature/member- +example: feature/member1-parser, feature/member2-validator +``` + +**Commit message format:** +``` +[ModuleName] Brief description + +[Parser] Add base prefix detection for O' and D' +[Validator] Validate octal characters (0-7 only) +[Converter] Implement octal to decimal conversion +[Arithmetic] Add octal addition operation +[Integration] Wire parser to validator +``` + +Keep commit messages focused on **one logical change per commit**. + +--- + +## 3. Code Style & Consistency + +### Python Style Guide +- **Indentation:** 4 spaces (not tabs) +- **Line length:** Max 100 characters +- **Naming conventions:** + - Functions/variables: `snake_case` (e.g., `parse_input`, `validate_octal`) + - Classes: `PascalCase` (e.g., `OctalParser`) + - Constants: `UPPER_SNAKE_CASE` +- **Type hints:** Use Python type annotations for all function parameters and return types + ```python + def parse_input(input_str: str) -> dict: + ``` +- **Docstrings:** Use Google-style docstrings + ```python + def convert_octal_to_decimal(octal_value: str) -> str: + """Converts octal value to decimal. + + Args: + octal_value: String in format "O'247" + + Returns: + Decimal string in format "D'167" + """ + ``` + +--- + +## 4. Function & Class Interfaces (Contracts) + +**Define & lock interfaces early.** Each module exports specific functions that downstream modules depend on. Do not change signatures without coordinating with dependent modules. + +### Member 1 - Parser Output Contract +```python +def parse_input(input_str: str) -> dict: + """ + Returns: { + 'base_mode': 'OCT' | 'DEC', + 'value': str, + 'is_valid': bool, + 'error': str | None + } + """ +``` + +### Member 2 - Validator Output Contract +```python +def validate_input(parsed: dict) -> dict: + """ + Returns: { + 'is_valid': bool, + 'error_message': str | None, + 'error_type': 'INVALID_CHAR' | 'FORMAT_ERROR' | None + } + """ +``` + +### Member 3 - Converter Output Contract +```python +def convert(value: str, from_base: str, to_base: str) -> str: + """ + Args: + value: numeric string (no prefix) + from_base: 'OCT' or 'DEC' + to_base: 'OCT' or 'DEC' + + Returns: Converted value with prefix (e.g., "O'247" or "D'167") + """ +``` + +### Member 4 - Arithmetic Output Contract +```python +def octal_add(operand1: str, operand2: str) -> str: + """Both operands assumed to be octal values (without prefix).""" + +def get_complement(value: str, complement_type: int) -> str: + """complement_type: 7 or 8""" +``` + +--- + +## 5. Testing Standards + +### Unit Tests (Each Member) +- Create `test_.py` for your module +- Test **only your module's functions** in isolation +- Name tests descriptively: `test_parse_octal_input`, `test_validate_invalid_char` +- Aim for >80% code coverage within your module + +**Example:** +```python +# test_parser.py +import unittest +from parser import parse_input + +class TestParser(unittest.TestCase): + def test_parse_octal_with_prefix(self): + result = parse_input("O'247") + self.assertEqual(result['base_mode'], 'OCT') + self.assertEqual(result['value'], '247') +``` + +### Integration Tests (Member 5) +- Create `test_calculator.py` (end-to-end workflows) +- Test complete flows: `parse → validate → convert → arithmetic` +- Example: `"O'247" + "O'15" = "O'264"` + +--- + +## 6. Import & Dependency Rules + +**Dependency Flow:** +``` +Calculator (Member 5) + ↓ +Parser (Member 1) → Validator (Member 2) → Converter (Member 3) → Arithmetic (Member 4) +``` + +**Rules:** +- Member 1 imports: Only standard library +- Member 2 imports: `parser` (from Member 1) +- Member 3 imports: `parser`, `validator` (from Members 1, 2) +- Member 4 imports: Only standard library +- Member 5 imports: All other modules + +**No circular imports allowed.** If you find yourself needing to import from a downstream module, refactor to extract common logic into a shared utility. + +--- + +## 7. Pull Request & Merge Workflow + +### Individual PRs (Members 1-4) +1. Create feature branch: `feature/member-` +2. Commit changes with tagged messages +3. Open PR with title: `[Member N] Implementation` +4. PR description should include: + - Functions implemented + - Test coverage + - Any interface changes (if applicable) +5. Require code review from Member 5 (Integration Lead) + +### Integration PR (Member 5) +1. After all 4 modules merged to `main`, Member 5 creates integration branch +2. Wire modules together in `calculator.py` +3. Run all integration tests +4. Open final PR: `[Integration] Octal Calculator E2E Implementation` + +--- + +## 8. Conflict Prevention Checklist + +**Before pushing, verify:** +- [ ] You've only edited your assigned module file(s) +- [ ] Your function signatures match the agreed contracts (Section 4) +- [ ] Your imports follow the dependency rules (Section 6) +- [ ] You've added unit tests for your code +- [ ] You've run your tests locally and they pass +- [ ] Your commit message follows the format in Section 2 +- [ ] You haven't modified other members' files + +--- + +## 9. Communication & Coordination + +- **Slack/Chat channel:** Use for quick questions about interfaces +- **Weekly sync:** 10 min standup before each PR submission +- **Interface changes:** Announce in channel immediately; update this README Section 4 +- **Blockers:** If waiting on another module, notify in chat; Member 5 can create mock interfaces temporarily + +--- + +## 10. Version Control Basics + +```bash +# Create your feature branch (do this first) +git checkout main +git pull origin main +git checkout -b feature/member1-parser + +# Make changes, commit frequently +git add +git commit -m "[Parser] Add base prefix detection" + +# Push and create PR +git push origin feature/member1-parser +``` + +--- + +## Quick Reference + +| Task | Owner | File | Interface | +|------|-------|------|-----------| +| Parse `O'247` | Member 1 | `parser.py` | `parse_input()` | +| Validate characters | Member 2 | `validator.py` | `validate_input()` | +| `O'247` → `D'167` | Member 3 | `converter.py` | `convert()` | +| `O'247` + `O'15` | Member 4 | `arithmetic.py` | `octal_add()`, `get_complement()` | +| Orchestrate all | Member 5 | `calculator.py` + tests | `main()` | + +--- + +**Last Updated:** [Date] +**Maintained By:** Integration Team +**Questions?** Contact Member 5 (Integration Lead) From 1004538a462cc03e7e2012e0a0ab17573b03a20a Mon Sep 17 00:00:00 2001 From: soumyajit-2005 Date: Sun, 22 Mar 2026 16:37:03 +0530 Subject: [PATCH 2/5] Add files via upload --- parser.py | 139 +++++++++++++++++++++++++++ test_parser.py | 252 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 391 insertions(+) create mode 100644 parser.py create mode 100644 test_parser.py diff --git a/parser.py b/parser.py new file mode 100644 index 0000000..815f6c0 --- /dev/null +++ b/parser.py @@ -0,0 +1,139 @@ +""" +parser.py +Input Parser Module — Octal Calculator Project +Owner: Soumyajit + +Parses strings like "O'247" or "D'123" into their components. +Format: ' where prefix is O (octal) or D (decimal). +""" + +import re +from typing import Optional + +# --------------------------------------------------------------------------- +# Constants +# --------------------------------------------------------------------------- + +# Maps a single-letter prefix to its canonical base-mode name. +PREFIX_TO_MODE: dict[str, str] = { + "O": "OCT", + "D": "DEC", +} + +# Regex pattern that a value must fully match for each base mode. +VALID_DIGIT_PATTERN: dict[str, str] = { + "OCT": r"^[0-7]+$", # octal: digits 0-7 only + "DEC": r"^[0-9]+$", # decimal: digits 0-9 +} + +# Separator between prefix and value. +SEPARATOR = "'" + +# Blank result template — returned (with an error filled in) on failure. +_BLANK: dict[str, Optional[str]] = { + "base_mode": None, + "value": None, + "error": None, +} + + +# --------------------------------------------------------------------------- +# Private helpers +# --------------------------------------------------------------------------- + +def _failure(error: str, base_mode: Optional[str] = None) -> dict: + """Return a failed-parse result with the given error message.""" + return {**_BLANK, "base_mode": base_mode, "error": error} + + +def _success(base_mode: str, value: str) -> dict: + """Return a successful parse result.""" + return {"base_mode": base_mode, "value": value, "error": None} + + +def _digit_error(base_mode: str, value_part: str) -> str: + """Build a human-readable digit-validation error message.""" + allowed = "0-7" if base_mode == "OCT" else "0-9" + label = "Octal" if base_mode == "OCT" else "Decimal" + return ( + f"Invalid {base_mode.lower()} value '{value_part}'. " + f"{label} accepts only digits {allowed}." + ) + + +# --------------------------------------------------------------------------- +# Public API +# --------------------------------------------------------------------------- + +def parse_input(input_str: str) -> dict: + """Parse a prefixed numeric string and return its components. + + Accepts inputs of the form ``'`` where: + - ``prefix`` is ``O`` (octal) or ``D`` (decimal), case-insensitive. + - ``value`` is a non-empty string of digits valid for that base. + + Args: + input_str: Raw input string, e.g. ``"O'247"`` or ``"D'123"``. + + Returns: + A dict with three keys: + + - ``base_mode`` (str | None): ``'OCT'`` or ``'DEC'``, or None on error. + - ``value`` (str | None): Extracted digit string, or None on error. + - ``error`` (str | None): Error message, or None on success. + + Examples: + >>> parse_input("O'247") + {'base_mode': 'OCT', 'value': '247', 'error': None} + + >>> parse_input("D'123") + {'base_mode': 'DEC', 'value': '123', 'error': None} + + >>> parse_input("O'89") + {'base_mode': 'OCT', 'value': None, 'error': "Invalid octal value '89'. Octal accepts only digits 0-7."} + + >>> parse_input("X'10") + {'base_mode': None, 'value': None, 'error': "Unknown prefix 'X'. Supported: O (octal), D (decimal)."} + """ + + # 1. Reject non-string input early. + if not isinstance(input_str, str): + return _failure("Input must be a string.") + + input_str = input_str.strip() + + # 2. Reject blank / whitespace-only input. + if not input_str: + return _failure("Input string is empty.") + + # 3. The separator must be present to split prefix from value. + if SEPARATOR not in input_str: + return _failure( + f"Missing separator '{SEPARATOR}' in '{input_str}'. " + f"Expected format: {SEPARATOR} e.g. O'247 or D'123." + ) + + # Split on the first apostrophe only; anything after belongs to the value. + prefix_raw, _, value_part = input_str.partition(SEPARATOR) + + # 4. Normalise and look up the prefix. + prefix = prefix_raw.upper() + if prefix not in PREFIX_TO_MODE: + known = ", ".join(f"{k} ({v.lower()})" for k, v in PREFIX_TO_MODE.items()) + return _failure(f"Unknown prefix '{prefix_raw}'. Supported: {known}.") + + base_mode = PREFIX_TO_MODE[prefix] + + # 5. A value must follow the separator. + if not value_part: + return _failure( + f"No value found after prefix '{prefix}{SEPARATOR}'.", + base_mode=base_mode, + ) + + # 6. Every character in the value must be a valid digit for the base. + if not re.fullmatch(VALID_DIGIT_PATTERN[base_mode], value_part): + return _failure(_digit_error(base_mode, value_part), base_mode=base_mode) + + # All checks passed. + return _success(base_mode, value_part) diff --git a/test_parser.py b/test_parser.py new file mode 100644 index 0000000..96e3097 --- /dev/null +++ b/test_parser.py @@ -0,0 +1,252 @@ +""" +test_parser.py +Unit Tests — Input Parser Module, Octal Calculator Project +Owner: Soumyajit + +Run with: + python -m pytest test_parser.py -v +""" + +import pytest +from parser import parse_input + + +# --------------------------------------------------------------------------- +# Test helpers +# --------------------------------------------------------------------------- + +def _ok(base_mode: str, value: str) -> dict: + """Expected result for a successful parse.""" + return {"base_mode": base_mode, "value": value, "error": None} + + +def _err(base_mode, value, fragment: str) -> dict: + """ + Expected shape for a failed parse. + fragment: a word that must appear (case-insensitive) in the error message. + """ + return {"base_mode": base_mode, "value": value, "_fragment": fragment} + + +def assert_result(result: dict, expected: dict) -> None: + """ + Assert a parse_input() result against an expected dict. + When expected has '_fragment', checks substring match on the error field + instead of exact equality, so tests stay decoupled from exact wording. + """ + assert isinstance(result, dict), "Result must be a dict" + assert set(result.keys()) == {"base_mode", "value", "error"}, ( + f"Unexpected keys: {result.keys()}" + ) + + if "_fragment" in expected: + assert result["base_mode"] == expected["base_mode"] + assert result["value"] == expected["value"] + assert result["error"] is not None, "Expected an error but got None" + assert expected["_fragment"].lower() in result["error"].lower(), ( + f"Error '{result['error']}' does not contain '{expected['_fragment']}'" + ) + else: + assert result == expected, f"\nExpected : {expected}\nActual : {result}" + + +# --------------------------------------------------------------------------- +# 1. Valid octal inputs +# --------------------------------------------------------------------------- + +class TestOctalValid: + """Correctly formed octal strings should parse without error.""" + + def test_typical_octal(self): + assert_result(parse_input("O'247"), _ok("OCT", "247")) + + def test_single_digit_zero(self): + assert_result(parse_input("O'0"), _ok("OCT", "0")) + + def test_single_digit_seven(self): + # 7 is the highest valid octal digit + assert_result(parse_input("O'7"), _ok("OCT", "7")) + + def test_leading_zeros_preserved(self): + # Leading zeros are kept as-is (value is a string, not an integer) + assert_result(parse_input("O'007"), _ok("OCT", "007")) + + def test_long_octal_value(self): + assert_result(parse_input("O'77777777"), _ok("OCT", "77777777")) + + def test_lowercase_prefix_accepted(self): + # Prefix matching is case-insensitive + assert_result(parse_input("o'247"), _ok("OCT", "247")) + + def test_surrounding_whitespace_stripped(self): + assert_result(parse_input(" O'247 "), _ok("OCT", "247")) + + +# --------------------------------------------------------------------------- +# 2. Valid decimal inputs +# --------------------------------------------------------------------------- + +class TestDecimalValid: + """Correctly formed decimal strings should parse without error.""" + + def test_typical_decimal(self): + assert_result(parse_input("D'123"), _ok("DEC", "123")) + + def test_single_digit_zero(self): + assert_result(parse_input("D'0"), _ok("DEC", "0")) + + def test_large_value(self): + assert_result(parse_input("D'9999999999"), _ok("DEC", "9999999999")) + + def test_lowercase_prefix_accepted(self): + assert_result(parse_input("d'456"), _ok("DEC", "456")) + + def test_leading_zeros_preserved(self): + assert_result(parse_input("D'00123"), _ok("DEC", "00123")) + + def test_surrounding_whitespace_stripped(self): + assert_result(parse_input(" D'789 "), _ok("DEC", "789")) + + def test_digits_8_and_9_valid_in_decimal(self): + # 8 and 9 are legal in decimal but NOT in octal + assert_result(parse_input("D'89"), _ok("DEC", "89")) + + +# --------------------------------------------------------------------------- +# 3. Illegal digits for the stated base +# --------------------------------------------------------------------------- + +class TestInvalidDigits: + """Values containing digits that are out-of-range for the base.""" + + def test_octal_digit_8(self): + assert_result(parse_input("O'128"), _err("OCT", None, "octal")) + + def test_octal_digit_9(self): + assert_result(parse_input("O'9"), _err("OCT", None, "octal")) + + def test_octal_digits_8_and_9(self): + assert_result(parse_input("O'89"), _err("OCT", None, "octal")) + + def test_octal_alpha_character(self): + assert_result(parse_input("O'2A4"), _err("OCT", None, "octal")) + + def test_octal_hex_digit(self): + assert_result(parse_input("O'1F3"), _err("OCT", None, "octal")) + + def test_decimal_alpha_character(self): + assert_result(parse_input("D'12A"), _err("DEC", None, "decimal")) + + def test_decimal_decimal_point(self): + # Floats are not valid — only integer strings are accepted + assert_result(parse_input("D'12.5"), _err("DEC", None, "decimal")) + + +# --------------------------------------------------------------------------- +# 4. Unknown or unsupported prefix +# --------------------------------------------------------------------------- + +class TestUnknownPrefix: + """Any prefix other than O or D must produce a prefix error.""" + + def test_hex_prefix(self): + assert_result(parse_input("H'1F"), _err(None, None, "prefix")) + + def test_binary_prefix(self): + assert_result(parse_input("B'1010"), _err(None, None, "prefix")) + + def test_arbitrary_letter(self): + assert_result(parse_input("X'10"), _err(None, None, "prefix")) + + def test_numeric_prefix(self): + assert_result(parse_input("8'123"), _err(None, None, "prefix")) + + def test_empty_prefix(self): + assert_result(parse_input("'123"), _err(None, None, "prefix")) + + def test_multi_char_prefix(self): + # "OC" is not a recognised single-letter prefix + assert_result(parse_input("OC'247"), _err(None, None, "prefix")) + + +# --------------------------------------------------------------------------- +# 5. Structural / format errors +# --------------------------------------------------------------------------- + +class TestMalformedInput: + """Input that is structurally broken (wrong format).""" + + def test_missing_separator_octal(self): + assert_result(parse_input("O247"), _err(None, None, "separator")) + + def test_missing_separator_decimal(self): + assert_result(parse_input("D123"), _err(None, None, "separator")) + + def test_empty_value_after_octal_prefix(self): + assert_result(parse_input("O'"), _err("OCT", None, "value")) + + def test_empty_value_after_decimal_prefix(self): + assert_result(parse_input("D'"), _err("DEC", None, "value")) + + def test_empty_string(self): + assert_result(parse_input(""), _err(None, None, "empty")) + + def test_whitespace_only(self): + assert_result(parse_input(" "), _err(None, None, "empty")) + + def test_space_inside_value(self): + # Spaces in the value part are not valid digits + assert parse_input("O' 247")["error"] is not None + + def test_extra_apostrophe_in_value(self): + # Second apostrophe becomes part of the value string → invalid digits + assert parse_input("O'24'7")["error"] is not None + + def test_only_separator(self): + assert parse_input("'")["error"] is not None + + +# --------------------------------------------------------------------------- +# 6. Non-string input types +# --------------------------------------------------------------------------- + +class TestTypeSafety: + """parse_input must not crash on non-string arguments.""" + + def test_integer(self): + assert parse_input(247)["error"] is not None # type: ignore[arg-type] + + def test_none(self): + assert parse_input(None)["error"] is not None # type: ignore[arg-type] + + def test_list(self): + assert parse_input(["O'247"])["error"] is not None # type: ignore[arg-type] + + def test_float(self): + assert parse_input(2.47)["error"] is not None # type: ignore[arg-type] + + +# --------------------------------------------------------------------------- +# 7. Return-value contract (parametrized) +# --------------------------------------------------------------------------- + +class TestReturnContract: + """parse_input must always honour the dict contract, success or failure.""" + + VALID_INPUTS = ["O'247", "D'123", "o'007", "D'89"] + INVALID_INPUTS = ["O'89", "X'10", "", "D'", "O'", "D123", "D'12A"] + + @pytest.mark.parametrize("s", VALID_INPUTS + INVALID_INPUTS) + def test_always_returns_three_keys(self, s: str): + result = parse_input(s) + assert isinstance(result, dict) + assert set(result.keys()) == {"base_mode", "value", "error"} + + @pytest.mark.parametrize("s", VALID_INPUTS) + def test_success_has_null_error(self, s: str): + assert parse_input(s)["error"] is None + + @pytest.mark.parametrize("s", INVALID_INPUTS) + def test_failure_has_non_empty_error_string(self, s: str): + error = parse_input(s)["error"] + assert isinstance(error, str) and len(error) > 0 From 53b408a7126f148421cd99ca4c90c10cce7cf288 Mon Sep 17 00:00:00 2001 From: mrinmoydbn <160152141+mrinmoydbn@users.noreply.github.com> Date: Sun, 22 Mar 2026 17:51:07 +0530 Subject: [PATCH 3/5] Add files via upload --- test_validator.py | 185 ++++++++++++++++++++++++++++++++++++++++++++++ validator.py | 75 +++++++++++++++++++ 2 files changed, 260 insertions(+) create mode 100644 test_validator.py create mode 100644 validator.py diff --git a/test_validator.py b/test_validator.py new file mode 100644 index 0000000..0fe3d52 --- /dev/null +++ b/test_validator.py @@ -0,0 +1,185 @@ +""" +test_validator.py + +Unit tests for the Validator module. + +Member 1's parse_input() is MOCKED here so that tests run independently +of the Parser module. When the full team integrates, these mocks can be +removed and replaced with real parser calls in integration tests. +""" + +import unittest +from unittest.mock import patch + +from validator import validate_input + + +class TestValidateInputOCT(unittest.TestCase): + """Tests for octal (OCT) base validation.""" + + def test_valid_octal(self): + parsed = {'value': '7045321', 'base': 'OCT'} + result = validate_input(parsed) + self.assertTrue(result['is_valid']) + self.assertIsNone(result['error']) + + def test_valid_octal_single_digit(self): + result = validate_input({'value': '0', 'base': 'OCT'}) + self.assertTrue(result['is_valid']) + + def test_valid_octal_all_digits(self): + result = validate_input({'value': '01234567', 'base': 'OCT'}) + self.assertTrue(result['is_valid']) + + def test_invalid_octal_digit_8(self): + result = validate_input({'value': '7081', 'base': 'OCT'}) + self.assertFalse(result['is_valid']) + self.assertIn('8', result['error']) + + def test_invalid_octal_digit_9(self): + result = validate_input({'value': '9', 'base': 'OCT'}) + self.assertFalse(result['is_valid']) + self.assertIn('9', result['error']) + + def test_invalid_octal_letter(self): + result = validate_input({'value': '75A2', 'base': 'OCT'}) + self.assertFalse(result['is_valid']) + self.assertIn('A', result['error']) + + def test_invalid_octal_multiple_bad_chars(self): + result = validate_input({'value': '89AB', 'base': 'OCT'}) + self.assertFalse(result['is_valid']) + # All bad chars should be mentioned + for ch in ['8', '9', 'A', 'B']: + self.assertIn(ch, result['error']) + + def test_octal_base_case_insensitive(self): + result = validate_input({'value': '754', 'base': 'oct'}) + self.assertTrue(result['is_valid']) + + +class TestValidateInputDEC(unittest.TestCase): + """Tests for decimal (DEC) base validation.""" + + def test_valid_decimal(self): + result = validate_input({'value': '9081234567', 'base': 'DEC'}) + self.assertTrue(result['is_valid']) + self.assertIsNone(result['error']) + + def test_valid_decimal_single_digit(self): + result = validate_input({'value': '5', 'base': 'DEC'}) + self.assertTrue(result['is_valid']) + + def test_valid_decimal_all_digits(self): + result = validate_input({'value': '0123456789', 'base': 'DEC'}) + self.assertTrue(result['is_valid']) + + def test_invalid_decimal_letter(self): + result = validate_input({'value': '12X4', 'base': 'DEC'}) + self.assertFalse(result['is_valid']) + self.assertIn('X', result['error']) + + def test_invalid_decimal_special_char(self): + result = validate_input({'value': '12.34', 'base': 'DEC'}) + self.assertFalse(result['is_valid']) + self.assertIn('.', result['error']) + + def test_decimal_base_case_insensitive(self): + result = validate_input({'value': '999', 'base': 'dec'}) + self.assertTrue(result['is_valid']) + + +class TestEdgeCases(unittest.TestCase): + """Edge-case and structural tests.""" + + def test_empty_value(self): + result = validate_input({'value': '', 'base': 'OCT'}) + self.assertFalse(result['is_valid']) + self.assertIn('empty', result['error'].lower()) + + def test_whitespace_only_value(self): + result = validate_input({'value': ' ', 'base': 'DEC'}) + self.assertFalse(result['is_valid']) + self.assertIn('empty', result['error'].lower()) + + def test_missing_value_key(self): + result = validate_input({'base': 'OCT'}) + self.assertFalse(result['is_valid']) + self.assertIn('value', result['error']) + + def test_missing_base_key(self): + result = validate_input({'value': '123'}) + self.assertFalse(result['is_valid']) + self.assertIn('base', result['error']) + + def test_missing_both_keys(self): + result = validate_input({}) + self.assertFalse(result['is_valid']) + + def test_non_dict_input(self): + result = validate_input("752") + self.assertFalse(result['is_valid']) + self.assertIn('dictionary', result['error'].lower()) + + def test_unsupported_base_HEX(self): + result = validate_input({'value': 'A3F', 'base': 'HEX'}) + self.assertFalse(result['is_valid']) + self.assertIn('HEX', result['error']) + + def test_unsupported_base_BIN(self): + result = validate_input({'value': '1010', 'base': 'BIN'}) + self.assertFalse(result['is_valid']) + + def test_value_not_a_string(self): + result = validate_input({'value': 752, 'base': 'OCT'}) + self.assertFalse(result['is_valid']) + self.assertIn('string', result['error'].lower()) + + def test_base_not_a_string(self): + result = validate_input({'value': '752', 'base': 8}) + self.assertFalse(result['is_valid']) + self.assertIn('string', result['error'].lower()) + + def test_none_value(self): + result = validate_input({'value': None, 'base': 'OCT'}) + self.assertFalse(result['is_valid']) + + def test_valid_result_structure(self): + """Ensure return dict always has exactly the required keys.""" + result = validate_input({'value': '123', 'base': 'DEC'}) + self.assertIn('is_valid', result) + self.assertIn('error', result) + self.assertEqual(len(result), 2) + + +class TestWithMockedParser(unittest.TestCase): + """ + Simulates the integration point with Member 1's parse_input(). + parse_input() is mocked — these tests do NOT require parser.py to exist. + """ + + @patch('validator.parse_input', return_value={'value': '754', 'base': 'OCT'}) + def test_pipeline_valid_octal(self, mock_parse): + from validator import parse_input + parsed = parse_input("754") # returns mocked dict + result = validate_input(parsed) + self.assertTrue(result['is_valid']) + mock_parse.assert_called_once_with("754") + + @patch('validator.parse_input', return_value={'value': '89F', 'base': 'OCT'}) + def test_pipeline_invalid_octal(self, mock_parse): + from validator import parse_input + parsed = parse_input("89F") + result = validate_input(parsed) + self.assertFalse(result['is_valid']) + + @patch('validator.parse_input', return_value={'value': '1234', 'base': 'DEC'}) + def test_pipeline_valid_decimal(self, mock_parse): + from validator import parse_input + parsed = parse_input("1234") + result = validate_input(parsed) + self.assertTrue(result['is_valid']) + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/validator.py b/validator.py new file mode 100644 index 0000000..7476c99 --- /dev/null +++ b/validator.py @@ -0,0 +1,75 @@ +from parser import parse_input # Member 1's module + +VALID_CHARS: dict[str, set[str]] = { + 'OCT': set('01234567'), + 'DEC': set('0123456789'), +} + + +def validate_input(parsed: dict) -> dict: + """ + Validates that the parsed input contains only characters + valid for its declared base. + + Args: + parsed: dict with keys 'value' (str) and 'base' (str, 'OCT' or 'DEC') + + Returns: + dict with keys: + 'is_valid' (bool) + 'error' (str | None) — None if valid, message if not + """ + # ── 1. Structural checks ────────────────────────────────────────────────── + + if not isinstance(parsed, dict): + return {'is_valid': False, 'error': "Input must be a dictionary."} + + if 'value' not in parsed or 'base' not in parsed: + return { + 'is_valid': False, + 'error': "Parsed input must contain 'value' and 'base' keys.", + } + + value: str = parsed['value'] + base: str = parsed['base'] + + # ── 2. Type checks ──────────────────────────────────────────────────────── + + if not isinstance(value, str): + return {'is_valid': False, 'error': f"'value' must be a string, got {type(value).__name__}."} + + if not isinstance(base, str): + return {'is_valid': False, 'error': f"'base' must be a string, got {type(base).__name__}."} + + base = base.upper() + + # ── 3. Empty value ──────────────────────────────────────────────────────── + + if value.strip() == '': + return {'is_valid': False, 'error': "Input value must not be empty."} + + # ── 4. Unsupported base ─────────────────────────────────────────────────── + + if base not in VALID_CHARS: + supported = ', '.join(VALID_CHARS.keys()) + return { + 'is_valid': False, + 'error': f"Unsupported base '{base}'. Supported bases: {supported}.", + } + + # ── 5. Character validation ─────────────────────────────────────────────── + + allowed = VALID_CHARS[base] + bad_chars = [ch for ch in value if ch not in allowed] + + if bad_chars: + unique_bad = sorted(set(bad_chars)) + return { + 'is_valid': False, + 'error': ( + f"Invalid character(s) {unique_bad} for base {base}. " + f"Allowed digits: {''.join(sorted(allowed))}." + ), + } + + return {'is_valid': True, 'error': None} From 8d977b35d6e783cdf0cf51879ad63f6159fc458c Mon Sep 17 00:00:00 2001 From: aazhnaa Date: Fri, 27 Mar 2026 15:52:27 +0530 Subject: [PATCH 4/5] added converter.py --- __pycache__/converter.cpython-313.pyc | Bin 0 -> 1833 bytes __pycache__/parser.cpython-313.pyc | Bin 0 -> 4603 bytes __pycache__/test_converter.cpython-313.pyc | Bin 0 -> 6032 bytes __pycache__/validator.cpython-313.pyc | Bin 0 -> 2454 bytes converter.py | 56 +++++++++++++ test_converter.py | 88 +++++++++++++++++++++ 6 files changed, 144 insertions(+) create mode 100644 __pycache__/converter.cpython-313.pyc create mode 100644 __pycache__/parser.cpython-313.pyc create mode 100644 __pycache__/test_converter.cpython-313.pyc create mode 100644 __pycache__/validator.cpython-313.pyc create mode 100644 converter.py create mode 100644 test_converter.py diff --git a/__pycache__/converter.cpython-313.pyc b/__pycache__/converter.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f6b8764d99071b2bcdbe3059238d5574b6de1104 GIT binary patch literal 1833 zcmah~O>7%Q6rT0|*xAHR+yokjKqjKFHHs`j{)7va@>8l%*$&y5wu&ojYmc36?Oijw zPKZyrk-z~Xfr!*gj^P%GOOGuF&X#eo=S*XRP@B+LWlLV@5IAAxt0jVe-u@b=a<*2&3tA~x zEort%@iH~8*8a+ZlXe%owsBueZmHF7-D@`fK)MxPoSyNxur z0_*uP?Hw3g+V`J)Uz$_7D_}wEM`*3AlbuWpA^m_FQE7NF;w891KMYB9i8XK z8aAqBLiHSWpYZ=*U!J2yD=uRgu0|XuJp-0ab)$$kb$i{c+PFeVLH`tACb42{0xR!K z%_>n>t(zoqtU?0jh?`jWvXWX@RWPKfvUqL5ll8JFj#o8WB=)~Z$}4V;d9XUQkoQ7j zfZB@ZAT26y&&;+xraXM|il#%7x8wVmCG|B*O{kTn0`B5w7(HNEx3F%&Md4f!~6V{4qk&cAtTsJ&)O08FMp|D!TlnFcB;t9*Eq{F2gPKk<+SglkDb>y>a zaCqIY9UdOq;h{Z5B`SahWyL|E;~^*LwTi5p8V=OHRV~?$)V_F&$*{!1*hwtJGii%Z zE2C-Fx{+BlbC5=cZDnTQwJcK3s5db&ip9*FW$8tO@$gjU1EN7UA#>T;%=7)v@p}{< z0T)nk$odtgZPdajJo0V-SN+X_@$IFf&`V#IcgtUx+t*tX3WWDUP5+z6q2b0Wv4gFH z`;B*38WFV_T5SYYkNly!(ez((xf2IB50)C!$wnmA3@tYT%PmO~SGl9!p*^wL`|9!N z*uJ>`$%Djh;;Fl(50blymW%@Z^*5USi^t)i`kiL@^&RObJhUr4^~n9X{rN^%aZh)o zpF%@Bx9T6&H|pzqTB8>?1pLSE7g68Kr-%bzE{>_g^x^e~R~~IWy5ESbG(&15ptg9% z{?md!fFAc))u$NFIYVR&?YijVpoe>R&pbJ?sT*;^GTP|7+aJc&l(`kfv(ciG>8 z4#By5f8U`~*diKXf`RwS?jPkU9S4$iQfme#aLaMO`6NzwHh{R%Cn)#?4YZ`UxbgaA V3&FDAUJeIc>fzusc1SU({sosM;)(zO literal 0 HcmV?d00001 diff --git a/__pycache__/parser.cpython-313.pyc b/__pycache__/parser.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3886ab2217a750614491ffad1adc57a5c0bc9645 GIT binary patch literal 4603 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 N=(~F9JN9O(_%D(tbPE6g literal 0 HcmV?d00001 diff --git a/__pycache__/test_converter.cpython-313.pyc b/__pycache__/test_converter.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a529911d7980963c32e64efa6f881d0c89fbfe22 GIT binary patch literal 6032 zcmd5=&uEs-MS#0}v@k+@{vtL~ZU8FvFYV9|)r zHC?ak$M?Q^^{TeA*#QF2KY#Zh`|AuL-{X(glL`x43<|dhB~7YJKfvFMd5QOx zT<5yQ{8IEADg3e3fWj?klu48@nMzEd@&UqB92iSc6);UxfPFL#sL?*aeyRZu(0;&O zbO10zcL8Q;25^vO0f*=y;4mEm+)al8_t4#dBXkeoC>>#AmC^hd&wgZi{#-Cbi#>3mUqCDGm3S2pUa+WJgb02YKeqru$dOkOOYBslcTjsk=&jUIa{#ZAw z4Ewz;an-Y`s~!$a=a=VB7ni}pE1ITv)hW)qrFzYBe6M)Qzp8sytw=x7EvHf`p7%Vv z;$WSAviP25`gLZVH)e}IjL`_aX0gJx&-s{m1p_vG*MQ*~Vg~%>F767kM#$gRL4YsF zcUtD2HhxDN|8ie*Y(2GJ-`GF%)n}X9J6lk_Q>?eh2Hpb)bg>R3Gh9@xAK-C%DF<%I zEizO*8w3bAX2VbnNkKM%6yzqElwPA<*xm@jD8O^nn~omR8y0i*Rp!>VX|BMfDKS?u z>xMd?n@XDQsBkYfB_LK#$E3Ofg0QBTZ|4d1L>HuRh+?&g?V~y(%a$lxWBG z4Rz_6nCLh*nTd{#@c7{!aC=_3J<#``Y*_YH%PiYYMJU0G#2g4TQ&Y1Y1v~}?BntkJ zP(av}fnN5aQg|&!3Q5NMfCTReQQ`WgcJ6s=;XLAP>`|KO4WBJa64Z32LdQLy=f?u&jWONH~F= z1Q`1T1a*|apGr{2_7#jRQwYy_UkNzyAf!|+y=+%(fBV+Lj^RLXBMpea!6j^^fkJ}j z4AI=v6p~<@$KG~sm2?r%(n2e zAUV>Nb}qZzmA5)_q(-JHO{s8HpmHm(bri^h+c_^Z7e31hO)pD>g!Y|KBAKt2?{6H$ zWv(-~?v!<(+1I+W-Ra{eI;-UY=FAU%hNmIV=eU$x%*!IvWm%}ms|+&=uDJ|!=a^NV z5^IjDD`x2ro(gq3JVA~&Btnnq)xci)p_pjXsh)<9y>|g1SsfX_q1_+ex282G*2gx6 z-@K8!KeYE|eeKd3yZPtl!urg{(DV)Un^sP{Hrm|NywE(oe(CEYZ*FQ++gD(l|Gz*7 zi6@Ehyg3~PFyYFQW-^m{Svay++yq_7lxv)0V zoVc4g_LD*eU%Qu`{BL&h@sI z0pMeX;rXU-mkiT~OlhSKG(cZvG{D1z$fp?QAp)@A6#@QE9;tHby!1$G;aMyS_)S<+ zuzn!GunOu^sp&_9N-B>v0*tR_YYUV~`L)rh0n)%1Yzh3;?(nE!)2gE@C4?vSQ82|tP literal 0 HcmV?d00001 diff --git a/__pycache__/validator.cpython-313.pyc b/__pycache__/validator.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..349abbcfb67498eada30e4d3bf4c73d37e2032e9 GIT binary patch literal 2454 zcmai0OH3O_7@l3PU%P;LhCGNf!93 zRHDkER6P`=RG}#cDpe{~d&n`z9HOdHa#+g;$f~MPTUF|fl=RSBXV$ww(pDMCJO9l5 zkMIBHpBZn~*7AU~J6C_me=Pv;D;+qEvxUuTMED#W0wNHZOJIP}7zk2qI}q6}rmAC& zT+T@3Vyxy$aVd9-O}X7frIV%Klt*-jNiTX*oajw)ot)^~3$AmTH|LM=e^6Hu#uN&& zhEY1N6pFg(J9hlU$dB z5!ly8@b8GQwSljTAh9EqzgJq9JyuwW8L53`kqn)6k&j;mz-CIYnPQW5js@jZK1%Yh z8EJ6lw)<_pk*4igM{;5~Tq!QHU$qiT_^Fdz7E5lMrLB09RgMZLF->61joG6BvrXV) z4FIYnan=r#Iv65HLvmkchlrg3AW%GtTk<%SA@=}~Y;jN|TOCRAI7pmc@e)J<$xE=* zTeuoT{y+2F@+NVPtS`No~ zNLtlf9A@(w9p23Alkf(b!7xnhizo~aV_l0tS;@jMHLXrWVVE4(FeJ{yA$*Lb2G{E`Y;00!zHL|1?kHECze0rqr9n zRW_d^H4%rS!;u=3&tpU!#A+*t(%{yc-xjeD-#vixxRbn*78N-)Z7XqKfKZ$>OcD8@R2Yj_3-}>m* zoevk;ig&Mte6SoCGJHdHt>$h|#T#5}-hD5zkXX908jO{Lu}bh*rTO@rpXSmUS?O5W zWgP8$lz7zhqinQX-p=hs9SH1Q>?qeAn0@{><;* z`kz|%E%hz?zZ|Z#yfMcUr|XU36@K~lgZRVj!?6v(^fRHg(4lX-zv}+})PwMM=PIEd zqy6l|-VGPiaPGO+L#gx-D(k_X{mXh;h#FkZ Z47*YD7Q-A_?A!pfU+(|Q>NWcR=pP0bRr>${ literal 0 HcmV?d00001 diff --git a/converter.py b/converter.py new file mode 100644 index 0000000..83c434b --- /dev/null +++ b/converter.py @@ -0,0 +1,56 @@ +""" +converter.py +Converter Module — Octal Calculator Project +Owner: [Your Name] + +Converts between octal and decimal bases. +""" + +from typing import Optional +import parser # Member 1's module +import validator # Member 2's module + + +def convert(value: str, from_base: str, to_base: str) -> str: + """ + Convert a numeric value between octal and decimal bases. + + Args: + value: Numeric string without prefix (e.g., '247') + from_base: Source base ('OCT' or 'DEC') + to_base: Target base ('OCT' or 'DEC') + + Returns: + Converted value with base prefix (e.g., 'D'167' or 'O'247') + + Raises: + ValueError: If from_base or to_base is invalid, or if value contains invalid digits for from_base + """ + # Normalize bases to uppercase + from_base = from_base.upper() + to_base = to_base.upper() + + # Validate bases + if from_base not in ('OCT', 'DEC'): + raise ValueError(f"Invalid from_base '{from_base}'. Must be 'OCT' or 'DEC'") + if to_base not in ('OCT', 'DEC'): + raise ValueError(f"Invalid to_base '{to_base}'. Must be 'OCT' or 'DEC'") + + # Convert to decimal first + try: + if from_base == 'OCT': + decimal_value = int(value, 8) + else: # from_base == 'DEC' + decimal_value = int(value, 10) + except ValueError: + raise ValueError(f"Invalid digits in value '{value}' for base {from_base}") + + # Convert to target base + if to_base == 'DEC': + result = str(decimal_value) + prefix = 'D' + else: # to_base == 'OCT' + result = oct(decimal_value)[2:] # Remove '0o' prefix + prefix = 'O' + + return f"{prefix}'{result}'" \ No newline at end of file diff --git a/test_converter.py b/test_converter.py new file mode 100644 index 0000000..3771188 --- /dev/null +++ b/test_converter.py @@ -0,0 +1,88 @@ +import unittest +from converter import convert + + +class TestConverter(unittest.TestCase): + + def test_oct_to_dec_basic(self): + """Test basic octal to decimal conversion.""" + self.assertEqual(convert('247', 'OCT', 'DEC'), "D'167'") + + def test_dec_to_oct_basic(self): + """Test basic decimal to octal conversion.""" + self.assertEqual(convert('167', 'DEC', 'OCT'), "O'247'") + + def test_zero_oct_to_dec(self): + """Test zero from octal to decimal.""" + self.assertEqual(convert('0', 'OCT', 'DEC'), "D'0'") + + def test_zero_dec_to_oct(self): + """Test zero from decimal to octal.""" + self.assertEqual(convert('0', 'DEC', 'OCT'), "O'0'") + + def test_leading_zeros_oct_to_dec(self): + """Test octal with leading zeros to decimal.""" + self.assertEqual(convert('007', 'OCT', 'DEC'), "D'7'") + + def test_dec_to_oct_no_leading_zeros(self): + """Test decimal to octal, no leading zeros in output.""" + self.assertEqual(convert('7', 'DEC', 'OCT'), "O'7'") + + def test_large_number_oct_to_dec(self): + """Test large octal number to decimal.""" + # 777 octal = 7*64 + 7*8 + 7 = 448 + 56 + 7 = 511 + self.assertEqual(convert('777', 'OCT', 'DEC'), "D'511'") + + def test_large_number_dec_to_oct(self): + """Test large decimal number to octal.""" + # 511 decimal = 777 octal + self.assertEqual(convert('511', 'DEC', 'OCT'), "O'777'") + + def test_single_digit_oct_to_dec(self): + """Test single digit octal to decimal.""" + self.assertEqual(convert('7', 'OCT', 'DEC'), "D'7'") + + def test_single_digit_dec_to_oct(self): + """Test single digit decimal to octal.""" + self.assertEqual(convert('7', 'DEC', 'OCT'), "O'7'") + + def test_round_trip(self): + """Test round trip conversion.""" + original = '123' + octal = convert(original, 'DEC', 'OCT') + back = convert(octal[2:-1], 'OCT', 'DEC') # Remove O' and ' + self.assertEqual(back, f"D'{original}'") + + def test_invalid_from_base(self): + """Test invalid from_base raises ValueError.""" + with self.assertRaises(ValueError): + convert('123', 'HEX', 'DEC') + + def test_invalid_to_base(self): + """Test invalid to_base raises ValueError.""" + with self.assertRaises(ValueError): + convert('123', 'DEC', 'HEX') + + def test_invalid_digits_oct(self): + """Test invalid digits for octal raises ValueError.""" + with self.assertRaises(ValueError): + convert('89', 'OCT', 'DEC') + + def test_invalid_digits_dec(self): + """Test invalid digits for decimal raises ValueError.""" + with self.assertRaises(ValueError): + convert('12a', 'DEC', 'OCT') + + def test_case_insensitive_bases(self): + """Test that bases are case insensitive.""" + self.assertEqual(convert('247', 'oct', 'dec'), "D'167'") + self.assertEqual(convert('167', 'Dec', 'Oct'), "O'247'") + + def test_empty_value(self): + """Test empty value raises ValueError.""" + with self.assertRaises(ValueError): + convert('', 'OCT', 'DEC') + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file From bf1681cdca8ef2cacddff49693c9319a824909f5 Mon Sep 17 00:00:00 2001 From: Olivia Chattopadhyay Date: Sat, 28 Mar 2026 22:50:57 +0530 Subject: [PATCH 5/5] Added arithmetic module --- arithmetic.py | 54 ++++++++++++++++++++++++++++++++++++++++++++++ test_arithmetic.py | 31 ++++++++++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 arithmetic.py create mode 100644 test_arithmetic.py diff --git a/arithmetic.py b/arithmetic.py new file mode 100644 index 0000000..bbcbfd5 --- /dev/null +++ b/arithmetic.py @@ -0,0 +1,54 @@ +from typing import Tuple + +def octal_add(op1: str, op2: str) -> str: + i, j = len(op1) - 1, len(op2) - 1 + carry = 0 + result = [] + + while i >= 0 or j >= 0 or carry: + d1 = int(op1[i]) if i >= 0 else 0 + d2 = int(op2[j]) if j >= 0 else 0 + + total = d1 + d2 + carry + result.append(str(total % 8)) + carry = total // 8 + + i -= 1 + j -= 1 + + return ''.join(result[::-1]) + + +def octal_multiply(op1: str, op2: str) -> str: + result = "0" + + op2 = op2[::-1] + for i, d2 in enumerate(op2): + carry = 0 + temp = [] + + for d1 in op1[::-1]: + prod = int(d1) * int(d2) + carry + temp.append(str(prod % 8)) + carry = prod // 8 + + if carry: + temp.append(str(carry)) + + temp = ''.join(temp[::-1]) + ('0' * i) + result = octal_add(result, temp) + + return result.lstrip('0') or "0" + + +def get_complement(value: str, comp_type: int) -> str: + if comp_type not in (7, 8): + raise ValueError("comp_type must be 7 or 8") + + # 7's complement → subtract each digit from 7 + if comp_type == 7: + return ''.join(str(7 - int(d)) for d in value) + + # 8's complement → 7's complement + 1 + seven_comp = ''.join(str(7 - int(d)) for d in value) + return octal_add(seven_comp, "1") \ No newline at end of file diff --git a/test_arithmetic.py b/test_arithmetic.py new file mode 100644 index 0000000..ec96e63 --- /dev/null +++ b/test_arithmetic.py @@ -0,0 +1,31 @@ +import unittest +from arithmetic import octal_add, octal_multiply, get_complement + + +class TestArithmetic(unittest.TestCase): + + def test_add(self): + self.assertEqual(octal_add("7", "1"), "10") + self.assertEqual(octal_add("10", "7"), "17") + self.assertEqual(octal_add("123", "456"), "601") + + def test_multiply(self): + self.assertEqual(octal_multiply("2", "3"), "6") + self.assertEqual(octal_multiply("7", "7"), "61") + self.assertEqual(octal_multiply("10", "10"), "100") + + def test_complement_7(self): + self.assertEqual(get_complement("123", 7), "654") + self.assertEqual(get_complement("0", 7), "7") + + def test_complement_8(self): + self.assertEqual(get_complement("123", 8), "655") + self.assertEqual(get_complement("0", 8), "10") + + def test_edge_cases(self): + self.assertEqual(octal_add("0", "0"), "0") + self.assertEqual(octal_multiply("0", "123"), "0") + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file