diff --git a/calculator/Calculator.py b/calculator/Calculator.py new file mode 100644 index 0000000000000000000000000000000000000000..f69d7238f0f2b3a48b51b380f256ffede8fb70dc --- /dev/null +++ b/calculator/Calculator.py @@ -0,0 +1,57 @@ +""" +This Calculator module is a simple calculator that can parse and evaluate expressions of the form: +expression ::= term | expression operator expression +operator ::= + | - | * | / +with the usual precedence rules. +""" + +from Operators import Operator, STANDARD_OPERATORS +from Expression import Token, Term, Expression, TermExpression, OperatorExpression + +class Calculator: + def __init__(self, operators = STANDARD_OPERATORS): + self.operators = operators + + def _tokenize(self, line: str) -> list[Token]: + """ + Tokenize an expression into a list of tokens. + """ + tokens = [] + for token in line.split(): + if token in self.operators: + tokens.append(self.operators[token]) + else: + try: + term = float(token) + tokens.append(term) + except ValueError: + raise ValueError(f"Unknown token: {token}") + return tokens + + def _parse(self, tokens: list[Token]) -> Expression: + if not tokens: + raise ValueError("Empty expression") + if len(tokens) == 1: + if isinstance(tokens[0], Term): + return TermExpression(tokens[0]) + raise ValueError(f"Expected a term, got {tokens[0]}") + if len(tokens) == 2: + raise ValueError("Invalid expression") + + # Find the rightest operator with the lowest precedence + operator = None + for i, token in enumerate(tokens): + if isinstance(token, Operator): + if operator is None or token.precedence <= operator.precedence: + operator = token + operator_index = i + + # Split the expression into two parts + left = tokens[:operator_index] + right = tokens[operator_index + 1:] + + # Parse the left and right parts recursively + return OperatorExpression(operator, self._parse(left), self._parse(right)) + + def __call__(self, expression: str) -> Term: + return self._parse(self._tokenize(expression))() diff --git a/calculator/Expression.py b/calculator/Expression.py new file mode 100644 index 0000000000000000000000000000000000000000..d19ea88ffd75c185ae706239408735ac488f9f79 --- /dev/null +++ b/calculator/Expression.py @@ -0,0 +1,32 @@ +from typing import Union +from Operators import Operator + +Term: type = float +Token: type = Union[Operator, Term] + + +class OperatorExpression: + def __init__(self, operator: Operator, left, right): + self.operator = operator + self.left = left + self.right = right + + def __repr__(self): + return f"({self.left} {self.operator} {self.right})" + + def __call__(self) -> Term: + return self.operator(self.left(), self.right()) + + +class TermExpression: + def __init__(self, value: Term): + self.value = value + + def __repr__(self): + return str(self.value) + + def __call__(self) -> Term: + return self.value + + +Expression: type = Union[OperatorExpression, TermExpression] diff --git a/calculator/Operators.py b/calculator/Operators.py new file mode 100644 index 0000000000000000000000000000000000000000..fdd2090d31600b428cabcbd97622e1dc6e1d7cb1 --- /dev/null +++ b/calculator/Operators.py @@ -0,0 +1,46 @@ +class Operator: + def __init__(self, symbol, precedence): + self.symbol = symbol + self.precedence = precedence + + def __repr__(self): + return self.symbol + + def __call__(self): + raise NotImplemented + +class Adder(Operator): + def __init__(self): + super().__init__('+', 1) + + def __call__(self, a, b): + return a + b + +class Subtracter(Operator): + def __init__(self): + super().__init__('-', 1) + + def __call__(self, a, b): + return a - b + +class Multiplyer(Operator): + def __init__(self): + super().__init__('*', 2) + + def __call__(self, a, b): + return a * b + +class Divider(Operator): + def __init__(self): + super().__init__('/', 2) + + def __call__(self, a, b): + return a / b + + +STANDARD_OPERATORS = { + '+': Adder(), + '-': Subtracter(), + '*': Multiplyer(), + '/': Divider(), +} diff --git a/calculator/README.md b/calculator/README.md new file mode 100644 index 0000000000000000000000000000000000000000..c8389d840435ab15abba9e16b53e28d250a28e26 --- /dev/null +++ b/calculator/README.md @@ -0,0 +1,3 @@ +# ViaRézo Calculator + +This is a simple two-operand calculator. It is a good example of how to use the [GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/index.html) to lint, test and deploy a project. diff --git a/calculator/__pycache__/Calculator.cpython-310.pyc b/calculator/__pycache__/Calculator.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..24c0a7f8401cd6d66646eb6938fc36c5b8dfbf5d Binary files /dev/null and b/calculator/__pycache__/Calculator.cpython-310.pyc differ diff --git a/calculator/__pycache__/Expression.cpython-310.pyc b/calculator/__pycache__/Expression.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..39cc4147cc27f4e3f9656a4e48fe56674b8f5f41 Binary files /dev/null and b/calculator/__pycache__/Expression.cpython-310.pyc differ diff --git a/calculator/__pycache__/Operator.cpython-310.pyc b/calculator/__pycache__/Operator.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..53418f864c433e3db52a95846f613ed4d9581a61 Binary files /dev/null and b/calculator/__pycache__/Operator.cpython-310.pyc differ diff --git a/calculator/__pycache__/Operators.cpython-310.pyc b/calculator/__pycache__/Operators.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6e8f7a6a441d4e1396401e05cfb89d4727635010 Binary files /dev/null and b/calculator/__pycache__/Operators.cpython-310.pyc differ diff --git a/calculator/__pycache__/calculator.cpython-310.pyc b/calculator/__pycache__/calculator.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e137347312a05e1267631372d1a44dda7fc76488 Binary files /dev/null and b/calculator/__pycache__/calculator.cpython-310.pyc differ diff --git a/calculator/__pycache__/server.cpython-310.pyc b/calculator/__pycache__/server.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..02f6dd8e95307bfb97827355b4dc24d6df06874c Binary files /dev/null and b/calculator/__pycache__/server.cpython-310.pyc differ diff --git a/calculator/requirements.txt b/calculator/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..9c7788af16fa05ef099b6c72c09d5702edf5712c --- /dev/null +++ b/calculator/requirements.txt @@ -0,0 +1,2 @@ +fastapi[all] +uvicorn[standard] diff --git a/calculator/server.py b/calculator/server.py new file mode 100644 index 0000000000000000000000000000000000000000..8f5fed6b46f404572e5e3dfdad0b628f8add9db8 --- /dev/null +++ b/calculator/server.py @@ -0,0 +1,19 @@ +from fastapi import FastAPI +from fastapi.requests import Request +from fastapi.templating import Jinja2Templates +from Calculator import Calculator + +app = FastAPI() +templates = Jinja2Templates(directory="templates") +calc = Calculator() + +@app.get("/icon") + +@app.get("/") +async def root(request: Request): + expression = request.query_params.get("expression", "") + context = { "request": request } + if expression: + result = calc(expression) + context = { "request": request, "expression": expression, "result": result} + return templates.TemplateResponse("index.html", context) diff --git a/calculator/templates/index.html b/calculator/templates/index.html new file mode 100644 index 0000000000000000000000000000000000000000..1806d83aff2e9f7c81cb7d20e746a821f84bafaa --- /dev/null +++ b/calculator/templates/index.html @@ -0,0 +1,25 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <title>Calculator</title> +</head> +<body> + <header> + <h1>ViaRézo Calculator</h1> + </header> + <div> + <form action="/" method="get"> + {% if expression %} + <input type="text" name="expression" placeholder="12 / 2 + 6 * 8" value="{{ expression }}"> + {% else %} + <input type="text" name="expression" placeholder="12 / 2 + 6 * 8"> + {% endif %} + <button type="submit">Calculate</button> + </form> + {% if result %} + <p>Result: {{ result }}</p> + {% endif %} + </div> +</body> +</html