diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml
index 73a39b0..e6590f5 100644
--- a/.github/workflows/codecov.yml
+++ b/.github/workflows/codecov.yml
@@ -1,11 +1,13 @@
---
-name: Codecov Coverage Report
+name: "Codecov Coverage Report"
-on: # yamllint disable-line rule:truthy
- push:
- branches: [ main ]
- pull_request:
- branches: [ main ]
+on: # yamllint disable-line rule:truthy
+ # Makes it reusable; called by other workflows
+ workflow_call:
+ secrets:
+ CODECOV_TOKEN:
+ description: "CODECOV Auth Token"
+ required: true
jobs:
run:
@@ -15,27 +17,28 @@ jobs:
timeout-minutes: 10
strategy:
matrix:
- os: [ubuntu-latest]
+ os: ["ubuntu-latest"]
python-version: ["3.12"]
steps:
- - uses: actions/checkout@main
- - name: Setup Python
- uses: actions/setup-python@main
+ - uses: "actions/checkout@main"
+ - name: "Setup Python"
+ uses: "actions/setup-python@main"
with:
python-version: ${{ matrix.python-version }}
- name: "Cache PIP Dependencies"
uses: "actions/cache@v4"
with:
path: ~/.cache/pip
- key: ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('requirements.txt') }}
+ key: |
+ ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-${{ matrix.python-version }}-
${{ runner.os }}-pip-
- - name: Install prerequisites
+ - name: "Install prerequisites"
run: |
python -m pip install --upgrade pip setuptools wheel
pip install -r requirements.txt
- - name: Install pytest, pytest-cov
+ - name: "Install pytest, pytest-cov"
run: |
pip install pytest
pip install pytest-cov
@@ -44,12 +47,12 @@ jobs:
run: |
python -c "import os; print(os.getcwd())"
python -m pytest . --ignore=solutions --log-cli-level=INFO -v --cov-report term-missing --cov --cov-branch --cov-report=xml
- - name: List test files
+ - name: "List test files"
run: |
ls *.xml
# yamllint enable rule:line-length
- - name: Upload coverage to Codecov
- uses: codecov/codecov-action@v5.5.1
+ - name: "Upload coverage to Codecov"
+ uses: "codecov/codecov-action@v5.5.1"
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: coverage.xml
diff --git a/.github/workflows/flake8.yml b/.github/workflows/flake8.yml
index bd102b3..176cefb 100644
--- a/.github/workflows/flake8.yml
+++ b/.github/workflows/flake8.yml
@@ -1,19 +1,9 @@
---
-name: "Flake8"
+name: "Flake8 Workflow"
-on: # yamllint disable-line rule:truthy
- push:
- branches: [ main ]
- paths:
- - '**/*.py'
- - 'requirements.txt'
- - '.pylintrc'
- pull_request:
- branches: [ main ]
- paths:
- - '**/*.py'
- - 'requirements.txt'
- - '.pylintrc'
+on: # yamllint disable-line rule:truthy
+ # Makes it reusable; called by other workflows
+ workflow_call:
permissions:
contents: "read"
@@ -33,21 +23,23 @@ jobs:
- name: Set up Python ${{ matrix.python-version }}
# This is the version of the action for setting up Python,
# not the Python version.
- uses: actions/setup-python@v6
+ uses: "actions/setup-python@v6"
with:
python-version: ${{ matrix.python-version }}
- name: "Cache PIP Dependencies"
uses: "actions/cache@v4"
with:
path: ~/.cache/pip
- key: ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('requirements.txt') }}
+ key: |
+ ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-${{ matrix.python-version }}-
${{ runner.os }}-pip-
# You can test your matrix by printing the current
# Python version
- name: "Display Python Version"
- run: python -c "import sys; print(sys.version)"
+ run: |
+ python -c "import sys; print(sys.version)"
- name: "Install Dependencies"
run: |
python -m pip install --upgrade pip
diff --git a/.github/workflows/lint_test_report.yml b/.github/workflows/lint_test_report.yml
new file mode 100644
index 0000000..8e0813c
--- /dev/null
+++ b/.github/workflows/lint_test_report.yml
@@ -0,0 +1,83 @@
+---
+name: "Main Pipeline"
+
+on: # yamllint disable-line rule:truthy
+ push:
+ branches: [ main ]
+ pull_request:
+ branches: [ main ]
+
+permissions:
+ contents: "read"
+ pull-requests: "read"
+
+jobs:
+ changed-files:
+ runs-on: "ubuntu-latest"
+ outputs:
+ python_changed: ${{ steps.python-files.outputs.any_changed }}
+ yaml_changed: ${{ steps.yaml-files.outputs.any_changed }}
+ steps:
+ - uses: "actions/checkout@v5"
+ - name: "Get changed Python files"
+ id: python-files
+ uses: "tj-actions/changed-files@v47"
+ with:
+ files: |
+ **/*.py
+ requirements.txt
+ .pylintrc
+ - name: "Get changed YAML files"
+ id: yaml-files
+ uses: "tj-actions/changed-files@v47"
+ with:
+ files: |
+ **/*.yml
+ **/*.yaml
+
+ yamllint:
+ name: "YAML Lint Workflow"
+ needs:
+ - "changed-files"
+ if: needs.changed-files.outputs.yaml_changed == 'true'
+ uses: "./.github/workflows/yamllint.yml"
+
+ ruff:
+ name: "Ruff Lint and Format"
+ needs:
+ - "changed-files"
+ - "yamllint"
+ if: needs.changed-files.outputs.python_changed == 'true'
+ uses: "./.github/workflows/ruff.yml"
+
+ pylint:
+ name: "Pylint Workflow"
+ needs:
+ - "changed-files"
+ - "yamllint"
+ if: needs.changed-files.outputs.python_changed == 'true'
+ uses: "./.github/workflows/pylint.yml"
+
+ flake8:
+ name: "Flake8 Workflow"
+ needs:
+ - "changed-files"
+ - "yamllint"
+ if: needs.changed-files.outputs.python_changed == 'true'
+ uses: "./.github/workflows/flake8.yml"
+
+ pytest:
+ name: "Pytest Workflow"
+ needs:
+ - "ruff"
+ - "pylint"
+ - "flake8"
+ uses: "./.github/workflows/pytest.yml"
+
+ codecov:
+ name: "Codecov Coverage Report"
+ needs:
+ - "pytest"
+ uses: "./.github/workflows/codecov.yml"
+ secrets:
+ CODECOV_TOKEN: "${{ secrets.CODECOV_TOKEN }}"
diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml
index 0ae436f..ff25eb1 100644
--- a/.github/workflows/pylint.yml
+++ b/.github/workflows/pylint.yml
@@ -1,19 +1,9 @@
---
-name: "Pylint"
+name: "Pylint Workflow"
-on:
- push:
- branches: [ main ]
- paths:
- - '**/*.py'
- - 'requirements.txt'
- - '.pylintrc'
- pull_request:
- branches: [ main ]
- paths:
- - '**/*.py'
- - 'requirements.txt'
- - '.pylintrc'
+on: # yamllint disable-line rule:truthy
+ # Makes it reusable; called by other workflows
+ workflow_call:
jobs:
build:
@@ -25,25 +15,25 @@ jobs:
matrix:
python-version: ["3.12"]
steps:
- - uses: "actions/checkout@v5"
- - name: Set up Python ${{ matrix.python-version }}
- uses: "actions/setup-python@v6"
- with:
- python-version: ${{ matrix.python-version }}
- - name: "Cache PIP Dependencies"
- uses: "actions/cache@v4"
- with:
- path: ~/.cache/pip
- key: |
- ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('requirements.txt') }}
- restore-keys: |
- ${{ runner.os }}-pip-${{ matrix.python-version }}-
- ${{ runner.os }}-pip-
- - name: "Install dependencies"
- run: |
- python -m pip install --upgrade pip
- pip install pylint
- pip install -r requirements.txt
- - name: "Analysing the code with pylint"
- run: |
- python -m pylint --verbose $(find . -name "*.py" ! -path "*/.venv/*" ! -path "*/venv/*") --rcfile=.pylintrc
\ No newline at end of file
+ - uses: "actions/checkout@v5"
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: "actions/setup-python@v6"
+ with:
+ python-version: ${{ matrix.python-version }}
+ - name: "Cache PIP Dependencies"
+ uses: "actions/cache@v4"
+ with:
+ path: ~/.cache/pip
+ key: |
+ ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('requirements.txt') }}
+ restore-keys: |
+ ${{ runner.os }}-pip-${{ matrix.python-version }}-
+ ${{ runner.os }}-pip-
+ - name: "Install dependencies"
+ run: |
+ python -m pip install --upgrade pip
+ pip install pylint
+ pip install -r requirements.txt
+ - name: "Analysing the code with pylint"
+ run: |
+ python -m pylint --verbose $(find . -name "*.py" ! -path "*/.venv/*" ! -path "*/venv/*") --rcfile=.pylintrc
diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml
index fb740ab..7737a16 100644
--- a/.github/workflows/pytest.yml
+++ b/.github/workflows/pytest.yml
@@ -1,11 +1,9 @@
---
name: "Pytest Workflow"
-on:
- push:
- branches: [ main ]
- pull_request:
- branches: [ main ]
+on: # yamllint disable-line rule:truthy
+ # Makes it reusable; called by other workflows
+ workflow_call:
jobs:
test:
@@ -14,26 +12,26 @@ jobs:
# indefinitely if something goes wrong
timeout-minutes: 10
steps:
- - name: "Checkout code"
- uses: "actions/checkout@v5"
- - name: "Set up Python 3.12"
- uses: "actions/setup-python@v6"
- with:
- python-version: "3.12"
- - name: "Cache PIP Dependencies"
- uses: "actions/cache@v4"
- with:
- path: ~/.cache/pip
- key: |
- ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('requirements.txt') }}
- restore-keys: |
- ${{ runner.os }}-pip-${{ matrix.python-version }}-
- ${{ runner.os }}-pip-
- - name: "Install dependencies"
- run: |
- python -m pip install --upgrade pip
- pip install -r requirements.txt
- pip install pytest
- - name: "Run pytest"
- run: |
- pytest . --verbose --ignore=solutions --log-cli-level=INFO
\ No newline at end of file
+ - name: "Checkout code"
+ uses: "actions/checkout@v5"
+ - name: "Set up Python 3.12"
+ uses: "actions/setup-python@v6"
+ with:
+ python-version: "3.12"
+ - name: "Cache PIP Dependencies"
+ uses: "actions/cache@v4"
+ with:
+ path: ~/.cache/pip
+ key: |
+ ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('requirements.txt') }}
+ restore-keys: |
+ ${{ runner.os }}-pip-${{ matrix.python-version }}-
+ ${{ runner.os }}-pip-
+ - name: "Install dependencies"
+ run: |
+ python -m pip install --upgrade pip
+ pip install -r requirements.txt
+ pip install pytest
+ - name: "Run pytest"
+ run: |
+ pytest . --verbose --ignore=solutions --log-cli-level=INFO
diff --git a/.github/workflows/ruff.yml b/.github/workflows/ruff.yml
index 7666f2e..7e4659c 100644
--- a/.github/workflows/ruff.yml
+++ b/.github/workflows/ruff.yml
@@ -1,19 +1,9 @@
---
name: "Ruff Lint and Format"
-on:
- push:
- branches: [ main ]
- paths:
- - '**/*.py'
- - 'requirements.txt'
- - '.pylintrc'
- pull_request:
- branches: [ main ]
- paths:
- - '**/*.py'
- - 'requirements.txt'
- - '.pylintrc'
+on: # yamllint disable-line rule:truthy
+ # Makes it reusable; called by other workflows
+ workflow_call:
jobs:
ruff:
@@ -31,7 +21,8 @@ jobs:
uses: "actions/cache@v4"
with:
path: ~/.cache/pip
- key: ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('requirements.txt') }}
+ key: |
+ ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-${{ matrix.python-version }}-
${{ runner.os }}-pip-
@@ -44,4 +35,4 @@ jobs:
ruff check --output-format=github .
- name: "Run Ruff format check"
run: |
- ruff format --check .
\ No newline at end of file
+ ruff format --check .
diff --git a/.github/workflows/yamllint.yml b/.github/workflows/yamllint.yml
new file mode 100644
index 0000000..1d797de
--- /dev/null
+++ b/.github/workflows/yamllint.yml
@@ -0,0 +1,38 @@
+---
+name: "YAML Lint Workflow"
+
+on: # yamllint disable-line rule:truthy
+ # Makes it reusable; called by other workflows
+ workflow_call:
+
+jobs:
+ yamllint:
+ runs-on: "ubuntu-latest"
+ # Adding 'timeout-minutes: 10' would prevent jobs from running
+ # indefinitely if something goes wrong
+ timeout-minutes: 10
+ steps:
+ - uses: "actions/checkout@v5"
+ - name: "Set up Python 3.12"
+ uses: "actions/setup-python@v6"
+ with:
+ python-version: "3.12"
+ - name: "Cache PIP Dependencies"
+ uses: "actions/cache@v4"
+ with:
+ path: ~/.cache/pip
+ key: |
+ ${{ runner.os }}-pip-3.12-${{ hashFiles('requirements.txt') }}
+ restore-keys: |
+ ${{ runner.os }}-pip-3.12-
+ ${{ runner.os }}-pip-
+ - name: "Install yamllint"
+ run: |
+ python -m pip install --upgrade pip
+ pip install yamllint
+ - name: "Check yamllint Version"
+ run: |
+ yamllint --version
+ - name: "Lint with yamllint"
+ run: |
+ yamllint .
diff --git a/.yamllint.yml b/.yamllint.yml
new file mode 100644
index 0000000..0c01e2b
--- /dev/null
+++ b/.yamllint.yml
@@ -0,0 +1,3 @@
+---
+rules:
+ line-length: disable
diff --git a/Dockerfile b/Dockerfile
index 8a235c6..54894c4 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -38,7 +38,8 @@ RUN pip install --no-cache-dir \
flake8 \
pylint \
pytest \
- pytest-cov
+ pytest-cov \
+ yamllint
# Copy the rest of the code (including .pylintrc if present)
COPY . .
diff --git a/README.md b/README.md
index 2877080..c3cd5c0 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,6 @@
# [Exercism Python Track](https://exercism.io/tracks/python)
-[](https://github.com/ikostan/python/actions/workflows/pytest.yml)
-[](https://github.com/ikostan/python/actions/workflows/pylint.yml)
-[](https://github.com/ikostan/python/actions/workflows/ruff.yml)
-[](https://github.com/ikostan/python/actions/workflows/flake8.yml)
+[](https://github.com/ikostan/python/actions/workflows/lint_test_report.yml)
[](https://codecov.io/github/ikostan/python)
diff --git a/codecov.yml b/codecov.yml
index ff4805c..63b1b89 100644
--- a/codecov.yml
+++ b/codecov.yml
@@ -1,4 +1,5 @@
+---
ignore:
- "**/tests/" # Ignores all files within any 'tests' directory
- - "**/*_test.py" # Ignores files ending with '_test.py'
- - "test_*.py" # Ignores files starting with 'test_'
\ No newline at end of file
+ - "**/*_test.py" # Ignores files ending with '_test.py'
+ - "test_*.py" # Ignores files starting with 'test_'
diff --git a/run_ci_tests.sh b/run_ci_tests.sh
index 716bb0c..3796ace 100644
--- a/run_ci_tests.sh
+++ b/run_ci_tests.sh
@@ -6,6 +6,10 @@ set -e
# Display Python version
python --version
+# Run YAML lint
+echo "Running YAML lint..."
+yamllint . || { echo "YAML lint failed"; exit 1; }
+
# Run Ruff lint
echo "Running Ruff lint..."
ruff check --output-format=github . || { echo "Ruff lint failed"; exit 1; }
diff --git a/solutions/python/wordy/1/wordy.py b/solutions/python/wordy/1/wordy.py
new file mode 100644
index 0000000..8673c38
--- /dev/null
+++ b/solutions/python/wordy/1/wordy.py
@@ -0,0 +1,89 @@
+"""
+Parse and evaluate simple math word problems
+returning the answer as an integer.
+
+Handle a set of operations, in sequence.
+
+Since these are verbal word problems, evaluate
+the expression from left-to-right, ignoring the
+typical order of operations.
+"""
+
+import string
+
+STR_TO_OPERATOR: dict = {
+ "plus": "+",
+ "minus": "-",
+ "multiplied": "*",
+ "divided": "/",
+}
+
+WRONG_OPERATORS: list[str] = [
+ "plus?",
+ "minus?",
+ "multiplied?",
+ "divided?",
+ "plus plus",
+ "plus multiplied",
+ "minus minus",
+ "multiplied multiplied",
+ "divided divided",
+ "What is?",
+]
+
+
+def answer(question: str) -> int:
+ if "cubed" in question:
+ raise ValueError("unknown operation")
+
+ for item in WRONG_OPERATORS:
+ if item in question:
+ raise ValueError("syntax error")
+
+ digits: list[bool] = []
+ for char in question:
+ if char.isdigit():
+ digits.append(True)
+
+ operators: list[bool] = []
+ for key, val in STR_TO_OPERATOR.items():
+ if key in question or val in question:
+ operators.append(True)
+
+ if not any(digits + operators):
+ raise ValueError("unknown operation")
+
+ print(f"\n{question}")
+ return eval(_reformat(question))
+
+
+def _reformat(question: str) -> str:
+ # 1
+ question = question.replace("?", "")
+ # 2
+ chars: list[str] = list(char for char in question)
+ for i, char in enumerate(chars):
+ if char in STR_TO_OPERATOR.values():
+ chars[i] = f" {char} "
+ # 3
+ question = "".join(chars)
+ print(f"\n new question:{question}")
+ # 4
+ question_list: list[str] = question.split()
+ for i, item in enumerate(question_list):
+ # print(f"\nitem: {item}")
+ if not (
+ item.isdigit()
+ or item in STR_TO_OPERATOR.keys()
+ or item in STR_TO_OPERATOR.values()
+ ):
+ question_list[i] = ""
+ # print("\n#1")
+ elif item in STR_TO_OPERATOR.keys():
+ # print("\n#2")
+ question_list[i] = STR_TO_OPERATOR[item]
+
+ print(f"\n{question_list}")
+ print(f"\n{' '.join(question_list)}")
+
+ return " ".join(question_list)
diff --git a/solutions/python/wordy/2/wordy.py b/solutions/python/wordy/2/wordy.py
new file mode 100644
index 0000000..09feb08
--- /dev/null
+++ b/solutions/python/wordy/2/wordy.py
@@ -0,0 +1,118 @@
+"""
+Parse and evaluate simple math word problems
+returning the answer as an integer.
+
+Handle a set of operations, in sequence.
+
+Since these are verbal word problems, evaluate
+the expression from left-to-right, ignoring the
+typical order of operations.
+"""
+
+import string
+
+STR_TO_OPERATOR: dict = {
+ "plus": "+",
+ "minus": "-",
+ "multiplied": "*",
+ "divided": "/",
+}
+
+WRONG_OPERATORS: list[str] = [
+ "plus?",
+ "minus?",
+ "multiplied?",
+ "divided?",
+ "plus plus",
+ "plus multiplied",
+ "minus multiplied",
+ "minus minus",
+ "multiplied multiplied",
+ "divided divided",
+ "What is?",
+]
+
+
+def answer(question: str) -> int:
+ _validate_errors(question)
+ result: int = 0
+ new_question: list[str] = _reformat(question)
+
+ if len(new_question) <= 3:
+ try:
+ _validate_evaluation_pattern(new_question)
+ eval_str: str = "".join(new_question)
+ result = eval(eval_str)
+ return result
+ except Exception:
+ raise ValueError("syntax error")
+
+ while len(new_question) >= 3:
+ try:
+ _validate_evaluation_pattern(new_question[:3])
+ eval_str: str = "".join(new_question[:3])
+ val: int = eval(eval_str)
+ result = val
+ new_question = [str(result)] + new_question[3:]
+ if len(new_question) < 3:
+ _validate_evaluation_pattern(new_question)
+ return eval("".join(new_question))
+ except Exception:
+ raise ValueError("syntax error")
+ return result
+
+
+def _validate_evaluation_pattern(val: list) -> None:
+ if len(val) == 3 and not val[1] in STR_TO_OPERATOR.values():
+ raise ValueError("syntax error")
+
+ if len(val) == 2 and not val[0] in STR_TO_OPERATOR.values():
+ raise ValueError("syntax error")
+
+
+def _reformat(question: str) -> list:
+ # 1: Remove '?' mark
+ question = question.replace("?", "")
+ # 2: Convert all operators writen in word into proper math sign
+ question_list: list[str] = question.split()
+ formated_question_list: list[str] = []
+ for i, item in enumerate(question_list):
+ if not (
+ item.isdigit()
+ or item in STR_TO_OPERATOR.keys()
+ or item in STR_TO_OPERATOR.values()
+ or any(val in item for val in STR_TO_OPERATOR.values())
+ ):
+ continue
+ elif item in STR_TO_OPERATOR.keys():
+ formated_question_list.append(STR_TO_OPERATOR[item])
+ elif item.isdigit():
+ formated_question_list.append(item)
+ elif item in STR_TO_OPERATOR.values():
+ formated_question_list.append(item)
+ elif any(val in item for val in STR_TO_OPERATOR.values()):
+ formated_question_list.append(item)
+
+ return formated_question_list
+
+
+def _validate_errors(question: str) -> None:
+ if "cubed" in question:
+ raise ValueError("unknown operation")
+
+ for item in WRONG_OPERATORS:
+ if item in question:
+ raise ValueError("syntax error")
+
+ digits: list[bool] = []
+ for char in question:
+ if char.isdigit():
+ digits.append(True)
+
+ operators: list[bool] = []
+ for key, val in STR_TO_OPERATOR.items():
+ if key in question or val in question:
+ operators.append(True)
+
+ if not any(digits + operators):
+ raise ValueError("unknown operation")
diff --git a/solutions/python/wordy/3/wordy.py b/solutions/python/wordy/3/wordy.py
new file mode 100644
index 0000000..12a6fcb
--- /dev/null
+++ b/solutions/python/wordy/3/wordy.py
@@ -0,0 +1,153 @@
+"""
+Parse and evaluate simple math word problems
+returning the answer as an integer.
+
+Handle a set of operations, in sequence.
+
+Since these are verbal word problems, evaluate
+the expression from left-to-right, ignoring the
+typical order of operations.
+"""
+
+STR_TO_OPERATOR: dict = {
+ "plus": "+",
+ "minus": "-",
+ "multiplied": "*",
+ "divided": "/",
+}
+
+WRONG_OPERATORS: list[str] = [
+ "plus?",
+ "minus?",
+ "multiplied?",
+ "divided?",
+ "plus plus",
+ "plus multiplied",
+ "minus multiplied",
+ "minus minus",
+ "multiplied multiplied",
+ "divided divided",
+ "What is?",
+]
+
+
+def answer(question: str) -> int:
+ """
+ Evaluate a simple word-based arithmetic question from left to right.
+
+ :param question: The input question, e.g., "What is 5 plus 13?"
+ :type question: str
+ :returns: The evaluated integer result.
+ :rtype: int
+ :raises ValueError: If the operation is unknown or the syntax is invalid.
+ """
+ _validate_errors(question)
+ result: int = 0
+ new_question: list[str] = _reformat(question)
+
+ if len(new_question) <= 3:
+ try:
+ _validate_evaluation_pattern(new_question)
+ eval_str: str = "".join(new_question)
+ result = eval(eval_str)
+ return result
+ except Exception as exc:
+ raise ValueError("syntax error") from exc
+
+ # Reduce iteratively: evaluate the first three-token slice
+ # and fold the result left-to-right.
+ while len(new_question) >= 3:
+ try:
+ _validate_evaluation_pattern(new_question[:3])
+ eval_str: str = "".join(new_question[:3])
+ val: int = eval(eval_str)
+ result = val
+ new_question = [str(result)] + new_question[3:]
+ if len(new_question) < 3:
+ _validate_evaluation_pattern(new_question)
+ return eval("".join(new_question))
+ except Exception as exc:
+ raise ValueError("syntax error") from exc
+ return result
+
+
+def _validate_evaluation_pattern(val: list) -> None:
+ """
+ Ensure a token slice matches expected evaluation patterns.
+
+ :param val: Token slice to validate, e.g.,
+ ['3', '+', '4'] or ['+', '4'] during reduction.
+ :type val: list
+ :raises ValueError: If the pattern is invalid (syntax error).
+ """
+ if len(val) == 3 and val[1] not in STR_TO_OPERATOR.values():
+ raise ValueError("syntax error")
+
+ if len(val) == 2 and val[0] not in STR_TO_OPERATOR.values():
+ raise ValueError("syntax error")
+
+
+def _reformat(question: str) -> list:
+ """
+ Tokenize a natural-language math question into numbers
+ and operator symbols.
+
+ :param question: Raw question string.
+ :type question: str
+ :returns: Token list with numbers and operator symbols.
+ :rtype: list[str]
+ """
+ # 1: Remove '?' mark
+ question = question.replace("?", "")
+ # 2: Convert all operators writen in word into proper math sign
+ question_list: list[str] = question.split()
+ formated_question_list: list[str] = []
+ for item in question_list:
+ if not (
+ item.isdigit()
+ or item in STR_TO_OPERATOR
+ or item in STR_TO_OPERATOR.values()
+ or any(val in item for val in STR_TO_OPERATOR.values())
+ ):
+ continue
+
+ if item in STR_TO_OPERATOR:
+ formated_question_list.append(STR_TO_OPERATOR[item])
+ elif item.isdigit():
+ formated_question_list.append(item)
+ elif item in STR_TO_OPERATOR.values():
+ formated_question_list.append(item)
+ elif any(val in item for val in STR_TO_OPERATOR.values()):
+ formated_question_list.append(item)
+
+ return formated_question_list
+
+
+def _validate_errors(question: str) -> None:
+ """
+ Pre-validate unsupported or malformed questions.
+
+ :param question: Raw question string.
+ :type question: str
+ :raises ValueError: Unknown operation or malformed
+ phrasing (syntax error).
+ """
+ if "cubed" in question:
+ raise ValueError("unknown operation")
+
+ for item in WRONG_OPERATORS:
+ if item in question:
+ raise ValueError("syntax error")
+
+ digits: list[bool] = []
+ for char in question:
+ if char.isdigit():
+ digits.append(True)
+
+ operators: list[bool] = []
+ for key, val in STR_TO_OPERATOR.items():
+ if key in question or val in question:
+ operators.append(True)
+
+ if not any(digits + operators):
+ raise ValueError("unknown operation")
diff --git a/solutions/python/wordy/4/wordy.py b/solutions/python/wordy/4/wordy.py
new file mode 100644
index 0000000..b3a54f8
--- /dev/null
+++ b/solutions/python/wordy/4/wordy.py
@@ -0,0 +1,158 @@
+"""
+Parse and evaluate simple math word problems
+returning the answer as an integer.
+
+Handle a set of operations, in sequence.
+
+Since these are verbal word problems, evaluate
+the expression from left-to-right, ignoring the
+typical order of operations.
+"""
+
+STR_TO_OPERATOR: dict = {
+ "plus": "+",
+ "minus": "-",
+ "multiplied": "*",
+ "divided": "/",
+}
+
+WRONG_OPERATORS: list[str] = [
+ "plus?",
+ "minus?",
+ "multiplied?",
+ "divided?",
+ "plus plus",
+ "plus multiplied",
+ "minus multiplied",
+ "minus minus",
+ "multiplied multiplied",
+ "divided divided",
+ "What is?",
+]
+
+
+def answer(question: str) -> int:
+ """
+ Evaluate a simple word-based arithmetic question from left to right.
+
+ :param question: The input question, e.g., "What is 5 plus 13?"
+ :type question: str
+ :returns: The evaluated integer result.
+ :rtype: int
+ :raises ValueError: If the operation is unknown or the syntax is invalid.
+ """
+ _validate_errors(question)
+ result: int = 0
+ new_question: list[str] = _reformat(question)
+ # Reduce iteratively: evaluate the first three-token slice
+ # and fold the result left-to-right.
+ while new_question:
+ try:
+ if len(new_question) == 3:
+ _validate_evaluation_pattern(new_question)
+ return _math_operation(new_question)
+ elif len(new_question) == 1:
+ return int(new_question[0])
+ else:
+ _validate_evaluation_pattern(new_question[:3])
+ result = _math_operation(new_question[:3])
+ new_question = [str(result)] + new_question[3:]
+ except Exception as exc:
+ raise ValueError("syntax error") from exc
+ return result
+
+
+def _math_operation(question: list[str]) -> int:
+ """
+ Compute a single binary arithmetic operation.
+
+ Expects a three-token slice like ``['3', '+', '4']`` and returns the integer
+ result. Division performs floor division (``//``) to match exercise rules.
+
+ :param question: Three tokens ``[lhs, operator, rhs]``.
+ :type question: list[str]
+ :returns: The computed integer result.
+ :rtype: int
+ """
+ math_operator: str = question[1]
+ result: int = 0
+
+ if math_operator == "+":
+ result = int(question[0]) + int(question[-1])
+ elif math_operator == "-":
+ result = int(question[0]) - int(question[-1])
+ elif math_operator == "/":
+ result = int(question[0]) // int(question[-1])
+ elif math_operator == "*":
+ result = int(question[0]) * int(question[-1])
+
+ return result
+
+
+def _validate_evaluation_pattern(val: list) -> None:
+ """
+ Ensure a token slice matches expected evaluation patterns.
+
+ :param val: Token slice to validate, e.g.,
+ ['3', '+', '4'] or ['+', '4'] during reduction.
+ :type val: list
+ :raises ValueError: If the pattern is invalid (syntax error).
+ """
+ if len(val) == 3 and val[1] not in STR_TO_OPERATOR.values():
+ raise ValueError("syntax error")
+
+ if len(val) == 2 and val[0] not in STR_TO_OPERATOR.values():
+ raise ValueError("syntax error")
+
+
+def _reformat(question: str) -> list:
+ """
+ Tokenize a natural-language math question into numbers
+ and operator symbols.
+
+ :param question: Raw question string.
+ :type question: str
+ :returns: Token list with numbers and operator symbols.
+ :rtype: list[str]
+ """
+ # 1: Remove '?' mark
+ question = question.replace("?", "")
+ # 2: Convert all operators writen in word into proper math sign
+ question_list: list[str] = question.split()
+ formated_question_list: list[str] = []
+ for item in question_list:
+ if not (
+ item.isdigit()
+ or item in STR_TO_OPERATOR
+ or item in STR_TO_OPERATOR.values()
+ or any(val in item for val in STR_TO_OPERATOR.values())
+ ):
+ continue
+
+ if item in STR_TO_OPERATOR:
+ formated_question_list.append(STR_TO_OPERATOR[item])
+ elif item.isdigit():
+ formated_question_list.append(item)
+ elif item in STR_TO_OPERATOR.values():
+ formated_question_list.append(item)
+ elif any(val in item for val in STR_TO_OPERATOR.values()):
+ formated_question_list.append(item)
+
+ return formated_question_list
+
+
+def _validate_errors(question: str) -> None:
+ """
+ Pre-validate unsupported or malformed questions.
+
+ :param question: Raw question string.
+ :type question: str
+ :raises ValueError: Unknown operation or malformed
+ phrasing (syntax error).
+ """
+ if "cubed" in question:
+ raise ValueError("unknown operation")
+
+ for item in WRONG_OPERATORS:
+ if item in question:
+ raise ValueError("syntax error")
diff --git a/wordy/wordy.py b/wordy/wordy.py
index 12a6fcb..da43570 100644
--- a/wordy/wordy.py
+++ b/wordy/wordy.py
@@ -44,33 +44,54 @@ def answer(question: str) -> int:
_validate_errors(question)
result: int = 0
new_question: list[str] = _reformat(question)
-
- if len(new_question) <= 3:
- try:
- _validate_evaluation_pattern(new_question)
- eval_str: str = "".join(new_question)
- result = eval(eval_str)
- return result
- except Exception as exc:
- raise ValueError("syntax error") from exc
-
# Reduce iteratively: evaluate the first three-token slice
# and fold the result left-to-right.
- while len(new_question) >= 3:
+ result: int = 0
+ while new_question:
try:
+ if len(new_question) == 3:
+ _validate_evaluation_pattern(new_question)
+ result = _math_operation(new_question)
+ break
+ if len(new_question) == 1:
+ result = int(new_question[0])
+ break
_validate_evaluation_pattern(new_question[:3])
- eval_str: str = "".join(new_question[:3])
- val: int = eval(eval_str)
- result = val
+ result = _math_operation(new_question[:3])
new_question = [str(result)] + new_question[3:]
- if len(new_question) < 3:
- _validate_evaluation_pattern(new_question)
- return eval("".join(new_question))
except Exception as exc:
raise ValueError("syntax error") from exc
return result
+def _math_operation(question: list[str]) -> int:
+ """
+ Compute a single binary arithmetic operation.
+
+ Expects a three-token slice like ``['3', '+', '4']`` and returns
+ the integer result. Division performs floor division (``//``) to
+ match exercise rules.
+
+ :param question: Three tokens ``[lhs, operator, rhs]``.
+ :type question: list[str]
+ :returns: The computed integer result.
+ :rtype: int
+ """
+ math_operator: str = question[1]
+ result: int = 0
+
+ if math_operator == "+":
+ result = int(question[0]) + int(question[-1])
+ elif math_operator == "-":
+ result = int(question[0]) - int(question[-1])
+ elif math_operator == "/":
+ result = int(question[0]) // int(question[-1])
+ elif math_operator == "*":
+ result = int(question[0]) * int(question[-1])
+
+ return result
+
+
def _validate_evaluation_pattern(val: list) -> None:
"""
Ensure a token slice matches expected evaluation patterns.
@@ -138,16 +159,3 @@ def _validate_errors(question: str) -> None:
for item in WRONG_OPERATORS:
if item in question:
raise ValueError("syntax error")
-
- digits: list[bool] = []
- for char in question:
- if char.isdigit():
- digits.append(True)
-
- operators: list[bool] = []
- for key, val in STR_TO_OPERATOR.items():
- if key in question or val in question:
- operators.append(True)
-
- if not any(digits + operators):
- raise ValueError("unknown operation")