diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index da6762123b04..b08139561639 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,7 +16,7 @@ repos: - id: auto-walrus - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.0.282 + rev: v0.0.284 hooks: - id: ruff @@ -33,7 +33,7 @@ repos: - tomli - repo: https://github.com/tox-dev/pyproject-fmt - rev: "0.13.0" + rev: "0.13.1" hooks: - id: pyproject-fmt @@ -51,7 +51,7 @@ repos: - id: validate-pyproject - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.4.1 + rev: v1.5.0 hooks: - id: mypy args: diff --git a/DIRECTORY.md b/DIRECTORY.md index 5578c1c9a6dd..be5fa3584a58 100644 --- a/DIRECTORY.md +++ b/DIRECTORY.md @@ -74,6 +74,7 @@ * [Game Of Life](cellular_automata/game_of_life.py) * [Nagel Schrekenberg](cellular_automata/nagel_schrekenberg.py) * [One Dimensional](cellular_automata/one_dimensional.py) + * [Wa Tor](cellular_automata/wa_tor.py) ## Ciphers * [A1Z26](ciphers/a1z26.py) @@ -335,9 +336,11 @@ * [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) + * [Regex Match](dynamic_programming/regex_match.py) * [Rod Cutting](dynamic_programming/rod_cutting.py) * [Subset Generation](dynamic_programming/subset_generation.py) * [Sum Of Subset](dynamic_programming/sum_of_subset.py) + * [Tribonacci](dynamic_programming/tribonacci.py) * [Viterbi](dynamic_programming/viterbi.py) * [Word Break](dynamic_programming/word_break.py) @@ -570,9 +573,7 @@ * [Fermat Little Theorem](maths/fermat_little_theorem.py) * [Fibonacci](maths/fibonacci.py) * [Find Max](maths/find_max.py) - * [Find Max Recursion](maths/find_max_recursion.py) * [Find Min](maths/find_min.py) - * [Find Min Recursion](maths/find_min_recursion.py) * [Floor](maths/floor.py) * [Gamma](maths/gamma.py) * [Gamma Recursive](maths/gamma_recursive.py) @@ -1169,6 +1170,7 @@ * [Is Pangram](strings/is_pangram.py) * [Is Spain National Id](strings/is_spain_national_id.py) * [Is Srilankan Phone Number](strings/is_srilankan_phone_number.py) + * [Is Valid Email Address](strings/is_valid_email_address.py) * [Jaro Winkler](strings/jaro_winkler.py) * [Join](strings/join.py) * [Knuth Morris Pratt](strings/knuth_morris_pratt.py) diff --git a/boolean_algebra/quine_mc_cluskey.py b/boolean_algebra/quine_mc_cluskey.py index 6788dfb28ba1..8e22e66726d4 100644 --- a/boolean_algebra/quine_mc_cluskey.py +++ b/boolean_algebra/quine_mc_cluskey.py @@ -74,10 +74,7 @@ def is_for_table(string1: str, string2: str, count: int) -> bool: """ list1 = list(string1) list2 = list(string2) - count_n = 0 - for i in range(len(list1)): - if list1[i] != list2[i]: - count_n += 1 + count_n = sum(item1 != item2 for item1, item2 in zip(list1, list2)) return count_n == count @@ -92,40 +89,34 @@ def selection(chart: list[list[int]], prime_implicants: list[str]) -> list[str]: temp = [] select = [0] * len(chart) for i in range(len(chart[0])): - count = 0 - rem = -1 - for j in range(len(chart)): - if chart[j][i] == 1: - count += 1 - rem = j + count = sum(row[i] == 1 for row in chart) if count == 1: + rem = max(j for j, row in enumerate(chart) if row[i] == 1) select[rem] = 1 - for i in range(len(select)): - if select[i] == 1: - for j in range(len(chart[0])): - if chart[i][j] == 1: - for k in range(len(chart)): - chart[k][j] = 0 - temp.append(prime_implicants[i]) + for i, item in enumerate(select): + if item != 1: + continue + for j in range(len(chart[0])): + if chart[i][j] != 1: + continue + for row in chart: + row[j] = 0 + temp.append(prime_implicants[i]) while True: - max_n = 0 - rem = -1 - count_n = 0 - for i in range(len(chart)): - count_n = chart[i].count(1) - if count_n > max_n: - max_n = count_n - rem = i + counts = [chart[i].count(1) for i in range(len(chart))] + max_n = max(counts) + rem = counts.index(max_n) if max_n == 0: return temp temp.append(prime_implicants[rem]) - for i in range(len(chart[0])): - if chart[rem][i] == 1: - for j in range(len(chart)): - chart[j][i] = 0 + for j in range(len(chart[0])): + if chart[rem][j] != 1: + continue + for i in range(len(chart)): + chart[i][j] = 0 def prime_implicant_chart( diff --git a/cellular_automata/wa_tor.py b/cellular_automata/wa_tor.py new file mode 100644 index 000000000000..e423d1595bdb --- /dev/null +++ b/cellular_automata/wa_tor.py @@ -0,0 +1,550 @@ +""" +Wa-Tor algorithm (1984) + +@ https://en.wikipedia.org/wiki/Wa-Tor +@ https://beltoforion.de/en/wator/ +@ https://beltoforion.de/en/wator/images/wator_medium.webm + +This solution aims to completely remove any systematic approach +to the Wa-Tor planet, and utilise fully random methods. + +The constants are a working set that allows the Wa-Tor planet +to result in one of the three possible results. +""" + +from collections.abc import Callable +from random import randint, shuffle +from time import sleep +from typing import Literal + +WIDTH = 50 # Width of the Wa-Tor planet +HEIGHT = 50 # Height of the Wa-Tor planet + +PREY_INITIAL_COUNT = 30 # The initial number of prey entities +PREY_REPRODUCTION_TIME = 5 # The chronons before reproducing + +PREDATOR_INITIAL_COUNT = 50 # The initial number of predator entities +# The initial energy value of predator entities +PREDATOR_INITIAL_ENERGY_VALUE = 15 +# The energy value provided when consuming prey +PREDATOR_FOOD_VALUE = 5 +PREDATOR_REPRODUCTION_TIME = 20 # The chronons before reproducing + +MAX_ENTITIES = 500 # The max number of organisms on the board +# The number of entities to delete from the unbalanced side +DELETE_UNBALANCED_ENTITIES = 50 + + +class Entity: + """ + Represents an entity (either prey or predator). + + >>> e = Entity(True, coords=(0, 0)) + >>> e.prey + True + >>> e.coords + (0, 0) + >>> e.alive + True + """ + + def __init__(self, prey: bool, coords: tuple[int, int]) -> None: + self.prey = prey + # The (row, col) pos of the entity + self.coords = coords + + self.remaining_reproduction_time = ( + PREY_REPRODUCTION_TIME if prey else PREDATOR_REPRODUCTION_TIME + ) + self.energy_value = None if prey is True else PREDATOR_INITIAL_ENERGY_VALUE + self.alive = True + + def reset_reproduction_time(self) -> None: + """ + >>> e = Entity(True, coords=(0, 0)) + >>> e.reset_reproduction_time() + >>> e.remaining_reproduction_time == PREY_REPRODUCTION_TIME + True + >>> e = Entity(False, coords=(0, 0)) + >>> e.reset_reproduction_time() + >>> e.remaining_reproduction_time == PREDATOR_REPRODUCTION_TIME + True + """ + self.remaining_reproduction_time = ( + PREY_REPRODUCTION_TIME if self.prey is True else PREDATOR_REPRODUCTION_TIME + ) + + def __repr__(self) -> str: + """ + >>> Entity(prey=True, coords=(1, 1)) + Entity(prey=True, coords=(1, 1), remaining_reproduction_time=5) + >>> Entity(prey=False, coords=(2, 1)) # doctest: +NORMALIZE_WHITESPACE + Entity(prey=False, coords=(2, 1), + remaining_reproduction_time=20, energy_value=15) + """ + repr_ = ( + f"Entity(prey={self.prey}, coords={self.coords}, " + f"remaining_reproduction_time={self.remaining_reproduction_time}" + ) + if self.energy_value is not None: + repr_ += f", energy_value={self.energy_value}" + return f"{repr_})" + + +class WaTor: + """ + Represents the main Wa-Tor algorithm. + + :attr time_passed: A function that is called every time + time passes (a chronon) in order to visually display + the new Wa-Tor planet. The time_passed function can block + using time.sleep to slow the algorithm progression. + + >>> wt = WaTor(10, 15) + >>> wt.width + 10 + >>> wt.height + 15 + >>> len(wt.planet) + 15 + >>> len(wt.planet[0]) + 10 + >>> len(wt.get_entities()) == PREDATOR_INITIAL_COUNT + PREY_INITIAL_COUNT + True + """ + + time_passed: Callable[["WaTor", int], None] | None + + def __init__(self, width: int, height: int) -> None: + self.width = width + self.height = height + self.time_passed = None + + self.planet: list[list[Entity | None]] = [[None] * width for _ in range(height)] + + # Populate planet with predators and prey randomly + for _ in range(PREY_INITIAL_COUNT): + self.add_entity(prey=True) + for _ in range(PREDATOR_INITIAL_COUNT): + self.add_entity(prey=False) + self.set_planet(self.planet) + + def set_planet(self, planet: list[list[Entity | None]]) -> None: + """ + Ease of access for testing + + >>> wt = WaTor(WIDTH, HEIGHT) + >>> planet = [ + ... [None, None, None], + ... [None, Entity(True, coords=(1, 1)), None] + ... ] + >>> wt.set_planet(planet) + >>> wt.planet == planet + True + >>> wt.width + 3 + >>> wt.height + 2 + """ + self.planet = planet + self.width = len(planet[0]) + self.height = len(planet) + + def add_entity(self, prey: bool) -> None: + """ + Adds an entity, making sure the entity does + not override another entity + + >>> wt = WaTor(WIDTH, HEIGHT) + >>> wt.set_planet([[None, None], [None, None]]) + >>> wt.add_entity(True) + >>> len(wt.get_entities()) + 1 + >>> wt.add_entity(False) + >>> len(wt.get_entities()) + 2 + """ + while True: + row, col = randint(0, self.height - 1), randint(0, self.width - 1) + if self.planet[row][col] is None: + self.planet[row][col] = Entity(prey=prey, coords=(row, col)) + return + + def get_entities(self) -> list[Entity]: + """ + Returns a list of all the entities within the planet. + + >>> wt = WaTor(WIDTH, HEIGHT) + >>> len(wt.get_entities()) == PREDATOR_INITIAL_COUNT + PREY_INITIAL_COUNT + True + """ + return [entity for column in self.planet for entity in column if entity] + + def balance_predators_and_prey(self) -> None: + """ + Balances predators and preys so that prey + can not dominate the predators, blocking up + space for them to reproduce. + + >>> wt = WaTor(WIDTH, HEIGHT) + >>> for i in range(2000): + ... row, col = i // HEIGHT, i % WIDTH + ... wt.planet[row][col] = Entity(True, coords=(row, col)) + >>> entities = len(wt.get_entities()) + >>> wt.balance_predators_and_prey() + >>> len(wt.get_entities()) == entities + False + """ + entities = self.get_entities() + shuffle(entities) + + if len(entities) >= MAX_ENTITIES - MAX_ENTITIES / 10: + prey = [entity for entity in entities if entity.prey] + predators = [entity for entity in entities if not entity.prey] + + prey_count, predator_count = len(prey), len(predators) + + entities_to_purge = ( + prey[:DELETE_UNBALANCED_ENTITIES] + if prey_count > predator_count + else predators[:DELETE_UNBALANCED_ENTITIES] + ) + for entity in entities_to_purge: + self.planet[entity.coords[0]][entity.coords[1]] = None + + def get_surrounding_prey(self, entity: Entity) -> list[Entity]: + """ + Returns all the prey entities around (N, S, E, W) a predator entity. + + Subtly different to the try_to_move_to_unoccupied square. + + >>> wt = WaTor(WIDTH, HEIGHT) + >>> wt.set_planet([ + ... [None, Entity(True, (0, 1)), None], + ... [None, Entity(False, (1, 1)), None], + ... [None, Entity(True, (2, 1)), None]]) + >>> wt.get_surrounding_prey( + ... Entity(False, (1, 1))) # doctest: +NORMALIZE_WHITESPACE + [Entity(prey=True, coords=(0, 1), remaining_reproduction_time=5), + Entity(prey=True, coords=(2, 1), remaining_reproduction_time=5)] + >>> wt.set_planet([[Entity(False, (0, 0))]]) + >>> wt.get_surrounding_prey(Entity(False, (0, 0))) + [] + >>> wt.set_planet([ + ... [Entity(True, (0, 0)), Entity(False, (1, 0)), Entity(False, (2, 0))], + ... [None, Entity(False, (1, 1)), Entity(True, (2, 1))], + ... [None, None, None]]) + >>> wt.get_surrounding_prey(Entity(False, (1, 0))) + [Entity(prey=True, coords=(0, 0), remaining_reproduction_time=5)] + """ + row, col = entity.coords + adjacent: list[tuple[int, int]] = [ + (row - 1, col), # North + (row + 1, col), # South + (row, col - 1), # West + (row, col + 1), # East + ] + + return [ + ent + for r, c in adjacent + if 0 <= r < self.height + and 0 <= c < self.width + and (ent := self.planet[r][c]) is not None + and ent.prey + ] + + def move_and_reproduce( + self, entity: Entity, direction_orders: list[Literal["N", "E", "S", "W"]] + ) -> None: + """ + Attempts to move to an unoccupied neighbouring square + in either of the four directions (North, South, East, West). + If the move was successful and the remaining_reproduction time is + equal to 0, then a new prey or predator can also be created + in the previous square. + + :param direction_orders: Ordered list (like priority queue) depicting + order to attempt to move. Removes any systematic + approach of checking neighbouring squares. + + >>> planet = [ + ... [None, None, None], + ... [None, Entity(True, coords=(1, 1)), None], + ... [None, None, None] + ... ] + >>> wt = WaTor(WIDTH, HEIGHT) + >>> wt.set_planet(planet) + >>> wt.move_and_reproduce(Entity(True, coords=(1, 1)), direction_orders=["N"]) + >>> wt.planet # doctest: +NORMALIZE_WHITESPACE + [[None, Entity(prey=True, coords=(0, 1), remaining_reproduction_time=4), None], + [None, None, None], + [None, None, None]] + >>> wt.planet[0][0] = Entity(True, coords=(0, 0)) + >>> wt.move_and_reproduce(Entity(True, coords=(0, 1)), + ... direction_orders=["N", "W", "E", "S"]) + >>> wt.planet # doctest: +NORMALIZE_WHITESPACE + [[Entity(prey=True, coords=(0, 0), remaining_reproduction_time=5), None, + Entity(prey=True, coords=(0, 2), remaining_reproduction_time=4)], + [None, None, None], + [None, None, None]] + >>> wt.planet[0][1] = wt.planet[0][2] + >>> wt.planet[0][2] = None + >>> wt.move_and_reproduce(Entity(True, coords=(0, 1)), + ... direction_orders=["N", "W", "S", "E"]) + >>> wt.planet # doctest: +NORMALIZE_WHITESPACE + [[Entity(prey=True, coords=(0, 0), remaining_reproduction_time=5), None, None], + [None, Entity(prey=True, coords=(1, 1), remaining_reproduction_time=4), None], + [None, None, None]] + + >>> wt = WaTor(WIDTH, HEIGHT) + >>> reproducable_entity = Entity(False, coords=(0, 1)) + >>> reproducable_entity.remaining_reproduction_time = 0 + >>> wt.planet = [[None, reproducable_entity]] + >>> wt.move_and_reproduce(reproducable_entity, + ... direction_orders=["N", "W", "S", "E"]) + >>> wt.planet # doctest: +NORMALIZE_WHITESPACE + [[Entity(prey=False, coords=(0, 0), + remaining_reproduction_time=20, energy_value=15), + Entity(prey=False, coords=(0, 1), remaining_reproduction_time=20, + energy_value=15)]] + """ + row, col = coords = entity.coords + + adjacent_squares: dict[Literal["N", "E", "S", "W"], tuple[int, int]] = { + "N": (row - 1, col), # North + "S": (row + 1, col), # South + "W": (row, col - 1), # West + "E": (row, col + 1), # East + } + # Weight adjacent locations + adjacent: list[tuple[int, int]] = [] + for order in direction_orders: + adjacent.append(adjacent_squares[order]) + + for r, c in adjacent: + if ( + 0 <= r < self.height + and 0 <= c < self.width + and self.planet[r][c] is None + ): + # Move entity to empty adjacent square + self.planet[r][c] = entity + self.planet[row][col] = None + entity.coords = (r, c) + break + + # (2.) See if it possible to reproduce in previous square + if coords != entity.coords and entity.remaining_reproduction_time <= 0: + # Check if the entities on the planet is less than the max limit + if len(self.get_entities()) < MAX_ENTITIES: + # Reproduce in previous square + self.planet[row][col] = Entity(prey=entity.prey, coords=coords) + entity.reset_reproduction_time() + else: + entity.remaining_reproduction_time -= 1 + + def perform_prey_actions( + self, entity: Entity, direction_orders: list[Literal["N", "E", "S", "W"]] + ) -> None: + """ + Performs the actions for a prey entity + + For prey the rules are: + 1. At each chronon, a prey moves randomly to one of the adjacent unoccupied + squares. If there are no free squares, no movement takes place. + 2. Once a prey has survived a certain number of chronons it may reproduce. + This is done as it moves to a neighbouring square, + leaving behind a new prey in its old position. + Its reproduction time is also reset to zero. + + >>> wt = WaTor(WIDTH, HEIGHT) + >>> reproducable_entity = Entity(True, coords=(0, 1)) + >>> reproducable_entity.remaining_reproduction_time = 0 + >>> wt.planet = [[None, reproducable_entity]] + >>> wt.perform_prey_actions(reproducable_entity, + ... direction_orders=["N", "W", "S", "E"]) + >>> wt.planet # doctest: +NORMALIZE_WHITESPACE + [[Entity(prey=True, coords=(0, 0), remaining_reproduction_time=5), + Entity(prey=True, coords=(0, 1), remaining_reproduction_time=5)]] + """ + self.move_and_reproduce(entity, direction_orders) + + def perform_predator_actions( + self, + entity: Entity, + occupied_by_prey_coords: tuple[int, int] | None, + direction_orders: list[Literal["N", "E", "S", "W"]], + ) -> None: + """ + Performs the actions for a predator entity + + :param occupied_by_prey_coords: Move to this location if there is prey there + + For predators the rules are: + 1. At each chronon, a predator moves randomly to an adjacent square occupied + by a prey. If there is none, the predator moves to a random adjacent + unoccupied square. If there are no free squares, no movement takes place. + 2. At each chronon, each predator is deprived of a unit of energy. + 3. Upon reaching zero energy, a predator dies. + 4. If a predator moves to a square occupied by a prey, + it eats the prey and earns a certain amount of energy. + 5. Once a predator has survived a certain number of chronons + it may reproduce in exactly the same way as the prey. + + >>> wt = WaTor(WIDTH, HEIGHT) + >>> wt.set_planet([[Entity(True, coords=(0, 0)), Entity(False, coords=(0, 1))]]) + >>> wt.perform_predator_actions(Entity(False, coords=(0, 1)), (0, 0), []) + >>> wt.planet # doctest: +NORMALIZE_WHITESPACE + [[Entity(prey=False, coords=(0, 0), + remaining_reproduction_time=20, energy_value=19), None]] + """ + assert entity.energy_value is not None # [type checking] + + # (3.) If the entity has 0 energy, it will die + if entity.energy_value == 0: + self.planet[entity.coords[0]][entity.coords[1]] = None + return + + # (1.) Move to entity if possible + if occupied_by_prey_coords is not None: + # Kill the prey + prey = self.planet[occupied_by_prey_coords[0]][occupied_by_prey_coords[1]] + assert prey is not None + prey.alive = False + + # Move onto prey + self.planet[occupied_by_prey_coords[0]][occupied_by_prey_coords[1]] = entity + self.planet[entity.coords[0]][entity.coords[1]] = None + + entity.coords = occupied_by_prey_coords + # (4.) Eats the prey and earns energy + entity.energy_value += PREDATOR_FOOD_VALUE + else: + # (5.) If it has survived the certain number of chronons it will also + # reproduce in this function + self.move_and_reproduce(entity, direction_orders) + + # (2.) Each chronon, the predator is deprived of a unit of energy + entity.energy_value -= 1 + + def run(self, *, iteration_count: int) -> None: + """ + Emulate time passing by looping iteration_count times + + >>> wt = WaTor(WIDTH, HEIGHT) + >>> wt.run(iteration_count=PREDATOR_INITIAL_ENERGY_VALUE - 1) + >>> len(list(filter(lambda entity: entity.prey is False, + ... wt.get_entities()))) >= PREDATOR_INITIAL_COUNT + True + """ + for iter_num in range(iteration_count): + # Generate list of all entities in order to randomly + # pop an entity at a time to simulate true randomness + # This removes the systematic approach of iterating + # through each entity width by height + all_entities = self.get_entities() + + for __ in range(len(all_entities)): + entity = all_entities.pop(randint(0, len(all_entities) - 1)) + if entity.alive is False: + continue + + directions: list[Literal["N", "E", "S", "W"]] = ["N", "E", "S", "W"] + shuffle(directions) # Randomly shuffle directions + + if entity.prey: + self.perform_prey_actions(entity, directions) + else: + # Create list of surrounding prey + surrounding_prey = self.get_surrounding_prey(entity) + surrounding_prey_coords = None + + if surrounding_prey: + # Again, randomly shuffle directions + shuffle(surrounding_prey) + surrounding_prey_coords = surrounding_prey[0].coords + + self.perform_predator_actions( + entity, surrounding_prey_coords, directions + ) + + # Balance out the predators and prey + self.balance_predators_and_prey() + + if self.time_passed is not None: + # Call time_passed function for Wa-Tor planet + # visualisation in a terminal or a graph. + self.time_passed(self, iter_num) + + +def visualise(wt: WaTor, iter_number: int, *, colour: bool = True) -> None: + """ + Visually displays the Wa-Tor planet using + an ascii code in terminal to clear and re-print + the Wa-Tor planet at intervals. + + Uses ascii colour codes to colourfully display + the predators and prey. + + (0x60f197) Prey = # + (0xfffff) Predator = x + + >>> wt = WaTor(30, 30) + >>> wt.set_planet([ + ... [Entity(True, coords=(0, 0)), Entity(False, coords=(0, 1)), None], + ... [Entity(False, coords=(1, 0)), None, Entity(False, coords=(1, 2))], + ... [None, Entity(True, coords=(2, 1)), None] + ... ]) + >>> visualise(wt, 0, colour=False) # doctest: +NORMALIZE_WHITESPACE + # x . + x . x + . # . + + Iteration: 0 | Prey count: 2 | Predator count: 3 | + """ + if colour: + __import__("os").system("") + print("\x1b[0;0H\x1b[2J\x1b[?25l") + + reprint = "\x1b[0;0H" if colour else "" + ansi_colour_end = "\x1b[0m " if colour else " " + + planet = wt.planet + output = "" + + # Iterate over every entity in the planet + for row in planet: + for entity in row: + if entity is None: + output += " . " + else: + if colour is True: + output += ( + "\x1b[38;2;96;241;151m" + if entity.prey + else "\x1b[38;2;255;255;15m" + ) + output += f" {'#' if entity.prey else 'x'}{ansi_colour_end}" + + output += "\n" + + entities = wt.get_entities() + prey_count = sum(entity.prey for entity in entities) + + print( + f"{output}\n Iteration: {iter_number} | Prey count: {prey_count} | " + f"Predator count: {len(entities) - prey_count} | {reprint}" + ) + # Block the thread to be able to visualise seeing the algorithm + sleep(0.05) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + + wt = WaTor(WIDTH, HEIGHT) + wt.time_passed = visualise + wt.run(iteration_count=100_000) diff --git a/ciphers/rsa_key_generator.py b/ciphers/rsa_key_generator.py index 2573ed01387b..eedc7336804a 100644 --- a/ciphers/rsa_key_generator.py +++ b/ciphers/rsa_key_generator.py @@ -2,8 +2,7 @@ import random import sys -from . import cryptomath_module as cryptoMath # noqa: N812 -from . import rabin_miller as rabinMiller # noqa: N812 +from . import cryptomath_module, rabin_miller def main() -> None: @@ -13,20 +12,26 @@ def main() -> None: def generate_key(key_size: int) -> tuple[tuple[int, int], tuple[int, int]]: - print("Generating prime p...") - p = rabinMiller.generate_large_prime(key_size) - print("Generating prime q...") - q = rabinMiller.generate_large_prime(key_size) + """ + >>> random.seed(0) # for repeatability + >>> public_key, private_key = generate_key(8) + >>> public_key + (26569, 239) + >>> private_key + (26569, 2855) + """ + p = rabin_miller.generate_large_prime(key_size) + q = rabin_miller.generate_large_prime(key_size) n = p * q - print("Generating e that is relatively prime to (p - 1) * (q - 1)...") + # Generate e that is relatively prime to (p - 1) * (q - 1) while True: e = random.randrange(2 ** (key_size - 1), 2 ** (key_size)) - if cryptoMath.gcd(e, (p - 1) * (q - 1)) == 1: + if cryptomath_module.gcd(e, (p - 1) * (q - 1)) == 1: break - print("Calculating d that is mod inverse of e...") - d = cryptoMath.find_mod_inverse(e, (p - 1) * (q - 1)) + # Calculate d that is mod inverse of e + d = cryptomath_module.find_mod_inverse(e, (p - 1) * (q - 1)) public_key = (n, e) private_key = (n, d) diff --git a/computer_vision/flip_augmentation.py b/computer_vision/flip_augmentation.py index 93b4e3f6da79..77a8cbd7b14f 100644 --- a/computer_vision/flip_augmentation.py +++ b/computer_vision/flip_augmentation.py @@ -32,13 +32,13 @@ def main() -> None: letter_code = random_chars(32) 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]) + cv2.imwrite(f"{file_root}.jpg", image, [cv2.IMWRITE_JPEG_QUALITY, 85]) 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]}" annos_list.append(obj) - with open(f"/{file_root}.txt", "w") as outfile: + with open(f"{file_root}.txt", "w") as outfile: outfile.write("\n".join(line for line in annos_list)) diff --git a/conversions/binary_to_decimal.py b/conversions/binary_to_decimal.py index 914a9318c225..63f632db177c 100644 --- a/conversions/binary_to_decimal.py +++ b/conversions/binary_to_decimal.py @@ -26,14 +26,20 @@ def bin_to_decimal(bin_string: str) -> int: bin_string = str(bin_string).strip() if not bin_string: raise ValueError("Empty string was passed to the function") + is_negative = bin_string[0] == "-" + if is_negative: bin_string = bin_string[1:] + if not all(char in "01" for char in bin_string): raise ValueError("Non-binary value was passed to the function") + decimal_number = 0 + for char in bin_string: decimal_number = 2 * decimal_number + int(char) + return -decimal_number if is_negative else decimal_number diff --git a/data_structures/binary_tree/binary_search_tree.py b/data_structures/binary_tree/binary_search_tree.py index c72195424c7c..a706d21e3bb2 100644 --- a/data_structures/binary_tree/binary_search_tree.py +++ b/data_structures/binary_tree/binary_search_tree.py @@ -1,5 +1,62 @@ -""" +r""" A binary search Tree + +Example + 8 + / \ + 3 10 + / \ \ + 1 6 14 + / \ / + 4 7 13 + +>>> t = BinarySearchTree() +>>> t.insert(8, 3, 6, 1, 10, 14, 13, 4, 7) +>>> print(" ".join(repr(i.value) for i in t.traversal_tree())) +8 3 1 6 4 7 10 14 13 +>>> print(" ".join(repr(i.value) for i in t.traversal_tree(postorder))) +1 4 7 6 3 13 14 10 8 +>>> t.remove(20) +Traceback (most recent call last): + ... +ValueError: Value 20 not found +>>> BinarySearchTree().search(6) +Traceback (most recent call last): + ... +IndexError: Warning: Tree is empty! please use another. + +Other example: + +>>> testlist = (8, 3, 6, 1, 10, 14, 13, 4, 7) +>>> t = BinarySearchTree() +>>> for i in testlist: +... t.insert(i) + +Prints all the elements of the list in order traversal +>>> print(t) +{'8': ({'3': (1, {'6': (4, 7)})}, {'10': (None, {'14': (13, None)})})} + +Test existence +>>> t.search(6) is not None +True +>>> t.search(-1) is not None +False + +>>> t.search(6).is_right +True +>>> t.search(1).is_right +False + +>>> t.get_max().value +14 +>>> t.get_min().value +1 +>>> t.empty() +False +>>> for i in testlist: +... t.remove(i) +>>> t.empty() +True """ from collections.abc import Iterable @@ -20,6 +77,10 @@ def __repr__(self) -> str: return str(self.value) return pformat({f"{self.value}": (self.left, self.right)}, indent=1) + @property + def is_right(self) -> bool: + return self.parent is not None and self is self.parent.right + class BinarySearchTree: def __init__(self, root: Node | None = None): @@ -35,18 +96,13 @@ def __reassign_nodes(self, node: Node, new_children: Node | None) -> None: if new_children is not None: # reset its kids new_children.parent = node.parent if node.parent is not None: # reset its parent - if self.is_right(node): # If it is the right children + if node.is_right: # If it is the right child node.parent.right = new_children else: node.parent.left = new_children else: self.root = new_children - def is_right(self, node: Node) -> bool: - if node.parent and node.parent.right: - return node == node.parent.right - return False - def empty(self) -> bool: return self.root is None @@ -119,22 +175,26 @@ def get_min(self, node: Node | None = None) -> Node | None: return node def remove(self, value: int) -> None: - node = self.search(value) # Look for the node with that label - if node is not None: - if node.left is None and node.right is None: # If it has no children - self.__reassign_nodes(node, None) - elif node.left is None: # Has only right children - self.__reassign_nodes(node, node.right) - elif node.right is None: # Has only left children - self.__reassign_nodes(node, node.left) - else: - tmp_node = self.get_max( - node.left - ) # Gets the max value of the left branch - self.remove(tmp_node.value) # type: ignore - node.value = ( - tmp_node.value # type: ignore - ) # Assigns the value to the node to delete and keep tree structure + # Look for the node with that label + node = self.search(value) + if node is None: + msg = f"Value {value} not found" + raise ValueError(msg) + + if node.left is None and node.right is None: # If it has no children + self.__reassign_nodes(node, None) + elif node.left is None: # Has only right children + self.__reassign_nodes(node, node.right) + elif node.right is None: # Has only left children + self.__reassign_nodes(node, node.left) + else: + predecessor = self.get_max( + node.left + ) # Gets the max value of the left branch + self.remove(predecessor.value) # type: ignore + node.value = ( + predecessor.value # type: ignore + ) # Assigns the value to the node to delete and keep tree structure def preorder_traverse(self, node: Node | None) -> Iterable: if node is not None: @@ -177,55 +237,6 @@ def postorder(curr_node: Node | None) -> list[Node]: return node_list -def binary_search_tree() -> None: - r""" - Example - 8 - / \ - 3 10 - / \ \ - 1 6 14 - / \ / - 4 7 13 - - >>> t = BinarySearchTree() - >>> t.insert(8, 3, 6, 1, 10, 14, 13, 4, 7) - >>> print(" ".join(repr(i.value) for i in t.traversal_tree())) - 8 3 1 6 4 7 10 14 13 - >>> print(" ".join(repr(i.value) for i in t.traversal_tree(postorder))) - 1 4 7 6 3 13 14 10 8 - >>> BinarySearchTree().search(6) - Traceback (most recent call last): - ... - IndexError: Warning: Tree is empty! please use another. - """ - testlist = (8, 3, 6, 1, 10, 14, 13, 4, 7) - t = BinarySearchTree() - for i in testlist: - t.insert(i) - - # Prints all the elements of the list in order traversal - print(t) - - if t.search(6) is not None: - print("The value 6 exists") - else: - print("The value 6 doesn't exist") - - if t.search(-1) is not None: - print("The value -1 exists") - else: - print("The value -1 doesn't exist") - - if not t.empty(): - print("Max Value: ", t.get_max().value) # type: ignore - print("Min Value: ", t.get_min().value) # type: ignore - - for i in testlist: - t.remove(i) - print(t) - - if __name__ == "__main__": import doctest diff --git a/digital_image_processing/morphological_operations/erosion_operation.py b/digital_image_processing/morphological_operations/erosion_operation.py index c2cde2ea6990..c0e1ef847237 100644 --- a/digital_image_processing/morphological_operations/erosion_operation.py +++ b/digital_image_processing/morphological_operations/erosion_operation.py @@ -21,6 +21,7 @@ def rgb2gray(rgb: np.array) -> np.array: def gray2binary(gray: np.array) -> np.array: """ Return binary image from gray image + >>> gray2binary(np.array([[127, 255, 0]])) array([[False, True, False]]) >>> gray2binary(np.array([[0]])) diff --git a/digital_image_processing/rotation/rotation.py b/digital_image_processing/rotation/rotation.py index 958d16fafb91..0f5e36ddd5be 100644 --- a/digital_image_processing/rotation/rotation.py +++ b/digital_image_processing/rotation/rotation.py @@ -10,12 +10,12 @@ def get_rotation( ) -> np.ndarray: """ Get image rotation - :param img: np.array + :param img: np.ndarray :param pt1: 3x2 list :param pt2: 3x2 list :param rows: columns image shape :param cols: rows image shape - :return: np.array + :return: np.ndarray """ matrix = cv2.getAffineTransform(pt1, pt2) return cv2.warpAffine(img, matrix, (rows, cols)) diff --git a/dynamic_programming/regex_match.py b/dynamic_programming/regex_match.py new file mode 100644 index 000000000000..200a882831c0 --- /dev/null +++ b/dynamic_programming/regex_match.py @@ -0,0 +1,97 @@ +""" +Regex matching check if a text matches pattern or not. +Pattern: + '.' Matches any single character. + '*' 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: + """ + Recursive matching algorithm. + + 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. + + >>> recursive_match('abc', 'a.c') + True + >>> recursive_match('abc', 'af*.c') + True + >>> recursive_match('abc', 'a.c*') + True + >>> recursive_match('abc', 'a.c*d') + False + >>> recursive_match('aa', '.*') + True + """ + if not pattern: + return not text + + if not text: + return pattern[-1] == "*" and recursive_match(text, pattern[:-2]) + + if text[-1] == pattern[-1] or pattern[-1] == ".": + return recursive_match(text[:-1], pattern[:-1]) + + if pattern[-1] == "*": + return recursive_match(text[:-1], pattern) or recursive_match( + text, pattern[:-2] + ) + + return False + + +def dp_match(text: str, pattern: str) -> bool: + """ + Dynamic programming matching algorithm. + + 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. + + >>> dp_match('abc', 'a.c') + True + >>> dp_match('abc', 'af*.c') + True + >>> dp_match('abc', 'a.c*') + True + >>> dp_match('abc', 'a.c*d') + False + >>> dp_match('aa', '.*') + True + """ + m = len(text) + n = len(pattern) + dp = [[False for _ in range(n + 1)] for _ in range(m + 1)] + dp[0][0] = True + + for j in range(1, n + 1): + dp[0][j] = pattern[j - 1] == "*" and dp[0][j - 2] + + for i in range(1, m + 1): + for j in range(1, n + 1): + if pattern[j - 1] in {".", text[i - 1]}: + dp[i][j] = dp[i - 1][j - 1] + elif pattern[j - 1] == "*": + dp[i][j] = dp[i][j - 2] + if pattern[j - 2] in {".", text[i - 1]}: + dp[i][j] |= dp[i - 1][j] + else: + dp[i][j] = False + + return dp[m][n] + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/dynamic_programming/tribonacci.py b/dynamic_programming/tribonacci.py new file mode 100644 index 000000000000..58e15da918e2 --- /dev/null +++ b/dynamic_programming/tribonacci.py @@ -0,0 +1,24 @@ +# Tribonacci sequence using Dynamic Programming + + +def tribonacci(num: int) -> list[int]: + """ + Given a number, return first n Tribonacci Numbers. + >>> tribonacci(5) + [0, 0, 1, 1, 2] + >>> tribonacci(8) + [0, 0, 1, 1, 2, 4, 7, 13] + """ + dp = [0] * num + dp[2] = 1 + + for i in range(3, num): + dp[i] = dp[i - 1] + dp[i - 2] + dp[i - 3] + + return dp + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/graphs/greedy_best_first.py b/graphs/greedy_best_first.py index 35f7ca9feeef..bb3160047e34 100644 --- a/graphs/greedy_best_first.py +++ b/graphs/greedy_best_first.py @@ -6,14 +6,32 @@ Path = list[tuple[int, int]] -grid = [ - [0, 0, 0, 0, 0, 0, 0], - [0, 1, 0, 0, 0, 0, 0], # 0 are free path whereas 1's are obstacles - [0, 0, 0, 0, 0, 0, 0], - [0, 0, 1, 0, 0, 0, 0], - [1, 0, 1, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 1, 0, 0], +# 0's are free path whereas 1's are obstacles +TEST_GRIDS = [ + [ + [0, 0, 0, 0, 0, 0, 0], + [0, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 0, 0, 0, 0], + [1, 0, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 0], + ], + [ + [0, 0, 0, 1, 1, 0, 0], + [0, 0, 0, 0, 1, 0, 1], + [0, 0, 0, 1, 1, 0, 0], + [0, 1, 0, 0, 1, 0, 0], + [1, 0, 0, 1, 1, 0, 1], + [0, 0, 0, 0, 0, 0, 0], + ], + [ + [0, 0, 1, 0, 0], + [0, 1, 0, 0, 0], + [0, 0, 1, 0, 1], + [1, 0, 0, 1, 1], + [0, 0, 0, 0, 0], + ], ] delta = ([-1, 0], [0, -1], [1, 0], [0, 1]) # up, left, down, right @@ -65,10 +83,14 @@ def calculate_heuristic(self) -> float: def __lt__(self, other) -> bool: return self.f_cost < other.f_cost + def __eq__(self, other) -> bool: + return self.pos == other.pos + class GreedyBestFirst: """ - >>> gbf = GreedyBestFirst((0, 0), (len(grid) - 1, len(grid[0]) - 1)) + >>> grid = TEST_GRIDS[2] + >>> gbf = GreedyBestFirst(grid, (0, 0), (len(grid) - 1, len(grid[0]) - 1)) >>> [x.pos for x in gbf.get_successors(gbf.start)] [(1, 0), (0, 1)] >>> (gbf.start.pos_y + delta[3][0], gbf.start.pos_x + delta[3][1]) @@ -78,11 +100,14 @@ class GreedyBestFirst: >>> gbf.retrace_path(gbf.start) [(0, 0)] >>> gbf.search() # doctest: +NORMALIZE_WHITESPACE - [(0, 0), (1, 0), (2, 0), (3, 0), (3, 1), (4, 1), (5, 1), (6, 1), - (6, 2), (6, 3), (5, 3), (5, 4), (5, 5), (6, 5), (6, 6)] + [(0, 0), (1, 0), (2, 0), (2, 1), (3, 1), (4, 1), (4, 2), (4, 3), + (4, 4)] """ - def __init__(self, start: tuple[int, int], goal: tuple[int, int]): + def __init__( + self, grid: list[list[int]], start: tuple[int, int], goal: tuple[int, int] + ): + self.grid = grid self.start = Node(start[1], start[0], goal[1], goal[0], 0, None) self.target = Node(goal[1], goal[0], goal[1], goal[0], 99999, None) @@ -114,14 +139,6 @@ def search(self) -> Path | None: if child_node not in self.open_nodes: self.open_nodes.append(child_node) - else: - # retrieve the best current path - better_node = self.open_nodes.pop(self.open_nodes.index(child_node)) - - if child_node.g_cost < better_node.g_cost: - self.open_nodes.append(child_node) - else: - self.open_nodes.append(better_node) if not self.reached: return [self.start.pos] @@ -131,28 +148,22 @@ def get_successors(self, parent: Node) -> list[Node]: """ Returns a list of successors (both in the grid and free spaces) """ - successors = [] - for action in delta: - pos_x = parent.pos_x + action[1] - pos_y = parent.pos_y + action[0] - - if not (0 <= pos_x <= len(grid[0]) - 1 and 0 <= pos_y <= len(grid) - 1): - continue - - if grid[pos_y][pos_x] != 0: - continue - - successors.append( - Node( - pos_x, - pos_y, - self.target.pos_y, - self.target.pos_x, - parent.g_cost + 1, - parent, - ) + return [ + Node( + pos_x, + pos_y, + self.target.pos_x, + self.target.pos_y, + parent.g_cost + 1, + parent, + ) + for action in delta + if ( + 0 <= (pos_x := parent.pos_x + action[1]) < len(self.grid[0]) + and 0 <= (pos_y := parent.pos_y + action[0]) < len(self.grid) + and self.grid[pos_y][pos_x] == 0 ) - return successors + ] def retrace_path(self, node: Node | None) -> Path: """ @@ -168,18 +179,21 @@ def retrace_path(self, node: Node | None) -> Path: if __name__ == "__main__": - init = (0, 0) - goal = (len(grid) - 1, len(grid[0]) - 1) - for elem in grid: - print(elem) - - print("------") - - greedy_bf = GreedyBestFirst(init, goal) - path = greedy_bf.search() - if path: - for pos_x, pos_y in path: - grid[pos_x][pos_y] = 2 + for idx, grid in enumerate(TEST_GRIDS): + print(f"==grid-{idx + 1}==") + init = (0, 0) + goal = (len(grid) - 1, len(grid[0]) - 1) for elem in grid: print(elem) + + print("------") + + greedy_bf = GreedyBestFirst(grid, init, goal) + path = greedy_bf.search() + if path: + for pos_x, pos_y in path: + grid[pos_x][pos_y] = 2 + + for elem in grid: + print(elem) diff --git a/linear_algebra/src/polynom_for_points.py b/linear_algebra/src/polynom_for_points.py index f5e3db0cbb13..a9a9a8117c18 100644 --- a/linear_algebra/src/polynom_for_points.py +++ b/linear_algebra/src/polynom_for_points.py @@ -43,62 +43,43 @@ def points_to_polynomial(coordinates: list[list[int]]) -> str: x = len(coordinates) - count_of_line = 0 - matrix: list[list[float]] = [] # put the x and x to the power values in a matrix - while count_of_line < x: - count_in_line = 0 - a = coordinates[count_of_line][0] - count_line: list[float] = [] - while count_in_line < x: - count_line.append(a ** (x - (count_in_line + 1))) - count_in_line += 1 - matrix.append(count_line) - count_of_line += 1 + matrix: list[list[float]] = [ + [ + coordinates[count_of_line][0] ** (x - (count_in_line + 1)) + for count_in_line in range(x) + ] + for count_of_line in range(x) + ] - count_of_line = 0 # put the y values into a vector - vector: list[float] = [] - while count_of_line < x: - vector.append(coordinates[count_of_line][1]) - count_of_line += 1 + vector: list[float] = [coordinates[count_of_line][1] for count_of_line in range(x)] - count = 0 - - while count < x: - zahlen = 0 - while zahlen < x: - if count == zahlen: - zahlen += 1 - if zahlen == x: - break - bruch = matrix[zahlen][count] / matrix[count][count] + for count in range(x): + for number in range(x): + if count == number: + continue + fraction = matrix[number][count] / matrix[count][count] for counting_columns, item in enumerate(matrix[count]): # manipulating all the values in the matrix - matrix[zahlen][counting_columns] -= item * bruch + matrix[number][counting_columns] -= item * fraction # manipulating the values in the vector - vector[zahlen] -= vector[count] * bruch - zahlen += 1 - count += 1 + vector[number] -= vector[count] * fraction - count = 0 # make solutions - solution: list[str] = [] - while count < x: - solution.append(str(vector[count] / matrix[count][count])) - count += 1 + solution: list[str] = [ + str(vector[count] / matrix[count][count]) for count in range(x) + ] - count = 0 solved = "f(x)=" - while count < x: + for count in range(x): remove_e: list[str] = solution[count].split("E") if len(remove_e) > 1: solution[count] = f"{remove_e[0]}*10^{remove_e[1]}" solved += f"x^{x - (count + 1)}*{solution[count]}" if count + 1 != x: solved += "+" - count += 1 return solved diff --git a/machine_learning/forecasting/ex_data.csv b/machine_learning/forecasting/ex_data.csv index 1c429e649755..e6e73c4a1ca4 100644 --- a/machine_learning/forecasting/ex_data.csv +++ b/machine_learning/forecasting/ex_data.csv @@ -1,4 +1,4 @@ -total_user,total_events,days +total_users,total_events,days 18231,0.0,1 22621,1.0,2 15675,0.0,3 diff --git a/machine_learning/forecasting/run.py b/machine_learning/forecasting/run.py index 0909b76d8907..64e719daacc2 100644 --- a/machine_learning/forecasting/run.py +++ b/machine_learning/forecasting/run.py @@ -1,6 +1,6 @@ """ this is code for forecasting -but i modified it and used it for safety checker of data +but I modified it and used it for safety checker of data for ex: you have an online shop and for some reason some data are missing (the amount of data that u expected are not supposed to be) then we can use it @@ -11,6 +11,8 @@ u can just adjust it for ur own purpose """ +from warnings import simplefilter + import numpy as np import pandas as pd from sklearn.preprocessing import Normalizer @@ -45,8 +47,10 @@ def sarimax_predictor(train_user: list, train_match: list, test_match: list) -> >>> sarimax_predictor([4,2,6,8], [3,1,2,4], [2]) 6.6666671111109626 """ + # Suppress the User Warning raised by SARIMAX due to insufficient observations + simplefilter("ignore", UserWarning) order = (1, 2, 1) - seasonal_order = (1, 1, 0, 7) + seasonal_order = (1, 1, 1, 7) model = SARIMAX( train_user, exog=train_match, order=order, seasonal_order=seasonal_order ) @@ -102,6 +106,10 @@ def data_safety_checker(list_vote: list, actual_result: float) -> bool: """ safe = 0 not_safe = 0 + + if not isinstance(actual_result, float): + raise TypeError("Actual result should be float. Value passed is a list") + for i in list_vote: if i > actual_result: safe = not_safe + 1 @@ -114,16 +122,11 @@ def data_safety_checker(list_vote: list, actual_result: float) -> bool: if __name__ == "__main__": - # data_input_df = pd.read_csv("ex_data.csv", header=None) - data_input = [[18231, 0.0, 1], [22621, 1.0, 2], [15675, 0.0, 3], [23583, 1.0, 4]] - data_input_df = pd.DataFrame( - data_input, columns=["total_user", "total_even", "days"] - ) - """ data column = total user in a day, how much online event held in one day, what day is that(sunday-saturday) """ + data_input_df = pd.read_csv("ex_data.csv") # start normalization normalize_df = Normalizer().fit_transform(data_input_df.values) @@ -138,23 +141,23 @@ def data_safety_checker(list_vote: list, actual_result: float) -> bool: x_test = x[len(x) - 1 :] # for linear regression & sarimax - trn_date = total_date[: len(total_date) - 1] - trn_user = total_user[: len(total_user) - 1] - trn_match = total_match[: len(total_match) - 1] + train_date = total_date[: len(total_date) - 1] + train_user = total_user[: len(total_user) - 1] + train_match = total_match[: len(total_match) - 1] - tst_date = total_date[len(total_date) - 1 :] - tst_user = total_user[len(total_user) - 1 :] - tst_match = total_match[len(total_match) - 1 :] + test_date = total_date[len(total_date) - 1 :] + test_user = total_user[len(total_user) - 1 :] + test_match = total_match[len(total_match) - 1 :] # voting system with forecasting res_vote = [ linear_regression_prediction( - trn_date, trn_user, trn_match, tst_date, tst_match + train_date, train_user, train_match, test_date, test_match ), - sarimax_predictor(trn_user, trn_match, tst_match), - support_vector_regressor(x_train, x_test, trn_user), + sarimax_predictor(train_user, train_match, test_match), + support_vector_regressor(x_train, x_test, train_user), ] # check the safety of today's data - not_str = "" if data_safety_checker(res_vote, tst_user) else "not " - print("Today's data is {not_str}safe.") + not_str = "" if data_safety_checker(res_vote, test_user[0]) else "not " + print(f"Today's data is {not_str}safe.") diff --git a/maths/average_median.py b/maths/average_median.py index cd1ec1574893..f24e525736b3 100644 --- a/maths/average_median.py +++ b/maths/average_median.py @@ -19,7 +19,9 @@ def median(nums: list) -> int | float: Returns: Median. """ - sorted_list = sorted(nums) + # The sorted function returns list[SupportsRichComparisonT@sorted] + # which does not support `+` + sorted_list: list[int] = sorted(nums) length = len(sorted_list) mid_index = length >> 1 return ( diff --git a/maths/euler_modified.py b/maths/euler_modified.py index 14bddadf4c53..d02123e1e2fb 100644 --- a/maths/euler_modified.py +++ b/maths/euler_modified.py @@ -5,7 +5,7 @@ def euler_modified( ode_func: Callable, y0: float, x0: float, step_size: float, x_end: float -) -> np.array: +) -> np.ndarray: """ Calculate solution at each step to an ODE using Euler's Modified Method The Euler Method is straightforward to implement, but can't give accurate solutions. diff --git a/maths/find_max.py b/maths/find_max.py index 684fbe8161e8..729a80ab421c 100644 --- a/maths/find_max.py +++ b/maths/find_max.py @@ -1,23 +1,23 @@ from __future__ import annotations -def find_max(nums: list[int | float]) -> int | float: +def find_max_iterative(nums: list[int | float]) -> int | float: """ >>> for nums in ([3, 2, 1], [-3, -2, -1], [3, -3, 0], [3.0, 3.1, 2.9]): - ... find_max(nums) == max(nums) + ... find_max_iterative(nums) == max(nums) True True True True - >>> find_max([2, 4, 9, 7, 19, 94, 5]) + >>> find_max_iterative([2, 4, 9, 7, 19, 94, 5]) 94 - >>> find_max([]) + >>> find_max_iterative([]) Traceback (most recent call last): ... - ValueError: find_max() arg is an empty sequence + ValueError: find_max_iterative() arg is an empty sequence """ if len(nums) == 0: - raise ValueError("find_max() arg is an empty sequence") + raise ValueError("find_max_iterative() arg is an empty sequence") max_num = nums[0] for x in nums: if x > max_num: @@ -25,6 +25,59 @@ def find_max(nums: list[int | float]) -> int | float: return max_num +# Divide and Conquer algorithm +def find_max_recursive(nums: list[int | float], left: int, right: int) -> int | float: + """ + find max value in list + :param nums: contains elements + :param left: index of first element + :param right: index of last element + :return: max in nums + + >>> for nums in ([3, 2, 1], [-3, -2, -1], [3, -3, 0], [3.0, 3.1, 2.9]): + ... find_max_recursive(nums, 0, len(nums) - 1) == max(nums) + True + True + True + True + >>> nums = [1, 3, 5, 7, 9, 2, 4, 6, 8, 10] + >>> find_max_recursive(nums, 0, len(nums) - 1) == max(nums) + True + >>> find_max_recursive([], 0, 0) + Traceback (most recent call last): + ... + ValueError: find_max_recursive() arg is an empty sequence + >>> find_max_recursive(nums, 0, len(nums)) == max(nums) + Traceback (most recent call last): + ... + IndexError: list index out of range + >>> find_max_recursive(nums, -len(nums), -1) == max(nums) + True + >>> find_max_recursive(nums, -len(nums) - 1, -1) == max(nums) + Traceback (most recent call last): + ... + IndexError: list index out of range + """ + if len(nums) == 0: + raise ValueError("find_max_recursive() arg is an empty sequence") + if ( + left >= len(nums) + or left < -len(nums) + or right >= len(nums) + or right < -len(nums) + ): + raise IndexError("list index out of range") + if left == right: + return nums[left] + mid = (left + right) >> 1 # the middle + left_max = find_max_recursive(nums, left, mid) # find max in range[left, mid] + right_max = find_max_recursive( + nums, mid + 1, right + ) # find max in range[mid + 1, right] + + return left_max if left_max >= right_max else right_max + + if __name__ == "__main__": import doctest diff --git a/maths/find_max_recursion.py b/maths/find_max_recursion.py deleted file mode 100644 index 629932e0818f..000000000000 --- a/maths/find_max_recursion.py +++ /dev/null @@ -1,58 +0,0 @@ -from __future__ import annotations - - -# Divide and Conquer algorithm -def find_max(nums: list[int | float], left: int, right: int) -> int | float: - """ - find max value in list - :param nums: contains elements - :param left: index of first element - :param right: index of last element - :return: max in nums - - >>> for nums in ([3, 2, 1], [-3, -2, -1], [3, -3, 0], [3.0, 3.1, 2.9]): - ... find_max(nums, 0, len(nums) - 1) == max(nums) - True - True - True - True - >>> nums = [1, 3, 5, 7, 9, 2, 4, 6, 8, 10] - >>> find_max(nums, 0, len(nums) - 1) == max(nums) - True - >>> find_max([], 0, 0) - Traceback (most recent call last): - ... - ValueError: find_max() arg is an empty sequence - >>> find_max(nums, 0, len(nums)) == max(nums) - Traceback (most recent call last): - ... - IndexError: list index out of range - >>> find_max(nums, -len(nums), -1) == max(nums) - True - >>> find_max(nums, -len(nums) - 1, -1) == max(nums) - Traceback (most recent call last): - ... - IndexError: list index out of range - """ - if len(nums) == 0: - raise ValueError("find_max() arg is an empty sequence") - if ( - left >= len(nums) - or left < -len(nums) - or right >= len(nums) - or right < -len(nums) - ): - raise IndexError("list index out of range") - if left == right: - return nums[left] - mid = (left + right) >> 1 # the middle - left_max = find_max(nums, left, mid) # find max in range[left, mid] - right_max = find_max(nums, mid + 1, right) # find max in range[mid + 1, right] - - return left_max if left_max >= right_max else right_max - - -if __name__ == "__main__": - import doctest - - doctest.testmod(verbose=True) diff --git a/maths/find_min.py b/maths/find_min.py index 2eac087c6388..762562e36ef9 100644 --- a/maths/find_min.py +++ b/maths/find_min.py @@ -1,33 +1,86 @@ from __future__ import annotations -def find_min(nums: list[int | float]) -> int | float: +def find_min_iterative(nums: list[int | float]) -> int | float: """ Find Minimum Number in a List :param nums: contains elements :return: min number in list >>> for nums in ([3, 2, 1], [-3, -2, -1], [3, -3, 0], [3.0, 3.1, 2.9]): - ... find_min(nums) == min(nums) + ... find_min_iterative(nums) == min(nums) True True True True - >>> find_min([0, 1, 2, 3, 4, 5, -3, 24, -56]) + >>> find_min_iterative([0, 1, 2, 3, 4, 5, -3, 24, -56]) -56 - >>> find_min([]) + >>> find_min_iterative([]) Traceback (most recent call last): ... - ValueError: find_min() arg is an empty sequence + ValueError: find_min_iterative() arg is an empty sequence """ if len(nums) == 0: - raise ValueError("find_min() arg is an empty sequence") + raise ValueError("find_min_iterative() arg is an empty sequence") min_num = nums[0] for num in nums: min_num = min(min_num, num) return min_num +# Divide and Conquer algorithm +def find_min_recursive(nums: list[int | float], left: int, right: int) -> int | float: + """ + find min value in list + :param nums: contains elements + :param left: index of first element + :param right: index of last element + :return: min in nums + + >>> for nums in ([3, 2, 1], [-3, -2, -1], [3, -3, 0], [3.0, 3.1, 2.9]): + ... find_min_recursive(nums, 0, len(nums) - 1) == min(nums) + True + True + True + True + >>> nums = [1, 3, 5, 7, 9, 2, 4, 6, 8, 10] + >>> find_min_recursive(nums, 0, len(nums) - 1) == min(nums) + True + >>> find_min_recursive([], 0, 0) + Traceback (most recent call last): + ... + ValueError: find_min_recursive() arg is an empty sequence + >>> find_min_recursive(nums, 0, len(nums)) == min(nums) + Traceback (most recent call last): + ... + IndexError: list index out of range + >>> find_min_recursive(nums, -len(nums), -1) == min(nums) + True + >>> find_min_recursive(nums, -len(nums) - 1, -1) == min(nums) + Traceback (most recent call last): + ... + IndexError: list index out of range + """ + if len(nums) == 0: + raise ValueError("find_min_recursive() arg is an empty sequence") + if ( + left >= len(nums) + or left < -len(nums) + or right >= len(nums) + or right < -len(nums) + ): + raise IndexError("list index out of range") + if left == right: + return nums[left] + mid = (left + right) >> 1 # the middle + left_min = find_min_recursive(nums, left, mid) # find min in range[left, mid] + right_min = find_min_recursive( + nums, mid + 1, right + ) # find min in range[mid + 1, right] + + return left_min if left_min <= right_min else right_min + + if __name__ == "__main__": import doctest diff --git a/maths/find_min_recursion.py b/maths/find_min_recursion.py deleted file mode 100644 index 4d11015efcd5..000000000000 --- a/maths/find_min_recursion.py +++ /dev/null @@ -1,58 +0,0 @@ -from __future__ import annotations - - -# Divide and Conquer algorithm -def find_min(nums: list[int | float], left: int, right: int) -> int | float: - """ - find min value in list - :param nums: contains elements - :param left: index of first element - :param right: index of last element - :return: min in nums - - >>> for nums in ([3, 2, 1], [-3, -2, -1], [3, -3, 0], [3.0, 3.1, 2.9]): - ... find_min(nums, 0, len(nums) - 1) == min(nums) - True - True - True - True - >>> nums = [1, 3, 5, 7, 9, 2, 4, 6, 8, 10] - >>> find_min(nums, 0, len(nums) - 1) == min(nums) - True - >>> find_min([], 0, 0) - Traceback (most recent call last): - ... - ValueError: find_min() arg is an empty sequence - >>> find_min(nums, 0, len(nums)) == min(nums) - Traceback (most recent call last): - ... - IndexError: list index out of range - >>> find_min(nums, -len(nums), -1) == min(nums) - True - >>> find_min(nums, -len(nums) - 1, -1) == min(nums) - Traceback (most recent call last): - ... - IndexError: list index out of range - """ - if len(nums) == 0: - raise ValueError("find_min() arg is an empty sequence") - if ( - left >= len(nums) - or left < -len(nums) - or right >= len(nums) - or right < -len(nums) - ): - raise IndexError("list index out of range") - if left == right: - return nums[left] - mid = (left + right) >> 1 # the middle - left_min = find_min(nums, left, mid) # find min in range[left, mid] - right_min = find_min(nums, mid + 1, right) # find min in range[mid + 1, right] - - return left_min if left_min <= right_min else right_min - - -if __name__ == "__main__": - import doctest - - doctest.testmod(verbose=True) diff --git a/maths/gaussian_error_linear_unit.py b/maths/gaussian_error_linear_unit.py index 7b5f875143b9..18384bb6c864 100644 --- a/maths/gaussian_error_linear_unit.py +++ b/maths/gaussian_error_linear_unit.py @@ -13,7 +13,7 @@ import numpy as np -def sigmoid(vector: np.array) -> np.array: +def sigmoid(vector: np.ndarray) -> np.ndarray: """ Mathematical function sigmoid takes a vector x of K real numbers as input and returns 1/ (1 + e^-x). @@ -25,7 +25,7 @@ def sigmoid(vector: np.array) -> np.array: return 1 / (1 + np.exp(-vector)) -def gaussian_error_linear_unit(vector: np.array) -> np.array: +def gaussian_error_linear_unit(vector: np.ndarray) -> np.ndarray: """ Implements the Gaussian Error Linear Unit (GELU) function diff --git a/maths/jaccard_similarity.py b/maths/jaccard_similarity.py index 32054414c0c2..6b6243458fa8 100644 --- a/maths/jaccard_similarity.py +++ b/maths/jaccard_similarity.py @@ -14,7 +14,11 @@ """ -def jaccard_similarity(set_a, set_b, alternative_union=False): +def jaccard_similarity( + set_a: set[str] | list[str] | tuple[str], + set_b: set[str] | list[str] | tuple[str], + alternative_union=False, +): """ Finds the jaccard similarity between two sets. Essentially, its intersection over union. @@ -37,41 +41,52 @@ def jaccard_similarity(set_a, set_b, alternative_union=False): >>> set_b = {'c', 'd', 'e', 'f', 'h', 'i'} >>> jaccard_similarity(set_a, set_b) 0.375 - >>> jaccard_similarity(set_a, set_a) 1.0 - >>> jaccard_similarity(set_a, set_a, True) 0.5 - >>> set_a = ['a', 'b', 'c', 'd', 'e'] >>> set_b = ('c', 'd', 'e', 'f', 'h', 'i') >>> jaccard_similarity(set_a, set_b) 0.375 + >>> set_a = ('c', 'd', 'e', 'f', 'h', 'i') + >>> set_b = ['a', 'b', 'c', 'd', 'e'] + >>> jaccard_similarity(set_a, set_b) + 0.375 + >>> set_a = ('c', 'd', 'e', 'f', 'h', 'i') + >>> set_b = ['a', 'b', 'c', 'd'] + >>> jaccard_similarity(set_a, set_b, True) + 0.2 + >>> set_a = {'a', 'b'} + >>> set_b = ['c', 'd'] + >>> jaccard_similarity(set_a, set_b) + Traceback (most recent call last): + ... + ValueError: Set a and b must either both be sets or be either a list or a tuple. """ if isinstance(set_a, set) and isinstance(set_b, set): - intersection = len(set_a.intersection(set_b)) + intersection_length = len(set_a.intersection(set_b)) if alternative_union: - union = len(set_a) + len(set_b) + union_length = len(set_a) + len(set_b) else: - union = len(set_a.union(set_b)) + union_length = len(set_a.union(set_b)) - return intersection / union + return intersection_length / union_length - if isinstance(set_a, (list, tuple)) and isinstance(set_b, (list, tuple)): + elif isinstance(set_a, (list, tuple)) and isinstance(set_b, (list, tuple)): intersection = [element for element in set_a if element in set_b] if alternative_union: - union = len(set_a) + len(set_b) - return len(intersection) / union + return len(intersection) / (len(set_a) + len(set_b)) else: - union = set_a + [element for element in set_b if element not in set_a] + # Cast set_a to list because tuples cannot be mutated + union = list(set_a) + [element for element in set_b if element not in set_a] return len(intersection) / len(union) - - return len(intersection) / len(union) - return None + raise ValueError( + "Set a and b must either both be sets or be either a list or a tuple." + ) if __name__ == "__main__": diff --git a/maths/newton_raphson.py b/maths/newton_raphson.py index 2c9cd1de95b0..f6b227b5c9c1 100644 --- a/maths/newton_raphson.py +++ b/maths/newton_raphson.py @@ -1,16 +1,20 @@ """ - Author: P Shreyas Shetty - Implementation of Newton-Raphson method for solving equations of kind - f(x) = 0. It is an iterative method where solution is found by the expression - x[n+1] = x[n] + f(x[n])/f'(x[n]) - If no solution exists, then either the solution will not be found when iteration - limit is reached or the gradient f'(x[n]) approaches zero. In both cases, exception - is raised. If iteration limit is reached, try increasing maxiter. - """ +Author: P Shreyas Shetty +Implementation of Newton-Raphson method for solving equations of kind +f(x) = 0. It is an iterative method where solution is found by the expression + x[n+1] = x[n] + f(x[n])/f'(x[n]) +If no solution exists, then either the solution will not be found when iteration +limit is reached or the gradient f'(x[n]) approaches zero. In both cases, exception +is raised. If iteration limit is reached, try increasing maxiter. +""" + import math as m +from collections.abc import Callable + +DerivativeFunc = Callable[[float], float] -def calc_derivative(f, a, h=0.001): +def calc_derivative(f: DerivativeFunc, a: float, h: float = 0.001) -> float: """ Calculates derivative at point a for function f using finite difference method @@ -18,7 +22,14 @@ def calc_derivative(f, a, h=0.001): return (f(a + h) - f(a - h)) / (2 * h) -def newton_raphson(f, x0=0, maxiter=100, step=0.0001, maxerror=1e-6, logsteps=False): +def newton_raphson( + f: DerivativeFunc, + x0: float = 0, + maxiter: int = 100, + step: float = 0.0001, + maxerror: float = 1e-6, + logsteps: bool = False, +) -> tuple[float, float, list[float]]: a = x0 # set the initial guess steps = [a] error = abs(f(a)) @@ -36,7 +47,7 @@ def newton_raphson(f, x0=0, maxiter=100, step=0.0001, maxerror=1e-6, logsteps=Fa if logsteps: # If logstep is true, then log intermediate steps return a, error, steps - return a, error + return a, error, [] if __name__ == "__main__": diff --git a/maths/qr_decomposition.py b/maths/qr_decomposition.py index a8414fbece87..670b49206aa7 100644 --- a/maths/qr_decomposition.py +++ b/maths/qr_decomposition.py @@ -1,7 +1,7 @@ import numpy as np -def qr_householder(a): +def qr_householder(a: np.ndarray): """Return a QR-decomposition of the matrix A using Householder reflection. The QR-decomposition decomposes the matrix A of shape (m, n) into an diff --git a/maths/sigmoid.py b/maths/sigmoid.py index 147588e8871f..cb45bde2702c 100644 --- a/maths/sigmoid.py +++ b/maths/sigmoid.py @@ -11,7 +11,7 @@ import numpy as np -def sigmoid(vector: np.array) -> np.array: +def sigmoid(vector: np.ndarray) -> np.ndarray: """ Implements the sigmoid function diff --git a/maths/tanh.py b/maths/tanh.py index ddab3e1ab717..38a369d9118d 100644 --- a/maths/tanh.py +++ b/maths/tanh.py @@ -12,12 +12,12 @@ import numpy as np -def tangent_hyperbolic(vector: np.array) -> np.array: +def tangent_hyperbolic(vector: np.ndarray) -> np.ndarray: """ Implements the tanh function Parameters: - vector: np.array + vector: np.ndarray Returns: tanh (np.array): The input numpy array after applying tanh. diff --git a/physics/ideal_gas_law.py b/physics/ideal_gas_law.py index 805da47b0079..09b4fb3a9c14 100644 --- a/physics/ideal_gas_law.py +++ b/physics/ideal_gas_law.py @@ -53,6 +53,40 @@ def volume_of_gas_system(moles: float, kelvin: float, pressure: float) -> float: return moles * kelvin * UNIVERSAL_GAS_CONSTANT / pressure +def temperature_of_gas_system(moles: float, volume: float, pressure: float) -> float: + """ + >>> temperature_of_gas_system(2, 100, 5) + 30.068090996146232 + >>> temperature_of_gas_system(11, 5009, 1000) + 54767.66101807144 + >>> temperature_of_gas_system(3, -0.46, 23.5) + Traceback (most recent call last): + ... + ValueError: Invalid inputs. Enter positive value. + """ + if moles < 0 or volume < 0 or pressure < 0: + raise ValueError("Invalid inputs. Enter positive value.") + + return pressure * volume / (moles * UNIVERSAL_GAS_CONSTANT) + + +def moles_of_gas_system(kelvin: float, volume: float, pressure: float) -> float: + """ + >>> moles_of_gas_system(100, 5, 10) + 0.06013618199229246 + >>> moles_of_gas_system(110, 5009, 1000) + 5476.766101807144 + >>> moles_of_gas_system(3, -0.46, 23.5) + Traceback (most recent call last): + ... + ValueError: Invalid inputs. Enter positive value. + """ + if kelvin < 0 or volume < 0 or pressure < 0: + raise ValueError("Invalid inputs. Enter positive value.") + + return pressure * volume / (kelvin * UNIVERSAL_GAS_CONSTANT) + + if __name__ == "__main__": from doctest import testmod diff --git a/strings/is_valid_email_address.py b/strings/is_valid_email_address.py new file mode 100644 index 000000000000..205394f81297 --- /dev/null +++ b/strings/is_valid_email_address.py @@ -0,0 +1,117 @@ +""" +Implements an is valid email address algorithm + +@ https://en.wikipedia.org/wiki/Email_address +""" + +import string + +email_tests: tuple[tuple[str, bool], ...] = ( + ("simple@example.com", True), + ("very.common@example.com", True), + ("disposable.style.email.with+symbol@example.com", True), + ("other-email-with-hyphen@and.subdomains.example.com", True), + ("fully-qualified-domain@example.com", True), + ("user.name+tag+sorting@example.com", True), + ("x@example.com", True), + ("example-indeed@strange-example.com", True), + ("test/test@test.com", True), + ( + "123456789012345678901234567890123456789012345678901234567890123@example.com", + True, + ), + ("admin@mailserver1", True), + ("example@s.example", True), + ("Abc.example.com", False), + ("A@b@c@example.com", False), + ("abc@example..com", False), + ("a(c)d,e:f;gi[j\\k]l@example.com", False), + ( + "12345678901234567890123456789012345678901234567890123456789012345@example.com", + False, + ), + ("i.like.underscores@but_its_not_allowed_in_this_part", False), + ("", False), +) + +# The maximum octets (one character as a standard unicode character is one byte) +# that the local part and the domain part can have +MAX_LOCAL_PART_OCTETS = 64 +MAX_DOMAIN_OCTETS = 255 + + +def is_valid_email_address(email: str) -> bool: + """ + Returns True if the passed email address is valid. + + The local part of the email precedes the singular @ symbol and + is associated with a display-name. For example, "john.smith" + The domain is stricter than the local part and follows the @ symbol. + + Global email checks: + 1. There can only be one @ symbol in the email address. Technically if the + @ symbol is quoted in the local-part, then it is valid, however this + implementation ignores "" for now. + (See https://en.wikipedia.org/wiki/Email_address#:~:text=If%20quoted,) + 2. The local-part and the domain are limited to a certain number of octets. With + unicode storing a single character in one byte, each octet is equivalent to + a character. Hence, we can just check the length of the string. + Checks for the local-part: + 3. The local-part may contain: upper and lowercase latin letters, digits 0 to 9, + and printable characters (!#$%&'*+-/=?^_`{|}~) + 4. The local-part may also contain a "." in any place that is not the first or + last character, and may not have more than one "." consecutively. + + Checks for the domain: + 5. The domain may contain: upper and lowercase latin letters and digits 0 to 9 + 6. Hyphen "-", provided that it is not the first or last character + 7. The domain may also contain a "." in any place that is not the first or + last character, and may not have more than one "." consecutively. + + >>> for email, valid in email_tests: + ... assert is_valid_email_address(email) == valid + """ + + # (1.) Make sure that there is only one @ symbol in the email address + if email.count("@") != 1: + return False + + local_part, domain = email.split("@") + # (2.) Check octet length of the local part and domain + if len(local_part) > MAX_LOCAL_PART_OCTETS or len(domain) > MAX_DOMAIN_OCTETS: + return False + + # (3.) Validate the characters in the local-part + if any( + char not in string.ascii_letters + string.digits + ".(!#$%&'*+-/=?^_`{|}~)" + for char in local_part + ): + return False + + # (4.) Validate the placement of "." characters in the local-part + if local_part.startswith(".") or local_part.endswith(".") or ".." in local_part: + return False + + # (5.) Validate the characters in the domain + if any(char not in string.ascii_letters + string.digits + ".-" for char in domain): + return False + + # (6.) Validate the placement of "-" characters + if domain.startswith("-") or domain.endswith("."): + return False + + # (7.) Validate the placement of "." characters + if domain.startswith(".") or domain.endswith(".") or ".." in domain: + return False + return True + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + + for email, valid in email_tests: + is_valid = is_valid_email_address(email) + assert is_valid == valid, f"{email} is {is_valid}" + print(f"Email address {email} is {'not ' if not is_valid else ''}valid") diff --git a/web_programming/current_stock_price.py b/web_programming/current_stock_price.py index df44da4ef351..0c06354d8998 100644 --- a/web_programming/current_stock_price.py +++ b/web_programming/current_stock_price.py @@ -3,12 +3,18 @@ def stock_price(symbol: str = "AAPL") -> str: - url = f"https://in.finance.yahoo.com/quote/{symbol}?s={symbol}" - soup = BeautifulSoup(requests.get(url).text, "html.parser") - class_ = "My(6px) Pos(r) smartphone_Mt(6px)" - return soup.find("div", class_=class_).find("span").text + url = f"https://finance.yahoo.com/quote/{symbol}?p={symbol}" + yahoo_finance_source = requests.get(url, headers={"USER-AGENT": "Mozilla/5.0"}).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." + +# Search for the symbol at https://finance.yahoo.com/lookup if __name__ == "__main__": for symbol in "AAPL AMZN IBM GOOG MSFT ORCL".split(): print(f"Current {symbol:<4} stock price is {stock_price(symbol):>8}")