From 6c1d783f08f4ba71501622be698762fbb70596d6 Mon Sep 17 00:00:00 2001 From: Souvik Ghosh Date: Mon, 6 Apr 2026 07:10:21 +0000 Subject: [PATCH] Implement Group B Feature 10: Matrix Operations --- README (1).md | 40 ++++++ __pycache__/calculator.cpython-312.pyc | Bin 0 -> 4157 bytes __pycache__/exceptions.cpython-312.pyc | Bin 0 -> 848 bytes __pycache__/matrix.cpython-312.pyc | Bin 0 -> 7056 bytes __pycache__/test_calculator.cpython-312.pyc | Bin 0 -> 2832 bytes __pycache__/test_matrix.cpython-312.pyc | Bin 0 -> 5561 bytes calculator.py | 66 +++++++++- exceptions.py | 10 ++ matrix.py | 136 ++++++++++++++++++++ test_calculator.py | 19 +-- test_matrix.py | 75 +++++++++++ 11 files changed, 332 insertions(+), 14 deletions(-) create mode 100644 README (1).md create mode 100644 __pycache__/calculator.cpython-312.pyc create mode 100644 __pycache__/exceptions.cpython-312.pyc create mode 100644 __pycache__/matrix.cpython-312.pyc create mode 100644 __pycache__/test_calculator.cpython-312.pyc create mode 100644 __pycache__/test_matrix.cpython-312.pyc create mode 100644 exceptions.py create mode 100644 matrix.py create mode 100644 test_matrix.py diff --git a/README (1).md b/README (1).md new file mode 100644 index 0000000..8f53e27 --- /dev/null +++ b/README (1).md @@ -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 +``` diff --git a/__pycache__/calculator.cpython-312.pyc b/__pycache__/calculator.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..79606c959cd4d21a07dd85db30da0a45d858f14c GIT binary patch literal 4157 zcmbssZEO?g`F-!h@mFHw!;sLlyA%q!C?uekh|&y5LPWb3=wwnA!)jf8XRvFZo%hZO zwv2>Oqy?x{r))~1Z33i81%b5vOn2##bjRlNg~Hq#y++QJ9WQ5~r9jgXx@2 zvMDairT8$95rYmRg_}eQe~YQy4m;E)#qn_`LgWI2RL89<(y-HoTmY}Sl%`vNpSDD> zGB)-#c#rByv^Abo=JvVbv}oWQws5+B5YA@RqqwbmjN;L-g2FATGx4LRsi@6r%WZE> zCcoOEc&vLqptb=%UiI4)?GYo*Nl)u17=4n@;jKswY(pd%7U-$ctO~zEzhUaPPJo`n z!b#zi&6RNiPoT5x2{eJFmhUKQM#W~4^y^4pP#Tdmk$%mVBZpmx9hvZ~5o0N?-A=DG1Y?UA#=muS{c%hp?1zceA zFbkoZ_~=b^lQlVAO~y|gLmHTJ*4U%8|h4M zG%4%4n3PB1Bpov*WTJ}0+UefVaHP&iZ?wi!PbO!wvZCZ$>c>5>_E4^9cOEVI{KefD zvs2y`Sgy}MJfjdI@PrVdFN#nkHt)m-3)87*M-3uJjl9r6)ZT2Nmp0HY0NuLkg15OI zxl}f3#52j5dZ0L(6Z0xbgTuN%05?^K2?2+AS~m^_ZYXMgLxW~2TN{B1{T1lIA|7uMU+Q}h zXfM6oTMq0hd3V)}WNFQGH#on8s797((P|t537 zj2httWp7QhiIV`3`(;1D>H2i`{`#s*%uJcEm>PmrZ5r|r*xse0HsrV2wJ$(mM^hHa#sc)n)EHArAt zlZG!a1-25aUjXB)>teK`=L=xmwk}31K)wLR?a#p&EU>j0DA{nUVj$Iu6$6d>IcsbG z0*3HlMMgqBF5fYt>Df#sO$=2LWfC{eq*Nmw73;xE3Yu;Rpqg&TT2zHhr5mPuxSrU# z(X^5?okM90!Z3MS0{~oHgLy2OmJPFMM3YJGSjJ@18Iv0sP!#Ax*-^78ol%LbDJDM{ z*WjE>sxi1l;-}9TCQIR13dS9Q^kK3P$q7yM5^rM7#N#GTnwXf(>9kIx8YE`%yp`U_ zJ}O#wL;Q~Da!ifTpcttO+0&m)N9ClxzvpzCI<Dwkv6nvEcOTyTaIw9wI9BrSpa1FnAPhFj6LTjPeLE@_$EU}y{G^C03m|;uUBDu|JS$&S z=9J$KeSY}*;YFc)>81ABvAI3P_kQ5f+t=S-6m~xE*5=mPfzQStywX{c-YUP+Uux~I zL(`OaupAgDc?YaaC^2L!XxHR3GSO9uBQ%JUm*^I!L3*;RrPF)^20U1)PlC@hSYBMu zq|`lh`XDVc20qPQ%*}4Ov3=3Edy0F^1zm&qVGr^LirX%qo$@_h-Ozk&<>Wz_RLYHU zD4!ahF18;M1X7fsuxnbO0YYMqO6+$uOPY>KMQl2bSd}s8gbGZ444uT*H9|z#SW~Lm zMfIxM$==*U;deksOA=p6=vwe}JrH)@NR)*)OWrq^e8N4SwBVC&bd`O3O3po&%uuvj zAwX&|osozpr_@NqbVnjkEM${(?u|sovU0L=V(~<1t|TdUcJ-Wp)(MXG9*|;I?6HPpp4^%GK;~(AbBL^t=^k*#JWYvxPL}_EitsO=#6W2~x z^l*@ZS(c>{$zqI9QB52*`M8$JLUaSA#WR*tO+J~PPzh9P5Kc{;vh)TKR@33J5E?*T z!)K#v#-Kk?SY4yhC(izg!S<68AVE{GeiyoB7GwNZQ p!7Kg;WN~#ov1;yH(%@_8>)=2O40m4R2M@4!x;%sX*gN~^{J-X4c8~x7 literal 0 HcmV?d00001 diff --git a/__pycache__/exceptions.cpython-312.pyc b/__pycache__/exceptions.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1e6e7a2b77b55a0da60b100d0ee0d7cde9db5a3f GIT binary patch literal 848 zcmb7Cv2GMG5VdzVy+?A?`2f1) z7b5(ZU=-FI8VbW^y4(iCZ?m~%w2 zJ)bfAU4+2mVTK+}_fkn3DVrE3G`P z4Y~i8Nhdft`Y1GEBo8SfbulT4bpEWoMY-VpkxiiK3WU)9I-qV7KwB&Daa{q)D+#<) zE8CR0(9Vyimbh_Q;zA&AN*n<6G5SB!*FZW1xKb^e$ literal 0 HcmV?d00001 diff --git a/__pycache__/matrix.cpython-312.pyc b/__pycache__/matrix.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..18da982d768015f5e06477185f79761405fead68 GIT binary patch literal 7056 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 literal 0 HcmV?d00001 diff --git a/__pycache__/test_matrix.cpython-312.pyc b/__pycache__/test_matrix.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f0888e54e666a90b11a93c4615d668b72957eaac GIT binary patch literal 5561 zcmd6rPfQ!x9mn6B@t?67$3Q8?Z2lOsO>pdm5J<{4yU8X2LJ|TY*`!)iuvMg-`un}{48}H#+Dd6g z;_G>Delu_0hu?hPpTBrKZURSid?S`{5%PB|n1{V|va<{)OGF_Gjgv6`3UQhc!a_m} zixkUZ+?KG1?FmQNk#L5c2`MZkTwzzj9d`5Uws>7a4$G7XRgJc8iwoM#7sOLOU38SU^x|K zni;8kwVC^A7M)C+m+PUF)Kb${NPz0@E7ADmOgx&NW|L+o@RybfJHLRFCFpIKDkLl@ z^eG98iU4X;L{Ph819c$3uv2k>N{SQIrAVM|#RXcYxIty54%DN_pobI>s8=}zT0id% z)a&lgRV{7sm^9nQR}kP2`R}xYERnQz%_O}*B39iz2~dE|>L6CPO+wFBsczHM_?H^S z_}|{@N+i==lU;W;m1$jfM(^IccB89b)xJtkr@AKNQBCv5qxa#;^q1+!QKtF_l2cuG z1|qwVy3+8TC7_+DdEKt5>AR^+{qE~`@|t>RPs8_kLS8i;{oU}Q?YFKSD3U$Bb3g^} z-5{JSkp=S5>I=piN4k`M1af_?bX^e?aY|5ZihW9$64sCs9FTP+;26nx#>cxuXC^}9 zJ)z!-i6Y)p#E>FBsb~X%jI4v+sw_S5XeJtG64WvN&KwkLgtFei`&yn;nZsrMth)LG zxJmmE{KLP77l{7Ko=}9iUaRbT#CC2SKSC}hW6NxP+CxYAVu_Y5CEHFR(I}r7MHk+quTPvd2~$RxWM$ zI zw9NoNJnVm?-JZRlZOL8E3%S0+>HaNgpqe(mF@9+~KL=SNQ{WYUqM!mOIK{!vVA&{l zmp#rR?lDu>NI;mOXO1T)^GjOGF7=oJh-QbR*Fo#Kb(nQe{I z8UH@1ZAJVTA9NPv3DG^xe{>b(c5n;R8#cie+BSTlEX{VW%;spWJ3E`F`R?3op>uFc zx>^m^8jq6jOtj$rU;^UOm;-^<@z9)|Sv7<#fQ|pzf(qjn*tr3`!X}&}nIn6~Eq~BI zK7KZI4#u&cg!(2XxPjn+o|slt!$aDjA=VB8F3cU-3+K9I&Y4yX-ntDZO?!a1aNuT~ zIk!TOK3>VZMo<7+(fUBZti7F!HjDW4rf+y7!L!ugw9(#ZdVJfADTKI;C?BEpqMQTSgZM~I#HLSV zIu7D&Mq(5H3GCbX#@tYzt)Iz1D4ZYNl5SPQw*2E3Jp237e0S=#P;=^4!fB;9pMU|* z!`2tt!BaZa14-N)!epKZ7=Ch)U*M-$4fSYEs8(y4z(G{4&~C(vf;s$ASq^Xddb0hy zSv$5)^MmVSg=cge!OAasDhy&a8ds>X1fs6D;)ah5S-U1nN<_8^*4 zGapuBtQCd6t92R#Jq<5CM_+i3ZXWC2@SOQ4p{|Zsa?A50oASvwA{5?W4X=K?gLBE> z%RzC?-s`MA0XE$CU1RiG7(*xLPJ-WFKy;Ohf)8p@Q{yVEVbhrt2XHIJeT?L~_9GVp zas}V1O*x3@LWnMe=t6JVCAjB(Le{p63>I2^z=DNW`PU8u(?!H+L*d_aZJ>&|2@pxd zG*}i)KHw{cDxkIFK82@iJs|Kg@1|nUhNl<74bxZhiNeY2oAM1LI!qDiFf}f{e^7m< zWls2U94cGMvH_%63DD0ovIZD)1jz&|)AD9k{yeVHnl>HoKKS$Y5-#e5# zfypXQKkkAx)6bZCs)Q%EU@`4+@<3mS+<0u8zp zi6o;5H4@R?kqFG_X5x4*M?JH5($>Cia#vmIu%0}@{++^WBe=B`r zK)%3!26ed0qg@4gEmBJVM!f$Z7vBg@D!d^e|Eedn{X0h=4L)mm+0g!?q5V4oh1au` e*yP2`Uq#=ywuUD|zq$FVo4+1?EfQNRZ|L8@e$OEQ literal 0 HcmV?d00001 diff --git a/calculator.py b/calculator.py index 88317c8..b919447 100644 --- a/calculator.py +++ b/calculator.py @@ -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}") diff --git a/exceptions.py b/exceptions.py new file mode 100644 index 0000000..f9926c7 --- /dev/null +++ b/exceptions.py @@ -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.""" diff --git a/matrix.py b/matrix.py new file mode 100644 index 0000000..da7360b --- /dev/null +++ b/matrix.py @@ -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) diff --git a/test_calculator.py b/test_calculator.py index 7cb79e9..5517745 100644 --- a/test_calculator.py +++ b/test_calculator.py @@ -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): @@ -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() diff --git a/test_matrix.py b/test_matrix.py new file mode 100644 index 0000000..8b1aa22 --- /dev/null +++ b/test_matrix.py @@ -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()