Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions README (1).md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Group B - Feature 10 - Matrix Operations

This submission implements **Feature 10** from the Software Engineering calculator assignment.

## Scope covered
- Row-major matrix input parsing from string
- Matrix addition
- Matrix subtraction
- Matrix multiplication
- Matrix transpose
- Dimension compatibility checks
- Invalid input handling
- Unit tests for normal, boundary, and invalid cases

## Files
- `calculator.py` - base calculator plus dispatch for matrix mode
- `matrix.py` - matrix parsing and matrix operations
- `exceptions.py` - custom exceptions for matrix errors
- `test_calculator.py` - base arithmetic tests
- `test_matrix.py` - matrix feature tests

## Matrix mode
Mode `6` is used for matrix operations.

### Supported inputs
- `[[1,2],[3,4]]`
- `[[1,2],[3,4]] + [[5,6],[7,8]]`
- `[[1,2],[3,4]] - [[5,6],[7,8]]`
- `[[1,2],[3,4]] * [[5,6],[7,8]]`
- `transpose([[1,2,3],[4,5,6]])`

## Run tests
```bash
python -m unittest test_calculator.py test_matrix.py -v
```

## Run program
```bash
python calculator.py
```
Binary file added __pycache__/calculator.cpython-312.pyc
Binary file not shown.
Binary file added __pycache__/exceptions.cpython-312.pyc
Binary file not shown.
Binary file added __pycache__/matrix.cpython-312.pyc
Binary file not shown.
Binary file added __pycache__/test_calculator.cpython-312.pyc
Binary file not shown.
Binary file added __pycache__/test_matrix.cpython-312.pyc
Binary file not shown.
66 changes: 61 additions & 5 deletions calculator.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,74 @@
import ast
from typing import Union

from matrix import evaluate_matrix_expression, format_matrix


class Calculator:
# mode can be 1: Fraction, 2: Bin, 3: Oct, 4: Hex, 5: Set, 6: Matrix, default = 0
# mode: 0 -> arithmetic, 6 -> matrix
mode = 0

def add(self, a, b):
return a + b

def subtract(self, a, b):
return a - b

def multiply(self, a, b):
return a * b

def divide(self, a, b):
if b == 0:
raise ValueError("Division by zero")
return a / b

def evaluate(mode = 0):
#check the mode and based on its values execute for different modes
print('evaluate method to extend for multiple derived classes')
def _safe_eval_arithmetic(self, expression: str) -> Union[int, float]:
node = ast.parse(expression, mode="eval")

def go(n):
if isinstance(n, ast.Expression):
return go(n.body)
if isinstance(n, ast.Constant) and isinstance(n.value, (int, float)):
return n.value
if isinstance(n, ast.UnaryOp) and isinstance(n.op, (ast.UAdd, ast.USub)):
v = go(n.operand)
return v if isinstance(n.op, ast.UAdd) else -v
if isinstance(n, ast.BinOp):
l = go(n.left)
r = go(n.right)
if isinstance(n.op, ast.Add):
return l + r
if isinstance(n.op, ast.Sub):
return l - r
if isinstance(n.op, ast.Mult):
return l * r
if isinstance(n.op, ast.Div):
if r == 0:
raise ValueError("Division by zero")
return l / r
raise ValueError("Unsupported arithmetic expression")

return go(node)

def evaluate(self, expression: str, mode=0):
if mode in (6, "matrix", "Matrix"):
return evaluate_matrix_expression(expression)
return self._safe_eval_arithmetic(expression)


if __name__ == "__main__":
c = Calculator()
while True:
try:
mode = input("Enter mode (0 for arithmetic, 6 for matrix, q to quit): ").strip()
if mode.lower() == "q":
break
expr = input("Enter expression: ").strip()
m = 6 if mode == "6" else 0
ans = c.evaluate(expr, mode=m)
if m == 6:
print(format_matrix(ans))
else:
print(ans)
except Exception as e:
print(f"Error: {e}")
10 changes: 10 additions & 0 deletions exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
class CalculatorError(Exception):
"""Base exception for calculator errors."""


class InvalidMatrixError(CalculatorError):
"""Raised when a matrix input is malformed."""


class MatrixDimensionError(CalculatorError):
"""Raised when matrix dimensions are incompatible for an operation."""
136 changes: 136 additions & 0 deletions matrix.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import ast
from typing import List, Sequence, Tuple, Union

from exceptions import InvalidMatrixError, MatrixDimensionError

Number = Union[int, float]
Matrix = List[List[Number]]


def _is_number(x) -> bool:
return isinstance(x, (int, float)) and not isinstance(x, bool)


def parse_matrix(text: str) -> Matrix:
"""Parse a row-major matrix string like [[1,2],[3,4]]."""
try:
obj = ast.literal_eval(text.strip())
except Exception as exc:
raise InvalidMatrixError("Invalid matrix format") from exc

if not isinstance(obj, list) or not obj:
raise InvalidMatrixError("Matrix must be a non-empty list of rows")

if not all(isinstance(row, list) for row in obj):
raise InvalidMatrixError("Matrix rows must be lists")

col_count = len(obj[0])
if col_count == 0:
raise InvalidMatrixError("Matrix rows cannot be empty")

out: Matrix = []
for row in obj:
if len(row) != col_count:
raise InvalidMatrixError("All rows must have the same length")
new_row: List[Number] = []
for val in row:
if not _is_number(val):
raise InvalidMatrixError("Matrix elements must be numeric")
new_row.append(val)
out.append(new_row)
return out


def matrix_shape(m: Matrix) -> Tuple[int, int]:
return len(m), len(m[0])


def add_matrix(a: Matrix, b: Matrix) -> Matrix:
if matrix_shape(a) != matrix_shape(b):
raise MatrixDimensionError("Matrix addition requires equal dimensions")
r, c = matrix_shape(a)
return [[a[i][j] + b[i][j] for j in range(c)] for i in range(r)]


def subtract_matrix(a: Matrix, b: Matrix) -> Matrix:
if matrix_shape(a) != matrix_shape(b):
raise MatrixDimensionError("Matrix subtraction requires equal dimensions")
r, c = matrix_shape(a)
return [[a[i][j] - b[i][j] for j in range(c)] for i in range(r)]


def multiply_matrix(a: Matrix, b: Matrix) -> Matrix:
ra, ca = matrix_shape(a)
rb, cb = matrix_shape(b)
if ca != rb:
raise MatrixDimensionError(
"Matrix multiplication requires columns of first matrix to equal rows of second matrix"
)
out: Matrix = []
for i in range(ra):
row: List[Number] = []
for j in range(cb):
s = 0
for k in range(ca):
s += a[i][k] * b[k][j]
row.append(s)
out.append(row)
return out


def transpose_matrix(a: Matrix) -> Matrix:
r, c = matrix_shape(a)
return [[a[i][j] for i in range(r)] for j in range(c)]


def _find_top_level_operator(expr: str, ops: Sequence[str]) -> Tuple[int, str]:
depth = 0
for i, ch in enumerate(expr):
if ch in "[({":
depth += 1
elif ch in "])}":
depth -= 1
elif depth == 0 and ch in ops:
return i, ch
return -1, ""


def _normalize(expr: str) -> str:
return " ".join(expr.strip().split())


def evaluate_matrix_expression(expression: str) -> Matrix:
expr = _normalize(expression)
if not expr:
raise InvalidMatrixError("Empty matrix expression")

low = expr.lower()
if low.startswith("transpose(") and expr.endswith(")"):
inside = expr[len("transpose(") : -1]
return transpose_matrix(parse_matrix(inside))

if low.startswith("t(") and expr.endswith(")"):
inside = expr[2:-1]
return transpose_matrix(parse_matrix(inside))

idx, op = _find_top_level_operator(expr, "+-*")
if idx == -1:
return parse_matrix(expr)

left = expr[:idx].strip()
right = expr[idx + 1 :].strip()
if not left or not right:
raise InvalidMatrixError("Invalid matrix expression")

a = parse_matrix(left)
b = parse_matrix(right)

if op == "+":
return add_matrix(a, b)
if op == "-":
return subtract_matrix(a, b)
return multiply_matrix(a, b)


def format_matrix(m: Matrix) -> str:
return str(m)
19 changes: 10 additions & 9 deletions test_calculator.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import unittest

from calculator import Calculator


class TestCalculator(unittest.TestCase):
# base test cases
def setUp(self):
self.calc = Calculator()

def test_add(self):
self.assertEqual(self.calc.add(2, 3), 5)

def test_sub(self):
def test_subtract(self):
self.assertEqual(self.calc.subtract(2, 3), -1)

def test_multiply(self):
Expand All @@ -18,16 +19,16 @@ def test_multiply(self):
def test_divide(self):
self.assertEqual(self.calc.divide(2, 4), 0.5)

def test_divide(self):
def test_divide_negative(self):
self.assertEqual(self.calc.divide(4, -2), -2)

def test_divide_fail(self): # this will fail
self.assertNotEqual(self.calc.divide(4, -2), 2)

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() #
def test_evaluate_arithmetic_string(self):
self.assertEqual(self.calc.evaluate("2 + 3 * 4"), 14)


if __name__ == "__main__":
unittest.main()
75 changes: 75 additions & 0 deletions test_matrix.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import unittest

from exceptions import InvalidMatrixError, MatrixDimensionError
from matrix import (
add_matrix,
evaluate_matrix_expression,
multiply_matrix,
parse_matrix,
subtract_matrix,
transpose_matrix,
)
from calculator import Calculator


class TestMatrix(unittest.TestCase):
def setUp(self):
self.calc = Calculator()

def test_parse_matrix(self):
self.assertEqual(parse_matrix("[[1,2],[3,4]]"), [[1, 2], [3, 4]])

def test_matrix_add(self):
a = [[1, 2], [3, 4]]
b = [[5, 6], [7, 8]]
self.assertEqual(add_matrix(a, b), [[6, 8], [10, 12]])

def test_matrix_subtract(self):
a = [[5, 6], [7, 8]]
b = [[1, 2], [3, 4]]
self.assertEqual(subtract_matrix(a, b), [[4, 4], [4, 4]])

def test_matrix_multiply(self):
a = [[1, 2], [3, 4]]
b = [[5, 6], [7, 8]]
self.assertEqual(multiply_matrix(a, b), [[19, 22], [43, 50]])

def test_matrix_transpose(self):
a = [[1, 2, 3], [4, 5, 6]]
self.assertEqual(transpose_matrix(a), [[1, 4], [2, 5], [3, 6]])

def test_evaluate_matrix_add(self):
expr = "[[1,2],[3,4]] + [[5,6],[7,8]]"
self.assertEqual(self.calc.evaluate(expr, mode=6), [[6, 8], [10, 12]])

def test_evaluate_matrix_multiply(self):
expr = "[[1,2],[3,4]] * [[5,6],[7,8]]"
self.assertEqual(self.calc.evaluate(expr, mode=6), [[19, 22], [43, 50]])

def test_evaluate_matrix_transpose(self):
expr = "transpose([[1,2,3],[4,5,6]])"
self.assertEqual(evaluate_matrix_expression(expr), [[1, 4], [2, 5], [3, 6]])

def test_invalid_matrix_shape(self):
with self.assertRaises(InvalidMatrixError):
parse_matrix("[[1,2],[3]]")

def test_invalid_matrix_element(self):
with self.assertRaises(InvalidMatrixError):
parse_matrix("[[1,2],[3,'x']]")

def test_dimension_mismatch_add(self):
with self.assertRaises(MatrixDimensionError):
evaluate_matrix_expression("[[1,2],[3,4]] + [[1,2,3],[4,5,6]]")

def test_dimension_mismatch_multiply(self):
with self.assertRaises(MatrixDimensionError):
evaluate_matrix_expression("[[1,2,3]] * [[1,2],[3,4]]")

def test_empty_expression(self):
with self.assertRaises(InvalidMatrixError):
evaluate_matrix_expression("")


if __name__ == "__main__":
unittest.main()