From 77cac6695ab2333afe45051e2d6310c023451072 Mon Sep 17 00:00:00 2001 From: CodingWithAishik Date: Sun, 22 Mar 2026 14:01:46 +0530 Subject: [PATCH 1/9] 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/9] 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/9] 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/9] 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/9] 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 From e1277e5b39fc932bd7fadf704d882011750b9356 Mon Sep 17 00:00:00 2001 From: CodingWithAishik Date: Sun, 29 Mar 2026 17:30:21 +0530 Subject: [PATCH 6/9] Adding pytest dependency and install requirements in Continuous Integration --- .github/workflows/python-unittest.yml | 2 +- requirements.txt | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 requirements.txt diff --git a/.github/workflows/python-unittest.yml b/.github/workflows/python-unittest.yml index 090b799..cf4496b 100644 --- a/.github/workflows/python-unittest.yml +++ b/.github/workflows/python-unittest.yml @@ -18,6 +18,6 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi # Installs any dependencies you have + python -m pip install -r requirements.txt # Installs project dependencies - name: Test with unittest run: python -m unittest discover # Runs the unittest discover command diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e079f8a --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +pytest From e958c1d04a6fe1fca787ac3af027ff492eb5c9d2 Mon Sep 17 00:00:00 2001 From: CodingWithAishik Date: Mon, 30 Mar 2026 12:50:43 +0530 Subject: [PATCH 7/9] Remove tracked Python cache files and ignore cache artifacts --- .gitignore | 2 ++ __pycache__/converter.cpython-313.pyc | Bin 1833 -> 0 bytes __pycache__/parser.cpython-313.pyc | Bin 4603 -> 0 bytes __pycache__/test_converter.cpython-313.pyc | Bin 6032 -> 0 bytes __pycache__/validator.cpython-313.pyc | Bin 2454 -> 0 bytes 5 files changed, 2 insertions(+) create mode 100644 .gitignore delete mode 100644 __pycache__/converter.cpython-313.pyc delete mode 100644 __pycache__/parser.cpython-313.pyc delete mode 100644 __pycache__/test_converter.cpython-313.pyc delete mode 100644 __pycache__/validator.cpython-313.pyc diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..43ae0e2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +*.py[cod] diff --git a/__pycache__/converter.cpython-313.pyc b/__pycache__/converter.cpython-313.pyc deleted file mode 100644 index f6b8764d99071b2bcdbe3059238d5574b6de1104..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 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 diff --git a/__pycache__/parser.cpython-313.pyc b/__pycache__/parser.cpython-313.pyc deleted file mode 100644 index 3886ab2217a750614491ffad1adc57a5c0bc9645..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4603 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 N=(~F9JN9O(_%D(tbPE6g diff --git a/__pycache__/test_converter.cpython-313.pyc b/__pycache__/test_converter.cpython-313.pyc deleted file mode 100644 index a529911d7980963c32e64efa6f881d0c89fbfe22..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 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 diff --git a/__pycache__/validator.cpython-313.pyc b/__pycache__/validator.cpython-313.pyc deleted file mode 100644 index 349abbcfb67498eada30e4d3bf4c73d37e2032e9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 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>${ From f1486983a15ace54261f68ba166c54ebe5f2459f Mon Sep 17 00:00:00 2001 From: CodingWithAishik Date: Mon, 30 Mar 2026 14:44:54 +0530 Subject: [PATCH 8/9] Minor cleanup --- converter.py | 12 ++---------- parser.py | 1 - test_parser.py | 1 - test_validator.py | 4 ++-- validator.py | 2 +- 5 files changed, 5 insertions(+), 15 deletions(-) diff --git a/converter.py b/converter.py index 83c434b..cf2a25f 100644 --- a/converter.py +++ b/converter.py @@ -1,14 +1,6 @@ -""" -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 +import parser +import validator def convert(value: str, from_base: str, to_base: str) -> str: diff --git a/parser.py b/parser.py index 815f6c0..c18f669 100644 --- a/parser.py +++ b/parser.py @@ -1,7 +1,6 @@ """ 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). diff --git a/test_parser.py b/test_parser.py index 96e3097..7d6b661 100644 --- a/test_parser.py +++ b/test_parser.py @@ -1,7 +1,6 @@ """ test_parser.py Unit Tests — Input Parser Module, Octal Calculator Project -Owner: Soumyajit Run with: python -m pytest test_parser.py -v diff --git a/test_validator.py b/test_validator.py index 0fe3d52..c396245 100644 --- a/test_validator.py +++ b/test_validator.py @@ -3,7 +3,7 @@ Unit tests for the Validator module. -Member 1's parse_input() is MOCKED here so that tests run independently +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. """ @@ -154,7 +154,7 @@ def test_valid_result_structure(self): class TestWithMockedParser(unittest.TestCase): """ - Simulates the integration point with Member 1's parse_input(). + Simulates the integration point with parse_input(). parse_input() is mocked — these tests do NOT require parser.py to exist. """ diff --git a/validator.py b/validator.py index 7476c99..023727f 100644 --- a/validator.py +++ b/validator.py @@ -1,4 +1,4 @@ -from parser import parse_input # Member 1's module +from parser import parse_input VALID_CHARS: dict[str, set[str]] = { 'OCT': set('01234567'), From 6e85a118fd28ad8c04d7dcc74f52346191692149 Mon Sep 17 00:00:00 2001 From: CodingWithAishik Date: Mon, 6 Apr 2026 14:29:52 +0530 Subject: [PATCH 9/9] terminal output added --- arithmetic.py | 11 ++++++++++- calculator.py | 10 ++++++++++ converter.py | 16 +++++++++++++++- parser.py | 9 +++++++++ test_calculator.py | 6 ++++-- test_parser.py | 5 +++++ validator.py | 13 +++++++++++++ 7 files changed, 66 insertions(+), 4 deletions(-) diff --git a/arithmetic.py b/arithmetic.py index bbcbfd5..5920ae7 100644 --- a/arithmetic.py +++ b/arithmetic.py @@ -51,4 +51,13 @@ def get_complement(value: str, comp_type: int) -> str: # 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 + return octal_add(seven_comp, "1") + + +if __name__ == "__main__": + print("Arithmetic module demo") + print("----------------------") + print(f"octal_add('123', '456') = {octal_add('123', '456')}") + print(f"octal_multiply('7', '7') = {octal_multiply('7', '7')}") + print(f"get_complement('123', 7) = {get_complement('123', 7)}") + print(f"get_complement('123', 8) = {get_complement('123', 8)}") \ No newline at end of file diff --git a/calculator.py b/calculator.py index 5daac9a..aea0a8d 100644 --- a/calculator.py +++ b/calculator.py @@ -9,3 +9,13 @@ def divide(self, a, b): if b == 0: raise ValueError("Division by zero") return a / b + + +if __name__ == "__main__": + calc = Calculator() + print("Calculator module demo") + print("----------------------") + print(f"2 + 3 = {calc.add(2, 3)}") + print(f"5 - 7 = {calc.subtract(5, 7)}") + print(f"4 * 6 = {calc.multiply(4, 6)}") + print(f"8 / 2 = {calc.divide(8, 2)}") diff --git a/converter.py b/converter.py index cf2a25f..9af1db0 100644 --- a/converter.py +++ b/converter.py @@ -45,4 +45,18 @@ def convert(value: str, from_base: str, to_base: str) -> str: result = oct(decimal_value)[2:] # Remove '0o' prefix prefix = 'O' - return f"{prefix}'{result}'" \ No newline at end of file + return f"{prefix}'{result}'" + + +if __name__ == "__main__": + print("Converter module demo") + print("---------------------") + demo_cases = [ + ("247", "OCT", "DEC"), + ("167", "DEC", "OCT"), + ("0", "OCT", "DEC"), + ] + + for value, src, dst in demo_cases: + converted = convert(value, src, dst) + print(f"{src} {value} -> {dst}: {converted}") \ No newline at end of file diff --git a/parser.py b/parser.py index c18f669..fb0b89e 100644 --- a/parser.py +++ b/parser.py @@ -136,3 +136,12 @@ def parse_input(input_str: str) -> dict: # All checks passed. return _success(base_mode, value_part) + + +if __name__ == "__main__": + print("Parser module demo") + print("------------------") + demo_inputs = ["O'247", "D'123", "O'89", "X'77"] + + for raw in demo_inputs: + print(f"input={raw!r} -> {parse_input(raw)}") diff --git a/test_calculator.py b/test_calculator.py index 7cb79e9..b8627df 100644 --- a/test_calculator.py +++ b/test_calculator.py @@ -28,6 +28,8 @@ def test_divide_by_zero(self): with self.assertRaises(ValueError): self.calc.divide(5, 0) + + # Optional: this allows running the script directly - if __name__ == '__main__': - unittest.main() # +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/test_parser.py b/test_parser.py index 7d6b661..110162e 100644 --- a/test_parser.py +++ b/test_parser.py @@ -249,3 +249,8 @@ def test_success_has_null_error(self, s: str): def test_failure_has_non_empty_error_string(self, s: str): error = parse_input(s)["error"] assert isinstance(error, str) and len(error) > 0 + + +if __name__ == "__main__": + # Allow direct execution: python test_parser.py + raise SystemExit(pytest.main([__file__, "-v"])) diff --git a/validator.py b/validator.py index 023727f..27d2fbe 100644 --- a/validator.py +++ b/validator.py @@ -73,3 +73,16 @@ def validate_input(parsed: dict) -> dict: } return {'is_valid': True, 'error': None} + + +if __name__ == '__main__': + print('Validator module demo') + print('---------------------') + demo_inputs = [ + {'value': '247', 'base': 'OCT'}, + {'value': '167', 'base': 'DEC'}, + {'value': '89', 'base': 'OCT'}, + ] + + for parsed in demo_inputs: + print(f"parsed={parsed} -> {validate_input(parsed)}")