diff --git a/ciphers/atbash.py b/ciphers/atbash.py index 4e8f663ed02d..6930f07861ef 100644 --- a/ciphers/atbash.py +++ b/ciphers/atbash.py @@ -3,52 +3,34 @@ import string -def atbash_slow(sequence: str) -> str: +def atbash(text: str) -> str: """ - >>> atbash_slow("ABCDEFG") - 'ZYXWVUT' + Encodes or decodes text using the Atbash cipher. - >>> atbash_slow("aW;;123BX") - 'zD;;123YC' - """ - output = "" - for i in sequence: - extract = ord(i) - if 65 <= extract <= 90: - output += chr(155 - extract) - elif 97 <= extract <= 122: - output += chr(219 - extract) - else: - output += i - return output - - -def atbash(sequence: str) -> str: - """ - >>> atbash("ABCDEFG") - 'ZYXWVUT' - - >>> atbash("aW;;123BX") - 'zD;;123YC' - """ - letters = string.ascii_letters - letters_reversed = string.ascii_lowercase[::-1] + string.ascii_uppercase[::-1] - return "".join( - letters_reversed[letters.index(c)] if c in letters else c for c in sequence - ) + The Atbash cipher substitutes each letter with its mirror in the alphabet: + A -> Z, B -> Y, C -> X, ... Z -> A (case is preserved) + Non-alphabetic characters are left unchanged. + Args: + text: The input string to encode/decode -def benchmark() -> None: - """Let's benchmark our functions side-by-side...""" - from timeit import timeit + Returns: + The transformed string + """ + # Create translation tables for uppercase and lowercase + lowercase_map = str.maketrans(string.ascii_lowercase, string.ascii_lowercase[::-1]) + uppercase_map = str.maketrans(string.ascii_uppercase, string.ascii_uppercase[::-1]) - print("Running performance benchmarks...") - setup = "from string import printable ; from __main__ import atbash, atbash_slow" - print(f"> atbash_slow(): {timeit('atbash_slow(printable)', setup=setup)} seconds") - print(f"> atbash(): {timeit('atbash(printable)', setup=setup)} seconds") + # Apply both translation mappings + return text.translate(lowercase_map).translate(uppercase_map) +# Example usage if __name__ == "__main__": - for example in ("ABCDEFGH", "123GGjj", "testStringtest", "with space"): - print(f"{example} encrypted in atbash: {atbash(example)}") - benchmark() + test_string = "Hello, World! 123" + encoded = atbash(test_string) + decoded = atbash(encoded) + + print(f"Original: {test_string}") + print(f"Encoded: {encoded}") + print(f"Decoded: {decoded}") diff --git a/ciphers/shuffled_shift_cipher.py b/ciphers/shuffled_shift_cipher.py index 08b2cab97c69..09c1c65d0117 100644 --- a/ciphers/shuffled_shift_cipher.py +++ b/ciphers/shuffled_shift_cipher.py @@ -42,7 +42,7 @@ def __str__(self) -> str: """ :return: passcode of the cipher object """ - return "".join(self.__passcode) + return self.__passcode def __neg_pos(self, iterlist: list[int]) -> list[int]: """ @@ -56,19 +56,19 @@ def __neg_pos(self, iterlist: list[int]) -> list[int]: iterlist[i] *= -1 return iterlist - def __passcode_creator(self) -> list[str]: + def __passcode_creator(self) -> str: """ Creates a random password from the selection buffer of 1. uppercase letters of the English alphabet 2. lowercase letters of the English alphabet 3. digits from 0 to 9 - :rtype: list + :rtype: str :return: a password of a random length between 10 to 20 """ choices = string.ascii_letters + string.digits password = [random.choice(choices) for _ in range(random.randint(10, 20))] - return password + return "".join(password) def __make_key_list(self) -> list[str]: """ @@ -104,15 +104,14 @@ def __make_key_list(self) -> list[str]: temp_list: list[str] = [] # algorithm for creating a new shuffled list, keys_l, out of key_list_options - for i in key_list_options: - temp_list.extend(i) + for char in key_list_options: + temp_list.append(char) # checking breakpoints at which to pivot temporary sublist and add it into # keys_l - if i in breakpoints or i == key_list_options[-1]: + if char in breakpoints or char == key_list_options[-1]: keys_l.extend(temp_list[::-1]) temp_list.clear() - # returning a shuffled keys_l to prevent brute force guessing of shift key return keys_l @@ -135,14 +134,13 @@ def decrypt(self, encoded_message: str) -> str: """ decoded_message = "" + key_len = len(self.__key_list) # decoding shift like Caesar cipher algorithm implementing negative shift or # reverse shift or left shift - for i in encoded_message: - position = self.__key_list.index(i) - decoded_message += self.__key_list[ - (position - self.__shift_key) % -len(self.__key_list) - ] + for char in encoded_message: + position = self.__key_list.index(char) + decoded_message += self.__key_list[(position - self.__shift_key) % key_len] return decoded_message @@ -157,14 +155,13 @@ def encrypt(self, plaintext: str) -> str: """ encoded_message = "" + key_len = len(self.__key_list) # encoding shift like Caesar cipher algorithm implementing positive shift or # forward shift or right shift - for i in plaintext: - position = self.__key_list.index(i) - encoded_message += self.__key_list[ - (position + self.__shift_key) % len(self.__key_list) - ] + for char in plaintext: + position = self.__key_list.index(char) + encoded_message += self.__key_list[(position + self.__shift_key) % key_len] return encoded_message diff --git a/data_structures/heap/skew_heap.py b/data_structures/heap/skew_heap.py index 0839db711cb1..dc0efbf1d2bb 100644 --- a/data_structures/heap/skew_heap.py +++ b/data_structures/heap/skew_heap.py @@ -2,25 +2,23 @@ from __future__ import annotations -from collections.abc import Iterable, Iterator -from typing import Any, Generic, TypeVar +from collections.abc import Callable, Iterable, Iterator +from typing import Any -T = TypeVar("T", bound=bool) - -class SkewNode(Generic[T]): +class SkewNode: """ One node of the skew heap. Contains the value and references to two children. """ - def __init__(self, value: T) -> None: - self._value: T = value - self.left: SkewNode[T] | None = None - self.right: SkewNode[T] | None = None + def __init__(self, value: Any) -> None: + self._value: Any = value + self.left: SkewNode | None = None + self.right: SkewNode | None = None @property - def value(self) -> T: + def value(self) -> Any: """ Return the value of the node. @@ -30,68 +28,56 @@ def value(self) -> T: 3.14159 >>> SkewNode("hello").value 'hello' - >>> SkewNode(None).value - >>> SkewNode(True).value True - >>> SkewNode([]).value - [] - >>> SkewNode({}).value - {} - >>> SkewNode(set()).value - set() - >>> SkewNode(0.0).value - 0.0 - >>> SkewNode(-1e-10).value - -1e-10 >>> SkewNode(10).value 10 - >>> SkewNode(-10.5).value - -10.5 - >>> SkewNode().value - Traceback (most recent call last): - ... - TypeError: SkewNode.__init__() missing 1 required positional argument: 'value' """ return self._value @staticmethod def merge( - root1: SkewNode[T] | None, root2: SkewNode[T] | None - ) -> SkewNode[T] | None: + root1: SkewNode | None, root2: SkewNode | None, comp: Callable[[Any, Any], bool] + ) -> SkewNode | None: """ - Merge 2 nodes together. - >>> SkewNode.merge(SkewNode(10),SkewNode(-10.5)).value + Merge two nodes together. + >>> def comp(a, b): return a < b + >>> SkewNode.merge(SkewNode(10), SkewNode(-10.5), comp).value -10.5 - >>> SkewNode.merge(SkewNode(10),SkewNode(10.5)).value + >>> SkewNode.merge(SkewNode(10), SkewNode(10.5), comp).value 10 - >>> SkewNode.merge(SkewNode(10),SkewNode(10)).value + >>> SkewNode.merge(SkewNode(10), SkewNode(10), comp).value 10 - >>> SkewNode.merge(SkewNode(-100),SkewNode(-10.5)).value + >>> SkewNode.merge(SkewNode(-100), SkewNode(-10.5), comp).value -100 """ + # Handle empty nodes if not root1: return root2 - if not root2: return root1 - if root1.value > root2.value: - root1, root2 = root2, root1 - - result = root1 - temp = root1.right - result.right = root1.left - result.left = SkewNode.merge(temp, root2) - - return result - - -class SkewHeap(Generic[T]): + # Compare values using provided comparison function + if comp(root1.value, root2.value): + # root1 is smaller, make it the new root + result = root1 + temp = root1.right + result.right = root1.left + result.left = SkewNode.merge(temp, root2, comp) + return result + else: + # root2 is smaller or equal, use it as new root + result = root2 + temp = root2.right + result.right = root2.left + result.left = SkewNode.merge(root1, temp, comp) + return result + + +class SkewHeap: """ - A data structure that allows inserting a new value and to pop the smallest - values. Both operations take O(logN) time where N is the size of the - structure. + A data structure that allows inserting a new value and popping the smallest + values. Both operations take O(logN) time where N is the size of the heap. Wiki: https://en.wikipedia.org/wiki/Skew_heap Visualization: https://www.cs.usfca.edu/~galles/visualization/SkewHeap.html @@ -111,20 +97,32 @@ class SkewHeap(Generic[T]): [-1, 0, 1] """ - def __init__(self, data: Iterable[T] | None = ()) -> None: + def __init__( + self, + data: Iterable[Any] | None = None, + comp: Callable[[Any, Any], bool] = lambda a, b: a < b, + ) -> None: """ + Initialize the skew heap with optional data and comparison function + >>> sh = SkewHeap([3, 1, 3, 7]) >>> list(sh) [1, 3, 3, 7] + + # Max-heap example + >>> max_heap = SkewHeap([3, 1, 3, 7], comp=lambda a, b: a > b) + >>> list(max_heap) + [7, 3, 3, 1] """ - self._root: SkewNode[T] | None = None + self._root: SkewNode | None = None + self._comp = comp if data: for item in data: self.insert(item) def __bool__(self) -> bool: """ - Check if the heap is not empty. + Check if the heap is not empty >>> sh = SkewHeap() >>> bool(sh) @@ -138,27 +136,31 @@ def __bool__(self) -> bool: """ return self._root is not None - def __iter__(self) -> Iterator[T]: + def __iter__(self) -> Iterator[Any]: """ - Returns sorted list containing all the values in the heap. + Iterate through all values in sorted order >>> sh = SkewHeap([3, 1, 3, 7]) >>> list(sh) [1, 3, 3, 7] """ + # Create a temporary heap for iteration + temp_heap = SkewHeap(comp=self._comp) result: list[Any] = [] - while self: - result.append(self.pop()) - # Pushing items back to the heap not to clear it. - for item in result: - self.insert(item) + # Pop all elements from the heap + while self: + item = self.pop() + result.append(item) + temp_heap.insert(item) + # Restore the heap state + self._root = temp_heap._root return iter(result) - def insert(self, value: T) -> None: + def insert(self, value: Any) -> None: """ - Insert the value into the heap. + Insert a new value into the heap >>> sh = SkewHeap() >>> sh.insert(3) @@ -168,11 +170,11 @@ def insert(self, value: T) -> None: >>> list(sh) [1, 3, 3, 7] """ - self._root = SkewNode.merge(self._root, SkewNode(value)) + self._root = SkewNode.merge(self._root, SkewNode(value), self._comp) - def pop(self) -> T | None: + def pop(self) -> Any: """ - Pop the smallest value from the heap and return it. + Remove and return the smallest value from the heap >>> sh = SkewHeap([3, 1, 3, 7]) >>> sh.pop() @@ -189,15 +191,13 @@ def pop(self) -> T | None: IndexError: Can't get top element for the empty heap. """ result = self.top() - self._root = ( - SkewNode.merge(self._root.left, self._root.right) if self._root else None - ) - + if self._root: + self._root = SkewNode.merge(self._root.left, self._root.right, self._comp) return result - def top(self) -> T: + def top(self) -> Any: """ - Return the smallest value from the heap. + Return the smallest value without removing it >>> sh = SkewHeap() >>> sh.insert(3) @@ -219,7 +219,7 @@ def top(self) -> T: def clear(self) -> None: """ - Clear the heap. + Clear all elements from the heap >>> sh = SkewHeap([3, 1, 3, 7]) >>> sh.clear() diff --git a/data_structures/stacks/stack_with_doubly_linked_list.py b/data_structures/stacks/stack_with_doubly_linked_list.py index 50c5236e073c..8b472293721f 100644 --- a/data_structures/stacks/stack_with_doubly_linked_list.py +++ b/data_structures/stacks/stack_with_doubly_linked_list.py @@ -3,19 +3,15 @@ from __future__ import annotations -from typing import Generic, TypeVar -T = TypeVar("T") - - -class Node(Generic[T]): +class Node[T]: def __init__(self, data: T): self.data = data # Assign data self.next: Node[T] | None = None # Initialize next as null self.prev: Node[T] | None = None # Initialize prev as null -class Stack(Generic[T]): +class Stack[T]: """ >>> stack = Stack() >>> stack.is_empty() diff --git a/digital_image_processing/test_digital_image_processing.py b/digital_image_processing/test_digital_image_processing.py index d1200f4d65ca..8543021f2eb2 100644 --- a/digital_image_processing/test_digital_image_processing.py +++ b/digital_image_processing/test_digital_image_processing.py @@ -2,9 +2,10 @@ PyTest's for Digital Image Processing """ +import os + import numpy as np from cv2 import COLOR_BGR2GRAY, cvtColor, imread -from numpy import array, uint8 from PIL import Image from digital_image_processing import change_contrast as cc @@ -23,112 +24,103 @@ gray = cvtColor(img, COLOR_BGR2GRAY) -# Test: convert_to_negative() def test_convert_to_negative(): + """Test negative image conversion.""" negative_img = cn.convert_to_negative(img) - # assert negative_img array for at least one True + # Verify output contains at least one non-zero value assert negative_img.any() -# Test: change_contrast() def test_change_contrast(): - with Image.open("digital_image_processing/image_data/lena_small.jpg") as img: - # Work around assertion for response - assert str(cc.change_contrast(img, 110)).startswith( + """Test contrast adjustment functionality.""" + with Image.open("digital_image_processing/image_data/lena_small.jpg") as img_pil: + # Verify returns a PIL Image object + assert str(cc.change_contrast(img_pil, 110)).startswith( " str: if __name__ == "__main__": n = int(input("Enter the size of the butterfly pattern: ")) print(butterfly_pattern(n)) + +if __name__ == "__main__": + import doctest + + # Run the doctests + doctest.testmod() diff --git a/graphs/minimum_spanning_tree_kruskal2.py b/graphs/minimum_spanning_tree_kruskal2.py index 0ddb43ce8e6e..36ca1cbb2615 100644 --- a/graphs/minimum_spanning_tree_kruskal2.py +++ b/graphs/minimum_spanning_tree_kruskal2.py @@ -1,11 +1,7 @@ from __future__ import annotations -from typing import Generic, TypeVar -T = TypeVar("T") - - -class DisjointSetTreeNode(Generic[T]): +class DisjointSetTreeNode[T]: # Disjoint Set Node to store the parent and rank def __init__(self, data: T) -> None: self.data = data @@ -13,7 +9,7 @@ def __init__(self, data: T) -> None: self.rank = 0 -class DisjointSetTree(Generic[T]): +class DisjointSetTree[T]: # Disjoint Set DataStructure def __init__(self) -> None: # map from node name to the node object @@ -46,7 +42,7 @@ def union(self, data1: T, data2: T) -> None: self.link(self.find_set(data1), self.find_set(data2)) -class GraphUndirectedWeighted(Generic[T]): +class GraphUndirectedWeighted[T]: def __init__(self) -> None: # connections: map from the node to the neighbouring nodes (with weights) self.connections: dict[T, dict[T, int]] = {} @@ -118,4 +114,5 @@ def kruskal(self) -> GraphUndirectedWeighted[T]: num_edges += 1 graph.add_edge(u, v, w) disjoint_set.union(u, v) + # Return the generated Minimum Spanning Tree return graph diff --git a/graphs/minimum_spanning_tree_prims2.py b/graphs/minimum_spanning_tree_prims2.py index 6870cc80f844..d961b5e764c3 100644 --- a/graphs/minimum_spanning_tree_prims2.py +++ b/graphs/minimum_spanning_tree_prims2.py @@ -10,9 +10,6 @@ from __future__ import annotations from sys import maxsize -from typing import Generic, TypeVar - -T = TypeVar("T") def get_parent_position(position: int) -> int: @@ -47,7 +44,7 @@ def get_child_right_position(position: int) -> int: return (2 * position) + 2 -class MinPriorityQueue(Generic[T]): +class MinPriorityQueue[T]: """ Minimum Priority Queue Class @@ -184,7 +181,7 @@ def _swap_nodes(self, node1_pos: int, node2_pos: int) -> None: self.position_map[node2_elem] = node1_pos -class GraphUndirectedWeighted(Generic[T]): +class GraphUndirectedWeighted[T]: """ Graph Undirected Weighted Class @@ -217,7 +214,7 @@ def add_edge(self, node1: T, node2: T, weight: int) -> None: self.connections[node2][node1] = weight -def prims_algo( +def prims_algo[T]( graph: GraphUndirectedWeighted[T], ) -> tuple[dict[T, int], dict[T, T | None]]: """ @@ -248,7 +245,6 @@ def prims_algo( if priority_queue.is_empty(): return dist, parent - # initialization node = priority_queue.extract_min() dist[node] = 0 diff --git a/maths/sum_of_digits.py b/maths/sum_of_digits.py index d5488bb9e9e0..ee5c51959619 100644 --- a/maths/sum_of_digits.py +++ b/maths/sum_of_digits.py @@ -1,3 +1,7 @@ +from collections.abc import Callable +from timeit import timeit + + def sum_of_digits(n: int) -> int: """ Find the sum of digits of a number. @@ -31,7 +35,7 @@ def sum_of_digits_recursion(n: int) -> int: 0 """ n = abs(n) - return n if n < 10 else n % 10 + sum_of_digits(n // 10) + return n if n < 10 else n % 10 + sum_of_digits_recursion(n // 10) def sum_of_digits_compact(n: int) -> int: @@ -53,8 +57,6 @@ def benchmark() -> None: """ Benchmark multiple functions, with three different length int values. """ - from collections.abc import Callable - from timeit import timeit def benchmark_a_function(func: Callable, value: int) -> None: call = f"{func.__name__}({value})" diff --git a/matrix/matrix_class.py b/matrix/matrix_class.py index a5940a38e836..394e38c164d6 100644 --- a/matrix/matrix_class.py +++ b/matrix/matrix_class.py @@ -2,118 +2,29 @@ from __future__ import annotations +from typing import final + +@final class Matrix: """ Matrix object generated from a 2D array where each element is an array representing - a row. - Rows can contain type int or float. - Common operations and information available. - >>> rows = [ - ... [1, 2, 3], - ... [4, 5, 6], - ... [7, 8, 9] - ... ] - >>> matrix = Matrix(rows) - >>> print(matrix) - [[1. 2. 3.] - [4. 5. 6.] - [7. 8. 9.]] - - Matrix rows and columns are available as 2D arrays - >>> matrix.rows - [[1, 2, 3], [4, 5, 6], [7, 8, 9]] - >>> matrix.columns() - [[1, 4, 7], [2, 5, 8], [3, 6, 9]] - - Order is returned as a tuple - >>> matrix.order - (3, 3) - - Squareness and invertability are represented as bool - >>> matrix.is_square - True - >>> matrix.is_invertable() - False - - Identity, Minors, Cofactors and Adjugate are returned as Matrices. Inverse can be - a Matrix or Nonetype - >>> print(matrix.identity()) - [[1. 0. 0.] - [0. 1. 0.] - [0. 0. 1.]] - >>> print(matrix.minors()) - [[-3. -6. -3.] - [-6. -12. -6.] - [-3. -6. -3.]] - >>> print(matrix.cofactors()) - [[-3. 6. -3.] - [6. -12. 6.] - [-3. 6. -3.]] - >>> # won't be apparent due to the nature of the cofactor matrix - >>> print(matrix.adjugate()) - [[-3. 6. -3.] - [6. -12. 6.] - [-3. 6. -3.]] - >>> matrix.inverse() - Traceback (most recent call last): - ... - TypeError: Only matrices with a non-zero determinant have an inverse - - Determinant is an int, float, or Nonetype - >>> matrix.determinant() - 0 - - Negation, scalar multiplication, addition, subtraction, multiplication and - exponentiation are available and all return a Matrix - >>> print(-matrix) - [[-1. -2. -3.] - [-4. -5. -6.] - [-7. -8. -9.]] - >>> matrix2 = matrix * 3 - >>> print(matrix2) - [[3. 6. 9.] - [12. 15. 18.] - [21. 24. 27.]] - >>> print(matrix + matrix2) - [[4. 8. 12.] - [16. 20. 24.] - [28. 32. 36.]] - >>> print(matrix - matrix2) - [[-2. -4. -6.] - [-8. -10. -12.] - [-14. -16. -18.]] - >>> print(matrix ** 3) - [[468. 576. 684.] - [1062. 1305. 1548.] - [1656. 2034. 2412.]] - - Matrices can also be modified - >>> matrix.add_row([10, 11, 12]) - >>> print(matrix) - [[1. 2. 3.] - [4. 5. 6.] - [7. 8. 9.] - [10. 11. 12.]] - >>> matrix2.add_column([8, 16, 32]) - >>> print(matrix2) - [[3. 6. 9. 8.] - [12. 15. 18. 16.] - [21. 24. 27. 32.]] - >>> print(matrix * matrix2) - [[90. 108. 126. 136.] - [198. 243. 288. 304.] - [306. 378. 450. 472.] - [414. 513. 612. 640.]] + a row. Supports both integer and float values. """ - def __init__(self, rows: list[list[int]]): + def __init__(self, rows: list[list[float]]) -> None: + """ + Initialize matrix from 2D list. Validates input structure and types. + Raises TypeError for invalid input structure or element types. + """ error = TypeError( "Matrices must be formed from a list of zero or more lists containing at " "least one and the same number of values, each of which must be of type " "int or float." ) - if len(rows) != 0: + + # Validate matrix structure and content + if rows: cols = len(rows[0]) if cols == 0: raise error @@ -127,55 +38,66 @@ def __init__(self, rows: list[list[int]]): else: self.rows = [] - # MATRIX INFORMATION - def columns(self) -> list[list[int]]: + # MATRIX INFORMATION METHODS + def columns(self) -> list[list[float]]: + """Return matrix columns as 2D list""" return [[row[i] for row in self.rows] for i in range(len(self.rows[0]))] @property def num_rows(self) -> int: + """Get number of rows in matrix""" return len(self.rows) @property def num_columns(self) -> int: + """Get number of columns in matrix""" return len(self.rows[0]) @property def order(self) -> tuple[int, int]: + """Get matrix dimensions as (rows, columns) tuple""" return self.num_rows, self.num_columns @property def is_square(self) -> bool: + """Check if matrix is square (rows == columns)""" return self.order[0] == self.order[1] def identity(self) -> Matrix: + """Generate identity matrix of same dimensions""" values = [ - [0 if column_num != row_num else 1 for column_num in range(self.num_rows)] + [ + 0.0 if column_num != row_num else 1.0 + for column_num in range(self.num_rows) + ] for row_num in range(self.num_rows) ] return Matrix(values) - def determinant(self) -> int: + def determinant(self) -> float: + """Calculate matrix determinant. Returns 0 for non-square matrices.""" if not self.is_square: - return 0 + return 0.0 if self.order == (0, 0): - return 1 + return 1.0 if self.order == (1, 1): - return int(self.rows[0][0]) + return float(self.rows[0][0]) if self.order == (2, 2): - return int( + return float( (self.rows[0][0] * self.rows[1][1]) - (self.rows[0][1] * self.rows[1][0]) ) - else: - return sum( - self.rows[0][column] * self.cofactors().rows[0][column] - for column in range(self.num_columns) - ) + return sum( + self.rows[0][column] * self.cofactors().rows[0][column] + for column in range(self.num_columns) + ) def is_invertable(self) -> bool: + """Check if matrix is invertible (non-zero determinant)""" return bool(self.determinant()) - def get_minor(self, row: int, column: int) -> int: + def get_minor(self, row: int, column: int) -> float: + """Calculate minor for specified element (determinant of submatrix)""" values = [ [ self.rows[other_row][other_column] @@ -187,12 +109,12 @@ def get_minor(self, row: int, column: int) -> int: ] return Matrix(values).determinant() - def get_cofactor(self, row: int, column: int) -> int: - if (row + column) % 2 == 0: - return self.get_minor(row, column) - return -1 * self.get_minor(row, column) + def get_cofactor(self, row: int, column: int) -> float: + """Calculate cofactor for specified element (signed minor)""" + return self.get_minor(row, column) * (-1 if (row + column) % 2 else 1) def minors(self) -> Matrix: + """Generate matrix of minors""" return Matrix( [ [self.get_minor(row, column) for column in range(self.num_columns)] @@ -201,103 +123,109 @@ def minors(self) -> Matrix: ) def cofactors(self) -> Matrix: + """Generate cofactor matrix""" return Matrix( [ - [ - self.minors().rows[row][column] - if (row + column) % 2 == 0 - else self.minors().rows[row][column] * -1 - for column in range(self.minors().num_columns) - ] - for row in range(self.minors().num_rows) + [self.get_cofactor(row, column) for column in range(self.num_columns)] + for row in range(self.num_rows) ] ) def adjugate(self) -> Matrix: - values = [ - [self.cofactors().rows[column][row] for column in range(self.num_columns)] - for row in range(self.num_rows) - ] - return Matrix(values) + """Generate adjugate matrix (transpose of cofactor matrix)""" + return Matrix( + [ + [ + self.cofactors().rows[column][row] + for column in range(self.num_columns) + ] + for row in range(self.num_rows) + ] + ) def inverse(self) -> Matrix: - determinant = self.determinant() - if not determinant: + """Calculate matrix inverse. Raises TypeError for singular matrices.""" + det = self.determinant() + if abs(det) < 1e-10: # Floating point tolerance raise TypeError("Only matrices with a non-zero determinant have an inverse") - return self.adjugate() * (1 / determinant) + return self.adjugate() * (1 / det) def __repr__(self) -> str: + """Official string representation of matrix""" return str(self.rows) def __str__(self) -> str: - if self.num_rows == 0: + """User-friendly string representation of matrix""" + if not self.rows: return "[]" if self.num_rows == 1: - return "[[" + ". ".join(str(self.rows[0])) + "]]" + return "[[" + ". ".join(str(val) for val in self.rows[0]) + "]]" return ( "[" + "\n ".join( - [ - "[" + ". ".join([str(value) for value in row]) + ".]" - for row in self.rows - ] + "[" + ". ".join(str(val) for val in row) + ".]" for row in self.rows ) + "]" ) - # MATRIX MANIPULATION - def add_row(self, row: list[int], position: int | None = None) -> None: - type_error = TypeError("Row must be a list containing all ints and/or floats") + # MATRIX MANIPULATION METHODS + def add_row(self, row: list[float], position: int | None = None) -> None: + """Add row to matrix. Validates type and length.""" if not isinstance(row, list): - raise type_error + raise TypeError("Row must be a list") for value in row: if not isinstance(value, (int, float)): - raise type_error + raise TypeError("Row elements must be int or float") if len(row) != self.num_columns: - raise ValueError( - "Row must be equal in length to the other rows in the matrix" - ) + raise ValueError("Row length must match matrix columns") + if position is None: self.rows.append(row) else: - self.rows = self.rows[0:position] + [row] + self.rows[position:] + # Fix RUF005: Use iterable unpacking instead of concatenation + self.rows = [*self.rows[:position], row, *self.rows[position:]] - def add_column(self, column: list[int], position: int | None = None) -> None: - type_error = TypeError( - "Column must be a list containing all ints and/or floats" - ) + def add_column(self, column: list[float], position: int | None = None) -> None: + """Add column to matrix. Validates type and length.""" if not isinstance(column, list): - raise type_error + raise TypeError("Column must be a list") for value in column: if not isinstance(value, (int, float)): - raise type_error + raise TypeError("Column elements must be int or float") if len(column) != self.num_rows: - raise ValueError( - "Column must be equal in length to the other columns in the matrix" - ) + raise ValueError("Column length must match matrix rows") + if position is None: - self.rows = [self.rows[i] + [column[i]] for i in range(self.num_rows)] + for i, value in enumerate(column): + self.rows[i].append(value) else: - self.rows = [ - self.rows[i][0:position] + [column[i]] + self.rows[i][position:] - for i in range(self.num_rows) - ] + # Fix RUF005: Use iterable unpacking instead of concatenation + for i, value in enumerate(column): + self.rows[i] = [ + *self.rows[i][:position], + value, + *self.rows[i][position:], + ] # MATRIX OPERATIONS def __eq__(self, other: object) -> bool: + """Check matrix equality""" if not isinstance(other, Matrix): return NotImplemented return self.rows == other.rows def __ne__(self, other: object) -> bool: + """Check matrix inequality""" return not self == other def __neg__(self) -> Matrix: - return self * -1 + """Negate matrix elements""" + return self * -1.0 def __add__(self, other: Matrix) -> Matrix: + """Matrix addition. Requires same dimensions.""" if self.order != other.order: - raise ValueError("Addition requires matrices of the same order") + raise ValueError("Addition requires matrices of same dimensions") return Matrix( [ [self.rows[i][j] + other.rows[i][j] for j in range(self.num_columns)] @@ -306,8 +234,9 @@ def __add__(self, other: Matrix) -> Matrix: ) def __sub__(self, other: Matrix) -> Matrix: + """Matrix subtraction. Requires same dimensions.""" if self.order != other.order: - raise ValueError("Subtraction requires matrices of the same order") + raise ValueError("Subtraction requires matrices of same dimensions") return Matrix( [ [self.rows[i][j] - other.rows[i][j] for j in range(self.num_columns)] @@ -316,47 +245,46 @@ def __sub__(self, other: Matrix) -> Matrix: ) def __mul__(self, other: Matrix | float) -> Matrix: + """Matrix multiplication (scalar or matrix)""" if isinstance(other, (int, float)): - return Matrix( - [[int(element * other) for element in row] for row in self.rows] - ) + # Preserve float precision by removing int conversion + return Matrix([[element * other for element in row] for row in self.rows]) elif isinstance(other, Matrix): if self.num_columns != other.num_rows: raise ValueError( - "The number of columns in the first matrix must " - "be equal to the number of rows in the second" + "Matrix multiplication requires columns of first matrix " + "to match rows of second matrix" ) return Matrix( [ - [Matrix.dot_product(row, column) for column in other.columns()] + [Matrix.dot_product(row, col) for col in other.columns()] for row in self.rows ] ) - else: - raise TypeError( - "A Matrix can only be multiplied by an int, float, or another matrix" - ) + raise TypeError("Matrix can only be multiplied by scalar or another matrix") - def __pow__(self, other: int) -> Matrix: - if not isinstance(other, int): - raise TypeError("A Matrix can only be raised to the power of an int") + def __pow__(self, exponent: int) -> Matrix: + """Matrix exponentiation. Requires square matrix.""" + if not isinstance(exponent, int): + raise TypeError("Exponent must be integer") if not self.is_square: raise ValueError("Only square matrices can be raised to a power") - if other == 0: + if exponent == 0: return self.identity() - if other < 0: + if exponent < 0: if self.is_invertable(): - return self.inverse() ** (-other) + return self.inverse() ** (-exponent) raise ValueError( - "Only invertable matrices can be raised to a negative power" + "Only invertible matrices can be raised to negative powers" ) result = self - for _ in range(other - 1): + for _ in range(exponent - 1): result *= self return result @classmethod - def dot_product(cls, row: list[int], column: list[int]) -> int: + def dot_product(cls, row: list[float], column: list[float]) -> float: + """Calculate dot product of two vectors""" return sum(row[i] * column[i] for i in range(len(row))) diff --git a/matrix/pascal_triangle.py b/matrix/pascal_triangle.py index 7f6555f9c8b9..4b087525de27 100644 --- a/matrix/pascal_triangle.py +++ b/matrix/pascal_triangle.py @@ -1,16 +1,19 @@ """ -This implementation demonstrates how to generate the elements of a Pascal's triangle. -The element havingva row index of r and column index of c can be derivedvas follows: -triangle[r][c] = triangle[r-1][c-1]+triangle[r-1][c] +This implementation demonstrates how to generate the elements of Pascal's Triangle. +An element with row index r and column index c can be derived as: +triangle[r][c] = triangle[r-1][c-1] + triangle[r-1][c] -A Pascal's triangle is a triangular array containing binomial coefficients. +Pascal's Triangle is a triangular array containing binomial coefficients. https://en.wikipedia.org/wiki/Pascal%27s_triangle """ +from collections.abc import Callable +from timeit import timeit + def print_pascal_triangle(num_rows: int) -> None: """ - Print Pascal's triangle for different number of rows + Print Pascal's triangle for the specified number of rows >>> print_pascal_triangle(5) 1 1 1 @@ -20,7 +23,7 @@ def print_pascal_triangle(num_rows: int) -> None: """ triangle = generate_pascal_triangle(num_rows) for row_idx in range(num_rows): - # Print left spaces + # Print leading spaces for _ in range(num_rows - row_idx - 1): print(end=" ") # Print row values @@ -34,7 +37,7 @@ def print_pascal_triangle(num_rows: int) -> None: def generate_pascal_triangle(num_rows: int) -> list[list[int]]: """ - Create Pascal's triangle for different number of rows + Generate Pascal's triangle for the specified number of rows >>> generate_pascal_triangle(0) [] >>> generate_pascal_triangle(1) @@ -50,22 +53,20 @@ def generate_pascal_triangle(num_rows: int) -> list[list[int]]: >>> generate_pascal_triangle(-5) Traceback (most recent call last): ... - ValueError: The input value of 'num_rows' should be greater than or equal to 0 + ValueError: Input value 'num_rows' must be >= 0 >>> generate_pascal_triangle(7.89) Traceback (most recent call last): ... - TypeError: The input value of 'num_rows' should be 'int' + TypeError: Input value 'num_rows' must be an integer """ if not isinstance(num_rows, int): - raise TypeError("The input value of 'num_rows' should be 'int'") + raise TypeError("Input value 'num_rows' must be an integer") if num_rows == 0: return [] - elif num_rows < 0: - raise ValueError( - "The input value of 'num_rows' should be greater than or equal to 0" - ) + if num_rows < 0: + raise ValueError("Input value 'num_rows' must be >= 0") triangle: list[list[int]] = [] for current_row_idx in range(num_rows): @@ -81,7 +82,7 @@ def populate_current_row(triangle: list[list[int]], current_row_idx: int) -> lis [1, 1] """ current_row = [-1] * (current_row_idx + 1) - # first and last elements of current row are equal to 1 + # First and last elements of current row are always 1 current_row[0], current_row[-1] = 1, 1 for current_col_idx in range(1, current_row_idx): calculate_current_element( @@ -103,22 +104,19 @@ def calculate_current_element( >>> current_row [1, 2, 1] """ - above_to_left_elt = triangle[current_row_idx - 1][current_col_idx - 1] - above_to_right_elt = triangle[current_row_idx - 1][current_col_idx] - current_row[current_col_idx] = above_to_left_elt + above_to_right_elt + above_left = triangle[current_row_idx - 1][current_col_idx - 1] + above_right = triangle[current_row_idx - 1][current_col_idx] + current_row[current_col_idx] = above_left + above_right def generate_pascal_triangle_optimized(num_rows: int) -> list[list[int]]: """ - This function returns a matrix representing the corresponding pascal's triangle - according to the given input of number of rows of Pascal's triangle to be generated. - It reduces the operations done to generate a row by half - by eliminating redundant calculations. + Returns a matrix representing Pascal's triangle. + Reduces operations by half by eliminating redundant calculations. - :param num_rows: Integer specifying the number of rows in the Pascal's triangle - :return: 2-D List (matrix) representing the Pascal's triangle + :param num_rows: Number of rows in the Pascal's triangle + :return: 2D list representing the Pascal's triangle - Return the Pascal's triangle of given rows >>> generate_pascal_triangle_optimized(3) [[1], [1, 1], [1, 2, 1]] >>> generate_pascal_triangle_optimized(1) @@ -128,29 +126,27 @@ def generate_pascal_triangle_optimized(num_rows: int) -> list[list[int]]: >>> generate_pascal_triangle_optimized(-5) Traceback (most recent call last): ... - ValueError: The input value of 'num_rows' should be greater than or equal to 0 + ValueError: Input value 'num_rows' must be >= 0 >>> generate_pascal_triangle_optimized(7.89) Traceback (most recent call last): ... - TypeError: The input value of 'num_rows' should be 'int' + TypeError: Input value 'num_rows' must be an integer """ if not isinstance(num_rows, int): - raise TypeError("The input value of 'num_rows' should be 'int'") + raise TypeError("Input value 'num_rows' must be an integer") if num_rows == 0: return [] - elif num_rows < 0: - raise ValueError( - "The input value of 'num_rows' should be greater than or equal to 0" - ) + if num_rows < 0: + raise ValueError("Input value 'num_rows' must be >= 0") result: list[list[int]] = [[1]] for row_index in range(1, num_rows): temp_row = [0] + result[-1] + [0] row_length = row_index + 1 - # Calculate the number of distinct elements in a row + # Calculate number of distinct elements in row distinct_elements = sum(divmod(row_length, 2)) row_first_half = [ temp_row[i - 1] + temp_row[i] for i in range(1, distinct_elements + 1) @@ -165,15 +161,12 @@ def generate_pascal_triangle_optimized(num_rows: int) -> list[list[int]]: def benchmark() -> None: """ - Benchmark multiple functions, with three different length int values. + Benchmark functions with different input sizes """ - from collections.abc import Callable - from timeit import timeit def benchmark_a_function(func: Callable, value: int) -> None: call = f"{func.__name__}({value})" timing = timeit(f"__main__.{call}", setup="import __main__") - # print(f"{call:38} = {func(value)} -- {timing:.4f} seconds") print(f"{call:38} -- {timing:.4f} seconds") for value in range(15): # (1, 7, 14): diff --git a/other/lfu_cache.py b/other/lfu_cache.py index 5a143c739b9d..6eaacff2966a 100644 --- a/other/lfu_cache.py +++ b/other/lfu_cache.py @@ -1,13 +1,13 @@ from __future__ import annotations from collections.abc import Callable -from typing import Generic, TypeVar +from typing import TypeVar T = TypeVar("T") U = TypeVar("U") -class DoubleLinkedListNode(Generic[T, U]): +class DoubleLinkedListNode[T, U]: """ Double Linked List Node built specifically for LFU Cache @@ -30,7 +30,7 @@ def __repr__(self) -> str: ) -class DoubleLinkedList(Generic[T, U]): +class DoubleLinkedList[T, U]: """ Double Linked List built specifically for LFU Cache @@ -161,7 +161,7 @@ def remove( return node -class LFUCache(Generic[T, U]): +class LFUCache[T, U]: """ LFU Cache to store a given capacity of data. Can be used as a stand-alone object or as a function decorator. diff --git a/other/lru_cache.py b/other/lru_cache.py index 4f0c843c86cc..1d9a67f4ad0b 100644 --- a/other/lru_cache.py +++ b/other/lru_cache.py @@ -1,13 +1,9 @@ from __future__ import annotations from collections.abc import Callable -from typing import Generic, TypeVar -T = TypeVar("T") -U = TypeVar("U") - -class DoubleLinkedListNode(Generic[T, U]): +class DoubleLinkedListNode[T, U]: """ Double Linked List Node built specifically for LRU Cache @@ -28,7 +24,7 @@ def __repr__(self) -> str: ) -class DoubleLinkedList(Generic[T, U]): +class DoubleLinkedList[T, U]: """ Double Linked List built specifically for LRU Cache @@ -143,7 +139,7 @@ def remove( return node -class LRUCache(Generic[T, U]): +class LRUCache[T, U]: """ LRU Cache to store a given capacity of data. Can be used as a stand-alone object or as a function decorator. @@ -222,7 +218,6 @@ def __repr__(self) -> str: Return the details for the cache instance [hits, misses, capacity, current_size] """ - return ( f"CacheInfo(hits={self.hits}, misses={self.miss}, " f"capacity={self.capacity}, current size={self.num_keys})" @@ -240,7 +235,6 @@ def __contains__(self, key: T) -> bool: >>> 1 in cache True """ - return key in self.cache def get(self, key: T) -> U | None: @@ -267,7 +261,6 @@ def put(self, key: T, value: U) -> None: """ Sets the value for the input key and updates the Double Linked List """ - if key not in self.cache: if self.num_keys >= self.capacity: # delete first node (oldest) when over capacity @@ -286,7 +279,6 @@ def put(self, key: T, value: U) -> None: self.cache[key] = DoubleLinkedListNode(key, value) self.list.add(self.cache[key]) self.num_keys += 1 - else: # bump node to the end of the list, update value node = self.list.remove(self.cache[key])