diff --git a/.github/workflows/sphinx.yml b/.github/workflows/sphinx.yml index bf0a74a239c8..5f5a90ce1a2d 100644 --- a/.github/workflows/sphinx.yml +++ b/.github/workflows/sphinx.yml @@ -39,7 +39,7 @@ jobs: python-version: 3.14 allow-prereleases: true - run: uv sync --group=docs - - uses: actions/configure-pages@v5 + - uses: actions/configure-pages@v6 - run: uv run sphinx-build -c docs . docs/_build/html - uses: actions/upload-pages-artifact@v4 with: @@ -53,5 +53,5 @@ jobs: needs: build_docs runs-on: ubuntu-latest steps: - - uses: actions/deploy-pages@v4 + - uses: actions/deploy-pages@v5 id: deployment diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2d95300a4797..765d5cff38d8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,7 +19,7 @@ repos: - id: auto-walrus - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.15.4 + rev: v0.14.14 hooks: - id: ruff-check - id: ruff-format @@ -32,7 +32,7 @@ repos: - tomli - repo: https://github.com/tox-dev/pyproject-fmt - rev: v2.16.2 + rev: v2.12.1 hooks: - id: pyproject-fmt @@ -45,12 +45,12 @@ repos: pass_filenames: false - repo: https://github.com/abravalheri/validate-pyproject - rev: v0.25 + rev: v0.24.1 hooks: - id: validate-pyproject - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.20.0 + rev: v1.19.1 hooks: - id: mypy args: diff --git a/DIRECTORY.md b/DIRECTORY.md index 568fc5e67398..ca454bd5fd82 100644 --- a/DIRECTORY.md +++ b/DIRECTORY.md @@ -986,6 +986,7 @@ * [Sol2](project_euler/problem_014/sol2.py) * Problem 015 * [Sol1](project_euler/problem_015/sol1.py) + * [Sol2](project_euler/problem_015/sol2.py) * Problem 016 * [Sol1](project_euler/problem_016/sol1.py) * [Sol2](project_euler/problem_016/sol2.py) diff --git a/backtracking/coloring.py b/backtracking/coloring.py index f10cdbcf9d26..abfdf16f1342 100644 --- a/backtracking/coloring.py +++ b/backtracking/coloring.py @@ -104,6 +104,14 @@ def color(graph: list[list[int]], max_colors: int) -> list[int]: >>> max_colors = 2 >>> color(graph, max_colors) [] + >>> color([], 2) # empty graph + [] + >>> color([[0]], 1) # single node, 1 color + [0] + >>> color([[0, 1], [1, 0]], 1) # 2 nodes, 1 color (impossible) + [] + >>> color([[0, 1], [1, 0]], 2) # 2 nodes, 2 colors (possible) + [0, 1] """ colored_vertices = [-1] * len(graph) diff --git a/backtracking/generate_parentheses.py b/backtracking/generate_parentheses.py index 18c21e2a9b51..5094f4b08619 100644 --- a/backtracking/generate_parentheses.py +++ b/backtracking/generate_parentheses.py @@ -64,6 +64,10 @@ def generate_parenthesis(n: int) -> list[str]: Example 2: >>> generate_parenthesis(1) ['()'] + + Example 3: + >>> generate_parenthesis(0) + [''] """ result: list[str] = [] diff --git a/backtracking/n_queens.py b/backtracking/n_queens.py index d10181f319b3..6fac93aa77d6 100644 --- a/backtracking/n_queens.py +++ b/backtracking/n_queens.py @@ -33,6 +33,14 @@ def is_safe(board: list[list[int]], row: int, column: int) -> bool: False >>> is_safe([[0, 0, 1], [0, 0, 0], [0, 0, 0]], 1, 1) False + >>> is_safe([[1, 0, 0], [0, 0, 0], [0, 0, 0]], 1, 2) + True + >>> is_safe([[1, 0, 0], [0, 0, 0], [0, 0, 0]], 2, 1) + True + >>> is_safe([[0, 0, 0], [1, 0, 0], [0, 0, 0]], 0, 2) + True + >>> is_safe([[0, 0, 0], [1, 0, 0], [0, 0, 0]], 2, 2) + True """ n = len(board) # Size of the board diff --git a/backtracking/word_break.py b/backtracking/word_break.py index 1f2ab073f499..2e874a02b61c 100644 --- a/backtracking/word_break.py +++ b/backtracking/word_break.py @@ -66,6 +66,9 @@ def word_break(input_string: str, word_dict: set[str]) -> bool: >>> word_break("catsandog", {"cats", "dog", "sand", "and", "cat"}) False + + >>> word_break("applepenapple", {}) + False """ return backtrack(input_string, word_dict, 0) diff --git a/geodesy/lamberts_ellipsoidal_distance.py b/geodesy/lamberts_ellipsoidal_distance.py index 4805674e51ab..a5c43c5656e9 100644 --- a/geodesy/lamberts_ellipsoidal_distance.py +++ b/geodesy/lamberts_ellipsoidal_distance.py @@ -32,6 +32,26 @@ def lamberts_ellipsoidal_distance( Returns: geographical distance between two points in metres + >>> lamberts_ellipsoidal_distance(100, 0, 0, 0) + Traceback (most recent call last): + ... + ValueError: Latitude must be between -90 and 90 degrees + + >>> lamberts_ellipsoidal_distance(0, 0, -100, 0) + Traceback (most recent call last): + ... + ValueError: Latitude must be between -90 and 90 degrees + + >>> lamberts_ellipsoidal_distance(0, 200, 0, 0) + Traceback (most recent call last): + ... + ValueError: Longitude must be between -180 and 180 degrees + + >>> lamberts_ellipsoidal_distance(0, 0, 0, -200) + Traceback (most recent call last): + ... + ValueError: Longitude must be between -180 and 180 degrees + >>> from collections import namedtuple >>> point_2d = namedtuple("point_2d", "lat lon") >>> SAN_FRANCISCO = point_2d(37.774856, -122.424227) @@ -46,6 +66,14 @@ def lamberts_ellipsoidal_distance( '9,737,326 meters' """ + # Validate latitude values + if not -90 <= lat1 <= 90 or not -90 <= lat2 <= 90: + raise ValueError("Latitude must be between -90 and 90 degrees") + + # Validate longitude values + if not -180 <= lon1 <= 180 or not -180 <= lon2 <= 180: + raise ValueError("Longitude must be between -180 and 180 degrees") + # CONSTANTS per WGS84 https://en.wikipedia.org/wiki/World_Geodetic_System # Distance in metres(m) # Equation Parameters diff --git a/maths/area.py b/maths/area.py index 31a654206977..e14cc0aa7195 100644 --- a/maths/area.py +++ b/maths/area.py @@ -552,7 +552,6 @@ def area_reg_polygon(sides: int, length: float) -> float: length of a side" ) return (sides * length**2) / (4 * tan(pi / sides)) - return (sides * length**2) / (4 * tan(pi / sides)) if __name__ == "__main__": diff --git a/maths/factorial.py b/maths/factorial.py index ba61447c7564..2b8b68764d89 100644 --- a/maths/factorial.py +++ b/maths/factorial.py @@ -41,21 +41,21 @@ def factorial_recursive(n: int) -> int: https://en.wikipedia.org/wiki/Factorial >>> import math - >>> all(factorial(i) == math.factorial(i) for i in range(20)) + >>> all(factorial_recursive(i) == math.factorial(i) for i in range(20)) True - >>> factorial(0.1) + >>> factorial_recursive(0.1) Traceback (most recent call last): ... - ValueError: factorial() only accepts integral values - >>> factorial(-1) + ValueError: factorial_recursive() only accepts integral values + >>> factorial_recursive(-1) Traceback (most recent call last): ... - ValueError: factorial() not defined for negative values + ValueError: factorial_recursive() not defined for negative values """ if not isinstance(n, int): - raise ValueError("factorial() only accepts integral values") + raise ValueError("factorial_recursive() only accepts integral values") if n < 0: - raise ValueError("factorial() not defined for negative values") + raise ValueError("factorial_recursive() not defined for negative values") return 1 if n in {0, 1} else n * factorial_recursive(n - 1) diff --git a/project_euler/problem_015/sol2.py b/project_euler/problem_015/sol2.py new file mode 100644 index 000000000000..903095e144ec --- /dev/null +++ b/project_euler/problem_015/sol2.py @@ -0,0 +1,32 @@ +""" +Problem 15: https://projecteuler.net/problem=15 + +Starting in the top left corner of a 2x2 grid, and only being able to move to +the right and down, there are exactly 6 routes to the bottom right corner. +How many such routes are there through a 20x20 grid? +""" + + +def solution(n: int = 20) -> int: + """ + Solve by explicitly counting the paths with dynamic programming. + + >>> solution(6) + 924 + >>> solution(2) + 6 + >>> solution(1) + 2 + """ + + counts = [[1 for _ in range(n + 1)] for _ in range(n + 1)] + + for i in range(1, n + 1): + for j in range(1, n + 1): + counts[i][j] = counts[i - 1][j] + counts[i][j - 1] + + return counts[n][n] + + +if __name__ == "__main__": + print(solution()) diff --git a/pyproject.toml b/pyproject.toml index 6ec899d7840f..51267224815b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,6 +37,7 @@ test = [ "pytest>=8.4.1", "pytest-cov>=6", ] + docs = [ "myst-parser>=4", "sphinx-autoapi>=3.4", @@ -49,6 +50,7 @@ euler-validate = [ [tool.ruff] target-version = "py314" + output-format = "full" lint.select = [ # https://beta.ruff.rs/docs/rules @@ -126,6 +128,7 @@ lint.ignore = [ "SLF001", # Private member accessed: `_Iterator` -- FIX ME "UP037", # FIX ME ] + lint.per-file-ignores."data_structures/hashing/tests/test_hash_map.py" = [ "BLE001", ] @@ -147,40 +150,37 @@ lint.per-file-ignores."project_euler/problem_099/sol1.py" = [ lint.per-file-ignores."sorts/external_sort.py" = [ "SIM115", ] -lint.mccabe.max-complexity = 17 # default: 10 +lint.mccabe.max-complexity = 17 # default: 10 lint.pylint.allow-magic-value-types = [ "float", "int", "str", ] -lint.pylint.max-args = 10 # default: 5 -lint.pylint.max-branches = 20 # default: 12 -lint.pylint.max-returns = 8 # default: 6 -lint.pylint.max-statements = 88 # default: 50 +lint.pylint.max-args = 10 # default: 5 +lint.pylint.max-branches = 20 # default: 12 +lint.pylint.max-returns = 8 # default: 6 +lint.pylint.max-statements = 88 # default: 50 [tool.codespell] ignore-words-list = "3rt,abd,aer,ans,bitap,crate,damon,fo,followings,hist,iff,kwanza,manuel,mater,secant,som,sur,tim,toi,zar" -skip = """\ - ./.*,*.json,*.lock,ciphers/prehistoric_men.txt,project_euler/problem_022/p022_names.txt,pyproject.toml,strings/dictio\ - nary.txt,strings/words.txt\ - """ +skip = "./.*,*.json,*.lock,ciphers/prehistoric_men.txt,project_euler/problem_022/p022_names.txt,pyproject.toml,strings/dictionary.txt,strings/words.txt" -[tool.pytest] -ini_options.markers = [ +[tool.pytest.ini_options] +markers = [ "mat_ops: mark a test as utilizing matrix operations.", ] -ini_options.addopts = [ +addopts = [ "--durations=10", "--doctest-modules", "--showlocals", ] -[tool.coverage] -report.omit = [ +[tool.coverage.report] +omit = [ ".env/*", "project_euler/*", ] -report.sort = "Cover" +sort = "Cover" [tool.mypy] python_version = "3.14" @@ -264,6 +264,7 @@ myst_fence_as_directive = [ "include", ] templates_path = [ "_templates" ] -source_suffix.".rst" = "restructuredtext" +[tool.sphinx-pyproject.source_suffix] +".rst" = "restructuredtext" # ".txt" = "markdown" -source_suffix.".md" = "markdown" +".md" = "markdown" diff --git a/searches/binary_search.py b/searches/binary_search.py index 5125dc6bdb9a..bec87b3c5aec 100644 --- a/searches/binary_search.py +++ b/searches/binary_search.py @@ -10,9 +10,8 @@ python3 binary_search.py """ -from __future__ import annotations - import bisect +from itertools import pairwise def bisect_left( @@ -198,7 +197,7 @@ def binary_search(sorted_collection: list[int], item: int) -> int: >>> binary_search([0, 5, 7, 10, 15], 6) -1 """ - if list(sorted_collection) != sorted(sorted_collection): + if any(a > b for a, b in pairwise(sorted_collection)): raise ValueError("sorted_collection must be sorted in ascending order") left = 0 right = len(sorted_collection) - 1 diff --git a/searches/linear_search.py b/searches/linear_search.py index ba6e81d6bae4..8adb4a7015f0 100644 --- a/searches/linear_search.py +++ b/searches/linear_search.py @@ -1,5 +1,5 @@ """ -This is pure Python implementation of linear search algorithm +This is a pure Python implementation of the linear search algorithm. For doctests run following command: python3 -m doctest -v linear_search.py @@ -12,8 +12,8 @@ def linear_search(sequence: list, target: int) -> int: """A pure Python implementation of a linear search algorithm - :param sequence: a collection with comparable items (as sorted items not required - in Linear Search) + :param sequence: a collection with comparable items (sorting is not required for + linear search) :param target: item value to search :return: index of found item or -1 if item is not found diff --git a/sorts/bubble_sort.py b/sorts/bubble_sort.py index 77d173290aff..4d658a4a12e4 100644 --- a/sorts/bubble_sort.py +++ b/sorts/bubble_sort.py @@ -69,7 +69,7 @@ def bubble_sort_recursive(collection: list[Any]) -> list[Any]: Examples: >>> bubble_sort_recursive([0, 5, 2, 3, 2]) [0, 2, 2, 3, 5] - >>> bubble_sort_iterative([]) + >>> bubble_sort_recursive([]) [] >>> bubble_sort_recursive([-2, -45, -5]) [-45, -5, -2]