diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 62829b2b45a5..8b83cb41c79a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: astral-sh/setup-uv@v5 + - uses: astral-sh/setup-uv@v6 with: enable-cache: true cache-dependency-glob: uv.lock @@ -30,6 +30,7 @@ jobs: --ignore=project_euler/ --ignore=quantum/q_fourier_transform.py --ignore=scripts/validate_solutions.py + --ignore=web_programming/fetch_anime_and_play.py --cov-report=term-missing:skip-covered --cov=. . - if: ${{ success() }} diff --git a/.github/workflows/project_euler.yml b/.github/workflows/project_euler.yml index 8d51ad8850cf..eaf4150e4eaa 100644 --- a/.github/workflows/project_euler.yml +++ b/.github/workflows/project_euler.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: astral-sh/setup-uv@v5 + - uses: astral-sh/setup-uv@v6 - uses: actions/setup-python@v5 with: python-version: 3.x @@ -25,7 +25,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: astral-sh/setup-uv@v5 + - uses: astral-sh/setup-uv@v6 - uses: actions/setup-python@v5 with: python-version: 3.x diff --git a/.github/workflows/ruff.yml b/.github/workflows/ruff.yml index cfe127b3521f..ec9f0202bd7e 100644 --- a/.github/workflows/ruff.yml +++ b/.github/workflows/ruff.yml @@ -12,5 +12,5 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: astral-sh/setup-uv@v5 + - uses: astral-sh/setup-uv@v6 - run: uvx ruff check --output-format=github . diff --git a/.github/workflows/sphinx.yml b/.github/workflows/sphinx.yml index d02435d98028..2010041d80c5 100644 --- a/.github/workflows/sphinx.yml +++ b/.github/workflows/sphinx.yml @@ -23,10 +23,10 @@ concurrency: jobs: build_docs: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04-arm steps: - uses: actions/checkout@v4 - - uses: astral-sh/setup-uv@v5 + - uses: astral-sh/setup-uv@v6 - uses: actions/setup-python@v5 with: python-version: 3.13 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0c8108ac55be..9e13416dc78d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,20 +16,20 @@ repos: - id: auto-walrus - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.8.3 + rev: v0.11.8 hooks: - id: ruff - id: ruff-format - repo: https://github.com/codespell-project/codespell - rev: v2.3.0 + rev: v2.4.1 hooks: - id: codespell additional_dependencies: - tomli - repo: https://github.com/tox-dev/pyproject-fmt - rev: "v2.5.0" + rev: "v2.5.1" hooks: - id: pyproject-fmt @@ -42,12 +42,12 @@ repos: pass_filenames: false - repo: https://github.com/abravalheri/validate-pyproject - rev: v0.23 + rev: v0.24.1 hooks: - id: validate-pyproject - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.13.0 + rev: v1.15.0 hooks: - id: mypy args: diff --git a/DIRECTORY.md b/DIRECTORY.md index d234d366df06..04e09c29de97 100644 --- a/DIRECTORY.md +++ b/DIRECTORY.md @@ -40,6 +40,7 @@ * [Count Number Of One Bits](bit_manipulation/count_number_of_one_bits.py) * [Excess 3 Code](bit_manipulation/excess_3_code.py) * [Find Previous Power Of Two](bit_manipulation/find_previous_power_of_two.py) + * [Find Unique Number](bit_manipulation/find_unique_number.py) * [Gray Code Sequence](bit_manipulation/gray_code_sequence.py) * [Highest Set Bit](bit_manipulation/highest_set_bit.py) * [Index Of Rightmost Set Bit](bit_manipulation/index_of_rightmost_set_bit.py) @@ -86,7 +87,7 @@ * [Baconian Cipher](ciphers/baconian_cipher.py) * [Base16](ciphers/base16.py) * [Base32](ciphers/base32.py) - * [Base64](ciphers/base64.py) + * [Base64 Cipher](ciphers/base64_cipher.py) * [Base85](ciphers/base85.py) * [Beaufort Cipher](ciphers/beaufort_cipher.py) * [Bifid](ciphers/bifid.py) @@ -142,6 +143,7 @@ * [Haralick Descriptors](computer_vision/haralick_descriptors.py) * [Harris Corner](computer_vision/harris_corner.py) * [Horn Schunck](computer_vision/horn_schunck.py) + * [Intensity Based Segmentation](computer_vision/intensity_based_segmentation.py) * [Mean Threshold](computer_vision/mean_threshold.py) * [Mosaic Augmentation](computer_vision/mosaic_augmentation.py) * [Pooling Functions](computer_vision/pooling_functions.py) @@ -169,6 +171,7 @@ * [Prefix Conversions](conversions/prefix_conversions.py) * [Prefix Conversions String](conversions/prefix_conversions_string.py) * [Pressure Conversions](conversions/pressure_conversions.py) + * [Rectangular To Polar](conversions/rectangular_to_polar.py) * [Rgb Cmyk Conversion](conversions/rgb_cmyk_conversion.py) * [Rgb Hsv Conversion](conversions/rgb_hsv_conversion.py) * [Roman Numerals](conversions/roman_numerals.py) @@ -273,20 +276,21 @@ * [Singly Linked List](data_structures/linked_list/singly_linked_list.py) * [Skip List](data_structures/linked_list/skip_list.py) * [Swap Nodes](data_structures/linked_list/swap_nodes.py) - * Queue - * [Circular Queue](data_structures/queue/circular_queue.py) - * [Circular Queue Linked List](data_structures/queue/circular_queue_linked_list.py) - * [Double Ended Queue](data_structures/queue/double_ended_queue.py) - * [Linked Queue](data_structures/queue/linked_queue.py) - * [Priority Queue Using List](data_structures/queue/priority_queue_using_list.py) - * [Queue By List](data_structures/queue/queue_by_list.py) - * [Queue By Two Stacks](data_structures/queue/queue_by_two_stacks.py) - * [Queue On Pseudo Stack](data_structures/queue/queue_on_pseudo_stack.py) + * Queues + * [Circular Queue](data_structures/queues/circular_queue.py) + * [Circular Queue Linked List](data_structures/queues/circular_queue_linked_list.py) + * [Double Ended Queue](data_structures/queues/double_ended_queue.py) + * [Linked Queue](data_structures/queues/linked_queue.py) + * [Priority Queue Using List](data_structures/queues/priority_queue_using_list.py) + * [Queue By List](data_structures/queues/queue_by_list.py) + * [Queue By Two Stacks](data_structures/queues/queue_by_two_stacks.py) + * [Queue On Pseudo Stack](data_structures/queues/queue_on_pseudo_stack.py) * Stacks * [Balanced Parentheses](data_structures/stacks/balanced_parentheses.py) * [Dijkstras Two Stack Algorithm](data_structures/stacks/dijkstras_two_stack_algorithm.py) * [Infix To Postfix Conversion](data_structures/stacks/infix_to_postfix_conversion.py) * [Infix To Prefix Conversion](data_structures/stacks/infix_to_prefix_conversion.py) + * [Largest Rectangle Histogram](data_structures/stacks/largest_rectangle_histogram.py) * [Lexicographical Numbers](data_structures/stacks/lexicographical_numbers.py) * [Next Greater Element](data_structures/stacks/next_greater_element.py) * [Postfix Evaluation](data_structures/stacks/postfix_evaluation.py) @@ -375,6 +379,7 @@ * [Longest Common Subsequence](dynamic_programming/longest_common_subsequence.py) * [Longest Common Substring](dynamic_programming/longest_common_substring.py) * [Longest Increasing Subsequence](dynamic_programming/longest_increasing_subsequence.py) + * [Longest Increasing Subsequence Iterative](dynamic_programming/longest_increasing_subsequence_iterative.py) * [Longest Increasing Subsequence O Nlogn](dynamic_programming/longest_increasing_subsequence_o_nlogn.py) * [Longest Palindromic Subsequence](dynamic_programming/longest_palindromic_subsequence.py) * [Matrix Chain Multiplication](dynamic_programming/matrix_chain_multiplication.py) @@ -392,6 +397,7 @@ * [Minimum Tickets Cost](dynamic_programming/minimum_tickets_cost.py) * [Optimal Binary Search Tree](dynamic_programming/optimal_binary_search_tree.py) * [Palindrome Partitioning](dynamic_programming/palindrome_partitioning.py) + * [Range Sum Query](dynamic_programming/range_sum_query.py) * [Regex Match](dynamic_programming/regex_match.py) * [Rod Cutting](dynamic_programming/rod_cutting.py) * [Smith Waterman](dynamic_programming/smith_waterman.py) @@ -437,6 +443,7 @@ * [Present Value](financial/present_value.py) * [Price Plus Tax](financial/price_plus_tax.py) * [Simple Moving Average](financial/simple_moving_average.py) + * [Time And Half Pay](financial/time_and_half_pay.py) ## Fractals * [Julia Sets](fractals/julia_sets.py) @@ -460,6 +467,8 @@ ## Graphics * [Bezier Curve](graphics/bezier_curve.py) + * [Butterfly Pattern](graphics/butterfly_pattern.py) + * [Digital Differential Analyzer Line](graphics/digital_differential_analyzer_line.py) * [Vector3 For 2D Rendering](graphics/vector3_for_2d_rendering.py) ## Graphs @@ -506,6 +515,7 @@ * [Kahns Algorithm Long](graphs/kahns_algorithm_long.py) * [Kahns Algorithm Topo](graphs/kahns_algorithm_topo.py) * [Karger](graphs/karger.py) + * [Lanczos Eigenvectors](graphs/lanczos_eigenvectors.py) * [Markov Chain](graphs/markov_chain.py) * [Matching Min Vertex Cover](graphs/matching_min_vertex_cover.py) * [Minimum Path Sum](graphs/minimum_path_sum.py) @@ -562,6 +572,7 @@ * [Gaussian Elimination](linear_algebra/gaussian_elimination.py) * [Jacobi Iteration Method](linear_algebra/jacobi_iteration_method.py) * [Lu Decomposition](linear_algebra/lu_decomposition.py) + * [Matrix Inversion](linear_algebra/matrix_inversion.py) * Src * [Conjugate Gradient](linear_algebra/src/conjugate_gradient.py) * [Gaussian Elimination Pivoting](linear_algebra/src/gaussian_elimination_pivoting.py) @@ -602,6 +613,7 @@ * [Mfcc](machine_learning/mfcc.py) * [Multilayer Perceptron Classifier](machine_learning/multilayer_perceptron_classifier.py) * [Polynomial Regression](machine_learning/polynomial_regression.py) + * [Principle Component Analysis](machine_learning/principle_component_analysis.py) * [Scoring Functions](machine_learning/scoring_functions.py) * [Self Organizing Map](machine_learning/self_organizing_map.py) * [Sequential Minimum Optimization](machine_learning/sequential_minimum_optimization.py) @@ -660,6 +672,7 @@ * [Gamma](maths/gamma.py) * [Gaussian](maths/gaussian.py) * [Gcd Of N Numbers](maths/gcd_of_n_numbers.py) + * [Geometric Mean](maths/geometric_mean.py) * [Germain Primes](maths/germain_primes.py) * [Greatest Common Divisor](maths/greatest_common_divisor.py) * [Hardy Ramanujanalgo](maths/hardy_ramanujanalgo.py) @@ -885,6 +898,7 @@ * [N Body Simulation](physics/n_body_simulation.py) * [Newtons Law Of Gravitation](physics/newtons_law_of_gravitation.py) * [Newtons Second Law Of Motion](physics/newtons_second_law_of_motion.py) + * [Period Of Pendulum](physics/period_of_pendulum.py) * [Photoelectric Effect](physics/photoelectric_effect.py) * [Potential Energy](physics/potential_energy.py) * [Rainfall Intensity](physics/rainfall_intensity.py) @@ -1142,6 +1156,8 @@ * [Sol1](project_euler/problem_120/sol1.py) * Problem 121 * [Sol1](project_euler/problem_121/sol1.py) + * Problem 122 + * [Sol1](project_euler/problem_122/sol1.py) * Problem 123 * [Sol1](project_euler/problem_123/sol1.py) * Problem 125 @@ -1152,10 +1168,14 @@ * [Sol1](project_euler/problem_131/sol1.py) * Problem 135 * [Sol1](project_euler/problem_135/sol1.py) + * Problem 136 + * [Sol1](project_euler/problem_136/sol1.py) * Problem 144 * [Sol1](project_euler/problem_144/sol1.py) * Problem 145 * [Sol1](project_euler/problem_145/sol1.py) + * Problem 164 + * [Sol1](project_euler/problem_164/sol1.py) * Problem 173 * [Sol1](project_euler/problem_173/sol1.py) * Problem 174 @@ -1166,6 +1186,8 @@ * [Sol1](project_euler/problem_187/sol1.py) * Problem 188 * [Sol1](project_euler/problem_188/sol1.py) + * Problem 190 + * [Sol1](project_euler/problem_190/sol1.py) * Problem 191 * [Sol1](project_euler/problem_191/sol1.py) * Problem 203 @@ -1325,7 +1347,7 @@ * [Title](strings/title.py) * [Top K Frequent Words](strings/top_k_frequent_words.py) * [Upper](strings/upper.py) - * [Wave](strings/wave.py) + * [Wave String](strings/wave_string.py) * [Wildcard Pattern Matching](strings/wildcard_pattern_matching.py) * [Word Occurrence](strings/word_occurrence.py) * [Word Patterns](strings/word_patterns.py) diff --git a/backtracking/all_combinations.py b/backtracking/all_combinations.py index 390decf3a05b..1d15c6263e14 100644 --- a/backtracking/all_combinations.py +++ b/backtracking/all_combinations.py @@ -12,6 +12,8 @@ def combination_lists(n: int, k: int) -> list[list[int]]: """ + Generates all possible combinations of k numbers out of 1 ... n using itertools. + >>> combination_lists(n=4, k=2) [[1, 2], [1, 3], [1, 4], [2, 3], [2, 4], [3, 4]] """ @@ -20,6 +22,8 @@ def combination_lists(n: int, k: int) -> list[list[int]]: def generate_all_combinations(n: int, k: int) -> list[list[int]]: """ + Generates all possible combinations of k numbers out of 1 ... n using backtracking. + >>> generate_all_combinations(n=4, k=2) [[1, 2], [1, 3], [1, 4], [2, 3], [2, 4], [3, 4]] >>> generate_all_combinations(n=0, k=0) @@ -34,6 +38,14 @@ def generate_all_combinations(n: int, k: int) -> list[list[int]]: ValueError: n must not be negative >>> generate_all_combinations(n=5, k=4) [[1, 2, 3, 4], [1, 2, 3, 5], [1, 2, 4, 5], [1, 3, 4, 5], [2, 3, 4, 5]] + >>> generate_all_combinations(n=3, k=3) + [[1, 2, 3]] + >>> generate_all_combinations(n=3, k=1) + [[1], [2], [3]] + >>> generate_all_combinations(n=1, k=0) + [[]] + >>> generate_all_combinations(n=1, k=1) + [[1]] >>> from itertools import combinations >>> all(generate_all_combinations(n, k) == combination_lists(n, k) ... for n in range(1, 6) for k in range(1, 6)) @@ -56,6 +68,28 @@ def create_all_state( current_list: list[int], total_list: list[list[int]], ) -> None: + """ + Helper function to recursively build all combinations. + + >>> create_all_state(1, 4, 2, [], result := []) + >>> result + [[1, 2], [1, 3], [1, 4], [2, 3], [2, 4], [3, 4]] + >>> create_all_state(1, 3, 3, [], result := []) + >>> result + [[1, 2, 3]] + >>> create_all_state(2, 2, 1, [1], result := []) + >>> result + [[1, 2]] + >>> create_all_state(1, 0, 0, [], result := []) + >>> result + [[]] + >>> create_all_state(1, 4, 0, [1, 2], result := []) + >>> result + [[1, 2]] + >>> create_all_state(5, 4, 2, [1, 2], result := []) + >>> result + [] + """ if level == 0: total_list.append(current_list[:]) return diff --git a/backtracking/n_queens.py b/backtracking/n_queens.py index 81668b17a0ac..d10181f319b3 100644 --- a/backtracking/n_queens.py +++ b/backtracking/n_queens.py @@ -27,21 +27,28 @@ def is_safe(board: list[list[int]], row: int, column: int) -> bool: >>> is_safe([[0, 0, 0], [0, 0, 0], [0, 0, 0]], 1, 1) True + >>> is_safe([[0, 1, 0], [0, 0, 0], [0, 0, 0]], 1, 1) + False >>> is_safe([[1, 0, 0], [0, 0, 0], [0, 0, 0]], 1, 1) False + >>> is_safe([[0, 0, 1], [0, 0, 0], [0, 0, 0]], 1, 1) + False """ n = len(board) # Size of the board - # Check if there is any queen in the same row, column, - # left upper diagonal, and right upper diagonal + # Check if there is any queen in the same upper column, + # left upper diagonal and right upper diagonal return ( - all(board[i][j] != 1 for i, j in zip(range(row, -1, -1), range(column, n))) + all(board[i][j] != 1 for i, j in zip(range(row), [column] * row)) + and all( + board[i][j] != 1 + for i, j in zip(range(row - 1, -1, -1), range(column - 1, -1, -1)) + ) and all( - board[i][j] != 1 for i, j in zip(range(row, -1, -1), range(column, -1, -1)) + board[i][j] != 1 + for i, j in zip(range(row - 1, -1, -1), range(column + 1, n)) ) - and all(board[i][j] != 1 for i, j in zip(range(row, n), range(column, n))) - and all(board[i][j] != 1 for i, j in zip(range(row, n), range(column, -1, -1))) ) diff --git a/bit_manipulation/find_unique_number.py b/bit_manipulation/find_unique_number.py new file mode 100644 index 000000000000..77970b4865d1 --- /dev/null +++ b/bit_manipulation/find_unique_number.py @@ -0,0 +1,37 @@ +def find_unique_number(arr: list[int]) -> int: + """ + Given a list of integers where every element appears twice except for one, + this function returns the element that appears only once using bitwise XOR. + + >>> find_unique_number([1, 1, 2, 2, 3]) + 3 + >>> find_unique_number([4, 5, 4, 6, 6]) + 5 + >>> find_unique_number([7]) + 7 + >>> find_unique_number([10, 20, 10]) + 20 + >>> find_unique_number([]) + Traceback (most recent call last): + ... + ValueError: input list must not be empty + >>> find_unique_number([1, 'a', 1]) + Traceback (most recent call last): + ... + TypeError: all elements must be integers + """ + if not arr: + raise ValueError("input list must not be empty") + if not all(isinstance(x, int) for x in arr): + raise TypeError("all elements must be integers") + + result = 0 + for num in arr: + result ^= num + return result + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/ciphers/autokey.py b/ciphers/autokey.py index 05d8c066b139..7751a32d7546 100644 --- a/ciphers/autokey.py +++ b/ciphers/autokey.py @@ -1,5 +1,6 @@ """ https://en.wikipedia.org/wiki/Autokey_cipher + An autokey cipher (also known as the autoclave cipher) is a cipher that incorporates the message (the plaintext) into the key. The key is generated from the message in some automated fashion, @@ -10,8 +11,9 @@ def encrypt(plaintext: str, key: str) -> str: """ - Encrypt a given plaintext (string) and key (string), returning the + Encrypt a given `plaintext` (string) and `key` (string), returning the encrypted ciphertext. + >>> encrypt("hello world", "coffee") 'jsqqs avvwo' >>> encrypt("coffee is good as python", "TheAlgorithms") @@ -74,8 +76,9 @@ def encrypt(plaintext: str, key: str) -> str: def decrypt(ciphertext: str, key: str) -> str: """ - Decrypt a given ciphertext (string) and key (string), returning the decrypted + Decrypt a given `ciphertext` (string) and `key` (string), returning the decrypted ciphertext. + >>> decrypt("jsqqs avvwo", "coffee") 'hello world' >>> decrypt("vvjfpk wj ohvp su ddylsv", "TheAlgorithms") diff --git a/ciphers/base64.py b/ciphers/base64_cipher.py similarity index 94% rename from ciphers/base64.py rename to ciphers/base64_cipher.py index 2b950b1be37d..038d13963d95 100644 --- a/ciphers/base64.py +++ b/ciphers/base64_cipher.py @@ -105,13 +105,13 @@ def base64_decode(encoded_data: str) -> bytes: # Check if the encoded string contains non base64 characters if padding: - assert all( - char in B64_CHARSET for char in encoded_data[:-padding] - ), "Invalid base64 character(s) found." + assert all(char in B64_CHARSET for char in encoded_data[:-padding]), ( + "Invalid base64 character(s) found." + ) else: - assert all( - char in B64_CHARSET for char in encoded_data - ), "Invalid base64 character(s) found." + assert all(char in B64_CHARSET for char in encoded_data), ( + "Invalid base64 character(s) found." + ) # Check the padding assert len(encoded_data) % 4 == 0 and padding < 3, "Incorrect padding" diff --git a/ciphers/caesar_cipher.py b/ciphers/caesar_cipher.py index d19b9a337221..1cf4d67cbaed 100644 --- a/ciphers/caesar_cipher.py +++ b/ciphers/caesar_cipher.py @@ -7,24 +7,29 @@ def encrypt(input_string: str, key: int, alphabet: str | None = None) -> str: """ encrypt ======= + Encodes a given string with the caesar cipher and returns the encoded message Parameters: ----------- - * input_string: the plain-text that needs to be encoded - * key: the number of letters to shift the message by + + * `input_string`: the plain-text that needs to be encoded + * `key`: the number of letters to shift the message by Optional: - * alphabet (None): the alphabet used to encode the cipher, if not + + * `alphabet` (``None``): the alphabet used to encode the cipher, if not specified, the standard english alphabet with upper and lowercase letters is used Returns: + * A string containing the encoded cipher-text More on the caesar cipher ========================= + The caesar cipher is named after Julius Caesar who used it when sending secret military messages to his troops. This is a simple substitution cipher where every character in the plain-text is shifted by a certain number known @@ -32,26 +37,28 @@ def encrypt(input_string: str, key: int, alphabet: str | None = None) -> str: Example: Say we have the following message: - "Hello, captain" + ``Hello, captain`` And our alphabet is made up of lower and uppercase letters: - "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + ``abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ`` - And our shift is "2" + And our shift is ``2`` - We can then encode the message, one letter at a time. "H" would become "J", - since "J" is two letters away, and so on. If the shift is ever two large, or + We can then encode the message, one letter at a time. ``H`` would become ``J``, + since ``J`` is two letters away, and so on. If the shift is ever two large, or our letter is at the end of the alphabet, we just start at the beginning - ("Z" would shift to "a" then "b" and so on). + (``Z`` would shift to ``a`` then ``b`` and so on). - Our final message would be "Jgnnq, ecrvckp" + Our final message would be ``Jgnnq, ecrvckp`` Further reading =============== + * https://en.m.wikipedia.org/wiki/Caesar_cipher Doctests ======== + >>> encrypt('The quick brown fox jumps over the lazy dog', 8) 'bpm yCqks jzwEv nwF rCuxA wDmz Bpm tiHG lwo' @@ -85,23 +92,28 @@ def decrypt(input_string: str, key: int, alphabet: str | None = None) -> str: """ decrypt ======= + Decodes a given string of cipher-text and returns the decoded plain-text Parameters: ----------- - * input_string: the cipher-text that needs to be decoded - * key: the number of letters to shift the message backwards by to decode + + * `input_string`: the cipher-text that needs to be decoded + * `key`: the number of letters to shift the message backwards by to decode Optional: - * alphabet (None): the alphabet used to decode the cipher, if not + + * `alphabet` (``None``): the alphabet used to decode the cipher, if not specified, the standard english alphabet with upper and lowercase letters is used Returns: + * A string containing the decoded plain-text More on the caesar cipher ========================= + The caesar cipher is named after Julius Caesar who used it when sending secret military messages to his troops. This is a simple substitution cipher where very character in the plain-text is shifted by a certain number known @@ -110,27 +122,29 @@ def decrypt(input_string: str, key: int, alphabet: str | None = None) -> str: Example: Say we have the following cipher-text: - "Jgnnq, ecrvckp" + ``Jgnnq, ecrvckp`` And our alphabet is made up of lower and uppercase letters: - "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + ``abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ`` - And our shift is "2" + And our shift is ``2`` To decode the message, we would do the same thing as encoding, but in - reverse. The first letter, "J" would become "H" (remember: we are decoding) - because "H" is two letters in reverse (to the left) of "J". We would - continue doing this. A letter like "a" would shift back to the end of - the alphabet, and would become "Z" or "Y" and so on. + reverse. The first letter, ``J`` would become ``H`` (remember: we are decoding) + because ``H`` is two letters in reverse (to the left) of ``J``. We would + continue doing this. A letter like ``a`` would shift back to the end of + the alphabet, and would become ``Z`` or ``Y`` and so on. - Our final message would be "Hello, captain" + Our final message would be ``Hello, captain`` Further reading =============== + * https://en.m.wikipedia.org/wiki/Caesar_cipher Doctests ======== + >>> decrypt('bpm yCqks jzwEv nwF rCuxA wDmz Bpm tiHG lwo', 8) 'The quick brown fox jumps over the lazy dog' @@ -150,41 +164,44 @@ def brute_force(input_string: str, alphabet: str | None = None) -> dict[int, str """ brute_force =========== + Returns all the possible combinations of keys and the decoded strings in the form of a dictionary Parameters: ----------- - * input_string: the cipher-text that needs to be used during brute-force + + * `input_string`: the cipher-text that needs to be used during brute-force Optional: - * alphabet: (None): the alphabet used to decode the cipher, if not + + * `alphabet` (``None``): the alphabet used to decode the cipher, if not specified, the standard english alphabet with upper and lowercase letters is used More about brute force ====================== + Brute force is when a person intercepts a message or password, not knowing the key and tries every single combination. This is easy with the caesar cipher since there are only all the letters in the alphabet. The more complex the cipher, the larger amount of time it will take to do brute force Ex: - Say we have a 5 letter alphabet (abcde), for simplicity and we intercepted the - following message: - - "dbc" - + Say we have a ``5`` letter alphabet (``abcde``), for simplicity and we intercepted + the following message: ``dbc``, we could then just write out every combination: - ecd... and so on, until we reach a combination that makes sense: - "cab" + ``ecd``... and so on, until we reach a combination that makes sense: + ``cab`` Further reading =============== + * https://en.wikipedia.org/wiki/Brute_force Doctests ======== + >>> brute_force("jFyuMy xIH'N vLONy zILwy Gy!")[20] "Please don't brute force me!" @@ -208,7 +225,7 @@ def brute_force(input_string: str, alphabet: str | None = None) -> dict[int, str if __name__ == "__main__": while True: - print(f'\n{"-" * 10}\n Menu\n{"-" * 10}') + print(f"\n{'-' * 10}\n Menu\n{'-' * 10}") print(*["1.Encrypt", "2.Decrypt", "3.BruteForce", "4.Quit"], sep="\n") # get user input diff --git a/ciphers/decrypt_caesar_with_chi_squared.py b/ciphers/decrypt_caesar_with_chi_squared.py index 10832203e531..fb95c0f90628 100644 --- a/ciphers/decrypt_caesar_with_chi_squared.py +++ b/ciphers/decrypt_caesar_with_chi_squared.py @@ -11,33 +11,31 @@ def decrypt_caesar_with_chi_squared( """ Basic Usage =========== + Arguments: - * ciphertext (str): the text to decode (encoded with the caesar cipher) + * `ciphertext` (str): the text to decode (encoded with the caesar cipher) Optional Arguments: - * cipher_alphabet (list): the alphabet used for the cipher (each letter is - a string separated by commas) - * frequencies_dict (dict): a dictionary of word frequencies where keys are - the letters and values are a percentage representation of the frequency as - a decimal/float - * case_sensitive (bool): a boolean value: True if the case matters during - decryption, False if it doesn't + * `cipher_alphabet` (list): the alphabet used for the cipher (each letter is + a string separated by commas) + * `frequencies_dict` (dict): a dictionary of word frequencies where keys are + the letters and values are a percentage representation of the frequency as + a decimal/float + * `case_sensitive` (bool): a boolean value: ``True`` if the case matters during + decryption, ``False`` if it doesn't Returns: - * A tuple in the form of: - ( - most_likely_cipher, - most_likely_cipher_chi_squared_value, - decoded_most_likely_cipher - ) + * A tuple in the form of: + (`most_likely_cipher`, `most_likely_cipher_chi_squared_value`, + `decoded_most_likely_cipher`) - where... - - most_likely_cipher is an integer representing the shift of the smallest - chi-squared statistic (most likely key) - - most_likely_cipher_chi_squared_value is a float representing the - chi-squared statistic of the most likely shift - - decoded_most_likely_cipher is a string with the decoded cipher - (decoded by the most_likely_cipher key) + where... + - `most_likely_cipher` is an integer representing the shift of the smallest + chi-squared statistic (most likely key) + - `most_likely_cipher_chi_squared_value` is a float representing the + chi-squared statistic of the most likely shift + - `decoded_most_likely_cipher` is a string with the decoded cipher + (decoded by the most_likely_cipher key) The Chi-squared test @@ -45,52 +43,57 @@ def decrypt_caesar_with_chi_squared( The caesar cipher ----------------- + The caesar cipher is a very insecure encryption algorithm, however it has been used since Julius Caesar. The cipher is a simple substitution cipher where each character in the plain text is replaced by a character in the alphabet a certain number of characters after the original character. The number of characters away is called the shift or key. For example: - Plain text: hello - Key: 1 - Cipher text: ifmmp - (each letter in hello has been shifted one to the right in the eng. alphabet) + | Plain text: ``hello`` + | Key: ``1`` + | Cipher text: ``ifmmp`` + | (each letter in ``hello`` has been shifted one to the right in the eng. alphabet) As you can imagine, this doesn't provide lots of security. In fact decrypting ciphertext by brute-force is extremely easy even by hand. However - one way to do that is the chi-squared test. + one way to do that is the chi-squared test. The chi-squared test - ------------------- + -------------------- + Each letter in the english alphabet has a frequency, or the amount of times it shows up compared to other letters (usually expressed as a decimal representing the percentage likelihood). The most common letter in the - english language is "e" with a frequency of 0.11162 or 11.162%. The test is - completed in the following fashion. + english language is ``e`` with a frequency of ``0.11162`` or ``11.162%``. + The test is completed in the following fashion. 1. The ciphertext is decoded in a brute force way (every combination of the - 26 possible combinations) + ``26`` possible combinations) 2. For every combination, for each letter in the combination, the average amount of times the letter should appear the message is calculated by - multiplying the total number of characters by the frequency of the letter + multiplying the total number of characters by the frequency of the letter. + + | For example: + | In a message of ``100`` characters, ``e`` should appear around ``11.162`` + times. - For example: - In a message of 100 characters, e should appear around 11.162 times. + 3. Then, to calculate the margin of error (the amount of times the letter + SHOULD appear with the amount of times the letter DOES appear), we use + the chi-squared test. The following formula is used: - 3. Then, to calculate the margin of error (the amount of times the letter - SHOULD appear with the amount of times the letter DOES appear), we use - the chi-squared test. The following formula is used: + Let: + - n be the number of times the letter actually appears + - p be the predicted value of the number of times the letter should + appear (see item ``2``) + - let v be the chi-squared test result (referred to here as chi-squared + value/statistic) - Let: - - n be the number of times the letter actually appears - - p be the predicted value of the number of times the letter should - appear (see #2) - - let v be the chi-squared test result (referred to here as chi-squared - value/statistic) + :: - (n - p)^2 - --------- = v - p + (n - p)^2 + --------- = v + p 4. Each chi squared value for each letter is then added up to the total. The total is the chi-squared statistic for that encryption key. @@ -98,16 +101,16 @@ def decrypt_caesar_with_chi_squared( to be the decoded answer. Further Reading - ================ + =============== - * http://practicalcryptography.com/cryptanalysis/text-characterisation/chi-squared- - statistic/ + * http://practicalcryptography.com/cryptanalysis/text-characterisation/chi-squared-statistic/ * https://en.wikipedia.org/wiki/Letter_frequency * https://en.wikipedia.org/wiki/Chi-squared_test * https://en.m.wikipedia.org/wiki/Caesar_cipher Doctests ======== + >>> decrypt_caesar_with_chi_squared( ... 'dof pz aol jhlzhy jpwoly zv wvwbshy? pa pz avv lhzf av jyhjr!' ... ) # doctest: +NORMALIZE_WHITESPACE diff --git a/ciphers/enigma_machine2.py b/ciphers/enigma_machine2.py index 163aa7172c11..e42fdd82ed41 100644 --- a/ciphers/enigma_machine2.py +++ b/ciphers/enigma_machine2.py @@ -1,14 +1,16 @@ """ -Wikipedia: https://en.wikipedia.org/wiki/Enigma_machine -Video explanation: https://youtu.be/QwQVMqfoB2E -Also check out Numberphile's and Computerphile's videos on this topic +| Wikipedia: https://en.wikipedia.org/wiki/Enigma_machine +| Video explanation: https://youtu.be/QwQVMqfoB2E +| Also check out Numberphile's and Computerphile's videos on this topic -This module contains function 'enigma' which emulates +This module contains function ``enigma`` which emulates the famous Enigma machine from WWII. + Module includes: -- enigma function + +- ``enigma`` function - showcase of function usage -- 9 randomly generated rotors +- ``9`` randomly generated rotors - reflector (aka static rotor) - original alphabet @@ -73,7 +75,7 @@ def _validator( rotpos: RotorPositionT, rotsel: RotorSelectionT, pb: str ) -> tuple[RotorPositionT, RotorSelectionT, dict[str, str]]: """ - Checks if the values can be used for the 'enigma' function + Checks if the values can be used for the ``enigma`` function >>> _validator((1,1,1), (rotor1, rotor2, rotor3), 'POLAND') ((1, 1, 1), ('EGZWVONAHDCLFQMSIPJBYUKXTR', 'FOBHMDKEXQNRAULPGSJVTYICZW', \ @@ -83,7 +85,7 @@ def _validator( :param rotpos: rotor_positon :param rotsel: rotor_selection :param pb: plugb -> validated and transformed - :return: (rotpos, rotsel, pb) + :return: (`rotpos`, `rotsel`, `pb`) """ # Checks if there are 3 unique rotors @@ -118,9 +120,10 @@ def _plugboard(pbstring: str) -> dict[str, str]: >>> _plugboard('POLAND') {'P': 'O', 'O': 'P', 'L': 'A', 'A': 'L', 'N': 'D', 'D': 'N'} - In the code, 'pb' stands for 'plugboard' + In the code, ``pb`` stands for ``plugboard`` Pairs can be separated by spaces + :param pbstring: string containing plugboard setting for the Enigma machine :return: dictionary containing converted pairs """ @@ -168,31 +171,34 @@ def enigma( plugb: str = "", ) -> str: """ - The only difference with real-world enigma is that I allowed string input. + The only difference with real-world enigma is that ``I`` allowed string input. All characters are converted to uppercase. (non-letter symbol are ignored) - How it works: - (for every letter in the message) + + | How it works: + | (for every letter in the message) - Input letter goes into the plugboard. - If it is connected to another one, switch it. + If it is connected to another one, switch it. + + - Letter goes through ``3`` rotors. + Each rotor can be represented as ``2`` sets of symbol, where one is shuffled. + Each symbol from the first set has corresponding symbol in + the second set and vice versa. - - Letter goes through 3 rotors. - Each rotor can be represented as 2 sets of symbol, where one is shuffled. - Each symbol from the first set has corresponding symbol in - the second set and vice versa. + example:: - example: - | ABCDEFGHIJKLMNOPQRSTUVWXYZ | e.g. F=D and D=F - | VKLEPDBGRNWTFCJOHQAMUZYIXS | + | ABCDEFGHIJKLMNOPQRSTUVWXYZ | e.g. F=D and D=F + | VKLEPDBGRNWTFCJOHQAMUZYIXS | - Symbol then goes through reflector (static rotor). - There it is switched with paired symbol - The reflector can be represented as2 sets, each with half of the alphanet. - There are usually 10 pairs of letters. + There it is switched with paired symbol. + The reflector can be represented as ``2`` sets, each with half of the alphanet. + There are usually ``10`` pairs of letters. + + Example:: - Example: - | ABCDEFGHIJKLM | e.g. E is paired to X - | ZYXWVUTSRQPON | so when E goes in X goes out and vice versa + | ABCDEFGHIJKLM | e.g. E is paired to X + | ZYXWVUTSRQPON | so when E goes in X goes out and vice versa - Letter then goes through the rotors again @@ -211,9 +217,9 @@ def enigma( :param text: input message - :param rotor_position: tuple with 3 values in range 1..26 - :param rotor_selection: tuple with 3 rotors () - :param plugb: string containing plugboard configuration (default '') + :param rotor_position: tuple with ``3`` values in range ``1``.. ``26`` + :param rotor_selection: tuple with ``3`` rotors + :param plugb: string containing plugboard configuration (default ``''``) :return: en/decrypted string """ diff --git a/ciphers/rsa_factorization.py b/ciphers/rsa_factorization.py index 0a358a4fc2d4..585b21fac856 100644 --- a/ciphers/rsa_factorization.py +++ b/ciphers/rsa_factorization.py @@ -3,8 +3,10 @@ The program can efficiently factor RSA prime number given the private key d and public key e. -Source: on page 3 of https://crypto.stanford.edu/~dabo/papers/RSA-survey.pdf -More readable source: https://www.di-mgt.com.au/rsa_factorize_n.html + +| Source: on page ``3`` of https://crypto.stanford.edu/~dabo/papers/RSA-survey.pdf +| More readable source: https://www.di-mgt.com.au/rsa_factorize_n.html + large number can take minutes to factor, therefore are not included in doctest. """ @@ -17,13 +19,14 @@ def rsafactor(d: int, e: int, n: int) -> list[int]: """ This function returns the factors of N, where p*q=N - Return: [p, q] + + Return: [p, q] We call N the RSA modulus, e the encryption exponent, and d the decryption exponent. The pair (N, e) is the public key. As its name suggests, it is public and is used to - encrypt messages. + encrypt messages. The pair (N, d) is the secret key or private key and is known only to the recipient - of encrypted messages. + of encrypted messages. >>> rsafactor(3, 16971, 25777) [149, 173] diff --git a/ciphers/simple_keyword_cypher.py b/ciphers/simple_keyword_cypher.py index 9dc624e7762c..bde137d826c3 100644 --- a/ciphers/simple_keyword_cypher.py +++ b/ciphers/simple_keyword_cypher.py @@ -1,9 +1,11 @@ def remove_duplicates(key: str) -> str: """ Removes duplicate alphabetic characters in a keyword (letter is ignored after its - first appearance). + first appearance). + :param key: Keyword to use :return: String with duplicates removed + >>> remove_duplicates('Hello World!!') 'Helo Wrd' """ @@ -18,6 +20,7 @@ def remove_duplicates(key: str) -> str: def create_cipher_map(key: str) -> dict[str, str]: """ Returns a cipher map given a keyword. + :param key: keyword to use :return: dictionary cipher map """ @@ -43,9 +46,11 @@ def create_cipher_map(key: str) -> dict[str, str]: def encipher(message: str, cipher_map: dict[str, str]) -> str: """ Enciphers a message given a cipher map. + :param message: Message to encipher :param cipher_map: Cipher map :return: enciphered string + >>> encipher('Hello World!!', create_cipher_map('Goodbye!!')) 'CYJJM VMQJB!!' """ @@ -55,9 +60,11 @@ def encipher(message: str, cipher_map: dict[str, str]) -> str: def decipher(message: str, cipher_map: dict[str, str]) -> str: """ Deciphers a message given a cipher map + :param message: Message to decipher :param cipher_map: Dictionary mapping to use :return: Deciphered string + >>> cipher_map = create_cipher_map('Goodbye!!') >>> decipher(encipher('Hello World!!', cipher_map), cipher_map) 'HELLO WORLD!!' @@ -70,6 +77,7 @@ def decipher(message: str, cipher_map: dict[str, str]) -> str: def main() -> None: """ Handles I/O + :return: void """ message = input("Enter message to encode or decode: ").strip() diff --git a/ciphers/trifid_cipher.py b/ciphers/trifid_cipher.py index 16b9faf67688..13a47e9dd03b 100644 --- a/ciphers/trifid_cipher.py +++ b/ciphers/trifid_cipher.py @@ -22,7 +22,7 @@ def __encrypt_part(message_part: str, character_to_number: dict[str, str]) -> str: """ - Arrange the triagram value of each letter of 'message_part' vertically and join + Arrange the triagram value of each letter of `message_part` vertically and join them horizontally. >>> __encrypt_part('ASK', TEST_CHARACTER_TO_NUMBER) @@ -65,8 +65,8 @@ def __prepare( """ A helper function that generates the triagrams and assigns each letter of the alphabet to its corresponding triagram and stores this in a dictionary - ("character_to_number" and "number_to_character") after confirming if the - alphabet's length is 27. + (`character_to_number` and `number_to_character`) after confirming if the + alphabet's length is ``27``. >>> test = __prepare('I aM a BOy','abCdeFghijkLmnopqrStuVwxYZ+') >>> expected = ('IAMABOY','ABCDEFGHIJKLMNOPQRSTUVWXYZ+', @@ -75,24 +75,28 @@ def __prepare( True Testing with incomplete alphabet + >>> __prepare('I aM a BOy','abCdeFghijkLmnopqrStuVw') Traceback (most recent call last): ... KeyError: 'Length of alphabet has to be 27.' Testing with extra long alphabets + >>> __prepare('I aM a BOy','abCdeFghijkLmnopqrStuVwxyzzwwtyyujjgfd') Traceback (most recent call last): ... KeyError: 'Length of alphabet has to be 27.' - Testing with punctuations that are not in the given alphabet + Testing with punctuation not in the given alphabet + >>> __prepare('am i a boy?','abCdeFghijkLmnopqrStuVwxYZ+') Traceback (most recent call last): ... ValueError: Each message character has to be included in alphabet! Testing with numbers + >>> __prepare(500,'abCdeFghijkLmnopqrStuVwxYZ+') Traceback (most recent call last): ... @@ -124,15 +128,15 @@ def encrypt_message( encrypt_message =============== - Encrypts a message using the trifid_cipher. Any punctuatuions that + Encrypts a message using the trifid_cipher. Any punctuatuion chars that would be used should be added to the alphabet. PARAMETERS ---------- - * message: The message you want to encrypt. - * alphabet (optional): The characters to be used for the cipher . - * period (optional): The number of characters you want in a group whilst + * `message`: The message you want to encrypt. + * `alphabet` (optional): The characters to be used for the cipher . + * `period` (optional): The number of characters you want in a group whilst encrypting. >>> encrypt_message('I am a boy') @@ -169,20 +173,21 @@ def decrypt_message( decrypt_message =============== - Decrypts a trifid_cipher encrypted message . + Decrypts a trifid_cipher encrypted message. PARAMETERS ---------- - * message: The message you want to decrypt . - * alphabet (optional): The characters used for the cipher. - * period (optional): The number of characters used in grouping when it + * `message`: The message you want to decrypt. + * `alphabet` (optional): The characters used for the cipher. + * `period` (optional): The number of characters used in grouping when it was encrypted. >>> decrypt_message('BCDGBQY') 'IAMABOY' Decrypting with your own alphabet and period + >>> decrypt_message('FMJFVOISSUFTFPUFEQQC','FELIXMARDSTBCGHJKNOPQUVWYZ+',5) 'AIDETOILECIELTAIDERA' """ diff --git a/computer_vision/flip_augmentation.py b/computer_vision/flip_augmentation.py index 77a8cbd7b14f..7301424824df 100644 --- a/computer_vision/flip_augmentation.py +++ b/computer_vision/flip_augmentation.py @@ -33,7 +33,7 @@ def main() -> None: file_name = paths[index].split(os.sep)[-1].rsplit(".", 1)[0] file_root = f"{OUTPUT_DIR}/{file_name}_FLIP_{letter_code}" cv2.imwrite(f"{file_root}.jpg", image, [cv2.IMWRITE_JPEG_QUALITY, 85]) - print(f"Success {index+1}/{len(new_images)} with {file_name}") + print(f"Success {index + 1}/{len(new_images)} with {file_name}") annos_list = [] for anno in new_annos[index]: obj = f"{anno[0]} {anno[1]} {anno[2]} {anno[3]} {anno[4]}" diff --git a/computer_vision/intensity_based_segmentation.py b/computer_vision/intensity_based_segmentation.py new file mode 100644 index 000000000000..7f2b1141acc4 --- /dev/null +++ b/computer_vision/intensity_based_segmentation.py @@ -0,0 +1,62 @@ +# Source: "https://www.ijcse.com/docs/IJCSE11-02-03-117.pdf" + +# Importing necessary libraries +import matplotlib.pyplot as plt +import numpy as np +from PIL import Image + + +def segment_image(image: np.ndarray, thresholds: list[int]) -> np.ndarray: + """ + Performs image segmentation based on intensity thresholds. + + Args: + image: Input grayscale image as a 2D array. + thresholds: Intensity thresholds to define segments. + + Returns: + A labeled 2D array where each region corresponds to a threshold range. + + Example: + >>> img = np.array([[80, 120, 180], [40, 90, 150], [20, 60, 100]]) + >>> segment_image(img, [50, 100, 150]) + array([[1, 2, 3], + [0, 1, 2], + [0, 1, 1]], dtype=int32) + """ + # Initialize segmented array with zeros + segmented = np.zeros_like(image, dtype=np.int32) + + # Assign labels based on thresholds + for i, threshold in enumerate(thresholds): + segmented[image > threshold] = i + 1 + + return segmented + + +if __name__ == "__main__": + # Load the image + image_path = "path_to_image" # Replace with your image path + original_image = Image.open(image_path).convert("L") + image_array = np.array(original_image) + + # Define thresholds + thresholds = [50, 100, 150, 200] + + # Perform segmentation + segmented_image = segment_image(image_array, thresholds) + + # Display the results + plt.figure(figsize=(10, 5)) + + plt.subplot(1, 2, 1) + plt.title("Original Image") + plt.imshow(image_array, cmap="gray") + plt.axis("off") + + plt.subplot(1, 2, 2) + plt.title("Segmented Image") + plt.imshow(segmented_image, cmap="tab20") + plt.axis("off") + + plt.show() diff --git a/computer_vision/mosaic_augmentation.py b/computer_vision/mosaic_augmentation.py index cd923dfe095f..d881347121ea 100644 --- a/computer_vision/mosaic_augmentation.py +++ b/computer_vision/mosaic_augmentation.py @@ -41,7 +41,7 @@ def main() -> None: file_name = path.split(os.sep)[-1].rsplit(".", 1)[0] file_root = f"{OUTPUT_DIR}/{file_name}_MOSAIC_{letter_code}" cv2.imwrite(f"{file_root}.jpg", new_image, [cv2.IMWRITE_JPEG_QUALITY, 85]) - print(f"Succeeded {index+1}/{NUMBER_IMAGES} with {file_name}") + print(f"Succeeded {index + 1}/{NUMBER_IMAGES} with {file_name}") annos_list = [] for anno in new_annos: width = anno[3] - anno[1] diff --git a/conversions/convert_number_to_words.py b/conversions/convert_number_to_words.py index dbab44c72e1f..6aa43738b9fe 100644 --- a/conversions/convert_number_to_words.py +++ b/conversions/convert_number_to_words.py @@ -1,5 +1,5 @@ from enum import Enum -from typing import ClassVar, Literal +from typing import Literal class NumberingSystem(Enum): @@ -54,7 +54,7 @@ def max_value(cls, system: str) -> int: class NumberWords(Enum): - ONES: ClassVar[dict[int, str]] = { + ONES = { # noqa: RUF012 0: "", 1: "one", 2: "two", @@ -67,7 +67,7 @@ class NumberWords(Enum): 9: "nine", } - TEENS: ClassVar[dict[int, str]] = { + TEENS = { # noqa: RUF012 0: "ten", 1: "eleven", 2: "twelve", @@ -80,7 +80,7 @@ class NumberWords(Enum): 9: "nineteen", } - TENS: ClassVar[dict[int, str]] = { + TENS = { # noqa: RUF012 2: "twenty", 3: "thirty", 4: "forty", diff --git a/conversions/prefix_conversions_string.py b/conversions/prefix_conversions_string.py index 9344c9672a1f..c5fef49874ca 100644 --- a/conversions/prefix_conversions_string.py +++ b/conversions/prefix_conversions_string.py @@ -53,7 +53,7 @@ class SIUnit(Enum): yocto = -24 @classmethod - def get_positive(cls: type[T]) -> dict: + def get_positive(cls) -> dict: """ Returns a dictionary with only the elements of this enum that has a positive value @@ -68,7 +68,7 @@ def get_positive(cls: type[T]) -> dict: return {unit.name: unit.value for unit in cls if unit.value > 0} @classmethod - def get_negative(cls: type[T]) -> dict: + def get_negative(cls) -> dict: """ Returns a dictionary with only the elements of this enum that has a negative value diff --git a/conversions/rectangular_to_polar.py b/conversions/rectangular_to_polar.py new file mode 100644 index 000000000000..bed97d7410ec --- /dev/null +++ b/conversions/rectangular_to_polar.py @@ -0,0 +1,32 @@ +import math + + +def rectangular_to_polar(real: float, img: float) -> tuple[float, float]: + """ + https://en.wikipedia.org/wiki/Polar_coordinate_system + + >>> rectangular_to_polar(5,-5) + (7.07, -45.0) + >>> rectangular_to_polar(-1,1) + (1.41, 135.0) + >>> rectangular_to_polar(-1,-1) + (1.41, -135.0) + >>> rectangular_to_polar(1e-10,1e-10) + (0.0, 45.0) + >>> rectangular_to_polar(-1e-10,1e-10) + (0.0, 135.0) + >>> rectangular_to_polar(9.75,5.93) + (11.41, 31.31) + >>> rectangular_to_polar(10000,99999) + (100497.76, 84.29) + """ + + mod = round(math.sqrt((real**2) + (img**2)), 2) + ang = round(math.degrees(math.atan2(img, real)), 2) + return (mod, ang) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/data_structures/arrays/monotonic_array.py b/data_structures/arrays/monotonic_array.py index c50a21530814..342d443a9cfc 100644 --- a/data_structures/arrays/monotonic_array.py +++ b/data_structures/arrays/monotonic_array.py @@ -9,6 +9,16 @@ def is_monotonic(nums: list[int]) -> bool: True >>> is_monotonic([1, 3, 2]) False + >>> is_monotonic([1,2,3,4,5,6,5]) + False + >>> is_monotonic([-3,-2,-1]) + True + >>> is_monotonic([-5,-6,-7]) + True + >>> is_monotonic([0,0,0]) + True + >>> is_monotonic([-100,0,100]) + True """ return all(nums[i] <= nums[i + 1] for i in range(len(nums) - 1)) or all( nums[i] >= nums[i + 1] for i in range(len(nums) - 1) @@ -21,3 +31,7 @@ def is_monotonic(nums: list[int]) -> bool: print(is_monotonic([1, 2, 2, 3])) # Output: True print(is_monotonic([6, 5, 4, 4])) # Output: True print(is_monotonic([1, 3, 2])) # Output: False + + import doctest + + doctest.testmod() diff --git a/data_structures/arrays/prefix_sum.py b/data_structures/arrays/prefix_sum.py index 2243a5308937..717b5f9d7e7e 100644 --- a/data_structures/arrays/prefix_sum.py +++ b/data_structures/arrays/prefix_sum.py @@ -30,11 +30,29 @@ def get_sum(self, start: int, end: int) -> int: 5 >>> PrefixSum([1,2,3]).get_sum(2, 2) 3 + >>> PrefixSum([]).get_sum(0, 0) + Traceback (most recent call last): + ... + ValueError: The array is empty. + >>> PrefixSum([1,2,3]).get_sum(-1, 2) + Traceback (most recent call last): + ... + ValueError: Invalid range specified. >>> PrefixSum([1,2,3]).get_sum(2, 3) Traceback (most recent call last): ... - IndexError: list index out of range + ValueError: Invalid range specified. + >>> PrefixSum([1,2,3]).get_sum(2, 1) + Traceback (most recent call last): + ... + ValueError: Invalid range specified. """ + if not self.prefix_sum: + raise ValueError("The array is empty.") + + if start < 0 or end >= len(self.prefix_sum) or start > end: + raise ValueError("Invalid range specified.") + if start == 0: return self.prefix_sum[end] diff --git a/data_structures/arrays/sudoku_solver.py b/data_structures/arrays/sudoku_solver.py index 7e38e1465728..4c722f12fd6e 100644 --- a/data_structures/arrays/sudoku_solver.py +++ b/data_structures/arrays/sudoku_solver.py @@ -9,7 +9,9 @@ def cross(items_a, items_b): - "Cross product of elements in A and elements in B." + """ + Cross product of elements in A and elements in B. + """ return [a + b for a in items_a for b in items_b] @@ -23,11 +25,11 @@ def cross(items_a, items_b): + [cross(rs, cs) for rs in ("ABC", "DEF", "GHI") for cs in ("123", "456", "789")] ) units = {s: [u for u in unitlist if s in u] for s in squares} -peers = {s: set(sum(units[s], [])) - {s} for s in squares} # noqa: RUF017 +peers = {s: {x for u in units[s] for x in u} - {s} for s in squares} def test(): - "A set of unit tests." + """A set of unit tests.""" assert len(squares) == 81 assert len(unitlist) == 27 assert all(len(units[s]) == 3 for s in squares) @@ -47,10 +49,12 @@ def test(): def parse_grid(grid): - """Convert grid to a dict of possible values, {square: digits}, or - return False if a contradiction is detected.""" + """ + Convert grid to a dict of possible values, {square: digits}, or + return False if a contradiction is detected. + """ ## To start, every square can be any digit; then assign values from the grid. - values = {s: digits for s in squares} + values = dict.fromkeys(squares, digits) for s, d in grid_values(grid).items(): if d in digits and not assign(values, s, d): return False ## (Fail if we can't assign d to square s.) @@ -58,15 +62,19 @@ def parse_grid(grid): def grid_values(grid): - "Convert grid into a dict of {square: char} with '0' or '.' for empties." + """ + Convert grid into a dict of {square: char} with '0' or '.' for empties. + """ chars = [c for c in grid if c in digits or c in "0."] assert len(chars) == 81 return dict(zip(squares, chars)) def assign(values, s, d): - """Eliminate all the other values (except d) from values[s] and propagate. - Return values, except return False if a contradiction is detected.""" + """ + Eliminate all the other values (except d) from values[s] and propagate. + Return values, except return False if a contradiction is detected. + """ other_values = values[s].replace(d, "") if all(eliminate(values, s, d2) for d2 in other_values): return values @@ -75,8 +83,10 @@ def assign(values, s, d): def eliminate(values, s, d): - """Eliminate d from values[s]; propagate when values or places <= 2. - Return values, except return False if a contradiction is detected.""" + """ + Eliminate d from values[s]; propagate when values or places <= 2. + Return values, except return False if a contradiction is detected. + """ if d not in values[s]: return values ## Already eliminated values[s] = values[s].replace(d, "") @@ -99,7 +109,9 @@ def eliminate(values, s, d): def display(values): - "Display these values as a 2-D grid." + """ + Display these values as a 2-D grid. + """ width = 1 + max(len(values[s]) for s in squares) line = "+".join(["-" * (width * 3)] * 3) for r in rows: @@ -114,11 +126,14 @@ def display(values): def solve(grid): + """ + Solve the grid. + """ return search(parse_grid(grid)) def some(seq): - "Return some element of seq that is true." + """Return some element of seq that is true.""" for e in seq: if e: return e @@ -126,7 +141,9 @@ def some(seq): def search(values): - "Using depth-first search and propagation, try all possible values." + """ + Using depth-first search and propagation, try all possible values. + """ if values is False: return False ## Failed earlier if all(len(values[s]) == 1 for s in squares): @@ -137,9 +154,11 @@ def search(values): def solve_all(grids, name="", showif=0.0): - """Attempt to solve a sequence of grids. Report results. + """ + Attempt to solve a sequence of grids. Report results. When showif is a number of seconds, display puzzles that take longer. - When showif is None, don't display any puzzles.""" + When showif is None, don't display any puzzles. + """ def time_solve(grid): start = time.monotonic() @@ -162,7 +181,9 @@ def time_solve(grid): def solved(values): - "A puzzle is solved if each unit is a permutation of the digits 1 to 9." + """ + A puzzle is solved if each unit is a permutation of the digits 1 to 9. + """ def unitsolved(unit): return {values[s] for s in unit} == set(digits) @@ -172,14 +193,17 @@ def unitsolved(unit): def from_file(filename, sep="\n"): "Parse a file into a list of strings, separated by sep." - return open(filename).read().strip().split(sep) + with open(filename) as file: + return file.read().strip().split(sep) def random_puzzle(assignments=17): - """Make a random puzzle with N or more assignments. Restart on contradictions. + """ + Make a random puzzle with N or more assignments. Restart on contradictions. Note the resulting puzzle is not guaranteed to be solvable, but empirically - about 99.8% of them are solvable. Some have multiple solutions.""" - values = {s: digits for s in squares} + about 99.8% of them are solvable. Some have multiple solutions. + """ + values = dict.fromkeys(squares, digits) for s in shuffled(squares): if not assign(values, s, random.choice(values[s])): break @@ -190,7 +214,9 @@ def random_puzzle(assignments=17): def shuffled(seq): - "Return a randomly shuffled copy of the input sequence." + """ + Return a randomly shuffled copy of the input sequence. + """ seq = list(seq) random.shuffle(seq) return seq diff --git a/data_structures/binary_tree/avl_tree.py b/data_structures/binary_tree/avl_tree.py index 9fca7237404c..8558305eefe4 100644 --- a/data_structures/binary_tree/avl_tree.py +++ b/data_structures/binary_tree/avl_tree.py @@ -221,6 +221,10 @@ def del_node(root: MyNode, data: Any) -> MyNode | None: else: root.set_right(del_node(right_child, data)) + # Re-fetch left_child and right_child references + left_child = root.get_left() + right_child = root.get_right() + if get_height(right_child) - get_height(left_child) == 2: assert right_child is not None if get_height(right_child.get_right()) > get_height(right_child.get_left()): diff --git a/data_structures/hashing/number_theory/prime_numbers.py b/data_structures/hashing/number_theory/prime_numbers.py index 2549a1477b2b..82071b5e9f09 100644 --- a/data_structures/hashing/number_theory/prime_numbers.py +++ b/data_structures/hashing/number_theory/prime_numbers.py @@ -32,9 +32,9 @@ def is_prime(number: int) -> bool: """ # precondition - assert isinstance(number, int) and ( - number >= 0 - ), "'number' must been an int and positive" + assert isinstance(number, int) and (number >= 0), ( + "'number' must been an int and positive" + ) if 1 < number < 4: # 2 and 3 are primes diff --git a/data_structures/heap/min_heap.py b/data_structures/heap/min_heap.py index ce7ed570a58d..577b98d788a1 100644 --- a/data_structures/heap/min_heap.py +++ b/data_structures/heap/min_heap.py @@ -124,9 +124,9 @@ def is_empty(self): return len(self.heap) == 0 def decrease_key(self, node, new_value): - assert ( - self.heap[self.idx_of_element[node]].val > new_value - ), "newValue must be less that current value" + assert self.heap[self.idx_of_element[node]].val > new_value, ( + "newValue must be less that current value" + ) node.val = new_value self.heap_dict[node.name] = new_value self.sift_up(self.idx_of_element[node]) diff --git a/data_structures/kd_tree/tests/test_kdtree.py b/data_structures/kd_tree/tests/test_kdtree.py index dce5e4f34ff4..d6a4a66dd24d 100644 --- a/data_structures/kd_tree/tests/test_kdtree.py +++ b/data_structures/kd_tree/tests/test_kdtree.py @@ -48,14 +48,14 @@ def test_build_kdtree(num_points, cube_size, num_dimensions, depth, expected_res assert kdtree is not None, "Expected a KDNode, got None" # Check if root has correct dimensions - assert ( - len(kdtree.point) == num_dimensions - ), f"Expected point dimension {num_dimensions}, got {len(kdtree.point)}" + assert len(kdtree.point) == num_dimensions, ( + f"Expected point dimension {num_dimensions}, got {len(kdtree.point)}" + ) # Check that the tree is balanced to some extent (simplistic check) - assert isinstance( - kdtree, KDNode - ), f"Expected KDNode instance, got {type(kdtree)}" + assert isinstance(kdtree, KDNode), ( + f"Expected KDNode instance, got {type(kdtree)}" + ) def test_nearest_neighbour_search(): diff --git a/data_structures/linked_list/doubly_linked_list_two.py b/data_structures/linked_list/doubly_linked_list_two.py index e993cc5a20af..a7f639a6e289 100644 --- a/data_structures/linked_list/doubly_linked_list_two.py +++ b/data_structures/linked_list/doubly_linked_list_two.py @@ -9,24 +9,20 @@ Delete operation is more efficient """ +from dataclasses import dataclass +from typing import Self, TypeVar -class Node: - def __init__(self, data: int, previous=None, next_node=None): - self.data = data - self.previous = previous - self.next = next_node +DataType = TypeVar("DataType") - def __str__(self) -> str: - return f"{self.data}" - - def get_data(self) -> int: - return self.data - def get_next(self): - return self.next +@dataclass +class Node[DataType]: + data: DataType + previous: Self | None = None + next: Self | None = None - def get_previous(self): - return self.previous + def __str__(self) -> str: + return f"{self.data}" class LinkedListIterator: @@ -40,30 +36,30 @@ def __next__(self): if not self.current: raise StopIteration else: - value = self.current.get_data() - self.current = self.current.get_next() + value = self.current.data + self.current = self.current.next return value +@dataclass class LinkedList: - def __init__(self): - self.head = None # First node in list - self.tail = None # Last node in list + head: Node | None = None # First node in list + tail: Node | None = None # Last node in list def __str__(self): current = self.head nodes = [] while current is not None: - nodes.append(current.get_data()) - current = current.get_next() + nodes.append(current.data) + current = current.next return " ".join(str(node) for node in nodes) - def __contains__(self, value: int): + def __contains__(self, value: DataType): current = self.head while current: - if current.get_data() == value: + if current.data == value: return True - current = current.get_next() + current = current.next return False def __iter__(self): @@ -71,12 +67,12 @@ def __iter__(self): def get_head_data(self): if self.head: - return self.head.get_data() + return self.head.data return None def get_tail_data(self): if self.tail: - return self.tail.get_data() + return self.tail.data return None def set_head(self, node: Node) -> None: @@ -87,12 +83,13 @@ def set_head(self, node: Node) -> None: self.insert_before_node(self.head, node) def set_tail(self, node: Node) -> None: - if self.head is None: - self.set_head(node) + if self.tail is None: + self.head = node + self.tail = node else: self.insert_after_node(self.tail, node) - def insert(self, value: int) -> None: + def insert(self, value: DataType) -> None: node = Node(value) if self.head is None: self.set_head(node) @@ -103,7 +100,7 @@ def insert_before_node(self, node: Node, node_to_insert: Node) -> None: node_to_insert.next = node node_to_insert.previous = node.previous - if node.get_previous() is None: + if node.previous is None: self.head = node_to_insert else: node.previous.next = node_to_insert @@ -114,14 +111,14 @@ def insert_after_node(self, node: Node, node_to_insert: Node) -> None: node_to_insert.previous = node node_to_insert.next = node.next - if node.get_next() is None: + if node.next is None: self.tail = node_to_insert else: node.next.previous = node_to_insert node.next = node_to_insert - def insert_at_position(self, position: int, value: int) -> None: + def insert_at_position(self, position: int, value: DataType) -> None: current_position = 1 new_node = Node(value) node = self.head @@ -131,32 +128,32 @@ def insert_at_position(self, position: int, value: int) -> None: return current_position += 1 node = node.next - self.insert_after_node(self.tail, new_node) + self.set_tail(new_node) - def get_node(self, item: int) -> Node: + def get_node(self, item: DataType) -> Node: node = self.head while node: - if node.get_data() == item: + if node.data == item: return node - node = node.get_next() + node = node.next raise Exception("Node not found") def delete_value(self, value): if (node := self.get_node(value)) is not None: if node == self.head: - self.head = self.head.get_next() + self.head = self.head.next if node == self.tail: - self.tail = self.tail.get_previous() + self.tail = self.tail.previous self.remove_node_pointers(node) @staticmethod def remove_node_pointers(node: Node) -> None: - if node.get_next(): + if node.next: node.next.previous = node.previous - if node.get_previous(): + if node.previous: node.previous.next = node.next node.next = None @@ -241,6 +238,22 @@ def create_linked_list() -> None: 7 8 9 + >>> linked_list = LinkedList() + >>> linked_list.insert_at_position(position=1, value=10) + >>> str(linked_list) + '10' + >>> linked_list.insert_at_position(position=2, value=20) + >>> str(linked_list) + '10 20' + >>> linked_list.insert_at_position(position=1, value=30) + >>> str(linked_list) + '30 10 20' + >>> linked_list.insert_at_position(position=3, value=40) + >>> str(linked_list) + '30 10 40 20' + >>> linked_list.insert_at_position(position=5, value=50) + >>> str(linked_list) + '30 10 40 20 50' """ diff --git a/data_structures/linked_list/from_sequence.py b/data_structures/linked_list/from_sequence.py index 94b44f15037f..fa43f4d10e08 100644 --- a/data_structures/linked_list/from_sequence.py +++ b/data_structures/linked_list/from_sequence.py @@ -1,4 +1,4 @@ -# Recursive Prorgam to create a Linked List from a sequence and +# Recursive Program to create a Linked List from a sequence and # print a string representation of it. diff --git a/data_structures/queue/__init__.py b/data_structures/queues/__init__.py similarity index 100% rename from data_structures/queue/__init__.py rename to data_structures/queues/__init__.py diff --git a/data_structures/queue/circular_queue.py b/data_structures/queues/circular_queue.py similarity index 87% rename from data_structures/queue/circular_queue.py rename to data_structures/queues/circular_queue.py index f2fb4c01e467..efbf1efdc42d 100644 --- a/data_structures/queue/circular_queue.py +++ b/data_structures/queues/circular_queue.py @@ -17,7 +17,9 @@ def __len__(self) -> int: >>> len(cq) 0 >>> cq.enqueue("A") # doctest: +ELLIPSIS - >> cq.array + ['A', None, None, None, None] >>> len(cq) 1 """ @@ -51,11 +53,13 @@ def enqueue(self, data): as an index. >>> cq = CircularQueue(5) >>> cq.enqueue("A") # doctest: +ELLIPSIS - >> (cq.size, cq.first()) (1, 'A') >>> cq.enqueue("B") # doctest: +ELLIPSIS - >> cq.array + ['A', 'B', None, None, None] >>> (cq.size, cq.first()) (2, 'A') """ diff --git a/data_structures/queue/circular_queue_linked_list.py b/data_structures/queues/circular_queue_linked_list.py similarity index 100% rename from data_structures/queue/circular_queue_linked_list.py rename to data_structures/queues/circular_queue_linked_list.py diff --git a/data_structures/queue/double_ended_queue.py b/data_structures/queues/double_ended_queue.py similarity index 100% rename from data_structures/queue/double_ended_queue.py rename to data_structures/queues/double_ended_queue.py diff --git a/data_structures/queue/linked_queue.py b/data_structures/queues/linked_queue.py similarity index 100% rename from data_structures/queue/linked_queue.py rename to data_structures/queues/linked_queue.py diff --git a/data_structures/queue/priority_queue_using_list.py b/data_structures/queues/priority_queue_using_list.py similarity index 96% rename from data_structures/queue/priority_queue_using_list.py rename to data_structures/queues/priority_queue_using_list.py index f61b5e8e664d..15e56c557069 100644 --- a/data_structures/queue/priority_queue_using_list.py +++ b/data_structures/queues/priority_queue_using_list.py @@ -59,12 +59,12 @@ class FixedPriorityQueue: >>> fpq.dequeue() Traceback (most recent call last): ... - data_structures.queue.priority_queue_using_list.UnderFlowError: All queues are empty + data_structures.queues.priority_queue_using_list.UnderFlowError: All queues are empty >>> print(fpq) Priority 0: [] Priority 1: [] Priority 2: [] - """ + """ # noqa: E501 def __init__(self): self.queues = [ @@ -141,7 +141,7 @@ class ElementPriorityQueue: >>> epq.dequeue() Traceback (most recent call last): ... - data_structures.queue.priority_queue_using_list.UnderFlowError: The queue is empty + data_structures.queues.priority_queue_using_list.UnderFlowError: The queue is empty >>> print(epq) [] """ diff --git a/data_structures/queue/queue_by_list.py b/data_structures/queues/queue_by_list.py similarity index 100% rename from data_structures/queue/queue_by_list.py rename to data_structures/queues/queue_by_list.py diff --git a/data_structures/queue/queue_by_two_stacks.py b/data_structures/queues/queue_by_two_stacks.py similarity index 100% rename from data_structures/queue/queue_by_two_stacks.py rename to data_structures/queues/queue_by_two_stacks.py diff --git a/data_structures/queue/queue_on_pseudo_stack.py b/data_structures/queues/queue_on_pseudo_stack.py similarity index 100% rename from data_structures/queue/queue_on_pseudo_stack.py rename to data_structures/queues/queue_on_pseudo_stack.py diff --git a/data_structures/stacks/largest_rectangle_histogram.py b/data_structures/stacks/largest_rectangle_histogram.py new file mode 100644 index 000000000000..7575bd9f628d --- /dev/null +++ b/data_structures/stacks/largest_rectangle_histogram.py @@ -0,0 +1,39 @@ +def largest_rectangle_area(heights: list[int]) -> int: + """ + Inputs an array of integers representing the heights of bars, + and returns the area of the largest rectangle that can be formed + + >>> largest_rectangle_area([2, 1, 5, 6, 2, 3]) + 10 + + >>> largest_rectangle_area([2, 4]) + 4 + + >>> largest_rectangle_area([6, 2, 5, 4, 5, 1, 6]) + 12 + + >>> largest_rectangle_area([1]) + 1 + """ + stack: list[int] = [] + max_area = 0 + heights = [*heights, 0] # make a new list by appending the sentinel 0 + n = len(heights) + + for i in range(n): + # make sure the stack remains in increasing order + while stack and heights[i] < heights[stack[-1]]: + h = heights[stack.pop()] # height of the bar + # if stack is empty, it means entire width can be taken from index 0 to i-1 + w = i if not stack else i - stack[-1] - 1 # calculate width + max_area = max(max_area, h * w) + + stack.append(i) + + return max_area + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/data_structures/stacks/prefix_evaluation.py b/data_structures/stacks/prefix_evaluation.py index f48eca23d7b5..03a70d884725 100644 --- a/data_structures/stacks/prefix_evaluation.py +++ b/data_structures/stacks/prefix_evaluation.py @@ -1,8 +1,9 @@ """ -Python3 program to evaluate a prefix expression. +Program to evaluate a prefix expression. +https://en.wikipedia.org/wiki/Polish_notation """ -calc = { +operators = { "+": lambda x, y: x + y, "-": lambda x, y: x - y, "*": lambda x, y: x * y, @@ -31,6 +32,10 @@ def evaluate(expression): 21 >>> evaluate("/ * 10 2 + 4 1 ") 4.0 + >>> evaluate("2") + 2 + >>> evaluate("+ * 2 3 / 8 4") + 8.0 """ stack = [] @@ -45,11 +50,39 @@ def evaluate(expression): # push the result onto the stack again o1 = stack.pop() o2 = stack.pop() - stack.append(calc[c](o1, o2)) + stack.append(operators[c](o1, o2)) return stack.pop() +def evaluate_recursive(expression: list[str]): + """ + Alternative recursive implementation + + >>> evaluate_recursive(['2']) + 2 + >>> expression = ['+', '*', '2', '3', '/', '8', '4'] + >>> evaluate_recursive(expression) + 8.0 + >>> expression + [] + >>> evaluate_recursive(['+', '9', '*', '2', '6']) + 21 + >>> evaluate_recursive(['/', '*', '10', '2', '+', '4', '1']) + 4.0 + """ + + op = expression.pop(0) + if is_operand(op): + return int(op) + + operation = operators[op] + + a = evaluate_recursive(expression) + b = evaluate_recursive(expression) + return operation(a, b) + + # Driver code if __name__ == "__main__": test_expression = "+ 9 * 2 6" diff --git a/data_structures/suffix_tree/tests/test_suffix_tree.py b/data_structures/suffix_tree/tests/test_suffix_tree.py index 45c6790ac48a..c9dbe199d19d 100644 --- a/data_structures/suffix_tree/tests/test_suffix_tree.py +++ b/data_structures/suffix_tree/tests/test_suffix_tree.py @@ -22,18 +22,18 @@ def test_search_existing_patterns(self) -> None: patterns = ["ana", "ban", "na"] for pattern in patterns: with self.subTest(pattern=pattern): - assert self.suffix_tree.search( - pattern - ), f"Pattern '{pattern}' should be found." + assert self.suffix_tree.search(pattern), ( + f"Pattern '{pattern}' should be found." + ) def test_search_non_existing_patterns(self) -> None: """Test searching for patterns that do not exist in the suffix tree.""" patterns = ["xyz", "apple", "cat"] for pattern in patterns: with self.subTest(pattern=pattern): - assert not self.suffix_tree.search( - pattern - ), f"Pattern '{pattern}' should not be found." + assert not self.suffix_tree.search(pattern), ( + f"Pattern '{pattern}' should not be found." + ) def test_search_empty_pattern(self) -> None: """Test searching for an empty pattern.""" @@ -41,18 +41,18 @@ def test_search_empty_pattern(self) -> None: def test_search_full_text(self) -> None: """Test searching for the full text.""" - assert self.suffix_tree.search( - self.text - ), "The full text should be found in the suffix tree." + assert self.suffix_tree.search(self.text), ( + "The full text should be found in the suffix tree." + ) def test_search_substrings(self) -> None: """Test searching for substrings of the full text.""" substrings = ["ban", "ana", "a", "na"] for substring in substrings: with self.subTest(substring=substring): - assert self.suffix_tree.search( - substring - ), f"Substring '{substring}' should be found." + assert self.suffix_tree.search(substring), ( + f"Substring '{substring}' should be found." + ) if __name__ == "__main__": diff --git a/divide_and_conquer/power.py b/divide_and_conquer/power.py index faf6a3476d40..492ee6dd12f0 100644 --- a/divide_and_conquer/power.py +++ b/divide_and_conquer/power.py @@ -1,4 +1,4 @@ -def actual_power(a: int, b: int): +def actual_power(a: int, b: int) -> int: """ Function using divide and conquer to calculate a^b. It only works for integer a,b. @@ -19,10 +19,12 @@ def actual_power(a: int, b: int): """ if b == 0: return 1 + half = actual_power(a, b // 2) + if (b % 2) == 0: - return actual_power(a, int(b / 2)) * actual_power(a, int(b / 2)) + return half * half else: - return a * actual_power(a, int(b / 2)) * actual_power(a, int(b / 2)) + return a * half * half def power(a: int, b: int) -> float: @@ -43,9 +45,9 @@ def power(a: int, b: int) -> float: -0.125 """ if b < 0: - return 1 / actual_power(a, b) + return 1 / actual_power(a, -b) return actual_power(a, b) if __name__ == "__main__": - print(power(-2, -3)) + print(power(-2, -3)) # output -0.125 diff --git a/dynamic_programming/all_construct.py b/dynamic_programming/all_construct.py index 5d585fc7fcec..ca00f2beb06a 100644 --- a/dynamic_programming/all_construct.py +++ b/dynamic_programming/all_construct.py @@ -8,9 +8,10 @@ def all_construct(target: str, word_bank: list[str] | None = None) -> list[list[str]]: """ - returns the list containing all the possible - combinations a string(target) can be constructed from - the given list of substrings(word_bank) + returns the list containing all the possible + combinations a string(`target`) can be constructed from + the given list of substrings(`word_bank`) + >>> all_construct("hello", ["he", "l", "o"]) [['he', 'l', 'l', 'o']] >>> all_construct("purple",["purp","p","ur","le","purpl"]) diff --git a/dynamic_programming/bitmask.py b/dynamic_programming/bitmask.py index a6e6a0cda7bf..4737a3419e8e 100644 --- a/dynamic_programming/bitmask.py +++ b/dynamic_programming/bitmask.py @@ -42,7 +42,7 @@ def count_ways_until(self, mask, task_no): return self.dp[mask][task_no] # Number of ways when we don't this task in the arrangement - total_ways_util = self.count_ways_until(mask, task_no + 1) + total_ways_until = self.count_ways_until(mask, task_no + 1) # now assign the tasks one by one to all possible persons and recursively # assign for the remaining tasks. @@ -54,10 +54,10 @@ def count_ways_until(self, mask, task_no): # assign this task to p and change the mask value. And recursively # assign tasks with the new mask value. - total_ways_util += self.count_ways_until(mask | (1 << p), task_no + 1) + total_ways_until += self.count_ways_until(mask | (1 << p), task_no + 1) # save the value. - self.dp[mask][task_no] = total_ways_util + self.dp[mask][task_no] = total_ways_until return self.dp[mask][task_no] diff --git a/dynamic_programming/climbing_stairs.py b/dynamic_programming/climbing_stairs.py index d6273d025f08..38bdb427eedc 100644 --- a/dynamic_programming/climbing_stairs.py +++ b/dynamic_programming/climbing_stairs.py @@ -25,9 +25,9 @@ def climb_stairs(number_of_steps: int) -> int: ... AssertionError: number_of_steps needs to be positive integer, your input -7 """ - assert ( - isinstance(number_of_steps, int) and number_of_steps > 0 - ), f"number_of_steps needs to be positive integer, your input {number_of_steps}" + assert isinstance(number_of_steps, int) and number_of_steps > 0, ( + f"number_of_steps needs to be positive integer, your input {number_of_steps}" + ) if number_of_steps == 1: return 1 previous, current = 1, 1 diff --git a/dynamic_programming/combination_sum_iv.py b/dynamic_programming/combination_sum_iv.py index 113c06a27a9e..ed8dcd88e6fd 100644 --- a/dynamic_programming/combination_sum_iv.py +++ b/dynamic_programming/combination_sum_iv.py @@ -1,24 +1,25 @@ """ Question: -You are given an array of distinct integers and you have to tell how many -different ways of selecting the elements from the array are there such that -the sum of chosen elements is equal to the target number tar. + You are given an array of distinct integers and you have to tell how many + different ways of selecting the elements from the array are there such that + the sum of chosen elements is equal to the target number tar. Example Input: -N = 3 -target = 5 -array = [1, 2, 5] + * N = 3 + * target = 5 + * array = [1, 2, 5] Output: -9 + 9 Approach: -The basic idea is to go over recursively to find the way such that the sum -of chosen elements is “tar”. For every element, we have two choices - 1. Include the element in our set of chosen elements. - 2. Don't include the element in our set of chosen elements. + The basic idea is to go over recursively to find the way such that the sum + of chosen elements is `target`. For every element, we have two choices + + 1. Include the element in our set of chosen elements. + 2. Don't include the element in our set of chosen elements. """ diff --git a/dynamic_programming/fizz_buzz.py b/dynamic_programming/fizz_buzz.py index e29116437a93..0cb48897875b 100644 --- a/dynamic_programming/fizz_buzz.py +++ b/dynamic_programming/fizz_buzz.py @@ -3,11 +3,12 @@ def fizz_buzz(number: int, iterations: int) -> str: """ - Plays FizzBuzz. - Prints Fizz if number is a multiple of 3. - Prints Buzz if its a multiple of 5. - Prints FizzBuzz if its a multiple of both 3 and 5 or 15. - Else Prints The Number Itself. + | Plays FizzBuzz. + | Prints Fizz if number is a multiple of ``3``. + | Prints Buzz if its a multiple of ``5``. + | Prints FizzBuzz if its a multiple of both ``3`` and ``5`` or ``15``. + | Else Prints The Number Itself. + >>> fizz_buzz(1,7) '1 2 Fizz 4 Buzz Fizz 7 ' >>> fizz_buzz(1,0) diff --git a/dynamic_programming/iterating_through_submasks.py b/dynamic_programming/iterating_through_submasks.py index 372dd2c74a71..efab6dacff3f 100644 --- a/dynamic_programming/iterating_through_submasks.py +++ b/dynamic_programming/iterating_through_submasks.py @@ -37,9 +37,9 @@ def list_of_submasks(mask: int) -> list[int]: """ - assert ( - isinstance(mask, int) and mask > 0 - ), f"mask needs to be positive integer, your input {mask}" + assert isinstance(mask, int) and mask > 0, ( + f"mask needs to be positive integer, your input {mask}" + ) """ first submask iterated will be mask itself then operation will be performed diff --git a/dynamic_programming/knapsack.py b/dynamic_programming/knapsack.py index 489b5ada450a..28c5b19dbe36 100644 --- a/dynamic_programming/knapsack.py +++ b/dynamic_programming/knapsack.py @@ -11,7 +11,7 @@ def mf_knapsack(i, wt, val, j): """ This code involves the concept of memory functions. Here we solve the subproblems which are needed unlike the below example - F is a 2D array with -1s filled up + F is a 2D array with ``-1`` s filled up """ global f # a global dp table for knapsack if f[i][j] < 0: @@ -45,22 +45,24 @@ def knapsack_with_example_solution(w: int, wt: list, val: list): the several possible optimal subsets. Parameters - --------- + ---------- - W: int, the total maximum weight for the given knapsack problem. - wt: list, the vector of weights for all items where wt[i] is the weight - of the i-th item. - val: list, the vector of values for all items where val[i] is the value - of the i-th item + * `w`: int, the total maximum weight for the given knapsack problem. + * `wt`: list, the vector of weights for all items where ``wt[i]`` is the weight + of the ``i``-th item. + * `val`: list, the vector of values for all items where ``val[i]`` is the value + of the ``i``-th item Returns ------- - optimal_val: float, the optimal value for the given knapsack problem - example_optional_set: set, the indices of one of the optimal subsets - which gave rise to the optimal value. + + * `optimal_val`: float, the optimal value for the given knapsack problem + * `example_optional_set`: set, the indices of one of the optimal subsets + which gave rise to the optimal value. Examples - ------- + -------- + >>> knapsack_with_example_solution(10, [1, 3, 5, 2], [10, 20, 100, 22]) (142, {2, 3, 4}) >>> knapsack_with_example_solution(6, [4, 3, 2, 3], [3, 2, 4, 4]) @@ -104,19 +106,19 @@ def _construct_solution(dp: list, wt: list, i: int, j: int, optimal_set: set): a filled DP table and the vector of weights Parameters - --------- - - dp: list of list, the table of a solved integer weight dynamic programming problem + ---------- - wt: list or tuple, the vector of weights of the items - i: int, the index of the item under consideration - j: int, the current possible maximum weight - optimal_set: set, the optimal subset so far. This gets modified by the function. + * `dp`: list of list, the table of a solved integer weight dynamic programming + problem + * `wt`: list or tuple, the vector of weights of the items + * `i`: int, the index of the item under consideration + * `j`: int, the current possible maximum weight + * `optimal_set`: set, the optimal subset so far. This gets modified by the function. Returns ------- - None + ``None`` """ # for the current item i at a maximum weight j to be part of an optimal subset, # the optimal value at (i, j) must be greater than the optimal value at (i-1, j). diff --git a/dynamic_programming/longest_common_substring.py b/dynamic_programming/longest_common_substring.py index e2f944a5e336..ea5233eb2d17 100644 --- a/dynamic_programming/longest_common_substring.py +++ b/dynamic_programming/longest_common_substring.py @@ -1,15 +1,19 @@ """ -Longest Common Substring Problem Statement: Given two sequences, find the -longest common substring present in both of them. A substring is -necessarily continuous. -Example: "abcdef" and "xabded" have two longest common substrings, "ab" or "de". -Therefore, algorithm should return any one of them. +Longest Common Substring Problem Statement: + Given two sequences, find the + longest common substring present in both of them. A substring is + necessarily continuous. + +Example: + ``abcdef`` and ``xabded`` have two longest common substrings, ``ab`` or ``de``. + Therefore, algorithm should return any one of them. """ def longest_common_substring(text1: str, text2: str) -> str: """ Finds the longest common substring between two strings. + >>> longest_common_substring("", "") '' >>> longest_common_substring("a","") diff --git a/dynamic_programming/longest_increasing_subsequence.py b/dynamic_programming/longest_increasing_subsequence.py index 2a78e2e7ad1d..1863a882c41e 100644 --- a/dynamic_programming/longest_increasing_subsequence.py +++ b/dynamic_programming/longest_increasing_subsequence.py @@ -4,11 +4,13 @@ This is a pure Python implementation of Dynamic Programming solution to the longest increasing subsequence of a given sequence. -The problem is : -Given an array, to find the longest and increasing sub-array in that given array and -return it. -Example: [10, 22, 9, 33, 21, 50, 41, 60, 80] as input will return - [10, 22, 33, 41, 60, 80] as output +The problem is: + Given an array, to find the longest and increasing sub-array in that given array and + return it. + +Example: + ``[10, 22, 9, 33, 21, 50, 41, 60, 80]`` as input will return + ``[10, 22, 33, 41, 60, 80]`` as output """ from __future__ import annotations @@ -17,12 +19,15 @@ def longest_subsequence(array: list[int]) -> list[int]: # This function is recursive """ Some examples + >>> longest_subsequence([10, 22, 9, 33, 21, 50, 41, 60, 80]) [10, 22, 33, 41, 60, 80] >>> longest_subsequence([4, 8, 7, 5, 1, 12, 2, 3, 9]) [1, 2, 3, 9] + >>> longest_subsequence([28, 26, 12, 23, 35, 39]) + [12, 23, 35, 39] >>> longest_subsequence([9, 8, 7, 6, 5, 7]) - [8] + [5, 7] >>> longest_subsequence([1, 1, 1]) [1, 1, 1] >>> longest_subsequence([]) @@ -41,7 +46,7 @@ def longest_subsequence(array: list[int]) -> list[int]: # This function is recu while not is_found and i < array_length: if array[i] < pivot: is_found = True - temp_array = [element for element in array[i:] if element >= array[i]] + temp_array = array[i:] temp_array = longest_subsequence(temp_array) if len(temp_array) > len(longest_subseq): longest_subseq = temp_array diff --git a/dynamic_programming/longest_increasing_subsequence_iterative.py b/dynamic_programming/longest_increasing_subsequence_iterative.py new file mode 100644 index 000000000000..665c86a35d2e --- /dev/null +++ b/dynamic_programming/longest_increasing_subsequence_iterative.py @@ -0,0 +1,72 @@ +""" +Author : Sanjay Muthu + +This is a pure Python implementation of Dynamic Programming solution to the longest +increasing subsequence of a given sequence. + +The problem is: + Given an array, to find the longest and increasing sub-array in that given array and + return it. + +Example: + ``[10, 22, 9, 33, 21, 50, 41, 60, 80]`` as input will return + ``[10, 22, 33, 50, 60, 80]`` as output +""" + +from __future__ import annotations + +import copy + + +def longest_subsequence(array: list[int]) -> list[int]: + """ + Some examples + + >>> longest_subsequence([10, 22, 9, 33, 21, 50, 41, 60, 80]) + [10, 22, 33, 50, 60, 80] + >>> longest_subsequence([4, 8, 7, 5, 1, 12, 2, 3, 9]) + [1, 2, 3, 9] + >>> longest_subsequence([9, 8, 7, 6, 5, 7]) + [7, 7] + >>> longest_subsequence([28, 26, 12, 23, 35, 39]) + [12, 23, 35, 39] + >>> longest_subsequence([1, 1, 1]) + [1, 1, 1] + >>> longest_subsequence([]) + [] + """ + n = len(array) + # The longest increasing subsequence ending at array[i] + longest_increasing_subsequence = [] + for i in range(n): + longest_increasing_subsequence.append([array[i]]) + + for i in range(1, n): + for prev in range(i): + # If array[prev] is less than or equal to array[i], then + # longest_increasing_subsequence[prev] + array[i] + # is a valid increasing subsequence + + # longest_increasing_subsequence[i] is only set to + # longest_increasing_subsequence[prev] + array[i] if the length is longer. + + if array[prev] <= array[i] and len( + longest_increasing_subsequence[prev] + ) + 1 > len(longest_increasing_subsequence[i]): + longest_increasing_subsequence[i] = copy.copy( + longest_increasing_subsequence[prev] + ) + longest_increasing_subsequence[i].append(array[i]) + + result: list[int] = [] + for i in range(n): + if len(longest_increasing_subsequence[i]) > len(result): + result = longest_increasing_subsequence[i] + + return result + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/dynamic_programming/matrix_chain_multiplication.py b/dynamic_programming/matrix_chain_multiplication.py index da6e525ce816..4c0c771f9092 100644 --- a/dynamic_programming/matrix_chain_multiplication.py +++ b/dynamic_programming/matrix_chain_multiplication.py @@ -1,42 +1,48 @@ """ -Find the minimum number of multiplications needed to multiply chain of matrices. -Reference: https://www.geeksforgeeks.org/matrix-chain-multiplication-dp-8/ +| Find the minimum number of multiplications needed to multiply chain of matrices. +| Reference: https://www.geeksforgeeks.org/matrix-chain-multiplication-dp-8/ -The algorithm has interesting real-world applications. Example: -1. Image transformations in Computer Graphics as images are composed of matrix. -2. Solve complex polynomial equations in the field of algebra using least processing - power. -3. Calculate overall impact of macroeconomic decisions as economic equations involve a - number of variables. -4. Self-driving car navigation can be made more accurate as matrix multiplication can - accurately determine position and orientation of obstacles in short time. +The algorithm has interesting real-world applications. -Python doctests can be run with the following command: -python -m doctest -v matrix_chain_multiply.py +Example: + 1. Image transformations in Computer Graphics as images are composed of matrix. + 2. Solve complex polynomial equations in the field of algebra using least processing + power. + 3. Calculate overall impact of macroeconomic decisions as economic equations involve a + number of variables. + 4. Self-driving car navigation can be made more accurate as matrix multiplication can + accurately determine position and orientation of obstacles in short time. -Given a sequence arr[] that represents chain of 2D matrices such that the dimension of -the ith matrix is arr[i-1]*arr[i]. -So suppose arr = [40, 20, 30, 10, 30] means we have 4 matrices of dimensions -40*20, 20*30, 30*10 and 10*30. +Python doctests can be run with the following command:: -matrix_chain_multiply() returns an integer denoting minimum number of multiplications to -multiply the chain. + python -m doctest -v matrix_chain_multiply.py + +Given a sequence ``arr[]`` that represents chain of 2D matrices such that the dimension +of the ``i`` th matrix is ``arr[i-1]*arr[i]``. +So suppose ``arr = [40, 20, 30, 10, 30]`` means we have ``4`` matrices of dimensions +``40*20``, ``20*30``, ``30*10`` and ``10*30``. + +``matrix_chain_multiply()`` returns an integer denoting minimum number of +multiplications to multiply the chain. We do not need to perform actual multiplication here. We only need to decide the order in which to perform the multiplication. Hints: -1. Number of multiplications (ie cost) to multiply 2 matrices -of size m*p and p*n is m*p*n. -2. Cost of matrix multiplication is associative ie (M1*M2)*M3 != M1*(M2*M3) -3. Matrix multiplication is not commutative. So, M1*M2 does not mean M2*M1 can be done. -4. To determine the required order, we can try different combinations. + 1. Number of multiplications (ie cost) to multiply ``2`` matrices + of size ``m*p`` and ``p*n`` is ``m*p*n``. + 2. Cost of matrix multiplication is not associative ie ``(M1*M2)*M3 != M1*(M2*M3)`` + 3. Matrix multiplication is not commutative. So, ``M1*M2`` does not mean ``M2*M1`` + can be done. + 4. To determine the required order, we can try different combinations. + So, this problem has overlapping sub-problems and can be solved using recursion. We use Dynamic Programming for optimal time complexity. Example input: -arr = [40, 20, 30, 10, 30] -output: 26000 + ``arr = [40, 20, 30, 10, 30]`` +output: + ``26000`` """ from collections.abc import Iterator @@ -50,25 +56,25 @@ def matrix_chain_multiply(arr: list[int]) -> int: Find the minimum number of multiplcations required to multiply the chain of matrices Args: - arr: The input array of integers. + `arr`: The input array of integers. Returns: Minimum number of multiplications needed to multiply the chain Examples: - >>> matrix_chain_multiply([1, 2, 3, 4, 3]) - 30 - >>> matrix_chain_multiply([10]) - 0 - >>> matrix_chain_multiply([10, 20]) - 0 - >>> matrix_chain_multiply([19, 2, 19]) - 722 - >>> matrix_chain_multiply(list(range(1, 100))) - 323398 - - # >>> matrix_chain_multiply(list(range(1, 251))) - # 2626798 + + >>> matrix_chain_multiply([1, 2, 3, 4, 3]) + 30 + >>> matrix_chain_multiply([10]) + 0 + >>> matrix_chain_multiply([10, 20]) + 0 + >>> matrix_chain_multiply([19, 2, 19]) + 722 + >>> matrix_chain_multiply(list(range(1, 100))) + 323398 + >>> # matrix_chain_multiply(list(range(1, 251))) + # 2626798 """ if len(arr) < 2: return 0 @@ -93,8 +99,10 @@ def matrix_chain_multiply(arr: list[int]) -> int: def matrix_chain_order(dims: list[int]) -> int: """ Source: https://en.wikipedia.org/wiki/Matrix_chain_multiplication + The dynamic programming solution is faster than cached the recursive solution and can handle larger inputs. + >>> matrix_chain_order([1, 2, 3, 4, 3]) 30 >>> matrix_chain_order([10]) @@ -105,8 +113,7 @@ def matrix_chain_order(dims: list[int]) -> int: 722 >>> matrix_chain_order(list(range(1, 100))) 323398 - - # >>> matrix_chain_order(list(range(1, 251))) # Max before RecursionError is raised + >>> # matrix_chain_order(list(range(1, 251))) # Max before RecursionError is raised # 2626798 """ @@ -127,7 +134,7 @@ def elapsed_time(msg: str) -> Iterator: start = perf_counter_ns() yield - print(f"Finished: {msg} in {(perf_counter_ns() - start) / 10 ** 9} seconds.") + print(f"Finished: {msg} in {(perf_counter_ns() - start) / 10**9} seconds.") if __name__ == "__main__": diff --git a/dynamic_programming/max_product_subarray.py b/dynamic_programming/max_product_subarray.py index 425859bc03e3..6f4f38e38942 100644 --- a/dynamic_programming/max_product_subarray.py +++ b/dynamic_programming/max_product_subarray.py @@ -1,9 +1,10 @@ def max_product_subarray(numbers: list[int]) -> int: """ Returns the maximum product that can be obtained by multiplying a - contiguous subarray of the given integer list `nums`. + contiguous subarray of the given integer list `numbers`. Example: + >>> max_product_subarray([2, 3, -2, 4]) 6 >>> max_product_subarray((-2, 0, -1)) diff --git a/dynamic_programming/minimum_squares_to_represent_a_number.py b/dynamic_programming/minimum_squares_to_represent_a_number.py index bf5849f5bcb3..98c0602fa831 100644 --- a/dynamic_programming/minimum_squares_to_represent_a_number.py +++ b/dynamic_programming/minimum_squares_to_represent_a_number.py @@ -5,6 +5,7 @@ def minimum_squares_to_represent_a_number(number: int) -> int: """ Count the number of minimum squares to represent a number + >>> minimum_squares_to_represent_a_number(25) 1 >>> minimum_squares_to_represent_a_number(37) diff --git a/dynamic_programming/range_sum_query.py b/dynamic_programming/range_sum_query.py new file mode 100644 index 000000000000..484fcf785fda --- /dev/null +++ b/dynamic_programming/range_sum_query.py @@ -0,0 +1,92 @@ +""" +Author: Sanjay Muthu + +This is an implementation of the Dynamic Programming solution to the Range Sum Query. + +The problem statement is: + Given an array and q queries, + each query stating you to find the sum of elements from l to r (inclusive) + +Example: + arr = [1, 4, 6, 2, 61, 12] + queries = 3 + l_1 = 2, r_1 = 5 + l_2 = 1, r_2 = 5 + l_3 = 3, r_3 = 4 + + as input will return + + [81, 85, 63] + + as output + +0-indexing: +NOTE: 0-indexing means the indexing of the array starts from 0 +Example: a = [1, 2, 3, 4, 5, 6] + Here, the 0th index of a is 1, + the 1st index of a is 2, + and so forth + +Time Complexity: O(N + Q) +* O(N) pre-calculation time to calculate the prefix sum array +* and O(1) time per each query = O(1 * Q) = O(Q) time + +Space Complexity: O(N) +* O(N) to store the prefix sum + +Algorithm: +So, first we calculate the prefix sum (dp) of the array. +The prefix sum of the index i is the sum of all elements indexed +from 0 to i (inclusive). +The prefix sum of the index i is the prefix sum of index (i - 1) + the current element. +So, the state of the dp is dp[i] = dp[i - 1] + a[i]. + +After we calculate the prefix sum, +for each query [l, r] +the answer is dp[r] - dp[l - 1] (we need to be careful because l might be 0). +For example take this array: + [4, 2, 1, 6, 3] +The prefix sum calculated for this array would be: + [4, 4 + 2, 4 + 2 + 1, 4 + 2 + 1 + 6, 4 + 2 + 1 + 6 + 3] + ==> [4, 6, 7, 13, 16] +If the query was l = 3, r = 4, +the answer would be 6 + 3 = 9 but this would require O(r - l + 1) time ≈ O(N) time + +If we use prefix sums we can find it in O(1) by using the formula +prefix[r] - prefix[l - 1]. +This formula works because prefix[r] is the sum of elements from [0, r] +and prefix[l - 1] is the sum of elements from [0, l - 1], +so if we do prefix[r] - prefix[l - 1] it will be +[0, r] - [0, l - 1] = [0, l - 1] + [l, r] - [0, l - 1] = [l, r] +""" + + +def prefix_sum(array: list[int], queries: list[tuple[int, int]]) -> list[int]: + """ + >>> prefix_sum([1, 4, 6, 2, 61, 12], [(2, 5), (1, 5), (3, 4)]) + [81, 85, 63] + >>> prefix_sum([4, 2, 1, 6, 3], [(3, 4), (1, 3), (0, 2)]) + [9, 9, 7] + """ + # The prefix sum array + dp = [0] * len(array) + dp[0] = array[0] + for i in range(1, len(array)): + dp[i] = dp[i - 1] + array[i] + + # See Algorithm section (Line 44) + result = [] + for query in queries: + left, right = query + res = dp[right] + if left > 0: + res -= dp[left - 1] + result.append(res) + + return result + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/dynamic_programming/regex_match.py b/dynamic_programming/regex_match.py index 200a882831c0..e94d82093c8b 100644 --- a/dynamic_programming/regex_match.py +++ b/dynamic_programming/regex_match.py @@ -1,23 +1,25 @@ """ Regex matching check if a text matches pattern or not. Pattern: - '.' Matches any single character. - '*' Matches zero or more of the preceding element. + + 1. ``.`` Matches any single character. + 2. ``*`` Matches zero or more of the preceding element. + More info: https://medium.com/trick-the-interviwer/regular-expression-matching-9972eb74c03 """ def recursive_match(text: str, pattern: str) -> bool: - """ + r""" Recursive matching algorithm. - Time complexity: O(2 ^ (|text| + |pattern|)) - Space complexity: Recursion depth is O(|text| + |pattern|). + | Time complexity: O(2^(\|text\| + \|pattern\|)) + | Space complexity: Recursion depth is O(\|text\| + \|pattern\|). :param text: Text to match. :param pattern: Pattern to match. - :return: True if text matches pattern, False otherwise. + :return: ``True`` if `text` matches `pattern`, ``False`` otherwise. >>> recursive_match('abc', 'a.c') True @@ -48,15 +50,15 @@ def recursive_match(text: str, pattern: str) -> bool: def dp_match(text: str, pattern: str) -> bool: - """ + r""" Dynamic programming matching algorithm. - Time complexity: O(|text| * |pattern|) - Space complexity: O(|text| * |pattern|) + | Time complexity: O(\|text\| * \|pattern\|) + | Space complexity: O(\|text\| * \|pattern\|) :param text: Text to match. :param pattern: Pattern to match. - :return: True if text matches pattern, False otherwise. + :return: ``True`` if `text` matches `pattern`, ``False`` otherwise. >>> dp_match('abc', 'a.c') True diff --git a/dynamic_programming/rod_cutting.py b/dynamic_programming/rod_cutting.py index f80fa440ae86..d12c759dc928 100644 --- a/dynamic_programming/rod_cutting.py +++ b/dynamic_programming/rod_cutting.py @@ -1,7 +1,7 @@ """ This module provides two implementations for the rod-cutting problem: -1. A naive recursive implementation which has an exponential runtime -2. Two dynamic programming implementations which have quadratic runtime + 1. A naive recursive implementation which has an exponential runtime + 2. Two dynamic programming implementations which have quadratic runtime The rod-cutting problem is the problem of finding the maximum possible revenue obtainable from a rod of length ``n`` given a list of prices for each integral piece @@ -20,18 +20,21 @@ def naive_cut_rod_recursive(n: int, prices: list): Runtime: O(2^n) Arguments - ------- - n: int, the length of the rod - prices: list, the prices for each piece of rod. ``p[i-i]`` is the - price for a rod of length ``i`` + --------- + + * `n`: int, the length of the rod + * `prices`: list, the prices for each piece of rod. ``p[i-i]`` is the + price for a rod of length ``i`` Returns ------- - The maximum revenue obtainable for a rod of length n given the list of prices + + The maximum revenue obtainable for a rod of length `n` given the list of prices for each piece. Examples -------- + >>> naive_cut_rod_recursive(4, [1, 5, 8, 9]) 10 >>> naive_cut_rod_recursive(10, [1, 5, 8, 9, 10, 17, 17, 20, 24, 30]) @@ -54,28 +57,30 @@ def top_down_cut_rod(n: int, prices: list): """ Constructs a top-down dynamic programming solution for the rod-cutting problem via memoization. This function serves as a wrapper for - _top_down_cut_rod_recursive + ``_top_down_cut_rod_recursive`` Runtime: O(n^2) Arguments - -------- - n: int, the length of the rod - prices: list, the prices for each piece of rod. ``p[i-i]`` is the - price for a rod of length ``i`` + --------- - Note - ---- - For convenience and because Python's lists using 0-indexing, length(max_rev) = - n + 1, to accommodate for the revenue obtainable from a rod of length 0. + * `n`: int, the length of the rod + * `prices`: list, the prices for each piece of rod. ``p[i-i]`` is the + price for a rod of length ``i`` + + .. note:: + For convenience and because Python's lists using ``0``-indexing, ``length(max_rev) + = n + 1``, to accommodate for the revenue obtainable from a rod of length ``0``. Returns ------- - The maximum revenue obtainable for a rod of length n given the list of prices + + The maximum revenue obtainable for a rod of length `n` given the list of prices for each piece. Examples - ------- + -------- + >>> top_down_cut_rod(4, [1, 5, 8, 9]) 10 >>> top_down_cut_rod(10, [1, 5, 8, 9, 10, 17, 17, 20, 24, 30]) @@ -94,16 +99,18 @@ def _top_down_cut_rod_recursive(n: int, prices: list, max_rev: list): Runtime: O(n^2) Arguments - -------- - n: int, the length of the rod - prices: list, the prices for each piece of rod. ``p[i-i]`` is the - price for a rod of length ``i`` - max_rev: list, the computed maximum revenue for a piece of rod. - ``max_rev[i]`` is the maximum revenue obtainable for a rod of length ``i`` + --------- + + * `n`: int, the length of the rod + * `prices`: list, the prices for each piece of rod. ``p[i-i]`` is the + price for a rod of length ``i`` + * `max_rev`: list, the computed maximum revenue for a piece of rod. + ``max_rev[i]`` is the maximum revenue obtainable for a rod of length ``i`` Returns ------- - The maximum revenue obtainable for a rod of length n given the list of prices + + The maximum revenue obtainable for a rod of length `n` given the list of prices for each piece. """ if max_rev[n] >= 0: @@ -130,18 +137,21 @@ def bottom_up_cut_rod(n: int, prices: list): Runtime: O(n^2) Arguments - ---------- - n: int, the maximum length of the rod. - prices: list, the prices for each piece of rod. ``p[i-i]`` is the - price for a rod of length ``i`` + --------- + + * `n`: int, the maximum length of the rod. + * `prices`: list, the prices for each piece of rod. ``p[i-i]`` is the + price for a rod of length ``i`` Returns ------- - The maximum revenue obtainable from cutting a rod of length n given + + The maximum revenue obtainable from cutting a rod of length `n` given the prices for each piece of rod p. Examples - ------- + -------- + >>> bottom_up_cut_rod(4, [1, 5, 8, 9]) 10 >>> bottom_up_cut_rod(10, [1, 5, 8, 9, 10, 17, 17, 20, 24, 30]) @@ -168,13 +178,12 @@ def _enforce_args(n: int, prices: list): """ Basic checks on the arguments to the rod-cutting algorithms - n: int, the length of the rod - prices: list, the price list for each piece of rod. - - Throws ValueError: + * `n`: int, the length of the rod + * `prices`: list, the price list for each piece of rod. - if n is negative or there are fewer items in the price list than the length of - the rod + Throws ``ValueError``: + if `n` is negative or there are fewer items in the price list than the length of + the rod """ if n < 0: msg = f"n must be greater than or equal to 0. Got n = {n}" diff --git a/dynamic_programming/subset_generation.py b/dynamic_programming/subset_generation.py index d490bca737ba..08daaac6f88a 100644 --- a/dynamic_programming/subset_generation.py +++ b/dynamic_programming/subset_generation.py @@ -1,38 +1,41 @@ def subset_combinations(elements: list[int], n: int) -> list: """ Compute n-element combinations from a given list using dynamic programming. + Args: - elements: The list of elements from which combinations will be generated. - n: The number of elements in each combination. + * `elements`: The list of elements from which combinations will be generated. + * `n`: The number of elements in each combination. + Returns: - A list of tuples, each representing a combination of n elements. - >>> subset_combinations(elements=[10, 20, 30, 40], n=2) - [(10, 20), (10, 30), (10, 40), (20, 30), (20, 40), (30, 40)] - >>> subset_combinations(elements=[1, 2, 3], n=1) - [(1,), (2,), (3,)] - >>> subset_combinations(elements=[1, 2, 3], n=3) - [(1, 2, 3)] - >>> subset_combinations(elements=[42], n=1) - [(42,)] - >>> subset_combinations(elements=[6, 7, 8, 9], n=4) - [(6, 7, 8, 9)] - >>> subset_combinations(elements=[10, 20, 30, 40, 50], n=0) - [()] - >>> subset_combinations(elements=[1, 2, 3, 4], n=2) - [(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)] - >>> subset_combinations(elements=[1, 'apple', 3.14], n=2) - [(1, 'apple'), (1, 3.14), ('apple', 3.14)] - >>> subset_combinations(elements=['single'], n=0) - [()] - >>> subset_combinations(elements=[], n=9) - [] - >>> from itertools import combinations - >>> all(subset_combinations(items, n) == list(combinations(items, n)) - ... for items, n in ( - ... ([10, 20, 30, 40], 2), ([1, 2, 3], 1), ([1, 2, 3], 3), ([42], 1), - ... ([6, 7, 8, 9], 4), ([10, 20, 30, 40, 50], 1), ([1, 2, 3, 4], 2), - ... ([1, 'apple', 3.14], 2), (['single'], 0), ([], 9))) - True + A list of tuples, each representing a combination of `n` elements. + + >>> subset_combinations(elements=[10, 20, 30, 40], n=2) + [(10, 20), (10, 30), (10, 40), (20, 30), (20, 40), (30, 40)] + >>> subset_combinations(elements=[1, 2, 3], n=1) + [(1,), (2,), (3,)] + >>> subset_combinations(elements=[1, 2, 3], n=3) + [(1, 2, 3)] + >>> subset_combinations(elements=[42], n=1) + [(42,)] + >>> subset_combinations(elements=[6, 7, 8, 9], n=4) + [(6, 7, 8, 9)] + >>> subset_combinations(elements=[10, 20, 30, 40, 50], n=0) + [()] + >>> subset_combinations(elements=[1, 2, 3, 4], n=2) + [(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)] + >>> subset_combinations(elements=[1, 'apple', 3.14], n=2) + [(1, 'apple'), (1, 3.14), ('apple', 3.14)] + >>> subset_combinations(elements=['single'], n=0) + [()] + >>> subset_combinations(elements=[], n=9) + [] + >>> from itertools import combinations + >>> all(subset_combinations(items, n) == list(combinations(items, n)) + ... for items, n in ( + ... ([10, 20, 30, 40], 2), ([1, 2, 3], 1), ([1, 2, 3], 3), ([42], 1), + ... ([6, 7, 8, 9], 4), ([10, 20, 30, 40, 50], 1), ([1, 2, 3, 4], 2), + ... ([1, 'apple', 3.14], 2), (['single'], 0), ([], 9))) + True """ r = len(elements) if n > r: diff --git a/dynamic_programming/viterbi.py b/dynamic_programming/viterbi.py index 764d45dc2c05..5b78fa9e46d0 100644 --- a/dynamic_programming/viterbi.py +++ b/dynamic_programming/viterbi.py @@ -9,119 +9,102 @@ def viterbi( emission_probabilities: dict, ) -> list: """ - Viterbi Algorithm, to find the most likely path of - states from the start and the expected output. - https://en.wikipedia.org/wiki/Viterbi_algorithm - sdafads - Wikipedia example - >>> observations = ["normal", "cold", "dizzy"] - >>> states = ["Healthy", "Fever"] - >>> start_p = {"Healthy": 0.6, "Fever": 0.4} - >>> trans_p = { - ... "Healthy": {"Healthy": 0.7, "Fever": 0.3}, - ... "Fever": {"Healthy": 0.4, "Fever": 0.6}, - ... } - >>> emit_p = { - ... "Healthy": {"normal": 0.5, "cold": 0.4, "dizzy": 0.1}, - ... "Fever": {"normal": 0.1, "cold": 0.3, "dizzy": 0.6}, - ... } - >>> viterbi(observations, states, start_p, trans_p, emit_p) - ['Healthy', 'Healthy', 'Fever'] + Viterbi Algorithm, to find the most likely path of + states from the start and the expected output. - >>> viterbi((), states, start_p, trans_p, emit_p) - Traceback (most recent call last): - ... - ValueError: There's an empty parameter - - >>> viterbi(observations, (), start_p, trans_p, emit_p) - Traceback (most recent call last): - ... - ValueError: There's an empty parameter - - >>> viterbi(observations, states, {}, trans_p, emit_p) - Traceback (most recent call last): - ... - ValueError: There's an empty parameter - - >>> viterbi(observations, states, start_p, {}, emit_p) - Traceback (most recent call last): - ... - ValueError: There's an empty parameter - - >>> viterbi(observations, states, start_p, trans_p, {}) - Traceback (most recent call last): - ... - ValueError: There's an empty parameter - - >>> viterbi("invalid", states, start_p, trans_p, emit_p) - Traceback (most recent call last): - ... - ValueError: observations_space must be a list + https://en.wikipedia.org/wiki/Viterbi_algorithm - >>> viterbi(["valid", 123], states, start_p, trans_p, emit_p) - Traceback (most recent call last): - ... - ValueError: observations_space must be a list of strings - - >>> viterbi(observations, "invalid", start_p, trans_p, emit_p) - Traceback (most recent call last): - ... - ValueError: states_space must be a list - - >>> viterbi(observations, ["valid", 123], start_p, trans_p, emit_p) - Traceback (most recent call last): - ... - ValueError: states_space must be a list of strings - - >>> viterbi(observations, states, "invalid", trans_p, emit_p) - Traceback (most recent call last): - ... - ValueError: initial_probabilities must be a dict - - >>> viterbi(observations, states, {2:2}, trans_p, emit_p) - Traceback (most recent call last): - ... - ValueError: initial_probabilities all keys must be strings - - >>> viterbi(observations, states, {"a":2}, trans_p, emit_p) - Traceback (most recent call last): - ... - ValueError: initial_probabilities all values must be float + Wikipedia example - >>> viterbi(observations, states, start_p, "invalid", emit_p) - Traceback (most recent call last): - ... - ValueError: transition_probabilities must be a dict - - >>> viterbi(observations, states, start_p, {"a":2}, emit_p) - Traceback (most recent call last): - ... - ValueError: transition_probabilities all values must be dict - - >>> viterbi(observations, states, start_p, {2:{2:2}}, emit_p) - Traceback (most recent call last): - ... - ValueError: transition_probabilities all keys must be strings - - >>> viterbi(observations, states, start_p, {"a":{2:2}}, emit_p) - Traceback (most recent call last): - ... - ValueError: transition_probabilities all keys must be strings - - >>> viterbi(observations, states, start_p, {"a":{"b":2}}, emit_p) - Traceback (most recent call last): - ... - ValueError: transition_probabilities nested dictionary all values must be float - - >>> viterbi(observations, states, start_p, trans_p, "invalid") - Traceback (most recent call last): - ... - ValueError: emission_probabilities must be a dict - - >>> viterbi(observations, states, start_p, trans_p, None) - Traceback (most recent call last): - ... - ValueError: There's an empty parameter + >>> observations = ["normal", "cold", "dizzy"] + >>> states = ["Healthy", "Fever"] + >>> start_p = {"Healthy": 0.6, "Fever": 0.4} + >>> trans_p = { + ... "Healthy": {"Healthy": 0.7, "Fever": 0.3}, + ... "Fever": {"Healthy": 0.4, "Fever": 0.6}, + ... } + >>> emit_p = { + ... "Healthy": {"normal": 0.5, "cold": 0.4, "dizzy": 0.1}, + ... "Fever": {"normal": 0.1, "cold": 0.3, "dizzy": 0.6}, + ... } + >>> viterbi(observations, states, start_p, trans_p, emit_p) + ['Healthy', 'Healthy', 'Fever'] + >>> viterbi((), states, start_p, trans_p, emit_p) + Traceback (most recent call last): + ... + ValueError: There's an empty parameter + >>> viterbi(observations, (), start_p, trans_p, emit_p) + Traceback (most recent call last): + ... + ValueError: There's an empty parameter + >>> viterbi(observations, states, {}, trans_p, emit_p) + Traceback (most recent call last): + ... + ValueError: There's an empty parameter + >>> viterbi(observations, states, start_p, {}, emit_p) + Traceback (most recent call last): + ... + ValueError: There's an empty parameter + >>> viterbi(observations, states, start_p, trans_p, {}) + Traceback (most recent call last): + ... + ValueError: There's an empty parameter + >>> viterbi("invalid", states, start_p, trans_p, emit_p) + Traceback (most recent call last): + ... + ValueError: observations_space must be a list + >>> viterbi(["valid", 123], states, start_p, trans_p, emit_p) + Traceback (most recent call last): + ... + ValueError: observations_space must be a list of strings + >>> viterbi(observations, "invalid", start_p, trans_p, emit_p) + Traceback (most recent call last): + ... + ValueError: states_space must be a list + >>> viterbi(observations, ["valid", 123], start_p, trans_p, emit_p) + Traceback (most recent call last): + ... + ValueError: states_space must be a list of strings + >>> viterbi(observations, states, "invalid", trans_p, emit_p) + Traceback (most recent call last): + ... + ValueError: initial_probabilities must be a dict + >>> viterbi(observations, states, {2:2}, trans_p, emit_p) + Traceback (most recent call last): + ... + ValueError: initial_probabilities all keys must be strings + >>> viterbi(observations, states, {"a":2}, trans_p, emit_p) + Traceback (most recent call last): + ... + ValueError: initial_probabilities all values must be float + >>> viterbi(observations, states, start_p, "invalid", emit_p) + Traceback (most recent call last): + ... + ValueError: transition_probabilities must be a dict + >>> viterbi(observations, states, start_p, {"a":2}, emit_p) + Traceback (most recent call last): + ... + ValueError: transition_probabilities all values must be dict + >>> viterbi(observations, states, start_p, {2:{2:2}}, emit_p) + Traceback (most recent call last): + ... + ValueError: transition_probabilities all keys must be strings + >>> viterbi(observations, states, start_p, {"a":{2:2}}, emit_p) + Traceback (most recent call last): + ... + ValueError: transition_probabilities all keys must be strings + >>> viterbi(observations, states, start_p, {"a":{"b":2}}, emit_p) + Traceback (most recent call last): + ... + ValueError: transition_probabilities nested dictionary all values must be float + >>> viterbi(observations, states, start_p, trans_p, "invalid") + Traceback (most recent call last): + ... + ValueError: emission_probabilities must be a dict + >>> viterbi(observations, states, start_p, trans_p, None) + Traceback (most recent call last): + ... + ValueError: There's an empty parameter """ _validation( @@ -213,7 +196,6 @@ def _validation( ... "Fever": {"normal": 0.1, "cold": 0.3, "dizzy": 0.6}, ... } >>> _validation(observations, states, start_p, trans_p, emit_p) - >>> _validation([], states, start_p, trans_p, emit_p) Traceback (most recent call last): ... @@ -242,7 +224,6 @@ def _validate_not_empty( """ >>> _validate_not_empty(["a"], ["b"], {"c":0.5}, ... {"d": {"e": 0.6}}, {"f": {"g": 0.7}}) - >>> _validate_not_empty(["a"], ["b"], {"c":0.5}, {}, {"f": {"g": 0.7}}) Traceback (most recent call last): ... @@ -267,12 +248,10 @@ def _validate_not_empty( def _validate_lists(observations_space: Any, states_space: Any) -> None: """ >>> _validate_lists(["a"], ["b"]) - >>> _validate_lists(1234, ["b"]) Traceback (most recent call last): ... ValueError: observations_space must be a list - >>> _validate_lists(["a"], [3]) Traceback (most recent call last): ... @@ -285,7 +264,6 @@ def _validate_lists(observations_space: Any, states_space: Any) -> None: def _validate_list(_object: Any, var_name: str) -> None: """ >>> _validate_list(["a"], "mock_name") - >>> _validate_list("a", "mock_name") Traceback (most recent call last): ... @@ -294,7 +272,6 @@ def _validate_list(_object: Any, var_name: str) -> None: Traceback (most recent call last): ... ValueError: mock_name must be a list of strings - """ if not isinstance(_object, list): msg = f"{var_name} must be a list" @@ -313,7 +290,6 @@ def _validate_dicts( ) -> None: """ >>> _validate_dicts({"c":0.5}, {"d": {"e": 0.6}}, {"f": {"g": 0.7}}) - >>> _validate_dicts("invalid", {"d": {"e": 0.6}}, {"f": {"g": 0.7}}) Traceback (most recent call last): ... @@ -339,7 +315,6 @@ def _validate_dicts( def _validate_nested_dict(_object: Any, var_name: str) -> None: """ >>> _validate_nested_dict({"a":{"b": 0.5}}, "mock_name") - >>> _validate_nested_dict("invalid", "mock_name") Traceback (most recent call last): ... @@ -367,7 +342,6 @@ def _validate_dict( ) -> None: """ >>> _validate_dict({"b": 0.5}, "mock_name", float) - >>> _validate_dict("invalid", "mock_name", float) Traceback (most recent call last): ... diff --git a/electronics/electric_conductivity.py b/electronics/electric_conductivity.py index 11f2a607d214..65bb6c5ceaf0 100644 --- a/electronics/electric_conductivity.py +++ b/electronics/electric_conductivity.py @@ -21,6 +21,26 @@ def electric_conductivity( ('conductivity', 5.12672e-14) >>> electric_conductivity(conductivity=1000, electron_conc=0, mobility=1200) ('electron_conc', 5.201506356240767e+18) + >>> electric_conductivity(conductivity=-10, electron_conc=100, mobility=0) + Traceback (most recent call last): + ... + ValueError: Conductivity cannot be negative + >>> electric_conductivity(conductivity=50, electron_conc=-10, mobility=0) + Traceback (most recent call last): + ... + ValueError: Electron concentration cannot be negative + >>> electric_conductivity(conductivity=50, electron_conc=0, mobility=-10) + Traceback (most recent call last): + ... + ValueError: mobility cannot be negative + >>> electric_conductivity(conductivity=50, electron_conc=0, mobility=0) + Traceback (most recent call last): + ... + ValueError: You cannot supply more or less than 2 values + >>> electric_conductivity(conductivity=50, electron_conc=200, mobility=300) + Traceback (most recent call last): + ... + ValueError: You cannot supply more or less than 2 values """ if (conductivity, electron_conc, mobility).count(0) != 1: raise ValueError("You cannot supply more or less than 2 values") diff --git a/financial/time_and_half_pay.py b/financial/time_and_half_pay.py new file mode 100644 index 000000000000..c5dff1bc1ce1 --- /dev/null +++ b/financial/time_and_half_pay.py @@ -0,0 +1,40 @@ +""" +Calculate time and a half pay +""" + + +def pay(hours_worked: float, pay_rate: float, hours: float = 40) -> float: + """ + hours_worked = The total hours worked + pay_rate = Amount of money per hour + hours = Number of hours that must be worked before you receive time and a half + + >>> pay(41, 1) + 41.5 + >>> pay(65, 19) + 1472.5 + >>> pay(10, 1) + 10.0 + """ + # Check that all input parameters are float or integer + assert isinstance(hours_worked, (float, int)), ( + "Parameter 'hours_worked' must be of type 'int' or 'float'" + ) + assert isinstance(pay_rate, (float, int)), ( + "Parameter 'pay_rate' must be of type 'int' or 'float'" + ) + assert isinstance(hours, (float, int)), ( + "Parameter 'hours' must be of type 'int' or 'float'" + ) + + normal_pay = hours_worked * pay_rate + over_time = max(0, hours_worked - hours) + over_time_pay = over_time * pay_rate / 2 + return normal_pay + over_time_pay + + +if __name__ == "__main__": + # Test + import doctest + + doctest.testmod() diff --git a/genetic_algorithm/basic_string.py b/genetic_algorithm/basic_string.py index a906ce85a779..b75491d9a949 100644 --- a/genetic_algorithm/basic_string.py +++ b/genetic_algorithm/basic_string.py @@ -144,18 +144,18 @@ def basic(target: str, genes: list[str], debug: bool = True) -> tuple[int, int, # Random population created. Now it's time to evaluate. - # Adding a bit of concurrency can make everything faster, + # (Option 1) Adding a bit of concurrency can make everything faster, # # import concurrent.futures # population_score: list[tuple[str, float]] = [] # with concurrent.futures.ThreadPoolExecutor( # max_workers=NUM_WORKERS) as executor: - # futures = {executor.submit(evaluate, item) for item in population} + # futures = {executor.submit(evaluate, item, target) for item in population} # concurrent.futures.wait(futures) # population_score = [item.result() for item in futures] # # but with a simple algorithm like this, it will probably be slower. - # We just need to call evaluate for every item inside the population. + # (Option 2) We just need to call evaluate for every item inside the population. population_score = [evaluate(item, target) for item in population] # Check if there is a matching evolution. diff --git a/geometry/geometry.py b/geometry/geometry.py index 9e353dee17a7..a0be8eb3befc 100644 --- a/geometry/geometry.py +++ b/geometry/geometry.py @@ -48,6 +48,18 @@ class Side: Side(length=5, angle=Angle(degrees=45.6), next_side=None) >>> Side(5, Angle(45.6), Side(1, Angle(2))) # doctest: +ELLIPSIS Side(length=5, angle=Angle(degrees=45.6), next_side=Side(length=1, angle=Angle(d... + >>> Side(-1) + Traceback (most recent call last): + ... + TypeError: length must be a positive numeric value. + >>> Side(5, None) + Traceback (most recent call last): + ... + TypeError: angle must be an Angle object. + >>> Side(5, Angle(90), "Invalid next_side") + Traceback (most recent call last): + ... + TypeError: next_side must be a Side or None. """ length: float @@ -162,6 +174,19 @@ class Polygon: >>> Polygon() Polygon(sides=[]) + >>> polygon = Polygon() + >>> polygon.add_side(Side(5)).get_side(0) + Side(length=5, angle=Angle(degrees=90), next_side=None) + >>> polygon.get_side(1) + Traceback (most recent call last): + ... + IndexError: list index out of range + >>> polygon.set_side(0, Side(10)).get_side(0) + Side(length=10, angle=Angle(degrees=90), next_side=None) + >>> polygon.set_side(1, Side(10)) + Traceback (most recent call last): + ... + IndexError: list assignment index out of range """ sides: list[Side] = field(default_factory=list) @@ -207,6 +232,10 @@ class Rectangle(Polygon): 30 >>> rectangle_one.area() 50 + >>> Rectangle(-5, 10) + Traceback (most recent call last): + ... + TypeError: length must be a positive numeric value. """ def __init__(self, short_side_length: float, long_side_length: float) -> None: diff --git a/graphics/butterfly_pattern.py b/graphics/butterfly_pattern.py new file mode 100644 index 000000000000..7913b03a7e95 --- /dev/null +++ b/graphics/butterfly_pattern.py @@ -0,0 +1,46 @@ +def butterfly_pattern(n: int) -> str: + """ + Creates a butterfly pattern of size n and returns it as a string. + + >>> print(butterfly_pattern(3)) + * * + ** ** + ***** + ** ** + * * + >>> print(butterfly_pattern(5)) + * * + ** ** + *** *** + **** **** + ********* + **** **** + *** *** + ** ** + * * + """ + result = [] + + # Upper part + for i in range(1, n): + left_stars = "*" * i + spaces = " " * (2 * (n - i) - 1) + right_stars = "*" * i + result.append(left_stars + spaces + right_stars) + + # Middle part + result.append("*" * (2 * n - 1)) + + # Lower part + for i in range(n - 1, 0, -1): + left_stars = "*" * i + spaces = " " * (2 * (n - i) - 1) + right_stars = "*" * i + result.append(left_stars + spaces + right_stars) + + return "\n".join(result) + + +if __name__ == "__main__": + n = int(input("Enter the size of the butterfly pattern: ")) + print(butterfly_pattern(n)) diff --git a/graphics/digital_differential_analyzer_line.py b/graphics/digital_differential_analyzer_line.py new file mode 100644 index 000000000000..f7269ab09856 --- /dev/null +++ b/graphics/digital_differential_analyzer_line.py @@ -0,0 +1,52 @@ +import matplotlib.pyplot as plt + + +def digital_differential_analyzer_line( + p1: tuple[int, int], p2: tuple[int, int] +) -> list[tuple[int, int]]: + """ + Draws a line between two points using the DDA algorithm. + + Args: + - p1: Coordinates of the starting point. + - p2: Coordinates of the ending point. + Returns: + - List of coordinate points that form the line. + + >>> digital_differential_analyzer_line((1, 1), (4, 4)) + [(2, 2), (3, 3), (4, 4)] + """ + x1, y1 = p1 + x2, y2 = p2 + dx = x2 - x1 + dy = y2 - y1 + steps = max(abs(dx), abs(dy)) + x_increment = dx / float(steps) + y_increment = dy / float(steps) + coordinates = [] + x: float = x1 + y: float = y1 + for _ in range(steps): + x += x_increment + y += y_increment + coordinates.append((round(x), round(y))) + return coordinates + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + + x1 = int(input("Enter the x-coordinate of the starting point: ")) + y1 = int(input("Enter the y-coordinate of the starting point: ")) + x2 = int(input("Enter the x-coordinate of the ending point: ")) + y2 = int(input("Enter the y-coordinate of the ending point: ")) + coordinates = digital_differential_analyzer_line((x1, y1), (x2, y2)) + x_points, y_points = zip(*coordinates) + plt.plot(x_points, y_points, marker="o") + plt.title("Digital Differential Analyzer Line Drawing Algorithm") + plt.xlabel("X-axis") + plt.ylabel("Y-axis") + plt.grid() + plt.show() diff --git a/graphs/basic_graphs.py b/graphs/basic_graphs.py index 567fa65040ae..286e9b195796 100644 --- a/graphs/basic_graphs.py +++ b/graphs/basic_graphs.py @@ -77,6 +77,14 @@ def initialize_weighted_undirected_graph( def dfs(g, s): + """ + >>> dfs({1: [2, 3], 2: [4, 5], 3: [], 4: [], 5: []}, 1) + 1 + 2 + 4 + 5 + 3 + """ vis, _s = {s}, [s] print(s) while _s: @@ -104,6 +112,17 @@ def dfs(g, s): def bfs(g, s): + """ + >>> bfs({1: [2, 3], 2: [4, 5], 3: [6, 7], 4: [], 5: [8], 6: [], 7: [], 8: []}, 1) + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + """ vis, q = {s}, deque([s]) print(s) while q: @@ -128,6 +147,19 @@ def bfs(g, s): def dijk(g, s): + """ + dijk({1: [(2, 7), (3, 9), (6, 14)], + 2: [(1, 7), (3, 10), (4, 15)], + 3: [(1, 9), (2, 10), (4, 11), (6, 2)], + 4: [(2, 15), (3, 11), (5, 6)], + 5: [(4, 6), (6, 9)], + 6: [(1, 14), (3, 2), (5, 9)]}, 1) + 7 + 9 + 11 + 20 + 20 + """ dist, known, path = {s: 0}, set(), {s: 0} while True: if len(known) == len(g) - 1: diff --git a/graphs/lanczos_eigenvectors.py b/graphs/lanczos_eigenvectors.py new file mode 100644 index 000000000000..581a81a1127f --- /dev/null +++ b/graphs/lanczos_eigenvectors.py @@ -0,0 +1,206 @@ +""" +Lanczos Method for Finding Eigenvalues and Eigenvectors of a Graph. + +This module demonstrates the Lanczos method to approximate the largest eigenvalues +and corresponding eigenvectors of a symmetric matrix represented as a graph's +adjacency list. The method efficiently handles large, sparse matrices by converting +the graph to a tridiagonal matrix, whose eigenvalues and eigenvectors are then +computed. + +Key Functions: +- `find_lanczos_eigenvectors`: Computes the k largest eigenvalues and vectors. +- `lanczos_iteration`: Constructs the tridiagonal matrix and orthonormal basis vectors. +- `multiply_matrix_vector`: Multiplies an adjacency list graph with a vector. + +Complexity: +- Time: O(k * n), where k is the number of eigenvalues and n is the matrix size. +- Space: O(n), due to sparse representation and tridiagonal matrix structure. + +Further Reading: +- Lanczos Algorithm: https://en.wikipedia.org/wiki/Lanczos_algorithm +- Eigenvector Centrality: https://en.wikipedia.org/wiki/Eigenvector_centrality + +Example Usage: +Given a graph represented by an adjacency list, the `find_lanczos_eigenvectors` +function returns the largest eigenvalues and eigenvectors. This can be used to +analyze graph centrality. +""" + +import numpy as np + + +def validate_adjacency_list(graph: list[list[int | None]]) -> None: + """Validates the adjacency list format for the graph. + + Args: + graph: A list of lists where each sublist contains the neighbors of a node. + + Raises: + ValueError: If the graph is not a list of lists, or if any node has + invalid neighbors (e.g., out-of-range or non-integer values). + + >>> validate_adjacency_list([[1, 2], [0], [0, 1]]) + >>> validate_adjacency_list([[]]) # No neighbors, valid case + >>> validate_adjacency_list([[1], [2], [-1]]) # Invalid neighbor + Traceback (most recent call last): + ... + ValueError: Invalid neighbor -1 in node 2 adjacency list. + """ + if not isinstance(graph, list): + raise ValueError("Graph should be a list of lists.") + + for node_index, neighbors in enumerate(graph): + if not isinstance(neighbors, list): + no_neighbors_message: str = ( + f"Node {node_index} should have a list of neighbors." + ) + raise ValueError(no_neighbors_message) + for neighbor_index in neighbors: + if ( + not isinstance(neighbor_index, int) + or neighbor_index < 0 + or neighbor_index >= len(graph) + ): + invalid_neighbor_message: str = ( + f"Invalid neighbor {neighbor_index} in node {node_index} " + f"adjacency list." + ) + raise ValueError(invalid_neighbor_message) + + +def lanczos_iteration( + graph: list[list[int | None]], num_eigenvectors: int +) -> tuple[np.ndarray, np.ndarray]: + """Constructs the tridiagonal matrix and orthonormal basis vectors using the + Lanczos method. + + Args: + graph: The graph represented as a list of adjacency lists. + num_eigenvectors: The number of largest eigenvalues and eigenvectors + to approximate. + + Returns: + A tuple containing: + - tridiagonal_matrix: A (num_eigenvectors x num_eigenvectors) symmetric + matrix. + - orthonormal_basis: A (num_nodes x num_eigenvectors) matrix of orthonormal + basis vectors. + + Raises: + ValueError: If num_eigenvectors is less than 1 or greater than the number of + nodes. + + >>> graph = [[1, 2], [0, 2], [0, 1]] + >>> T, Q = lanczos_iteration(graph, 2) + >>> T.shape == (2, 2) and Q.shape == (3, 2) + True + """ + num_nodes: int = len(graph) + if not (1 <= num_eigenvectors <= num_nodes): + raise ValueError( + "Number of eigenvectors must be between 1 and the number of " + "nodes in the graph." + ) + + orthonormal_basis: np.ndarray = np.zeros((num_nodes, num_eigenvectors)) + tridiagonal_matrix: np.ndarray = np.zeros((num_eigenvectors, num_eigenvectors)) + + rng = np.random.default_rng() + initial_vector: np.ndarray = rng.random(num_nodes) + initial_vector /= np.sqrt(np.dot(initial_vector, initial_vector)) + orthonormal_basis[:, 0] = initial_vector + + prev_beta: float = 0.0 + for iter_index in range(num_eigenvectors): + result_vector: np.ndarray = multiply_matrix_vector( + graph, orthonormal_basis[:, iter_index] + ) + if iter_index > 0: + result_vector -= prev_beta * orthonormal_basis[:, iter_index - 1] + alpha_value: float = np.dot(orthonormal_basis[:, iter_index], result_vector) + result_vector -= alpha_value * orthonormal_basis[:, iter_index] + + prev_beta = np.sqrt(np.dot(result_vector, result_vector)) + if iter_index < num_eigenvectors - 1 and prev_beta > 1e-10: + orthonormal_basis[:, iter_index + 1] = result_vector / prev_beta + tridiagonal_matrix[iter_index, iter_index] = alpha_value + if iter_index < num_eigenvectors - 1: + tridiagonal_matrix[iter_index, iter_index + 1] = prev_beta + tridiagonal_matrix[iter_index + 1, iter_index] = prev_beta + return tridiagonal_matrix, orthonormal_basis + + +def multiply_matrix_vector( + graph: list[list[int | None]], vector: np.ndarray +) -> np.ndarray: + """Performs multiplication of a graph's adjacency list representation with a vector. + + Args: + graph: The adjacency list of the graph. + vector: A 1D numpy array representing the vector to multiply. + + Returns: + A numpy array representing the product of the adjacency list and the vector. + + Raises: + ValueError: If the vector's length does not match the number of nodes in the + graph. + + >>> multiply_matrix_vector([[1, 2], [0, 2], [0, 1]], np.array([1, 1, 1])) + array([2., 2., 2.]) + >>> multiply_matrix_vector([[1, 2], [0, 2], [0, 1]], np.array([0, 1, 0])) + array([1., 0., 1.]) + """ + num_nodes: int = len(graph) + if vector.shape[0] != num_nodes: + raise ValueError("Vector length must match the number of nodes in the graph.") + + result: np.ndarray = np.zeros(num_nodes) + for node_index, neighbors in enumerate(graph): + for neighbor_index in neighbors: + result[node_index] += vector[neighbor_index] + return result + + +def find_lanczos_eigenvectors( + graph: list[list[int | None]], num_eigenvectors: int +) -> tuple[np.ndarray, np.ndarray]: + """Computes the largest eigenvalues and their corresponding eigenvectors using the + Lanczos method. + + Args: + graph: The graph as a list of adjacency lists. + num_eigenvectors: Number of largest eigenvalues and eigenvectors to compute. + + Returns: + A tuple containing: + - eigenvalues: 1D array of the largest eigenvalues in descending order. + - eigenvectors: 2D array where each column is an eigenvector corresponding + to an eigenvalue. + + Raises: + ValueError: If the graph format is invalid or num_eigenvectors is out of bounds. + + >>> eigenvalues, eigenvectors = find_lanczos_eigenvectors( + ... [[1, 2], [0, 2], [0, 1]], 2 + ... ) + >>> len(eigenvalues) == 2 and eigenvectors.shape[1] == 2 + True + """ + validate_adjacency_list(graph) + tridiagonal_matrix, orthonormal_basis = lanczos_iteration(graph, num_eigenvectors) + eigenvalues, eigenvectors = np.linalg.eigh(tridiagonal_matrix) + return eigenvalues[::-1], np.dot(orthonormal_basis, eigenvectors[:, ::-1]) + + +def main() -> None: + """ + Main driver function for testing the implementation with doctests. + """ + import doctest + + doctest.testmod() + + +if __name__ == "__main__": + main() diff --git a/graphs/minimum_spanning_tree_prims2.py b/graphs/minimum_spanning_tree_prims2.py index cc918f81dfe8..6870cc80f844 100644 --- a/graphs/minimum_spanning_tree_prims2.py +++ b/graphs/minimum_spanning_tree_prims2.py @@ -239,8 +239,8 @@ def prims_algo( 13 """ # prim's algorithm for minimum spanning tree - dist: dict[T, int] = {node: maxsize for node in graph.connections} - parent: dict[T, T | None] = {node: None for node in graph.connections} + dist: dict[T, int] = dict.fromkeys(graph.connections, maxsize) + parent: dict[T, T | None] = dict.fromkeys(graph.connections) priority_queue: MinPriorityQueue[T] = MinPriorityQueue() for node, weight in dist.items(): diff --git a/hashes/enigma_machine.py b/hashes/enigma_machine.py index d95437d12c34..0da8e4113de9 100644 --- a/hashes/enigma_machine.py +++ b/hashes/enigma_machine.py @@ -15,12 +15,12 @@ def rotator(): gear_one.append(i) del gear_one[0] gear_one_pos += 1 - if gear_one_pos % int(len(alphabets)) == 0: + if gear_one_pos % len(alphabets) == 0: i = gear_two[0] gear_two.append(i) del gear_two[0] gear_two_pos += 1 - if gear_two_pos % int(len(alphabets)) == 0: + if gear_two_pos % len(alphabets) == 0: i = gear_three[0] gear_three.append(i) del gear_three[0] diff --git a/linear_algebra/gaussian_elimination.py b/linear_algebra/gaussian_elimination.py index 724773c0db98..6f4075b710fd 100644 --- a/linear_algebra/gaussian_elimination.py +++ b/linear_algebra/gaussian_elimination.py @@ -1,6 +1,6 @@ """ -Gaussian elimination method for solving a system of linear equations. -Gaussian elimination - https://en.wikipedia.org/wiki/Gaussian_elimination +| Gaussian elimination method for solving a system of linear equations. +| Gaussian elimination - https://en.wikipedia.org/wiki/Gaussian_elimination """ import numpy as np @@ -13,12 +13,17 @@ def retroactive_resolution( ) -> NDArray[float64]: """ This function performs a retroactive linear system resolution - for triangular matrix + for triangular matrix Examples: - 2x1 + 2x2 - 1x3 = 5 2x1 + 2x2 = -1 - 0x1 - 2x2 - 1x3 = -7 0x1 - 2x2 = -1 - 0x1 + 0x2 + 5x3 = 15 + 1. + * 2x1 + 2x2 - 1x3 = 5 + * 0x1 - 2x2 - 1x3 = -7 + * 0x1 + 0x2 + 5x3 = 15 + 2. + * 2x1 + 2x2 = -1 + * 0x1 - 2x2 = -1 + >>> gaussian_elimination([[2, 2, -1], [0, -2, -1], [0, 0, 5]], [[5], [-7], [15]]) array([[2.], [2.], @@ -45,9 +50,14 @@ def gaussian_elimination( This function performs Gaussian elimination method Examples: - 1x1 - 4x2 - 2x3 = -2 1x1 + 2x2 = 5 - 5x1 + 2x2 - 2x3 = -3 5x1 + 2x2 = 5 - 1x1 - 1x2 + 0x3 = 4 + 1. + * 1x1 - 4x2 - 2x3 = -2 + * 5x1 + 2x2 - 2x3 = -3 + * 1x1 - 1x2 + 0x3 = 4 + 2. + * 1x1 + 2x2 = 5 + * 5x1 + 2x2 = 5 + >>> gaussian_elimination([[1, -4, -2], [5, 2, -2], [1, -1, 0]], [[-2], [-3], [4]]) array([[ 2.3 ], [-1.7 ], diff --git a/linear_algebra/lu_decomposition.py b/linear_algebra/lu_decomposition.py index 3620674835cd..3d89b53a48fb 100644 --- a/linear_algebra/lu_decomposition.py +++ b/linear_algebra/lu_decomposition.py @@ -2,13 +2,14 @@ Lower-upper (LU) decomposition factors a matrix as a product of a lower triangular matrix and an upper triangular matrix. A square matrix has an LU decomposition under the following conditions: + - If the matrix is invertible, then it has an LU decomposition if and only - if all of its leading principal minors are non-zero (see - https://en.wikipedia.org/wiki/Minor_(linear_algebra) for an explanation of - leading principal minors of a matrix). + if all of its leading principal minors are non-zero (see + https://en.wikipedia.org/wiki/Minor_(linear_algebra) for an explanation of + leading principal minors of a matrix). - If the matrix is singular (i.e., not invertible) and it has a rank of k - (i.e., it has k linearly independent columns), then it has an LU - decomposition if its first k leading principal minors are non-zero. + (i.e., it has k linearly independent columns), then it has an LU + decomposition if its first k leading principal minors are non-zero. This algorithm will simply attempt to perform LU decomposition on any square matrix and raise an error if no such decomposition exists. @@ -25,6 +26,7 @@ def lower_upper_decomposition(table: np.ndarray) -> tuple[np.ndarray, np.ndarray """ Perform LU decomposition on a given matrix and raises an error if the matrix isn't square or if no such decomposition exists + >>> matrix = np.array([[2, -2, 1], [0, 1, 2], [5, 3, 1]]) >>> lower_mat, upper_mat = lower_upper_decomposition(matrix) >>> lower_mat @@ -45,7 +47,7 @@ def lower_upper_decomposition(table: np.ndarray) -> tuple[np.ndarray, np.ndarray array([[ 4. , 3. ], [ 0. , -1.5]]) - # Matrix is not square + >>> # Matrix is not square >>> matrix = np.array([[2, -2, 1], [0, 1, 2]]) >>> lower_mat, upper_mat = lower_upper_decomposition(matrix) Traceback (most recent call last): @@ -54,14 +56,14 @@ def lower_upper_decomposition(table: np.ndarray) -> tuple[np.ndarray, np.ndarray [[ 2 -2 1] [ 0 1 2]] - # Matrix is invertible, but its first leading principal minor is 0 + >>> # Matrix is invertible, but its first leading principal minor is 0 >>> matrix = np.array([[0, 1], [1, 0]]) >>> lower_mat, upper_mat = lower_upper_decomposition(matrix) Traceback (most recent call last): ... ArithmeticError: No LU decomposition exists - # Matrix is singular, but its first leading principal minor is 1 + >>> # Matrix is singular, but its first leading principal minor is 1 >>> matrix = np.array([[1, 0], [1, 0]]) >>> lower_mat, upper_mat = lower_upper_decomposition(matrix) >>> lower_mat @@ -71,7 +73,7 @@ def lower_upper_decomposition(table: np.ndarray) -> tuple[np.ndarray, np.ndarray array([[1., 0.], [0., 0.]]) - # Matrix is singular, but its first leading principal minor is 0 + >>> # Matrix is singular, but its first leading principal minor is 0 >>> matrix = np.array([[0, 1], [0, 1]]) >>> lower_mat, upper_mat = lower_upper_decomposition(matrix) Traceback (most recent call last): diff --git a/linear_algebra/matrix_inversion.py b/linear_algebra/matrix_inversion.py new file mode 100644 index 000000000000..50dae1c2e825 --- /dev/null +++ b/linear_algebra/matrix_inversion.py @@ -0,0 +1,36 @@ +import numpy as np + + +def invert_matrix(matrix: list[list[float]]) -> list[list[float]]: + """ + Returns the inverse of a square matrix using NumPy. + + Parameters: + matrix (list[list[float]]): A square matrix. + + Returns: + list[list[float]]: Inverted matrix if invertible, else raises error. + + >>> invert_matrix([[4.0, 7.0], [2.0, 6.0]]) + [[0.6000000000000001, -0.7000000000000001], [-0.2, 0.4]] + >>> invert_matrix([[1.0, 2.0], [0.0, 0.0]]) + Traceback (most recent call last): + ... + ValueError: Matrix is not invertible + """ + np_matrix = np.array(matrix) + + try: + inv_matrix = np.linalg.inv(np_matrix) + except np.linalg.LinAlgError: + raise ValueError("Matrix is not invertible") + + return inv_matrix.tolist() + + +if __name__ == "__main__": + mat = [[4.0, 7.0], [2.0, 6.0]] + print("Original Matrix:") + print(mat) + print("Inverted Matrix:") + print(invert_matrix(mat)) diff --git a/linear_algebra/src/gaussian_elimination_pivoting.py b/linear_algebra/src/gaussian_elimination_pivoting.py index efc1ddd64a2e..540f57b0cff6 100644 --- a/linear_algebra/src/gaussian_elimination_pivoting.py +++ b/linear_algebra/src/gaussian_elimination_pivoting.py @@ -6,17 +6,18 @@ def solve_linear_system(matrix: np.ndarray) -> np.ndarray: Solve a linear system of equations using Gaussian elimination with partial pivoting Args: - - matrix: Coefficient matrix with the last column representing the constants. + - `matrix`: Coefficient matrix with the last column representing the constants. Returns: - - Solution vector. + - Solution vector. Raises: - - ValueError: If the matrix is not correct (i.e., singular). + - ``ValueError``: If the matrix is not correct (i.e., singular). https://courses.engr.illinois.edu/cs357/su2013/lect.htm Lecture 7 Example: + >>> A = np.array([[2, 1, -1], [-3, -1, 2], [-2, 1, 2]], dtype=float) >>> B = np.array([8, -11, -3], dtype=float) >>> solution = solve_linear_system(np.column_stack((A, B))) diff --git a/linear_algebra/src/rank_of_matrix.py b/linear_algebra/src/rank_of_matrix.py index 7ff3c1699a69..2c4fe2a8d1da 100644 --- a/linear_algebra/src/rank_of_matrix.py +++ b/linear_algebra/src/rank_of_matrix.py @@ -8,11 +8,15 @@ def rank_of_matrix(matrix: list[list[int | float]]) -> int: """ Finds the rank of a matrix. + Args: - matrix: The matrix as a list of lists. + `matrix`: The matrix as a list of lists. + Returns: The rank of the matrix. + Example: + >>> matrix1 = [[1, 2, 3], ... [4, 5, 6], ... [7, 8, 9]] diff --git a/linear_algebra/src/schur_complement.py b/linear_algebra/src/schur_complement.py index 7c79bb70abfc..74ac75e3fce2 100644 --- a/linear_algebra/src/schur_complement.py +++ b/linear_algebra/src/schur_complement.py @@ -12,13 +12,14 @@ def schur_complement( ) -> np.ndarray: """ Schur complement of a symmetric matrix X given as a 2x2 block matrix - consisting of matrices A, B and C. - Matrix A must be quadratic and non-singular. - In case A is singular, a pseudo-inverse may be provided using - the pseudo_inv argument. + consisting of matrices `A`, `B` and `C`. + Matrix `A` must be quadratic and non-singular. + In case `A` is singular, a pseudo-inverse may be provided using + the `pseudo_inv` argument. + + | Link to Wiki: https://en.wikipedia.org/wiki/Schur_complement + | See also Convex Optimization - Boyd and Vandenberghe, A.5.5 - Link to Wiki: https://en.wikipedia.org/wiki/Schur_complement - See also Convex Optimization - Boyd and Vandenberghe, A.5.5 >>> import numpy as np >>> a = np.array([[1, 2], [2, 1]]) >>> b = np.array([[0, 3], [3, 0]]) diff --git a/linear_algebra/src/test_linear_algebra.py b/linear_algebra/src/test_linear_algebra.py index fc5f90fd5cbe..5209c152013e 100644 --- a/linear_algebra/src/test_linear_algebra.py +++ b/linear_algebra/src/test_linear_algebra.py @@ -181,7 +181,7 @@ def test_component_matrix(self) -> None: test for Matrix method component() """ a = Matrix([[1, 2, 3], [2, 4, 5], [6, 7, 8]], 3, 3) - assert a.component(2, 1) == 7, 0.01 + assert a.component(2, 1) == 7, "0.01" def test__add__matrix(self) -> None: """ diff --git a/linear_algebra/src/transformations_2d.py b/linear_algebra/src/transformations_2d.py index b4185cd2848f..5dee59024752 100644 --- a/linear_algebra/src/transformations_2d.py +++ b/linear_algebra/src/transformations_2d.py @@ -3,13 +3,15 @@ I have added the codes for reflection, projection, scaling and rotation 2D matrices. +.. code-block:: python + scaling(5) = [[5.0, 0.0], [0.0, 5.0]] - rotation(45) = [[0.5253219888177297, -0.8509035245341184], - [0.8509035245341184, 0.5253219888177297]] -projection(45) = [[0.27596319193541496, 0.446998331800279], - [0.446998331800279, 0.7240368080645851]] -reflection(45) = [[0.05064397763545947, 0.893996663600558], - [0.893996663600558, 0.7018070490682369]] + rotation(45) = [[0.5253219888177297, -0.8509035245341184], + [0.8509035245341184, 0.5253219888177297]] + projection(45) = [[0.27596319193541496, 0.446998331800279], + [0.446998331800279, 0.7240368080645851]] + reflection(45) = [[0.05064397763545947, 0.893996663600558], + [0.893996663600558, 0.7018070490682369]] """ from math import cos, sin diff --git a/machine_learning/linear_discriminant_analysis.py b/machine_learning/linear_discriminant_analysis.py index 86f28aef671a..8528ccbbae51 100644 --- a/machine_learning/linear_discriminant_analysis.py +++ b/machine_learning/linear_discriminant_analysis.py @@ -322,7 +322,7 @@ def main(): user_count = valid_input( input_type=int, condition=lambda x: x > 0, - input_msg=(f"Enter The number of instances for class_{i+1}: "), + input_msg=(f"Enter The number of instances for class_{i + 1}: "), err_msg="Number of instances should be positive!", ) counts.append(user_count) @@ -333,7 +333,7 @@ def main(): for a in range(n_classes): user_mean = valid_input( input_type=float, - input_msg=(f"Enter the value of mean for class_{a+1}: "), + input_msg=(f"Enter the value of mean for class_{a + 1}: "), err_msg="This is an invalid value.", ) user_means.append(user_mean) diff --git a/machine_learning/principle_component_analysis.py b/machine_learning/principle_component_analysis.py new file mode 100644 index 000000000000..46ccdb968494 --- /dev/null +++ b/machine_learning/principle_component_analysis.py @@ -0,0 +1,85 @@ +""" +Principal Component Analysis (PCA) is a dimensionality reduction technique +used in machine learning. It transforms high-dimensional data into a lower-dimensional +representation while retaining as much variance as possible. + +This implementation follows best practices, including: +- Standardizing the dataset. +- Computing principal components using Singular Value Decomposition (SVD). +- Returning transformed data and explained variance ratio. +""" + +import doctest + +import numpy as np +from sklearn.datasets import load_iris +from sklearn.decomposition import PCA +from sklearn.preprocessing import StandardScaler + + +def collect_dataset() -> tuple[np.ndarray, np.ndarray]: + """ + Collects the dataset (Iris dataset) and returns feature matrix and target values. + + :return: Tuple containing feature matrix (X) and target labels (y) + + Example: + >>> X, y = collect_dataset() + >>> X.shape + (150, 4) + >>> y.shape + (150,) + """ + data = load_iris() + return np.array(data.data), np.array(data.target) + + +def apply_pca(data_x: np.ndarray, n_components: int) -> tuple[np.ndarray, np.ndarray]: + """ + Applies Principal Component Analysis (PCA) to reduce dimensionality. + + :param data_x: Original dataset (features) + :param n_components: Number of principal components to retain + :return: Tuple containing transformed dataset and explained variance ratio + + Example: + >>> X, _ = collect_dataset() + >>> transformed_X, variance = apply_pca(X, 2) + >>> transformed_X.shape + (150, 2) + >>> len(variance) == 2 + True + """ + # Standardizing the dataset + scaler = StandardScaler() + data_x_scaled = scaler.fit_transform(data_x) + + # Applying PCA + pca = PCA(n_components=n_components) + principal_components = pca.fit_transform(data_x_scaled) + + return principal_components, pca.explained_variance_ratio_ + + +def main() -> None: + """ + Driver function to execute PCA and display results. + """ + data_x, data_y = collect_dataset() + + # Number of principal components to retain + n_components = 2 + + # Apply PCA + transformed_data, variance_ratio = apply_pca(data_x, n_components) + + print("Transformed Dataset (First 5 rows):") + print(transformed_data[:5]) + + print("\nExplained Variance Ratio:") + print(variance_ratio) + + +if __name__ == "__main__": + doctest.testmod() + main() diff --git a/maths/decimal_to_fraction.py b/maths/decimal_to_fraction.py index 2aa8e3c3dfd6..be42b9fb3b5a 100644 --- a/maths/decimal_to_fraction.py +++ b/maths/decimal_to_fraction.py @@ -16,6 +16,20 @@ def decimal_to_fraction(decimal: float | str) -> tuple[int, int]: >>> decimal_to_fraction("78td") Traceback (most recent call last): ValueError: Please enter a valid number + >>> decimal_to_fraction(0) + (0, 1) + >>> decimal_to_fraction(-2.5) + (-5, 2) + >>> decimal_to_fraction(0.125) + (1, 8) + >>> decimal_to_fraction(1000000.25) + (4000001, 4) + >>> decimal_to_fraction(1.3333) + (13333, 10000) + >>> decimal_to_fraction("1.23e2") + (123, 1) + >>> decimal_to_fraction("0.500") + (1, 2) """ try: decimal = float(decimal) @@ -34,8 +48,8 @@ def decimal_to_fraction(decimal: float | str) -> tuple[int, int]: if remainder == 0: break dividend, divisor = divisor, remainder - numerator, denominator = numerator / divisor, denominator / divisor - return int(numerator), int(denominator) + numerator, denominator = numerator // divisor, denominator // divisor + return numerator, denominator if __name__ == "__main__": diff --git a/maths/dual_number_automatic_differentiation.py b/maths/dual_number_automatic_differentiation.py index f98997c8be4d..09aeb17a4aea 100644 --- a/maths/dual_number_automatic_differentiation.py +++ b/maths/dual_number_automatic_differentiation.py @@ -17,10 +17,8 @@ def __init__(self, real, rank): self.duals = rank def __repr__(self): - return ( - f"{self.real}+" - f"{'+'.join(str(dual)+'E'+str(n+1)for n,dual in enumerate(self.duals))}" - ) + s = "+".join(f"{dual}E{n}" for n, dual in enumerate(self.duals, 1)) + return f"{self.real}+{s}" def reduce(self): cur = self.duals.copy() diff --git a/maths/geometric_mean.py b/maths/geometric_mean.py new file mode 100644 index 000000000000..240d519ad398 --- /dev/null +++ b/maths/geometric_mean.py @@ -0,0 +1,55 @@ +""" +The Geometric Mean of n numbers is defined as the n-th root of the product +of those numbers. It is used to measure the central tendency of the numbers. +https://en.wikipedia.org/wiki/Geometric_mean +""" + + +def compute_geometric_mean(*args: int) -> float: + """ + Return the geometric mean of the argument numbers. + >>> compute_geometric_mean(2,8) + 4.0 + >>> compute_geometric_mean('a', 4) + Traceback (most recent call last): + ... + TypeError: Not a Number + >>> compute_geometric_mean(5, 125) + 25.0 + >>> compute_geometric_mean(1, 0) + 0.0 + >>> compute_geometric_mean(1, 5, 25, 5) + 5.0 + >>> compute_geometric_mean(2, -2) + Traceback (most recent call last): + ... + ArithmeticError: Cannot Compute Geometric Mean for these numbers. + >>> compute_geometric_mean(-5, 25, 1) + -5.0 + """ + product = 1 + for number in args: + if not isinstance(number, int) and not isinstance(number, float): + raise TypeError("Not a Number") + product *= number + # Cannot calculate the even root for negative product. + # Frequently they are restricted to being positive. + if product < 0 and len(args) % 2 == 0: + raise ArithmeticError("Cannot Compute Geometric Mean for these numbers.") + mean = abs(product) ** (1 / len(args)) + # Since python calculates complex roots for negative products with odd roots. + if product < 0: + mean = -mean + # Since it does floating point arithmetic, it gives 64**(1/3) as 3.99999996 + possible_mean = float(round(mean)) + # To check if the rounded number is actually the mean. + if possible_mean ** len(args) == product: + mean = possible_mean + return mean + + +if __name__ == "__main__": + from doctest import testmod + + testmod(name="compute_geometric_mean") + print(compute_geometric_mean(-3, -27)) diff --git a/maths/is_ip_v4_address_valid.py b/maths/is_ip_v4_address_valid.py index 0ae8e021ead1..305afabffed3 100644 --- a/maths/is_ip_v4_address_valid.py +++ b/maths/is_ip_v4_address_valid.py @@ -1,13 +1,15 @@ """ +wiki: https://en.wikipedia.org/wiki/IPv4 + Is IP v4 address valid? A valid IP address must be four octets in the form of A.B.C.D, -where A,B,C and D are numbers from 0-254 -for example: 192.168.23.1, 172.254.254.254 are valid IP address - 192.168.255.0, 255.192.3.121 are invalid IP address +where A, B, C and D are numbers from 0-255 +for example: 192.168.23.1, 172.255.255.255 are valid IP address + 192.168.256.0, 256.192.3.121 are invalid IP address """ -def is_ip_v4_address_valid(ip_v4_address: str) -> bool: +def is_ip_v4_address_valid(ip: str) -> bool: """ print "Valid IP address" If IP is valid. or @@ -16,13 +18,13 @@ def is_ip_v4_address_valid(ip_v4_address: str) -> bool: >>> is_ip_v4_address_valid("192.168.0.23") True - >>> is_ip_v4_address_valid("192.255.15.8") + >>> is_ip_v4_address_valid("192.256.15.8") False >>> is_ip_v4_address_valid("172.100.0.8") True - >>> is_ip_v4_address_valid("254.255.0.255") + >>> is_ip_v4_address_valid("255.256.0.256") False >>> is_ip_v4_address_valid("1.2.33333333.4") @@ -45,12 +47,29 @@ def is_ip_v4_address_valid(ip_v4_address: str) -> bool: >>> is_ip_v4_address_valid("1.2.3.") False + + >>> is_ip_v4_address_valid("1.2.3.05") + False """ - octets = [int(i) for i in ip_v4_address.split(".") if i.isdigit()] - return len(octets) == 4 and all(0 <= int(octet) <= 254 for octet in octets) + octets = ip.split(".") + if len(octets) != 4: + return False + + for octet in octets: + if not octet.isdigit(): + return False + + number = int(octet) + if len(str(number)) != len(octet): + return False + + if not 0 <= number <= 255: + return False + + return True if __name__ == "__main__": ip = input().strip() valid_or_invalid = "valid" if is_ip_v4_address_valid(ip) else "invalid" - print(f"{ip} is a {valid_or_invalid} IP v4 address.") + print(f"{ip} is a {valid_or_invalid} IPv4 address.") diff --git a/maths/max_sum_sliding_window.py b/maths/max_sum_sliding_window.py index 090117429604..c7492978a6c9 100644 --- a/maths/max_sum_sliding_window.py +++ b/maths/max_sum_sliding_window.py @@ -43,4 +43,6 @@ def max_sum_in_array(array: list[int], k: int) -> int: testmod() array = [randint(-1000, 1000) for i in range(100)] k = randint(0, 110) - print(f"The maximum sum of {k} consecutive elements is {max_sum_in_array(array,k)}") + print( + f"The maximum sum of {k} consecutive elements is {max_sum_in_array(array, k)}" + ) diff --git a/maths/numerical_analysis/integration_by_simpson_approx.py b/maths/numerical_analysis/integration_by_simpson_approx.py index 934299997aac..043f3a9a72af 100644 --- a/maths/numerical_analysis/integration_by_simpson_approx.py +++ b/maths/numerical_analysis/integration_by_simpson_approx.py @@ -88,18 +88,18 @@ def simpson_integration(function, a: float, b: float, precision: int = 4) -> flo AssertionError: precision should be positive integer your input : -1 """ - assert callable( - function - ), f"the function(object) passed should be callable your input : {function}" + assert callable(function), ( + f"the function(object) passed should be callable your input : {function}" + ) assert isinstance(a, (float, int)), f"a should be float or integer your input : {a}" assert isinstance(function(a), (float, int)), ( "the function should return integer or float return type of your function, " f"{type(a)}" ) assert isinstance(b, (float, int)), f"b should be float or integer your input : {b}" - assert ( - isinstance(precision, int) and precision > 0 - ), f"precision should be positive integer your input : {precision}" + assert isinstance(precision, int) and precision > 0, ( + f"precision should be positive integer your input : {precision}" + ) # just applying the formula of simpson for approximate integration written in # mentioned article in first comment of this file and above this function diff --git a/maths/numerical_analysis/intersection.py b/maths/numerical_analysis/intersection.py index 826c0ead0a00..325abeaca996 100644 --- a/maths/numerical_analysis/intersection.py +++ b/maths/numerical_analysis/intersection.py @@ -42,6 +42,11 @@ def intersection(function: Callable[[float], float], x0: float, x1: float) -> fl def f(x: float) -> float: + """ + function is f(x) = x^3 - 2x - 5 + >>> f(2) + -1.0 + """ return math.pow(x, 3) - (2 * x) - 5 diff --git a/maths/power_using_recursion.py b/maths/power_using_recursion.py index 29283ca0f67c..eb775b161ae8 100644 --- a/maths/power_using_recursion.py +++ b/maths/power_using_recursion.py @@ -38,6 +38,14 @@ def power(base: int, exponent: int) -> float: Traceback (most recent call last): ... RecursionError: maximum recursion depth exceeded + >>> power(0, 0) + 1 + >>> power(0, 1) + 0 + >>> power(5,6) + 15625 + >>> power(23, 12) + 21914624432020321 """ return base * power(base, (exponent - 1)) if exponent else 1 diff --git a/maths/prime_check.py b/maths/prime_check.py index f1bc4def2469..a757c4108f24 100644 --- a/maths/prime_check.py +++ b/maths/prime_check.py @@ -73,12 +73,12 @@ def test_primes(self): def test_not_primes(self): with pytest.raises(ValueError): is_prime(-19) - assert not is_prime( - 0 - ), "Zero doesn't have any positive factors, primes must have exactly two." - assert not is_prime( - 1 - ), "One only has 1 positive factor, primes must have exactly two." + assert not is_prime(0), ( + "Zero doesn't have any positive factors, primes must have exactly two." + ) + assert not is_prime(1), ( + "One only has 1 positive factor, primes must have exactly two." + ) assert not is_prime(2 * 2) assert not is_prime(2 * 3) assert not is_prime(3 * 3) diff --git a/maths/primelib.py b/maths/primelib.py index a26b0eaeb328..9f031efc50a9 100644 --- a/maths/primelib.py +++ b/maths/primelib.py @@ -66,9 +66,9 @@ def is_prime(number: int) -> bool: """ # precondition - assert isinstance(number, int) and ( - number >= 0 - ), "'number' must been an int and positive" + assert isinstance(number, int) and (number >= 0), ( + "'number' must been an int and positive" + ) status = True @@ -76,7 +76,7 @@ def is_prime(number: int) -> bool: if number <= 1: status = False - for divisor in range(2, int(round(sqrt(number))) + 1): + for divisor in range(2, round(sqrt(number)) + 1): # if 'number' divisible by 'divisor' then sets 'status' # of false and break up the loop. if number % divisor == 0: @@ -254,9 +254,9 @@ def greatest_prime_factor(number): """ # precondition - assert isinstance(number, int) and ( - number >= 0 - ), "'number' must been an int and >= 0" + assert isinstance(number, int) and (number >= 0), ( + "'number' must been an int and >= 0" + ) ans = 0 @@ -296,9 +296,9 @@ def smallest_prime_factor(number): """ # precondition - assert isinstance(number, int) and ( - number >= 0 - ), "'number' must been an int and >= 0" + assert isinstance(number, int) and (number >= 0), ( + "'number' must been an int and >= 0" + ) ans = 0 @@ -399,9 +399,9 @@ def goldbach(number): """ # precondition - assert ( - isinstance(number, int) and (number > 2) and is_even(number) - ), "'number' must been an int, even and > 2" + assert isinstance(number, int) and (number > 2) and is_even(number), ( + "'number' must been an int, even and > 2" + ) ans = [] # this list will returned @@ -525,9 +525,9 @@ def kg_v(number1, number2): done.append(n) # precondition - assert isinstance(ans, int) and ( - ans >= 0 - ), "'ans' must been from type int and positive" + assert isinstance(ans, int) and (ans >= 0), ( + "'ans' must been from type int and positive" + ) return ans @@ -574,9 +574,9 @@ def get_prime(n): ans += 1 # precondition - assert isinstance(ans, int) and is_prime( - ans - ), "'ans' must been a prime number and from type int" + assert isinstance(ans, int) and is_prime(ans), ( + "'ans' must been a prime number and from type int" + ) return ans @@ -705,9 +705,9 @@ def is_perfect_number(number): """ # precondition - assert isinstance(number, int) and ( - number > 1 - ), "'number' must been an int and >= 1" + assert isinstance(number, int) and (number > 1), ( + "'number' must been an int and >= 1" + ) divisors = get_divisors(number) diff --git a/maths/trapezoidal_rule.py b/maths/trapezoidal_rule.py index 0186629ee378..21b10b239b5f 100644 --- a/maths/trapezoidal_rule.py +++ b/maths/trapezoidal_rule.py @@ -1,28 +1,25 @@ """ Numerical integration or quadrature for a smooth function f with known values at x_i - -This method is the classical approach of suming 'Equally Spaced Abscissas' - -method 1: -"extended trapezoidal rule" -int(f) = dx/2 * (f1 + 2f2 + ... + fn) - """ -def method_1(boundary, steps): +def trapezoidal_rule(boundary, steps): """ - Apply the extended trapezoidal rule to approximate the integral of function f(x) - over the interval defined by 'boundary' with the number of 'steps'. - - Args: - boundary (list of floats): A list containing the start and end values [a, b]. - steps (int): The number of steps or subintervals. - Returns: - float: Approximation of the integral of f(x) over [a, b]. - Examples: - >>> method_1([0, 1], 10) - 0.3349999999999999 + Implements the extended trapezoidal rule for numerical integration. + The function f(x) is provided below. + + :param boundary: List containing the lower and upper bounds of integration [a, b] + :param steps: The number of steps (intervals) used in the approximation + :return: The numerical approximation of the integral + + >>> abs(trapezoidal_rule([0, 1], 10) - 0.33333) < 0.01 + True + >>> abs(trapezoidal_rule([0, 1], 100) - 0.33333) < 0.01 + True + >>> abs(trapezoidal_rule([0, 2], 1000) - 2.66667) < 0.01 + True + >>> abs(trapezoidal_rule([1, 2], 1000) - 2.33333) < 0.01 + True """ h = (boundary[1] - boundary[0]) / steps a = boundary[0] @@ -31,7 +28,6 @@ def method_1(boundary, steps): y = 0.0 y += (h / 2.0) * f(a) for i in x_i: - # print(i) y += h * f(i) y += (h / 2.0) * f(b) return y @@ -39,49 +35,66 @@ def method_1(boundary, steps): def make_points(a, b, h): """ - Generates points between 'a' and 'b' with step size 'h', excluding the end points. - Args: - a (float): Start value - b (float): End value - h (float): Step size - Examples: + Generates points between a and b with step size h for trapezoidal integration. + + :param a: The lower bound of integration + :param b: The upper bound of integration + :param h: The step size + :yield: The next x-value in the range (a, b) + + >>> list(make_points(0, 1, 0.1)) # doctest: +NORMALIZE_WHITESPACE + [0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6, 0.7, 0.7999999999999999, \ + 0.8999999999999999] >>> list(make_points(0, 10, 2.5)) [2.5, 5.0, 7.5] - >>> list(make_points(0, 10, 2)) [2, 4, 6, 8] - >>> list(make_points(1, 21, 5)) [6, 11, 16] - >>> list(make_points(1, 5, 2)) [3] - >>> list(make_points(1, 4, 3)) [] """ x = a + h while x <= (b - h): yield x - x = x + h + x += h -def f(x): # enter your function here +def f(x): """ - Example: - >>> f(2) - 4 + This is the function to integrate, f(x) = (x - 0)^2 = x^2. + + :param x: The input value + :return: The value of f(x) + + >>> f(0) + 0 + >>> f(1) + 1 + >>> f(0.5) + 0.25 """ - y = (x - 0) * (x - 0) - return y + return x**2 def main(): - a = 0.0 # Lower bound of integration - b = 1.0 # Upper bound of integration - steps = 10.0 # define number of steps or resolution - boundary = [a, b] # define boundary of integration - y = method_1(boundary, steps) + """ + Main function to test the trapezoidal rule. + :a: Lower bound of integration + :b: Upper bound of integration + :steps: define number of steps or resolution + :boundary: define boundary of integration + + >>> main() + y = 0.3349999999999999 + """ + a = 0.0 + b = 1.0 + steps = 10.0 + boundary = [a, b] + y = trapezoidal_rule(boundary, steps) print(f"y = {y}") diff --git a/maths/zellers_congruence.py b/maths/zellers_congruence.py index 483fb000f86b..b958ed3b8659 100644 --- a/maths/zellers_congruence.py +++ b/maths/zellers_congruence.py @@ -4,13 +4,14 @@ def zeller(date_input: str) -> str: """ - Zellers Congruence Algorithm - Find the day of the week for nearly any Gregorian or Julian calendar date + | Zellers Congruence Algorithm + | Find the day of the week for nearly any Gregorian or Julian calendar date >>> zeller('01-31-2010') 'Your date 01-31-2010, is a Sunday!' - Validate out of range month + Validate out of range month: + >>> zeller('13-31-2010') Traceback (most recent call last): ... @@ -21,6 +22,7 @@ def zeller(date_input: str) -> str: ValueError: invalid literal for int() with base 10: '.2' Validate out of range date: + >>> zeller('01-33-2010') Traceback (most recent call last): ... @@ -31,30 +33,35 @@ def zeller(date_input: str) -> str: ValueError: invalid literal for int() with base 10: '.4' Validate second separator: + >>> zeller('01-31*2010') Traceback (most recent call last): ... ValueError: Date separator must be '-' or '/' Validate first separator: + >>> zeller('01^31-2010') Traceback (most recent call last): ... ValueError: Date separator must be '-' or '/' Validate out of range year: + >>> zeller('01-31-8999') Traceback (most recent call last): ... ValueError: Year out of range. There has to be some sort of limit...right? Test null input: + >>> zeller() Traceback (most recent call last): ... TypeError: zeller() missing 1 required positional argument: 'date_input' - Test length of date_input: + Test length of `date_input`: + >>> zeller('') Traceback (most recent call last): ... diff --git a/matrix/matrix_based_game.py b/matrix/matrix_based_game.py index 1ff0cbe93435..6181086c6704 100644 --- a/matrix/matrix_based_game.py +++ b/matrix/matrix_based_game.py @@ -273,7 +273,7 @@ def process_game(size: int, matrix: list[str], moves: list[tuple[int, int]]) -> size = int(input("Enter the size of the matrix: ")) validate_matrix_size(size) print(f"Enter the {size} rows of the matrix:") - matrix = [input(f"Row {i+1}: ") for i in range(size)] + matrix = [input(f"Row {i + 1}: ") for i in range(size)] validate_matrix_content(matrix, size) moves_input = input("Enter the moves (e.g., '0 0, 1 1'): ") moves = parse_moves(moves_input) diff --git a/neural_network/input_data.py b/neural_network/input_data.py index 72debabb566a..3a8628f939f8 100644 --- a/neural_network/input_data.py +++ b/neural_network/input_data.py @@ -160,9 +160,9 @@ def __init__( self._num_examples = 10000 self.one_hot = one_hot else: - assert ( - images.shape[0] == labels.shape[0] - ), f"images.shape: {images.shape} labels.shape: {labels.shape}" + assert images.shape[0] == labels.shape[0], ( + f"images.shape: {images.shape} labels.shape: {labels.shape}" + ) self._num_examples = images.shape[0] # Convert shape from [num examples, rows, columns, depth] diff --git a/other/bankers_algorithm.py b/other/bankers_algorithm.py index d4254f479a4f..b1da851fc0f3 100644 --- a/other/bankers_algorithm.py +++ b/other/bankers_algorithm.py @@ -10,9 +10,10 @@ predetermined maximum possible amounts of all resources, and then makes a "s-state" check to test for possible deadlock conditions for all other pending activities, before deciding whether allocation should be allowed to continue. -[Source] Wikipedia -[Credit] Rosetta Code C implementation helped very much. - (https://rosettacode.org/wiki/Banker%27s_algorithm) + +| [Source] Wikipedia +| [Credit] Rosetta Code C implementation helped very much. +| (https://rosettacode.org/wiki/Banker%27s_algorithm) """ from __future__ import annotations @@ -75,7 +76,7 @@ def __available_resources(self) -> list[int]: def __need(self) -> list[list[int]]: """ Implement safety checker that calculates the needs by ensuring that - max_claim[i][j] - alloc_table[i][j] <= avail[j] + ``max_claim[i][j] - alloc_table[i][j] <= avail[j]`` """ return [ list(np.array(self.__maximum_claim_table[i]) - np.array(allocated_resource)) @@ -86,7 +87,9 @@ def __need_index_manager(self) -> dict[int, list[int]]: """ This function builds an index control dictionary to track original ids/indices of processes when altered during execution of method "main" - Return: {0: [a: int, b: int], 1: [c: int, d: int]} + + :Return: {0: [a: int, b: int], 1: [c: int, d: int]} + >>> index_control = BankersAlgorithm( ... test_claim_vector, test_allocated_res_table, test_maximum_claim_table ... )._BankersAlgorithm__need_index_manager() @@ -100,7 +103,8 @@ def __need_index_manager(self) -> dict[int, list[int]]: def main(self, **kwargs) -> None: """ Utilize various methods in this class to simulate the Banker's algorithm - Return: None + :Return: None + >>> BankersAlgorithm(test_claim_vector, test_allocated_res_table, ... test_maximum_claim_table).main(describe=True) Allocated Resource Table diff --git a/other/davis_putnam_logemann_loveland.py b/other/davis_putnam_logemann_loveland.py index 0f3100b1bc2e..7d0bcce15a29 100644 --- a/other/davis_putnam_logemann_loveland.py +++ b/other/davis_putnam_logemann_loveland.py @@ -17,13 +17,15 @@ class Clause: """ - A clause represented in Conjunctive Normal Form. - A clause is a set of literals, either complemented or otherwise. + | A clause represented in Conjunctive Normal Form. + | A clause is a set of literals, either complemented or otherwise. + For example: - {A1, A2, A3'} is the clause (A1 v A2 v A3') - {A5', A2', A1} is the clause (A5' v A2' v A1) + * {A1, A2, A3'} is the clause (A1 v A2 v A3') + * {A5', A2', A1} is the clause (A5' v A2' v A1) Create model + >>> clause = Clause(["A1", "A2'", "A3"]) >>> clause.evaluate({"A1": True}) True @@ -34,11 +36,12 @@ def __init__(self, literals: list[str]) -> None: Represent the literals and an assignment in a clause." """ # Assign all literals to None initially - self.literals: dict[str, bool | None] = {literal: None for literal in literals} + self.literals: dict[str, bool | None] = dict.fromkeys(literals) def __str__(self) -> str: """ To print a clause as in Conjunctive Normal Form. + >>> str(Clause(["A1", "A2'", "A3"])) "{A1 , A2' , A3}" """ @@ -47,6 +50,7 @@ def __str__(self) -> str: def __len__(self) -> int: """ To print a clause as in Conjunctive Normal Form. + >>> len(Clause([])) 0 >>> len(Clause(["A1", "A2'", "A3"])) @@ -72,11 +76,13 @@ def assign(self, model: dict[str, bool | None]) -> None: def evaluate(self, model: dict[str, bool | None]) -> bool | None: """ Evaluates the clause with the assignments in model. + This has the following steps: - 1. Return True if both a literal and its complement exist in the clause. - 2. Return True if a single literal has the assignment True. - 3. Return None(unable to complete evaluation) if a literal has no assignment. - 4. Compute disjunction of all values assigned in clause. + 1. Return ``True`` if both a literal and its complement exist in the clause. + 2. Return ``True`` if a single literal has the assignment ``True``. + 3. Return ``None`` (unable to complete evaluation) + if a literal has no assignment. + 4. Compute disjunction of all values assigned in clause. """ for literal in self.literals: symbol = literal.rstrip("'") if literal.endswith("'") else literal + "'" @@ -92,10 +98,10 @@ def evaluate(self, model: dict[str, bool | None]) -> bool | None: class Formula: """ - A formula represented in Conjunctive Normal Form. - A formula is a set of clauses. - For example, - {{A1, A2, A3'}, {A5', A2', A1}} is ((A1 v A2 v A3') and (A5' v A2' v A1)) + | A formula represented in Conjunctive Normal Form. + | A formula is a set of clauses. + | For example, + | {{A1, A2, A3'}, {A5', A2', A1}} is ((A1 v A2 v A3') and (A5' v A2' v A1)) """ def __init__(self, clauses: Iterable[Clause]) -> None: @@ -107,7 +113,8 @@ def __init__(self, clauses: Iterable[Clause]) -> None: def __str__(self) -> str: """ To print a formula as in Conjunctive Normal Form. - str(Formula([Clause(["A1", "A2'", "A3"]), Clause(["A5'", "A2'", "A1"])])) + + >>> str(Formula([Clause(["A1", "A2'", "A3"]), Clause(["A5'", "A2'", "A1"])])) "{{A1 , A2' , A3} , {A5' , A2' , A1}}" """ return "{" + " , ".join(str(clause) for clause in self.clauses) + "}" @@ -115,8 +122,8 @@ def __str__(self) -> str: def generate_clause() -> Clause: """ - Randomly generate a clause. - All literals have the name Ax, where x is an integer from 1 to 5. + | Randomly generate a clause. + | All literals have the name Ax, where x is an integer from ``1`` to ``5``. """ literals = [] no_of_literals = random.randint(1, 5) @@ -149,11 +156,12 @@ def generate_formula() -> Formula: def generate_parameters(formula: Formula) -> tuple[list[Clause], list[str]]: """ - Return the clauses and symbols from a formula. - A symbol is the uncomplemented form of a literal. + | Return the clauses and symbols from a formula. + | A symbol is the uncomplemented form of a literal. + For example, - Symbol of A3 is A3. - Symbol of A5' is A5. + * Symbol of A3 is A3. + * Symbol of A5' is A5. >>> formula = Formula([Clause(["A1", "A2'", "A3"]), Clause(["A5'", "A2'", "A1"])]) >>> clauses, symbols = generate_parameters(formula) @@ -177,21 +185,20 @@ def find_pure_symbols( clauses: list[Clause], symbols: list[str], model: dict[str, bool | None] ) -> tuple[list[str], dict[str, bool | None]]: """ - Return pure symbols and their values to satisfy clause. - Pure symbols are symbols in a formula that exist only - in one form, either complemented or otherwise. - For example, - { { A4 , A3 , A5' , A1 , A3' } , { A4 } , { A3 } } has - pure symbols A4, A5' and A1. + | Return pure symbols and their values to satisfy clause. + | Pure symbols are symbols in a formula that exist only in one form, + | either complemented or otherwise. + | For example, + | {{A4 , A3 , A5' , A1 , A3'} , {A4} , {A3}} has pure symbols A4, A5' and A1. + This has the following steps: - 1. Ignore clauses that have already evaluated to be True. - 2. Find symbols that occur only in one form in the rest of the clauses. - 3. Assign value True or False depending on whether the symbols occurs - in normal or complemented form respectively. + 1. Ignore clauses that have already evaluated to be ``True``. + 2. Find symbols that occur only in one form in the rest of the clauses. + 3. Assign value ``True`` or ``False`` depending on whether the symbols occurs + in normal or complemented form respectively. >>> formula = Formula([Clause(["A1", "A2'", "A3"]), Clause(["A5'", "A2'", "A1"])]) >>> clauses, symbols = generate_parameters(formula) - >>> pure_symbols, values = find_pure_symbols(clauses, symbols, {}) >>> pure_symbols ['A1', 'A2', 'A3', 'A5'] @@ -231,20 +238,21 @@ def find_unit_clauses( ) -> tuple[list[str], dict[str, bool | None]]: """ Returns the unit symbols and their values to satisfy clause. + Unit symbols are symbols in a formula that are: - - Either the only symbol in a clause - - Or all other literals in that clause have been assigned False + - Either the only symbol in a clause + - Or all other literals in that clause have been assigned ``False`` + This has the following steps: - 1. Find symbols that are the only occurrences in a clause. - 2. Find symbols in a clause where all other literals are assigned False. - 3. Assign True or False depending on whether the symbols occurs in - normal or complemented form respectively. + 1. Find symbols that are the only occurrences in a clause. + 2. Find symbols in a clause where all other literals are assigned ``False``. + 3. Assign ``True`` or ``False`` depending on whether the symbols occurs in + normal or complemented form respectively. >>> clause1 = Clause(["A4", "A3", "A5'", "A1", "A3'"]) >>> clause2 = Clause(["A4"]) >>> clause3 = Clause(["A3"]) >>> clauses, symbols = generate_parameters(Formula([clause1, clause2, clause3])) - >>> unit_clauses, values = find_unit_clauses(clauses, {}) >>> unit_clauses ['A4', 'A3'] @@ -278,16 +286,16 @@ def dpll_algorithm( clauses: list[Clause], symbols: list[str], model: dict[str, bool | None] ) -> tuple[bool | None, dict[str, bool | None] | None]: """ - Returns the model if the formula is satisfiable, else None + Returns the model if the formula is satisfiable, else ``None`` + This has the following steps: - 1. If every clause in clauses is True, return True. - 2. If some clause in clauses is False, return False. - 3. Find pure symbols. - 4. Find unit symbols. + 1. If every clause in clauses is ``True``, return ``True``. + 2. If some clause in clauses is ``False``, return ``False``. + 3. Find pure symbols. + 4. Find unit symbols. >>> formula = Formula([Clause(["A4", "A3", "A5'", "A1", "A3'"]), Clause(["A4"])]) >>> clauses, symbols = generate_parameters(formula) - >>> soln, model = dpll_algorithm(clauses, symbols, {}) >>> soln True diff --git a/other/doomsday.py b/other/doomsday.py index d8fe261156a1..be3b18eeecaa 100644 --- a/other/doomsday.py +++ b/other/doomsday.py @@ -46,7 +46,7 @@ def get_week_day(year: int, month: int, day: int) -> str: ) % 7 day_anchor = ( DOOMSDAY_NOT_LEAP[month - 1] - if (year % 4 != 0) or (centurian == 0 and (year % 400) == 0) + if year % 4 != 0 or (centurian == 0 and year % 400 != 0) else DOOMSDAY_LEAP[month - 1] ) week_day = (dooms_day + day - day_anchor) % 7 diff --git a/other/lfu_cache.py b/other/lfu_cache.py index 788fdf19bb60..5a143c739b9d 100644 --- a/other/lfu_cache.py +++ b/other/lfu_cache.py @@ -196,9 +196,6 @@ class LFUCache(Generic[T, U]): CacheInfo(hits=196, misses=100, capacity=100, current_size=100) """ - # class variable to map the decorator functions to their respective instance - decorator_function_to_instance_map: dict[Callable[[T], U], LFUCache[T, U]] = {} - def __init__(self, capacity: int): self.list: DoubleLinkedList[T, U] = DoubleLinkedList() self.capacity = capacity @@ -291,18 +288,23 @@ def decorator( """ def cache_decorator_inner(func: Callable[[T], U]) -> Callable[..., U]: + # variable to map the decorator functions to their respective instance + decorator_function_to_instance_map: dict[ + Callable[[T], U], LFUCache[T, U] + ] = {} + def cache_decorator_wrapper(*args: T) -> U: - if func not in cls.decorator_function_to_instance_map: - cls.decorator_function_to_instance_map[func] = LFUCache(size) + if func not in decorator_function_to_instance_map: + decorator_function_to_instance_map[func] = LFUCache(size) - result = cls.decorator_function_to_instance_map[func].get(args[0]) + result = decorator_function_to_instance_map[func].get(args[0]) if result is None: result = func(*args) - cls.decorator_function_to_instance_map[func].put(args[0], result) + decorator_function_to_instance_map[func].put(args[0], result) return result def cache_info() -> LFUCache[T, U]: - return cls.decorator_function_to_instance_map[func] + return decorator_function_to_instance_map[func] setattr(cache_decorator_wrapper, "cache_info", cache_info) # noqa: B010 diff --git a/other/lru_cache.py b/other/lru_cache.py index 1e5eeac45b4e..4f0c843c86cc 100644 --- a/other/lru_cache.py +++ b/other/lru_cache.py @@ -209,9 +209,6 @@ class LRUCache(Generic[T, U]): CacheInfo(hits=194, misses=99, capacity=100, current size=99) """ - # class variable to map the decorator functions to their respective instance - decorator_function_to_instance_map: dict[Callable[[T], U], LRUCache[T, U]] = {} - def __init__(self, capacity: int): self.list: DoubleLinkedList[T, U] = DoubleLinkedList() self.capacity = capacity @@ -308,18 +305,23 @@ def decorator( """ def cache_decorator_inner(func: Callable[[T], U]) -> Callable[..., U]: + # variable to map the decorator functions to their respective instance + decorator_function_to_instance_map: dict[ + Callable[[T], U], LRUCache[T, U] + ] = {} + def cache_decorator_wrapper(*args: T) -> U: - if func not in cls.decorator_function_to_instance_map: - cls.decorator_function_to_instance_map[func] = LRUCache(size) + if func not in decorator_function_to_instance_map: + decorator_function_to_instance_map[func] = LRUCache(size) - result = cls.decorator_function_to_instance_map[func].get(args[0]) + result = decorator_function_to_instance_map[func].get(args[0]) if result is None: result = func(*args) - cls.decorator_function_to_instance_map[func].put(args[0], result) + decorator_function_to_instance_map[func].put(args[0], result) return result def cache_info() -> LRUCache[T, U]: - return cls.decorator_function_to_instance_map[func] + return decorator_function_to_instance_map[func] setattr(cache_decorator_wrapper, "cache_info", cache_info) # noqa: B010 diff --git a/other/quine.py b/other/quine.py index 08e885bc1ce7..0fc78333fed1 100644 --- a/other/quine.py +++ b/other/quine.py @@ -1,5 +1,5 @@ #!/bin/python3 -# ruff: noqa +# ruff: noqa: PLC3002 """ Quine: diff --git a/other/scoring_algorithm.py b/other/scoring_algorithm.py index af04f432e433..0185d7a2e0c0 100644 --- a/other/scoring_algorithm.py +++ b/other/scoring_algorithm.py @@ -1,25 +1,26 @@ """ -developed by: markmelnic -original repo: https://github.com/markmelnic/Scoring-Algorithm +| developed by: markmelnic +| original repo: https://github.com/markmelnic/Scoring-Algorithm Analyse data using a range based percentual proximity algorithm and calculate the linear maximum likelihood estimation. The basic principle is that all values supplied will be broken -down to a range from 0 to 1 and each column's score will be added +down to a range from ``0`` to ``1`` and each column's score will be added up to get the total score. -========== Example for data of vehicles -price|mileage|registration_year -20k |60k |2012 -22k |50k |2011 -23k |90k |2015 -16k |210k |2010 +:: + + price|mileage|registration_year + 20k |60k |2012 + 22k |50k |2011 + 23k |90k |2015 + 16k |210k |2010 We want the vehicle with the lowest price, lowest mileage but newest registration year. Thus the weights for each column are as follows: -[0, 0, 1] +``[0, 0, 1]`` """ @@ -97,10 +98,11 @@ def procentual_proximity( source_data: list[list[float]], weights: list[int] ) -> list[list[float]]: """ - weights - int list - possible values - 0 / 1 - 0 if lower values have higher weight in the data set - 1 if higher values have higher weight in the data set + | `weights` - ``int`` list + | possible values - ``0`` / ``1`` + + * ``0`` if lower values have higher weight in the data set + * ``1`` if higher values have higher weight in the data set >>> procentual_proximity([[20, 60, 2012],[23, 90, 2015],[22, 50, 2011]], [0, 0, 1]) [[20, 60, 2012, 2.0], [23, 90, 2015, 1.0], [22, 50, 2011, 1.3333333333333335]] diff --git a/physics/newtons_second_law_of_motion.py b/physics/newtons_second_law_of_motion.py index 53fab6ce78b9..4149e2494f31 100644 --- a/physics/newtons_second_law_of_motion.py +++ b/physics/newtons_second_law_of_motion.py @@ -1,18 +1,22 @@ -""" -Description : -Newton's second law of motion pertains to the behavior of objects for which -all existing forces are not balanced. -The second law states that the acceleration of an object is dependent upon two variables -- the net force acting upon the object and the mass of the object. -The acceleration of an object depends directly -upon the net force acting upon the object, -and inversely upon the mass of the object. -As the force acting upon an object is increased, -the acceleration of the object is increased. -As the mass of an object is increased, the acceleration of the object is decreased. +r""" +Description: + Newton's second law of motion pertains to the behavior of objects for which + all existing forces are not balanced. + The second law states that the acceleration of an object is dependent upon + two variables - the net force acting upon the object and the mass of the object. + The acceleration of an object depends directly + upon the net force acting upon the object, + and inversely upon the mass of the object. + As the force acting upon an object is increased, + the acceleration of the object is increased. + As the mass of an object is increased, the acceleration of the object is decreased. + Source: https://www.physicsclassroom.com/class/newtlaws/Lesson-3/Newton-s-Second-Law -Formulation: Fnet = m • a -Diagrammatic Explanation: + +Formulation: F_net = m • a + +Diagrammatic Explanation:: + Forces are unbalanced | | @@ -26,35 +30,42 @@ / \ / \ / \ - __________________ ____ ________________ - |The acceleration | |The acceleration | - |depends directly | |depends inversely | - |on the net Force | |upon the object's | - |_________________| |mass_______________| -Units: -1 Newton = 1 kg X meters / (seconds^2) + __________________ ____________________ + | The acceleration | | The acceleration | + | depends directly | | depends inversely | + | on the net force | | upon the object's | + | | | mass | + |__________________| |____________________| + +Units: 1 Newton = 1 kg • meters/seconds^2 + How to use? -Inputs: - ___________________________________________________ - |Name | Units | Type | - |-------------|-------------------------|-----------| - |mass | (in kgs) | float | - |-------------|-------------------------|-----------| - |acceleration | (in meters/(seconds^2)) | float | - |_____________|_________________________|___________| - -Output: - ___________________________________________________ - |Name | Units | Type | - |-------------|-------------------------|-----------| - |force | (in Newtons) | float | - |_____________|_________________________|___________| + +Inputs:: + + ______________ _____________________ ___________ + | Name | Units | Type | + |--------------|---------------------|-----------| + | mass | in kgs | float | + |--------------|---------------------|-----------| + | acceleration | in meters/seconds^2 | float | + |______________|_____________________|___________| + +Output:: + + ______________ _______________________ ___________ + | Name | Units | Type | + |--------------|-----------------------|-----------| + | force | in Newtons | float | + |______________|_______________________|___________| """ def newtons_second_law_of_motion(mass: float, acceleration: float) -> float: """ + Calculates force from `mass` and `acceleration` + >>> newtons_second_law_of_motion(10, 10) 100 >>> newtons_second_law_of_motion(2.0, 1) diff --git a/physics/period_of_pendulum.py b/physics/period_of_pendulum.py new file mode 100644 index 000000000000..2e3c7bc3ef1e --- /dev/null +++ b/physics/period_of_pendulum.py @@ -0,0 +1,53 @@ +""" +Title : Computing the time period of a simple pendulum + +The simple pendulum is a mechanical system that sways or moves in an +oscillatory motion. The simple pendulum comprises of a small bob of +mass m suspended by a thin string of length L and secured to a platform +at its upper end. Its motion occurs in a vertical plane and is mainly +driven by gravitational force. The period of the pendulum depends on the +length of the string and the amplitude (the maximum angle) of oscillation. +However, the effect of the amplitude can be ignored if the amplitude is +small. It should be noted that the period does not depend on the mass of +the bob. + +For small amplitudes, the period of a simple pendulum is given by the +following approximation: +T ≈ 2π * √(L / g) + +where: +L = length of string from which the bob is hanging (in m) +g = acceleration due to gravity (approx 9.8 m/s²) + +Reference : https://byjus.com/jee/simple-pendulum/ +""" + +from math import pi + +from scipy.constants import g + + +def period_of_pendulum(length: float) -> float: + """ + >>> period_of_pendulum(1.23) + 2.2252155506257845 + >>> period_of_pendulum(2.37) + 3.0888278441908574 + >>> period_of_pendulum(5.63) + 4.76073193364765 + >>> period_of_pendulum(-12) + Traceback (most recent call last): + ... + ValueError: The length should be non-negative + >>> period_of_pendulum(0) + 0.0 + """ + if length < 0: + raise ValueError("The length should be non-negative") + return 2 * pi * (length / g) ** 0.5 + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/physics/speeds_of_gas_molecules.py b/physics/speeds_of_gas_molecules.py index a50d1c0f6d76..42f90a9fd6f3 100644 --- a/physics/speeds_of_gas_molecules.py +++ b/physics/speeds_of_gas_molecules.py @@ -4,43 +4,43 @@ distribution is a probability distribution that describes the distribution of speeds of particles in an ideal gas. -The distribution is given by the following equation: +The distribution is given by the following equation:: ------------------------------------------------- | f(v) = (M/2πRT)^(3/2) * 4πv^2 * e^(-Mv^2/2RT) | ------------------------------------------------- where: - f(v) is the fraction of molecules with a speed v - M is the molar mass of the gas in kg/mol - R is the gas constant - T is the absolute temperature + * ``f(v)`` is the fraction of molecules with a speed ``v`` + * ``M`` is the molar mass of the gas in kg/mol + * ``R`` is the gas constant + * ``T`` is the absolute temperature More information about the Maxwell-Boltzmann distribution can be found here: https://en.wikipedia.org/wiki/Maxwell%E2%80%93Boltzmann_distribution The average speed can be calculated by integrating the Maxwell-Boltzmann distribution -from 0 to infinity and dividing by the total number of molecules. The result is: +from 0 to infinity and dividing by the total number of molecules. The result is:: - --------------------- - | vavg = √(8RT/πM) | - --------------------- + ---------------------- + | v_avg = √(8RT/πM) | + ---------------------- The most probable speed is the speed at which the Maxwell-Boltzmann distribution is at its maximum. This can be found by differentiating the Maxwell-Boltzmann -distribution with respect to v and setting the result equal to zero. The result is: +distribution with respect to ``v`` and setting the result equal to zero. The result is:: - --------------------- - | vmp = √(2RT/M) | - --------------------- + ---------------------- + | v_mp = √(2RT/M) | + ---------------------- The root-mean-square speed is another measure of the average speed of the molecules in a gas. It is calculated by taking the square root -of the average of the squares of the speeds of the molecules. The result is: +of the average of the squares of the speeds of the molecules. The result is:: - --------------------- - | vrms = √(3RT/M) | - --------------------- + ---------------------- + | v_rms = √(3RT/M) | + ---------------------- Here we have defined functions to calculate the average and most probable speeds of molecules in a gas given the @@ -57,6 +57,7 @@ def avg_speed_of_molecule(temperature: float, molar_mass: float) -> float: and returns the average speed of a molecule in the gas (in m/s). Examples: + >>> avg_speed_of_molecule(273, 0.028) # nitrogen at 273 K 454.3488755020387 >>> avg_speed_of_molecule(300, 0.032) # oxygen at 300 K @@ -84,6 +85,7 @@ def mps_speed_of_molecule(temperature: float, molar_mass: float) -> float: and returns the most probable speed of a molecule in the gas (in m/s). Examples: + >>> mps_speed_of_molecule(273, 0.028) # nitrogen at 273 K 402.65620701908966 >>> mps_speed_of_molecule(300, 0.032) # oxygen at 300 K diff --git a/project_euler/problem_002/sol4.py b/project_euler/problem_002/sol4.py index 3a2e4fce341c..a13d34fd760e 100644 --- a/project_euler/problem_002/sol4.py +++ b/project_euler/problem_002/sol4.py @@ -61,7 +61,7 @@ def solution(n: int = 4000000) -> int: if n <= 0: raise ValueError("Parameter n must be greater than or equal to one.") getcontext().prec = 100 - phi = (Decimal(5) ** Decimal(0.5) + 1) / Decimal(2) + phi = (Decimal(5) ** Decimal("0.5") + 1) / Decimal(2) index = (math.floor(math.log(n * (phi + 2), phi) - 1) // 3) * 3 + 2 num = Decimal(round(phi ** Decimal(index + 1))) / (phi + 2) diff --git a/project_euler/problem_028/sol1.py b/project_euler/problem_028/sol1.py index 1ea5d4fcafd4..0a4648af36c4 100644 --- a/project_euler/problem_028/sol1.py +++ b/project_euler/problem_028/sol1.py @@ -37,7 +37,7 @@ def solution(n: int = 1001) -> int: """ total = 1 - for i in range(1, int(ceil(n / 2.0))): + for i in range(1, ceil(n / 2.0)): odd = 2 * i + 1 even = 2 * i total = total + 4 * odd**2 - 6 * even diff --git a/project_euler/problem_122/__init__.py b/project_euler/problem_122/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/project_euler/problem_122/sol1.py b/project_euler/problem_122/sol1.py new file mode 100644 index 000000000000..cd8b1e67708c --- /dev/null +++ b/project_euler/problem_122/sol1.py @@ -0,0 +1,89 @@ +""" +Project Euler Problem 122: https://projecteuler.net/problem=122 + +Efficient Exponentiation + +The most naive way of computing n^15 requires fourteen multiplications: + + n x n x ... x n = n^15. + +But using a "binary" method you can compute it in six multiplications: + + n x n = n^2 + n^2 x n^2 = n^4 + n^4 x n^4 = n^8 + n^8 x n^4 = n^12 + n^12 x n^2 = n^14 + n^14 x n = n^15 + +However it is yet possible to compute it in only five multiplications: + + n x n = n^2 + n^2 x n = n^3 + n^3 x n^3 = n^6 + n^6 x n^6 = n^12 + n^12 x n^3 = n^15 + +We shall define m(k) to be the minimum number of multiplications to compute n^k; +for example m(15) = 5. + +Find sum_{k = 1}^200 m(k). + +It uses the fact that for rather small n, applicable for this problem, the solution +for each number can be formed by increasing the largest element. + +References: +- https://en.wikipedia.org/wiki/Addition_chain +""" + + +def solve(nums: list[int], goal: int, depth: int) -> bool: + """ + Checks if nums can have a sum equal to goal, given that length of nums does + not exceed depth. + + >>> solve([1], 2, 2) + True + >>> solve([1], 2, 0) + False + """ + if len(nums) > depth: + return False + for el in nums: + if el + nums[-1] == goal: + return True + nums.append(el + nums[-1]) + if solve(nums=nums, goal=goal, depth=depth): + return True + del nums[-1] + return False + + +def solution(n: int = 200) -> int: + """ + Calculates sum of smallest number of multiplactions for each number up to + and including n. + + >>> solution(1) + 0 + >>> solution(2) + 1 + >>> solution(14) + 45 + >>> solution(15) + 50 + """ + total = 0 + for i in range(2, n + 1): + max_length = 0 + while True: + nums = [1] + max_length += 1 + if solve(nums=nums, goal=i, depth=max_length): + break + total += max_length + return total + + +if __name__ == "__main__": + print(f"{solution() = }") diff --git a/project_euler/problem_136/__init__.py b/project_euler/problem_136/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/project_euler/problem_136/sol1.py b/project_euler/problem_136/sol1.py new file mode 100644 index 000000000000..688a9a5d7f24 --- /dev/null +++ b/project_euler/problem_136/sol1.py @@ -0,0 +1,63 @@ +""" +Project Euler Problem 136: https://projecteuler.net/problem=136 + +Singleton Difference + +The positive integers, x, y, and z, are consecutive terms of an arithmetic progression. +Given that n is a positive integer, the equation, x^2 - y^2 - z^2 = n, +has exactly one solution when n = 20: + 13^2 - 10^2 - 7^2 = 20. + +In fact there are twenty-five values of n below one hundred for which +the equation has a unique solution. + +How many values of n less than fifty million have exactly one solution? + +By change of variables + +x = y + delta +z = y - delta + +The expression can be rewritten: + +x^2 - y^2 - z^2 = y * (4 * delta - y) = n + +The algorithm loops over delta and y, which is restricted in upper and lower limits, +to count how many solutions each n has. +In the end it is counted how many n's have one solution. +""" + + +def solution(n_limit: int = 50 * 10**6) -> int: + """ + Define n count list and loop over delta, y to get the counts, then check + which n has count == 1. + + >>> solution(3) + 0 + >>> solution(10) + 3 + >>> solution(100) + 25 + >>> solution(110) + 27 + """ + n_sol = [0] * n_limit + + for delta in range(1, (n_limit + 1) // 4 + 1): + for y in range(4 * delta - 1, delta, -1): + n = y * (4 * delta - y) + if n >= n_limit: + break + n_sol[n] += 1 + + ans = 0 + for i in range(n_limit): + if n_sol[i] == 1: + ans += 1 + + return ans + + +if __name__ == "__main__": + print(f"{solution() = }") diff --git a/project_euler/problem_164/__init__.py b/project_euler/problem_164/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/project_euler/problem_164/sol1.py b/project_euler/problem_164/sol1.py new file mode 100644 index 000000000000..5387c89bd757 --- /dev/null +++ b/project_euler/problem_164/sol1.py @@ -0,0 +1,65 @@ +""" +Project Euler Problem 164: https://projecteuler.net/problem=164 + +Three Consecutive Digital Sum Limit + +How many 20 digit numbers n (without any leading zero) exist such that no three +consecutive digits of n have a sum greater than 9? + +Brute-force recursive solution with caching of intermediate results. +""" + + +def solve( + digit: int, prev1: int, prev2: int, sum_max: int, first: bool, cache: dict[str, int] +) -> int: + """ + Solve for remaining 'digit' digits, with previous 'prev1' digit, and + previous-previous 'prev2' digit, total sum of 'sum_max'. + Pass around 'cache' to store/reuse intermediate results. + + >>> solve(digit=1, prev1=0, prev2=0, sum_max=9, first=True, cache={}) + 9 + >>> solve(digit=1, prev1=0, prev2=0, sum_max=9, first=False, cache={}) + 10 + """ + if digit == 0: + return 1 + + cache_str = f"{digit},{prev1},{prev2}" + if cache_str in cache: + return cache[cache_str] + + comb = 0 + for curr in range(sum_max - prev1 - prev2 + 1): + if first and curr == 0: + continue + + comb += solve( + digit=digit - 1, + prev1=curr, + prev2=prev1, + sum_max=sum_max, + first=False, + cache=cache, + ) + + cache[cache_str] = comb + return comb + + +def solution(n_digits: int = 20) -> int: + """ + Solves the problem for n_digits number of digits. + + >>> solution(2) + 45 + >>> solution(10) + 21838806 + """ + cache: dict[str, int] = {} + return solve(digit=n_digits, prev1=0, prev2=0, sum_max=9, first=True, cache=cache) + + +if __name__ == "__main__": + print(f"{solution(10) = }") diff --git a/project_euler/problem_190/__init__.py b/project_euler/problem_190/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/project_euler/problem_190/sol1.py b/project_euler/problem_190/sol1.py new file mode 100644 index 000000000000..b18d45be16b4 --- /dev/null +++ b/project_euler/problem_190/sol1.py @@ -0,0 +1,48 @@ +""" +Project Euler Problem 190: https://projecteuler.net/problem=190 + +Maximising a Weighted Product + +Let S_m = (x_1, x_2, ..., x_m) be the m-tuple of positive real numbers with +x_1 + x_2 + ... + x_m = m for which P_m = x_1 * x_2^2 * ... * x_m^m is maximised. + +For example, it can be verified that |_ P_10 _| = 4112 +(|_ _| is the integer part function). + +Find Sum_{m=2}^15 = |_ P_m _|. + +Solution: +- Fix x_1 = m - x_2 - ... - x_m. +- Calculate partial derivatives of P_m wrt the x_2, ..., x_m. This gives that + x_2 = 2 * x_1, x_3 = 3 * x_1, ..., x_m = m * x_1. +- Calculate partial second order derivatives of P_m wrt the x_2, ..., x_m. + By plugging in the values from the previous step, can verify that solution is maximum. +""" + + +def solution(n: int = 15) -> int: + """ + Calculate sum of |_ P_m _| for m from 2 to n. + + >>> solution(2) + 1 + >>> solution(3) + 2 + >>> solution(4) + 4 + >>> solution(5) + 10 + """ + total = 0 + for m in range(2, n + 1): + x1 = 2 / (m + 1) + p = 1.0 + for i in range(1, m + 1): + xi = i * x1 + p *= xi**i + total += int(p) + return total + + +if __name__ == "__main__": + print(f"{solution() = }") diff --git a/project_euler/problem_345/__init__.py b/project_euler/problem_345/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/project_euler/problem_345/sol1.py b/project_euler/problem_345/sol1.py new file mode 100644 index 000000000000..4234458c5ad5 --- /dev/null +++ b/project_euler/problem_345/sol1.py @@ -0,0 +1,117 @@ +""" +Project Euler Problem 345: https://projecteuler.net/problem=345 + +Matrix Sum + +We define the Matrix Sum of a matrix as the maximum possible sum of +matrix elements such that none of the selected elements share the same row or column. + +For example, the Matrix Sum of the matrix below equals +3315 ( = 863 + 383 + 343 + 959 + 767): + 7 53 183 439 863 + 497 383 563 79 973 + 287 63 343 169 583 + 627 343 773 959 943 + 767 473 103 699 303 + +Find the Matrix Sum of: + 7 53 183 439 863 497 383 563 79 973 287 63 343 169 583 + 627 343 773 959 943 767 473 103 699 303 957 703 583 639 913 + 447 283 463 29 23 487 463 993 119 883 327 493 423 159 743 + 217 623 3 399 853 407 103 983 89 463 290 516 212 462 350 + 960 376 682 962 300 780 486 502 912 800 250 346 172 812 350 + 870 456 192 162 593 473 915 45 989 873 823 965 425 329 803 + 973 965 905 919 133 673 665 235 509 613 673 815 165 992 326 + 322 148 972 962 286 255 941 541 265 323 925 281 601 95 973 + 445 721 11 525 473 65 511 164 138 672 18 428 154 448 848 + 414 456 310 312 798 104 566 520 302 248 694 976 430 392 198 + 184 829 373 181 631 101 969 613 840 740 778 458 284 760 390 + 821 461 843 513 17 901 711 993 293 157 274 94 192 156 574 + 34 124 4 878 450 476 712 914 838 669 875 299 823 329 699 + 815 559 813 459 522 788 168 586 966 232 308 833 251 631 107 + 813 883 451 509 615 77 281 613 459 205 380 274 302 35 805 + +Brute force solution, with caching intermediate steps to speed up the calculation. +""" + +import numpy as np +from numpy.typing import NDArray + +MATRIX_1 = [ + "7 53 183 439 863", + "497 383 563 79 973", + "287 63 343 169 583", + "627 343 773 959 943", + "767 473 103 699 303", +] + +MATRIX_2 = [ + "7 53 183 439 863 497 383 563 79 973 287 63 343 169 583", + "627 343 773 959 943 767 473 103 699 303 957 703 583 639 913", + "447 283 463 29 23 487 463 993 119 883 327 493 423 159 743", + "217 623 3 399 853 407 103 983 89 463 290 516 212 462 350", + "960 376 682 962 300 780 486 502 912 800 250 346 172 812 350", + "870 456 192 162 593 473 915 45 989 873 823 965 425 329 803", + "973 965 905 919 133 673 665 235 509 613 673 815 165 992 326", + "322 148 972 962 286 255 941 541 265 323 925 281 601 95 973", + "445 721 11 525 473 65 511 164 138 672 18 428 154 448 848", + "414 456 310 312 798 104 566 520 302 248 694 976 430 392 198", + "184 829 373 181 631 101 969 613 840 740 778 458 284 760 390", + "821 461 843 513 17 901 711 993 293 157 274 94 192 156 574", + "34 124 4 878 450 476 712 914 838 669 875 299 823 329 699", + "815 559 813 459 522 788 168 586 966 232 308 833 251 631 107", + "813 883 451 509 615 77 281 613 459 205 380 274 302 35 805", +] + + +def solve(arr: NDArray, row: int, cols: set[int], cache: dict[str, int]) -> int: + """ + Finds the max sum for array `arr` starting with row index `row`, and with columns + included in `cols`. `cache` is used for caching intermediate results. + + >>> solve(arr=np.array([[1, 2], [3, 4]]), row=0, cols={0, 1}, cache={}) + 5 + """ + + cache_id = f"{row}, {sorted(cols)}" + if cache_id in cache: + return cache[cache_id] + + if row == len(arr): + return 0 + + max_sum = 0 + for col in cols: + new_cols = cols - {col} + max_sum = max( + max_sum, + int(arr[row, col]) + + solve(arr=arr, row=row + 1, cols=new_cols, cache=cache), + ) + cache[cache_id] = max_sum + return max_sum + + +def solution(matrix_str: list[str] = MATRIX_2) -> int: + """ + Takes list of strings `matrix_str` to parse the matrix and calculates the max sum. + + >>> solution(["1 2", "3 4"]) + 5 + >>> solution(MATRIX_1) + 3315 + """ + + n = len(matrix_str) + arr = np.empty(shape=(n, n), dtype=int) + for row, matrix_row_str in enumerate(matrix_str): + matrix_row_list_str = matrix_row_str.split() + for col, elem_str in enumerate(matrix_row_list_str): + arr[row, col] = int(elem_str) + + cache: dict[str, int] = {} + return solve(arr=arr, row=0, cols=set(range(n)), cache=cache) + + +if __name__ == "__main__": + print(f"{solution() = }") diff --git a/pyproject.toml b/pyproject.toml index 7b7176705c44..60f8d4ffc96f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -119,6 +119,7 @@ lint.ignore = [ "PT018", # Assertion should be broken down into multiple parts "S101", # Use of `assert` detected -- DO NOT FIX "S311", # Standard pseudo-random generators are not suitable for cryptographic purposes -- FIX ME + "SIM905", # Consider using a list literal instead of `str.split` -- DO NOT FIX "SLF001", # Private member accessed: `_Iterator` -- FIX ME "UP038", # Use `X | Y` in `{}` call instead of `(X, Y)` -- DO NOT FIX ] @@ -135,9 +136,6 @@ lint.per-file-ignores."machine_learning/sequential_minimum_optimization.py" = [ lint.per-file-ignores."matrix/sherman_morrison.py" = [ "SIM103", ] -lint.per-file-ignores."other/l*u_cache.py" = [ - "RUF012", -] lint.per-file-ignores."physics/newtons_second_law_of_motion.py" = [ "BLE001", ] @@ -159,7 +157,7 @@ lint.pylint.max-returns = 8 # default: 6 lint.pylint.max-statements = 88 # default: 50 [tool.codespell] -ignore-words-list = "3rt,ans,bitap,crate,damon,fo,followings,hist,iff,kwanza,manuel,mater,secant,som,sur,tim,toi,zar" +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/dictionary.txt,strings/words.txt" [tool.pytest.ini_options] diff --git a/requirements.txt b/requirements.txt index 4cc83f44987d..b104505e01bc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ beautifulsoup4 -fake_useragent +fake-useragent imageio keras lxml @@ -11,7 +11,7 @@ pillow requests rich scikit-learn -sphinx_pyproject +sphinx-pyproject statsmodels sympy tweepy diff --git a/scripts/validate_filenames.py b/scripts/validate_filenames.py index e76b4dbfe288..80399673cced 100755 --- a/scripts/validate_filenames.py +++ b/scripts/validate_filenames.py @@ -9,28 +9,25 @@ filepaths = list(good_file_paths()) assert filepaths, "good_file_paths() failed!" -upper_files = [file for file in filepaths if file != file.lower()] -if upper_files: +if upper_files := [file for file in filepaths if file != file.lower()]: print(f"{len(upper_files)} files contain uppercase characters:") print("\n".join(upper_files) + "\n") -space_files = [file for file in filepaths if " " in file] -if space_files: +if space_files := [file for file in filepaths if " " in file]: print(f"{len(space_files)} files contain space characters:") print("\n".join(space_files) + "\n") -hyphen_files = [file for file in filepaths if "-" in file] -if hyphen_files: +if hyphen_files := [ + file for file in filepaths if "-" in file and "/site-packages/" not in file +]: print(f"{len(hyphen_files)} files contain hyphen characters:") print("\n".join(hyphen_files) + "\n") -nodir_files = [file for file in filepaths if os.sep not in file] -if nodir_files: +if nodir_files := [file for file in filepaths if os.sep not in file]: print(f"{len(nodir_files)} files are not in a directory:") print("\n".join(nodir_files) + "\n") -bad_files = len(upper_files + space_files + hyphen_files + nodir_files) -if bad_files: +if bad_files := len(upper_files + space_files + hyphen_files + nodir_files): import sys sys.exit(bad_files) diff --git a/scripts/validate_solutions.py b/scripts/validate_solutions.py index 325c245e0d77..df5d01086bbe 100755 --- a/scripts/validate_solutions.py +++ b/scripts/validate_solutions.py @@ -94,6 +94,6 @@ def test_project_euler(solution_path: pathlib.Path) -> None: solution_module = convert_path_to_module(solution_path) answer = str(solution_module.solution()) answer = hashlib.sha256(answer.encode()).hexdigest() - assert ( - answer == expected - ), f"Expected solution to {problem_number} to have hash {expected}, got {answer}" + assert answer == expected, ( + f"Expected solution to {problem_number} to have hash {expected}, got {answer}" + ) diff --git a/sorts/bubble_sort.py b/sorts/bubble_sort.py index bdf85c70dd35..9ec3d5384f38 100644 --- a/sorts/bubble_sort.py +++ b/sorts/bubble_sort.py @@ -85,6 +85,8 @@ def bubble_sort_recursive(collection: list[Any]) -> list[Any]: [1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7] >>> bubble_sort_recursive([1, 3.3, 5, 7.7, 2, 4.4, 6]) [1, 2, 3.3, 4.4, 5, 6, 7.7] + >>> bubble_sort_recursive(['a', 'Z', 'B', 'C', 'A', 'c']) + ['A', 'B', 'C', 'Z', 'a', 'c'] >>> import random >>> collection_arg = random.sample(range(-50, 50), 100) >>> bubble_sort_recursive(collection_arg) == sorted(collection_arg) diff --git a/sorts/external_sort.py b/sorts/external_sort.py index 3fa7cacc0592..cfddee4fe7f8 100644 --- a/sorts/external_sort.py +++ b/sorts/external_sort.py @@ -61,7 +61,7 @@ def __init__(self, files): self.files = files self.empty = set() self.num_buffers = len(files) - self.buffers = {i: None for i in range(self.num_buffers)} + self.buffers = dict.fromkeys(range(self.num_buffers)) def get_dict(self): return { diff --git a/sorts/heap_sort.py b/sorts/heap_sort.py index 4dca879bd89c..44ee1d4b39f1 100644 --- a/sorts/heap_sort.py +++ b/sorts/heap_sort.py @@ -1,17 +1,22 @@ """ -This is a pure Python implementation of the heap sort algorithm. - -For doctests run following command: -python -m doctest -v heap_sort.py -or -python3 -m doctest -v heap_sort.py - -For manual testing run: -python heap_sort.py +A pure Python implementation of the heap sort algorithm. """ -def heapify(unsorted, index, heap_size): +def heapify(unsorted: list[int], index: int, heap_size: int) -> None: + """ + :param unsorted: unsorted list containing integers numbers + :param index: index + :param heap_size: size of the heap + :return: None + >>> unsorted = [1, 4, 3, 5, 2] + >>> heapify(unsorted, 0, len(unsorted)) + >>> unsorted + [4, 5, 3, 1, 2] + >>> heapify(unsorted, 0, len(unsorted)) + >>> unsorted + [5, 4, 3, 1, 2] + """ largest = index left_index = 2 * index + 1 right_index = 2 * index + 2 @@ -22,26 +27,26 @@ def heapify(unsorted, index, heap_size): largest = right_index if largest != index: - unsorted[largest], unsorted[index] = unsorted[index], unsorted[largest] + unsorted[largest], unsorted[index] = (unsorted[index], unsorted[largest]) heapify(unsorted, largest, heap_size) -def heap_sort(unsorted): +def heap_sort(unsorted: list[int]) -> list[int]: """ - Pure implementation of the heap sort algorithm in Python - :param collection: some mutable ordered collection with heterogeneous - comparable items inside + A pure Python implementation of the heap sort algorithm + + :param collection: a mutable ordered collection of heterogeneous comparable items :return: the same collection ordered by ascending Examples: >>> heap_sort([0, 5, 3, 2, 2]) [0, 2, 2, 3, 5] - >>> heap_sort([]) [] - >>> heap_sort([-2, -5, -45]) [-45, -5, -2] + >>> heap_sort([3, 7, 9, 28, 123, -5, 8, -30, -200, 0, 4]) + [-200, -30, -5, 0, 3, 4, 7, 8, 9, 28, 123] """ n = len(unsorted) for i in range(n // 2 - 1, -1, -1): @@ -53,6 +58,10 @@ def heap_sort(unsorted): if __name__ == "__main__": + import doctest + + doctest.testmod() user_input = input("Enter numbers separated by a comma:\n").strip() - unsorted = [int(item) for item in user_input.split(",")] - print(heap_sort(unsorted)) + if user_input: + unsorted = [int(item) for item in user_input.split(",")] + print(f"{heap_sort(unsorted) = }") diff --git a/sorts/quick_sort_3_partition.py b/sorts/quick_sort_3_partition.py index 1a6db6a364f0..279b9a68f5a6 100644 --- a/sorts/quick_sort_3_partition.py +++ b/sorts/quick_sort_3_partition.py @@ -1,4 +1,27 @@ def quick_sort_3partition(sorting: list, left: int, right: int) -> None: + """ " + Python implementation of quick sort algorithm with 3-way partition. + The idea of 3-way quick sort is based on "Dutch National Flag algorithm". + + :param sorting: sort list + :param left: left endpoint of sorting + :param right: right endpoint of sorting + :return: None + + Examples: + >>> array1 = [5, -1, -1, 5, 5, 24, 0] + >>> quick_sort_3partition(array1, 0, 6) + >>> array1 + [-1, -1, 0, 5, 5, 5, 24] + >>> array2 = [9, 0, 2, 6] + >>> quick_sort_3partition(array2, 0, 3) + >>> array2 + [0, 2, 6, 9] + >>> array3 = [] + >>> quick_sort_3partition(array3, 0, 0) + >>> array3 + [] + """ if right <= left: return a = i = left diff --git a/strings/frequency_finder.py b/strings/frequency_finder.py index e5afee891bd9..98720dc36d6e 100644 --- a/strings/frequency_finder.py +++ b/strings/frequency_finder.py @@ -36,7 +36,7 @@ def get_letter_count(message: str) -> dict[str, int]: - letter_count = {letter: 0 for letter in string.ascii_uppercase} + letter_count = dict.fromkeys(string.ascii_uppercase, 0) for letter in message.upper(): if letter in LETTERS: letter_count[letter] += 1 diff --git a/strings/jaro_winkler.py b/strings/jaro_winkler.py index cae2068fabc1..0ce5d83b3c41 100644 --- a/strings/jaro_winkler.py +++ b/strings/jaro_winkler.py @@ -33,7 +33,9 @@ def get_matched_characters(_str1: str, _str2: str) -> str: right = int(min(i + limit + 1, len(_str2))) if char in _str2[left:right]: matched.append(char) - _str2 = f"{_str2[0:_str2.index(char)]} {_str2[_str2.index(char) + 1:]}" + _str2 = ( + f"{_str2[0 : _str2.index(char)]} {_str2[_str2.index(char) + 1 :]}" + ) return "".join(matched) diff --git a/strings/join.py b/strings/join.py index 5c02f65a20ce..cdcc3a1377f4 100644 --- a/strings/join.py +++ b/strings/join.py @@ -24,6 +24,8 @@ def join(separator: str, separated: list[str]) -> str: 'a' >>> join(" ", ["You", "are", "amazing!"]) 'You are amazing!' + >>> join(",", ["", "", ""]) + ',,' This example should raise an exception for non-string elements: @@ -37,15 +39,33 @@ def join(separator: str, separated: list[str]) -> str: 'apple-banana-cherry' """ - joined = "" + # Check that all elements are strings for word_or_phrase in separated: + # If the element is not a string, raise an exception if not isinstance(word_or_phrase, str): raise Exception("join() accepts only strings") + + joined: str = "" + """ + The last element of the list is not followed by the separator. + So, we need to iterate through the list and join each element + with the separator except the last element. + """ + last_index: int = len(separated) - 1 + """ + Iterate through the list and join each element with the separator. + Except the last element, all other elements are followed by the separator. + """ + for word_or_phrase in separated[:last_index]: + # join the element with the separator. joined += word_or_phrase + separator - # Remove the trailing separator - # by stripping it from the result - return joined.strip(separator) + # If the list is not empty, join the last element. + if separated != []: + joined += separated[last_index] + + # Return the joined string. + return joined if __name__ == "__main__": diff --git a/strings/min_cost_string_conversion.py b/strings/min_cost_string_conversion.py index 93791e2a7ed3..87eb5189e16a 100644 --- a/strings/min_cost_string_conversion.py +++ b/strings/min_cost_string_conversion.py @@ -91,6 +91,14 @@ def assemble_transformation(ops: list[list[str]], i: int, j: int) -> list[str]: >>> y1 = len(ops1[0]) - 1 >>> assemble_transformation(ops1, x1, y1) [] + + >>> ops2 = [['0', 'I1', 'I2', 'I3'], + ... ['D1', 'C1', 'I2', 'I3'], + ... ['D2', 'D2', 'R23', 'R23']] + >>> x2 = len(ops2) - 1 + >>> y2 = len(ops2[0]) - 1 + >>> assemble_transformation(ops2, x2, y2) + ['C1', 'I2', 'R23'] """ if i == 0 and j == 0: return [] diff --git a/strings/wave.py b/strings/wave_string.py similarity index 100% rename from strings/wave.py rename to strings/wave_string.py diff --git a/web_programming/current_stock_price.py b/web_programming/current_stock_price.py index 9567c05b0558..573e1f575c8e 100644 --- a/web_programming/current_stock_price.py +++ b/web_programming/current_stock_price.py @@ -1,22 +1,40 @@ import requests from bs4 import BeautifulSoup +""" +Get the HTML code of finance yahoo and select the current qsp-price +Current AAPL stock price is 228.43 +Current AMZN stock price is 201.85 +Current IBM stock price is 210.30 +Current GOOG stock price is 177.86 +Current MSFT stock price is 414.82 +Current ORCL stock price is 188.87 +""" + def stock_price(symbol: str = "AAPL") -> str: + """ + >>> stock_price("EEEE") + '- ' + >>> isinstance(float(stock_price("GOOG")),float) + True + """ url = f"https://finance.yahoo.com/quote/{symbol}?p={symbol}" yahoo_finance_source = requests.get( url, headers={"USER-AGENT": "Mozilla/5.0"}, timeout=10 ).text soup = BeautifulSoup(yahoo_finance_source, "html.parser") - specific_fin_streamer_tag = soup.find("fin-streamer", {"data-test": "qsp-price"}) - if specific_fin_streamer_tag: - text = specific_fin_streamer_tag.get_text() - return text - return "No tag with the specified data-test attribute found." + if specific_fin_streamer_tag := soup.find("span", {"data-testid": "qsp-price"}): + return specific_fin_streamer_tag.get_text() + return "No tag with the specified data-testid attribute found." # Search for the symbol at https://finance.yahoo.com/lookup if __name__ == "__main__": + from doctest import testmod + + testmod() + for symbol in "AAPL AMZN IBM GOOG MSFT ORCL".split(): print(f"Current {symbol:<4} stock price is {stock_price(symbol):>8}") diff --git a/web_programming/fetch_anime_and_play.py b/web_programming/fetch_anime_and_play.py index fd7c3a3a7381..e56b7124eeb5 100644 --- a/web_programming/fetch_anime_and_play.py +++ b/web_programming/fetch_anime_and_play.py @@ -165,7 +165,7 @@ def get_anime_episode(episode_endpoint: str) -> list: print(f"Found {len(anime_list)} results: ") for i, anime in enumerate(anime_list): anime_title = anime["title"] - print(f"{i+1}. {anime_title}") + print(f"{i + 1}. {anime_title}") anime_choice = int(input("\nPlease choose from the following list: ").strip()) chosen_anime = anime_list[anime_choice - 1] @@ -177,7 +177,7 @@ def get_anime_episode(episode_endpoint: str) -> list: else: print(f"Found {len(episode_list)} results: ") for i, episode in enumerate(episode_list): - print(f"{i+1}. {episode['title']}") + print(f"{i + 1}. {episode['title']}") episode_choice = int(input("\nChoose an episode by serial no: ").strip()) chosen_episode = episode_list[episode_choice - 1]