diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..806f317 Binary files /dev/null and b/.DS_Store differ diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index 48d1bde..e1ba390 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -30,10 +30,11 @@ jobs: if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - name: Test with pytest run: | - pytest --continue-on-collection-errors + git fetch --no-tags --prune --depth=1 origin +refs/heads/*:refs/remotes/origin/* + git diff origin/main HEAD --name-only -- practice | xargs dirname | sort | uniq | xargs pytest - name: Lint with flake8 run: | # stop the build if there are Python syntax errors or undefined names flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics \ No newline at end of file diff --git a/practice/.DS_Store b/practice/.DS_Store new file mode 100644 index 0000000..144571b Binary files /dev/null and b/practice/.DS_Store differ diff --git a/practice/affine-cipher/affine_cipher.py b/practice/affine-cipher/affine_cipher.py index 2d41e04..0101c10 100644 --- a/practice/affine-cipher/affine_cipher.py +++ b/practice/affine-cipher/affine_cipher.py @@ -1,6 +1,96 @@ +import math +import string + +def is_coprime(x, y): + """ + Check if two numbers are coprime. + + Args: + x (int): First number. + y (int): Second number. + + Returns: + bool: True if x and y are coprime, False otherwise. + """ + return math.gcd(x, y) == 1 + def encode(plain_text, a, b): - pass + """ + Encode plain text using the affine cipher. + Args: + plain_text (str): The plain text to be encoded. + a (int): Coefficient 'a' for the affine cipher. + b (int): Coefficient 'b' for the affine cipher. + + Returns: + str: The encoded cipher text. + + Raises: + ValueError: If 'a' and the length of the alphabet are not coprime. + """ + alphabet = list(string.ascii_lowercase) + numbers = list(string.digits) + + if not is_coprime(a, len(alphabet)): + raise ValueError("a and m must be coprime.") + + indexes = [] + for letter in plain_text.lower(): + if letter in alphabet: + indexes.append((a * alphabet.index(letter) + b) % len(alphabet)) + elif letter in numbers: + indexes.append(int(letter) + 1000) + + out = "" + for place, index in enumerate(indexes): + if place % 5 == 0 and place > 1: + out += " " + if index < len(alphabet): + out += alphabet[index] + else: + out += str(index - 1000) + else: + if index < len(alphabet): + out += alphabet[index] + else: + out += str(index - 1000) + + return out def decode(ciphered_text, a, b): - pass + """ + Decode ciphered text using the affine cipher. + + Args: + ciphered_text (str): The ciphered text to be decoded. + a (int): Coefficient 'a' for the affine cipher. + b (int): Coefficient 'b' for the affine cipher. + + Returns: + str: The decoded plain text. + + Raises: + ValueError: If 'a' and the length of the alphabet are not coprime. + """ + alphabet = list(string.ascii_lowercase) + numbers = list(string.digits) + + if not is_coprime(a, len(alphabet)): + raise ValueError("a and m must be coprime.") + + indexes = [] + for letter in ciphered_text: + if letter in numbers: + indexes.append(int(letter) + 1000) + elif letter in alphabet: + indexes.append( + pow(a, -1, len(alphabet)) * (alphabet.index(letter) - b) % len(alphabet) + ) + + return "".join( + [ + str(letter - 1000) if letter > len(alphabet) else alphabet[letter] + for letter in indexes + ] + ) diff --git a/practice/alphametics/alphametics.py b/practice/alphametics/alphametics.py index ca9a272..7fc185c 100644 --- a/practice/alphametics/alphametics.py +++ b/practice/alphametics/alphametics.py @@ -1,2 +1,55 @@ -def solve(puzzle): - pass +from itertools import permutations +from typing import Dict, Optional, List, Tuple + + +def parse_puzzle(puzzle: str) -> Tuple[List[str], str]: + """ + Parse the alphametics puzzle into a list of words and the result. + + Args: + puzzle (str): The alphametics puzzle to parse. + + Returns: + Tuple[List[str], str]: A tuple containing a list of words and the result. + """ + # Split the puzzle into words and the result + inputs, value = puzzle.split(" == ") + # Split the words based on the '+' sign and strip whitespace + words = [i.strip() for i in inputs.split("+")] + return (words, value.strip()) + + +def solve(puzzle: str) -> Optional[Dict[str, int]]: + """ + Solve an alphametics puzzle. + + Args: + puzzle (str): The alphametics puzzle to solve. + + Returns: + Optional[Dict[str, int]]: A dictionary mapping characters to digits if a solution is found, + otherwise None. + """ + # Parse the puzzle into words and the result + words, value = parse_puzzle(puzzle) + # Find the first character of each word and the result + nonzero = set([w[0] for w in words + [value] if len(w) > 1]) + # Gather all unique letters in the puzzle + letters = list(set("".join(words + [value])) - nonzero) + list(nonzero) + + # Iterate through all permutations of digits + for perm in permutations("0123456789", len(letters)): + conv_dict = dict(zip(letters, perm)) + # Skip permutations where leading letters map to '0' + if "0" in perm[-len(nonzero):]: + continue + + # Convert words to integers based on the permutation + values = [int("".join(conv_dict[w] for w in word)) for word in words] + # Convert the result to an integer based on the permutation + summed = int("".join(conv_dict[v] for v in value)) + + # Check if the sum of the words equals the result + if sum(values) == summed: + return {k: int(v) for k, v in conv_dict.items()} + return None diff --git a/practice/alphametics/alphametics_test.py b/practice/alphametics/alphametics_test.py index 6279b80..ce78d29 100644 --- a/practice/alphametics/alphametics_test.py +++ b/practice/alphametics/alphametics_test.py @@ -64,7 +64,7 @@ def test_puzzle_with_ten_letters(self): ) # See https://github.com/exercism/python/pull/1358 - @unittest.skip("extra-credit") + # @unittest.skip("extra-credit") def test_puzzle_with_ten_letters_and_199_addends(self): """This test may take a long time to run. Please be patient when running it.""" puzzle = ( diff --git a/practice/bank-account/bank_account.py b/practice/bank-account/bank_account.py index edeab45..b31c61f 100644 --- a/practice/bank-account/bank_account.py +++ b/practice/bank-account/bank_account.py @@ -1,18 +1,79 @@ class BankAccount: def __init__(self): - pass + """Initialize a BankAccount object. + + The balance is set to zero and the account status is closed by default. + """ + self._balance = 0 + self._opened = False + + def _raise_error_if_not_open(self): + """Raise ValueError if the account is closed.""" + if not self._opened: + raise ValueError("account not open") + + def _raise_error_if_amount_less_than_0(self, amount): + """Raise ValueError if the amount is less than zero.""" + if amount < 0: + raise ValueError("amount must be greater than 0") def get_balance(self): - pass + """Get the current balance of the account. + + Returns: + int: The current balance of the account. + + Raises: + ValueError: If the account is closed. + """ + self._raise_error_if_not_open() + return self._balance def open(self): - pass + """Open the bank account. + + Raises: + ValueError: If the account is already open. + """ + if self._opened: + raise ValueError("account already open") + self._opened = True def deposit(self, amount): - pass + """Deposit funds into the bank account. + + Args: + amount (int): The amount to deposit. + + Raises: + ValueError: If the account is closed or the amount is negative. + """ + self._raise_error_if_not_open() + self._raise_error_if_amount_less_than_0(amount) + self._balance += amount def withdraw(self, amount): - pass + """Withdraw funds from the bank account. + + Args: + amount (int): The amount to withdraw. + + Raises: + ValueError: If the account is closed, the amount is negative, + or the withdrawal amount exceeds the balance. + """ + self._raise_error_if_not_open() + self._raise_error_if_amount_less_than_0(amount) + if amount > self._balance: + raise ValueError("amount must be less than balance") + self._balance -= amount def close(self): - pass + """Close the bank account and reset the balance to zero. + + Raises: + ValueError: If the account is already closed. + """ + self._raise_error_if_not_open() + self._opened = False + self._balance = 0 diff --git a/practice/binary-search-tree/binary_search_tree.py b/practice/binary-search-tree/binary_search_tree.py index afca5d4..7fe270b 100644 --- a/practice/binary-search-tree/binary_search_tree.py +++ b/practice/binary-search-tree/binary_search_tree.py @@ -1,19 +1,117 @@ class TreeNode: + """Represents a single node in a binary search tree.""" + def __init__(self, data, left=None, right=None): - self.data = None - self.left = None - self.right = None + """ + Initialize a TreeNode. + + Args: + data: The data stored in the node. + left (TreeNode): Reference to the left child node. + right (TreeNode): Reference to the right child node. + """ + self.data = data + self.left = left + self.right = right def __str__(self): + """Return a string representation of the TreeNode.""" return f'TreeNode(data={self.data}, left={self.left}, right={self.right})' class BinarySearchTree: + """Represents a binary search tree.""" + def __init__(self, tree_data): - pass + """ + Initialize a BinarySearchTree with a list of data elements. + + Args: + tree_data (list): List of data elements to insert into the binary search tree. + """ + self.root = None + for data in tree_data: + self.insert(data) + + def insert(self, data): + """ + Insert a data element into the binary search tree. + + Args: + data: The data element to insert. + """ + if self.root is None: + self.root = TreeNode(data) + else: + self._insert_recursive(data, self.root) + + def _insert_recursive(self, data, node): + """ + Recursively insert a data element into the binary search tree. + + Args: + data: The data element to insert. + node (TreeNode): The current node being considered. + """ + if data <= node.data: + if node.left is None: + node.left = TreeNode(data) + else: + self._insert_recursive(data, node.left) + else: + if node.right is None: + node.right = TreeNode(data) + else: + self._insert_recursive(data, node.right) + + def search(self, data): + """ + Search for a specific data element in the binary search tree. + + Args: + data: The data element to search for. + + Returns: + TreeNode: The node containing the data if found, otherwise None. + """ + return self._search_recursive(data, self.root) + + def _search_recursive(self, data, node): + """ + Recursively search for a specific data element in the binary search tree. + + Args: + data: The data element to search for. + node (TreeNode): The current node being considered. + + Returns: + TreeNode: The node containing the data if found, otherwise None. + """ + if node is None or node.data == data: + return node + if data < node.data: + return self._search_recursive(data, node.left) + else: + return self._search_recursive(data, node.right) def data(self): - pass + """Get the root node of the binary search tree.""" + return self.root def sorted_data(self): - pass + """Get a list of data elements in sorted order.""" + return self._inorder_traversal(self.root) + + def _inorder_traversal(self, node): + """ + Perform an inorder traversal of the binary search tree. + + Args: + node (TreeNode): The current node being considered. + + Returns: + list: A list of data elements in sorted order. + """ + if node is None: + return [] + return self._inorder_traversal(node.left) + [node.data] + self._inorder_traversal(node.right) diff --git a/practice/book-store/book_store.py b/practice/book-store/book_store.py index 8a84288..3c588d7 100644 --- a/practice/book-store/book_store.py +++ b/practice/book-store/book_store.py @@ -1,2 +1,57 @@ +from collections import Counter, defaultdict + +# Bundle rates for different sizes of bundles +BUNDLE_RATES = [1.0, 1.0, 0.95, 0.90, 0.80, 0.75] + def total(basket): - pass + """ + Calculate the total cost of purchasing books in bundles. + + Args: + basket (list): A list representing the basket containing book titles. + + Returns: + float: The total cost of purchasing the books. + + Approach: + - The problem involves optimizing the total cost of purchasing books in bundles, considering different bundle sizes and discounts. + - The code implements a solution using a greedy approach, initially aiming to create bundles with the maximum size possible. + - However, there's a special case where having two bundles of four books is cheaper than bundles of three and five, making the greedy solution less effective. + - To address this, after applying the greedy solution, the code adjusts any (5, 3) bundle pairs to (4, 4) bundles to optimize the cost. + - Here's how the algorithm works: + 1. Count the number of each book title in the basket using Counter. + 2. Initialize a defaultdict to track the number of bundles by size. + 3. While there are still books in the basket: + - Determine the size of the current bundle (number of distinct titles). + - Update the bundle count for the current size. + - Remove the titles of the current bundle from the basket. + 4. Identify the number of fixes needed to adjust (5, 3) bundle pairs to (4, 4) bundles. + 5. Adjust the bundle counts accordingly: decrease counts for 5-bundles and 3-bundles and increase counts for 4-bundles. + 6. Calculate the total cost by summing the cost of each bundle size multiplied by the number of bundles and the book price, considering discounted rates for each bundle size. + """ + # Dictionary to store the number of bundles by size + num_bundles_by_size = defaultdict(int) + # Count the number of each book title in the basket + title_counts = Counter(basket) + # Iterate until all titles are processed + while title_counts: + # Get the distinct titles in the basket + titles = title_counts.keys() + # Determine the size of the current bundle and update the bundle count + num_bundles_by_size[len(titles)] += 1 + # Remove the titles of the current bundle from the basket + title_counts.subtract(titles) + title_counts += Counter() + + # Identify the number of fixes needed to adjust (5, 3) bundle pairs to (4, 4) bundles + fixes = min(num_bundles_by_size[5], num_bundles_by_size[3]) + # Adjust the bundle counts accordingly + num_bundles_by_size[5] -= fixes + num_bundles_by_size[3] -= fixes + num_bundles_by_size[4] += 2 * fixes + + # Calculate the total cost + return sum( + num_bundles * bundle_size * 800 * BUNDLE_RATES[bundle_size] + for (bundle_size, num_bundles) in num_bundles_by_size.items() + ) diff --git a/practice/bowling/bowling.py b/practice/bowling/bowling.py index de2dbb0..9eaa557 100644 --- a/practice/bowling/bowling.py +++ b/practice/bowling/bowling.py @@ -1,9 +1,125 @@ -class BowlingGame: +class Frame(object): + """Represents a single frame in a bowling game.""" + + def __init__(self, tenth=False): + """ + Initialize a Frame. + + Args: + tenth (bool): Indicates whether the frame is the tenth frame. + """ + self.tenth = tenth + self.pins = [] + + def is_full(self): + """Check if the frame is full.""" + if self.tenth: + return len(self.pins) == 2 and sum(self.pins) < 10 or \ + len(self.pins) == 3 and (sum(self.pins[:2]) == 10 or self.pins[0] == 10) + else: + return len(self.pins) == 2 or len(self.pins) == 1 and self.pins[0] == 10 + + def is_invalid(self): + """Check if the frame is invalid.""" + if self.tenth: + return len(self.pins) == 2 and self.pins[0] < 10 and sum(self.pins) > 10 or \ + len(self.pins) == 3 and self.pins[0] == 10 and self.pins[1] != 10 and sum(self.pins[1:]) > 10 + else: + return sum(self.pins) > 10 + + def roll(self, pins): + """ + Add pins to the frame. + + Args: + pins (int): The number of pins knocked down. + + Returns: + bool: True if the frame is full after the roll, False otherwise. + + Raises: + RuntimeError: If trying to add to a full frame. + ValueError: If the frame becomes invalid after the roll. + """ + if self.is_full(): + raise RuntimeError("Adding to full frame") + else: + self.pins.append(pins) + if self.is_full() and self.is_invalid(): + raise ValueError("This frame is not valid: {}".format(self.pins)) + return self.is_full() + + def score(self, next_throws): + """ + Calculate the score for the frame. + + Args: + next_throws (list): The pins knocked down in the next throws. + + Returns: + int: The score for the frame. + """ + if next_throws: + if self.pins[0] == 10: + return sum(self.pins) + sum(next_throws[:2]) + elif sum(self.pins) == 10: + return sum(self.pins) + next_throws[0] + else: + return sum(self.pins) + else: + return sum(self.pins) + + def __repr__(self): + """Return a string representation of the Frame.""" + return "pins={}, full={}, is_invalid={}".format(self.pins, self.is_full(), self.is_invalid()) + +class BowlingGame(object): + """Represents a bowling game.""" + def __init__(self): - pass + """Initialize a BowlingGame.""" + self.frames = [Frame() for _ in range(9)] + [Frame(tenth=True)] + self.current = 0 + + def is_full(self): + """Check if the game is full.""" + return all([frame.is_full() for frame in self.frames]) def roll(self, pins): - pass + """ + Roll the ball and add pins to the current frame. + + Args: + pins (int): The number of pins knocked down. + + Raises: + ValueError: If the pins value is invalid. + IndexError: If trying to roll for a full game. + """ + if not 0 <= pins <= 10: + raise ValueError("Invalid pins value") + elif self.is_full(): + raise IndexError("Trying to roll for full game") + else: + if self.frames[self.current].roll(pins): + self.current += 1 def score(self): - pass + """ + Calculate the final score of the game. + + Returns: + int: The final score of the game. + + Raises: + IndexError: If the game is incomplete. + """ + if not self.is_full(): + raise IndexError("An incomplete game cannot be scored") + + score = 0 + for (i, frame) in enumerate(self.frames): + next_throws = [throw for next_frame in self.frames[i+1:] for throw in next_frame.pins] + score += frame.score(next_throws) + + return score diff --git a/practice/circular-buffer/circular_buffer.py b/practice/circular-buffer/circular_buffer.py index 87583f6..137995b 100644 --- a/practice/circular-buffer/circular_buffer.py +++ b/practice/circular-buffer/circular_buffer.py @@ -1,35 +1,82 @@ class BufferFullException(BufferError): - """Exception raised when CircularBuffer is full. - - message: explanation of the error. - - """ + """Exception raised when CircularBuffer is full.""" def __init__(self, message): - pass + """ + Initialize BufferFullException. + Args: + message (str): Explanation of the error. + """ + self.message = message -class BufferEmptyException(BufferError): - """Exception raised when CircularBuffer is empty. - - message: explanation of the error. - """ +class BufferEmptyException(BufferError): + """Exception raised when CircularBuffer is empty.""" def __init__(self, message): - pass + """ + Initialize BufferEmptyException. + + Args: + message (str): Explanation of the error. + """ + self.message = message class CircularBuffer: + """Simulates a circular buffer data structure.""" + def __init__(self, capacity): - pass + """ + Initialize CircularBuffer. + + Args: + capacity (int): The maximum capacity of the buffer. + """ + self.capacity = capacity + self.store = [] def read(self): - pass + """ + Read data from the buffer. + + Returns: + Any: The data read from the buffer. + + Raises: + BufferEmptyException: If the buffer is empty. + """ + if len(self.store) == 0: + raise BufferEmptyException("Circular buffer is empty") + out = self.store[0] + self.store.remove(out) + return out def write(self, data): - pass + """ + Write data to the buffer. + + Args: + data (Any): The data to be written to the buffer. + + Raises: + BufferFullException: If the buffer is full. + """ + if len(self.store) == self.capacity: + raise BufferFullException("Circular buffer is full") + self.store.append(data) def overwrite(self, data): - pass + """ + Overwrite data in the buffer. + + Args: + data (Any): The data to overwrite in the buffer. + """ + if len(self.store) == self.capacity: + self.store = self.store[1:] + [data] + else: + self.store.append(data) def clear(self): - pass + """Clear the buffer.""" + self.store = [] diff --git a/practice/custom-set/custom_set.py b/practice/custom-set/custom_set.py index d4f71b1..6f9e586 100644 --- a/practice/custom-set/custom_set.py +++ b/practice/custom-set/custom_set.py @@ -1,30 +1,112 @@ class CustomSet: + """Implements a custom set data structure.""" + def __init__(self, elements=[]): - pass + """ + Initialize CustomSet. + + Args: + elements (list, optional): List of elements to initialize the set. Defaults to an empty list. + """ + self.elements = set(elements) def isempty(self): - pass + """Check if the set is empty. + + Returns: + bool: True if the set is empty, False otherwise. + """ + return len(self.elements) == 0 def __contains__(self, element): - pass + """ + Check if the set contains a specific element. + + Args: + element: The element to check for membership. + + Returns: + bool: True if the element is in the set, False otherwise. + """ + return element in self.elements def issubset(self, other): - pass + """ + Check if the set is a subset of another set. + + Args: + other (CustomSet): The other set to compare against. + + Returns: + bool: True if the set is a subset of the other set, False otherwise. + """ + return self.elements.issubset(other.elements) def isdisjoint(self, other): - pass + """ + Check if the set is disjoint from another set. + + Args: + other (CustomSet): The other set to check for disjointness. + + Returns: + bool: True if the set is disjoint from the other set, False otherwise. + """ + return self.elements.isdisjoint(other.elements) def __eq__(self, other): - pass + """ + Check if two CustomSets are equal. + + Args: + other (CustomSet): The other CustomSet to compare against. + + Returns: + bool: True if the two sets are equal, False otherwise. + """ + return self.elements == other.elements def add(self, element): - pass + """ + Add an element to the set. + + Args: + element: The element to add to the set. + """ + self.elements.add(element) def intersection(self, other): - pass + """ + Compute the intersection of two sets. + + Args: + other (CustomSet): The other set to compute the intersection with. + + Returns: + CustomSet: The intersection of the two sets. + """ + return CustomSet(self.elements.intersection(other.elements)) def __sub__(self, other): - pass + """ + Compute the set difference between two sets. + + Args: + other (CustomSet): The other set to compute the difference with. + + Returns: + CustomSet: The set difference. + """ + return CustomSet(self.elements - other.elements) def __add__(self, other): - pass + """ + Compute the union of two sets. + + Args: + other (CustomSet): The other set to compute the union with. + + Returns: + CustomSet: The union of the two sets. + """ + return CustomSet(self.elements.union(other.elements)) diff --git a/practice/food-chain/food_chain.py b/practice/food-chain/food_chain.py index ed87e69..1268ba3 100644 --- a/practice/food-chain/food_chain.py +++ b/practice/food-chain/food_chain.py @@ -1,2 +1,74 @@ -def recite(start_verse, end_verse): - pass +from typing import List + +# Constants defining the animals and their associated phrases +ANIMAL_LINES = [ + ("fly", "I don't know why she swallowed the fly. Perhaps she'll die."), + ("spider", "It wriggled and jiggled and tickled inside her."), + ("bird", "How absurd to swallow a bird!"), + ("cat", "Imagine that, to swallow a cat!"), + ("dog", "What a hog, to swallow a dog!"), + ("goat", "Just opened her throat and swallowed a goat!"), + ("cow", "I don't know how she swallowed a cow!"), + ("horse", "She's dead, of course!"), +] +SPIDER_LONG = "spider that wriggled and jiggled and tickled inside her" +FIRST_LINE = "I know an old lady who swallowed a " + + +def she_swallowed(curr_animal: str, prev_animal: str) -> str: + """Constructs the line describing swallowing one animal to catch another.""" + return f"She swallowed the {curr_animal} to catch the {prev_animal}." + + +def recite(start_verse: int, end_verse: int) -> List[str]: + """ + Generate verses of the song "I Know an Old Lady Who Swallowed a Fly" for the specified range. + + Args: + start_verse (int): The starting verse number. + end_verse (int): The ending verse number. + + Returns: + List[str]: A list of strings containing the requested verses of the song, with empty strings between verses. + """ + verses = {} # Dictionary to store verses indexed by verse number + cumulative_verse = [] # List to store lines of each verse + prev_animal = "" # Track the previous animal for constructing "swallowed" lines + + # Iterate over the ANIMAL_LINES to construct the verses + for verse_num, line in enumerate(ANIMAL_LINES): + curr_animal = line[0] + animal_phrase = line[1] + first_line = f"{FIRST_LINE}{curr_animal}." + second_line = f"{animal_phrase}" + + # For the first verse, use the initial phrase and the animal's line + if verse_num == 0: + verses[verse_num] = [first_line, second_line] + prev_animal = curr_animal + cumulative_verse += [second_line] + + # For the second verse, add the phrase about the spider + elif verse_num == 2: + cumulative_verse = [she_swallowed(curr_animal, SPIDER_LONG)] + cumulative_verse + verse = [first_line, second_line] + cumulative_verse + verses[verse_num] = verse + + # For the last verse, use only the animal's line + elif verse_num == 7: + verses[verse_num] = [first_line, second_line] + + # For other verses, construct the "swallowed" line and add to the cumulative verse + else: + cumulative_verse = [she_swallowed(curr_animal, prev_animal)] + cumulative_verse + verse = [first_line, second_line] + cumulative_verse + verses[verse_num] = verse + prev_animal = curr_animal + + # Recite the requested verses + recited = [] + for verse_num in range(start_verse, end_verse + 1): + recited += verses[verse_num - 1] + if verse_num < end_verse: + recited.append("") # Add an empty string between verses + return recited diff --git a/practice/forth/forth.py b/practice/forth/forth.py index 7cf4ff3..1dccfdc 100644 --- a/practice/forth/forth.py +++ b/practice/forth/forth.py @@ -1,6 +1,73 @@ -class StackUnderflowError(Exception): - pass +import operator +class StackUnderflowError(Exception): + """Exception raised when attempting to pop from an empty stack.""" + def __init__(self, message): + self.message = message def evaluate(input_data): - pass + """ + Evaluate Forth-like expressions. + + Args: + input_data (list): List of Forth-like expressions. + + Returns: + list: Resulting stack after evaluating the input data. + """ + data = [i.lower() for i in input_data] + stack = [] + operators = {"+": operator.add, "-": operator.sub, + "*": operator.mul, "/": operator.floordiv} + manipulations = {"dup": operator.getitem, "drop": operator.delitem, + "over": operator.getitem} + index = {"dup": -1, "drop": -1, "over": -2} + user_defined = {} + + # Define user-defined operations + for i in data: + if i[0] == ":" and i[-1] == ";": + definition = i[1:-1].split() + if definition[0].isnumeric() or (definition[0][0] == "-" and definition[0][1:].isnumeric()): + raise ValueError("illegal operation") + else: + instructions = [] + for op in definition[1:]: + if op in user_defined: + instructions += user_defined[op] + else: + instructions.append(op) + user_defined[definition[0]] = instructions + + # Evaluate input expressions + for i in data[-1].split(): + try: + stack.append(int(i)) + except ValueError: + if i in operators and i not in user_defined: + try: + second, first = stack.pop(), stack.pop() + stack.append(operators[i](first, second)) + except IndexError: + raise StackUnderflowError("Insufficient number of items in stack") + except ZeroDivisionError: + raise ZeroDivisionError("divide by zero") + elif i == "swap" and "swap" not in user_defined: + try: + first, second = stack.pop(), stack.pop() + stack += [first, second] + except IndexError: + raise StackUnderflowError("Insufficient number of items in stack") + elif i in manipulations: + try: + new = manipulations[i](stack, index[i]) + if new: + stack.append(new) + except IndexError: + raise StackUnderflowError("Insufficient number of items in stack") + elif i in user_defined: + stack = evaluate([" ".join([str(i) for i in stack] + user_defined[i])]) + else: + raise ValueError("undefined operation") + + return stack diff --git a/practice/go-counting/go_counting.py b/practice/go-counting/go_counting.py index d1c689e..b8442a3 100644 --- a/practice/go-counting/go_counting.py +++ b/practice/go-counting/go_counting.py @@ -1,39 +1,96 @@ +WHITE = "W" +BLACK = "B" +NONE = " " +UNKNOWN = "?" class Board: - """Count territories of each player in a Go game - - Args: - board (list[str]): A two-dimensional Go board - """ + """Class to analyze territories in a Go game.""" def __init__(self, board): - pass + """Initialize the Board instance with the given board configuration. - def territory(self, x, y): - """Find the owner and the territories given a coordinate on - the board + Args: + board (list[str]): A two-dimensional Go board + """ + self.board = board + self.rows = len(board) + self.cols = 0 if self.rows == 0 else len(board[0]) + + def onboard(self, x, y): + """Check if a given coordinate is within the boundaries of the board. Args: - x (int): Column on the board - y (int): Row on the board + x (int): Column index + y (int): Row index Returns: - (str, set): A tuple, the first element being the owner - of that area. One of "W", "B", "". The - second being a set of coordinates, representing - the owner's territories. + bool: True if the coordinate is within the board boundaries, False otherwise. """ - pass + return 0 <= x < self.cols and 0 <= y < self.rows - def territories(self): - """Find the owners and the territories of the whole board + def neighbors(self, x, y): + """Find the neighboring coordinates of a given coordinate. + + Args: + x (int): Column index + y (int): Row index + + Returns: + list: List of neighboring coordinates. + """ + nbr = [] + for dx in (-1, 1): + if self.onboard(x + dx, y): + nbr.append((x + dx, y)) + for dy in (-1, 1): + if self.onboard(x, y + dy): + nbr.append((x, y + dy)) + return nbr + + def territory(self, x, y): + """Find the owner and territories given a coordinate on the board. Args: - none + x (int): Column index + y (int): Row index + + Returns: + tuple: A tuple containing the owner of the area ("W", "B", or ""), and a set of coordinates representing the owner's territories. + """ + if not self.onboard(x, y): + raise ValueError("Invalid coordinate") + if self.board[y][x] != NONE: + return NONE, set() + owner = UNKNOWN + visited = set() + enclosed = set() + stack = [(x, y)] + while len(stack) != 0: + c, r = stack.pop() + visited.add((c, r)) + stone = self.board[r][c] + if stone == NONE: + enclosed.add((c, r)) + for n in self.neighbors(c, r): + if n not in visited: + stack.append(n) + elif stone != owner: + owner = stone if owner == UNKNOWN else NONE + if owner == UNKNOWN: + owner = NONE + return owner, enclosed + + def territories(self): + """Find the owners and territories of the entire board. Returns: - dict(str, set): A dictionary whose key being the owner - , i.e. "W", "B", "". The value being a set - of coordinates owned by the owner. + dict: A dictionary containing the owners ("W", "B", or "") as keys, and sets of coordinates representing their territories as values. """ - pass + territories = {WHITE: set(), BLACK: set(), NONE: set()} + vacant = set((c, r) for c in range(self.cols) for r in range(self.rows) if self.board[r][c] == NONE) + while len(vacant) != 0: + v = vacant.pop() + who, where = self.territory(v[0], v[1]) + vacant -= where + territories[who] |= where + return territories diff --git a/practice/hangman/hangman.py b/practice/hangman/hangman.py index 5f0db27..f126990 100644 --- a/practice/hangman/hangman.py +++ b/practice/hangman/hangman.py @@ -1,20 +1,69 @@ -# Game status categories -# Change the values as you see fit -STATUS_WIN = 'win' -STATUS_LOSE = 'lose' -STATUS_ONGOING = 'ongoing' - +STATUS_WIN = "win" +STATUS_LOSE = "lose" +STATUS_ONGOING = "ongoing" class Hangman: - def __init__(self, word): + """ + Class for the Hangman game. + """ + + def __init__(self, word: str): + """ + Initializes a Hangman game instance. + + Args: + word (str): The word to be guessed in the game. + """ + self.word = word + self._set_word = set(word) self.remaining_guesses = 9 self.status = STATUS_ONGOING + self._masked_word = [0] * len(self.word) + self._guessed: set[str] = set() + + def guess(self, char: str) -> None: + """ + Attempts to guess a letter in the word. + + Args: + char (str): The character to be guessed. + + Raises: + ValueError: If the game has already ended. + """ + if self.status == STATUS_ONGOING: + if char not in self._set_word or char in self._guessed: + self.remaining_guesses -= 1 + if self.remaining_guesses < 0: + self.status = STATUS_LOSE + else: + self._guessed.add(char) + self._masked_word = [ + value if self.word[i] != char else 1 + for i, value in enumerate(self._masked_word) + ] + if sum(self._masked_word) == len(self.word): + self.status = STATUS_WIN + else: + raise ValueError("The game has already ended.") + + def get_masked_word(self) -> str: + """ + Returns the word with guessed letters revealed and unguessed letters masked. - def guess(self, char): - pass + Returns: + str: The masked word. + """ + return "".join( + letter if self._masked_word[i] else "_" + for i, letter in enumerate(self.word) + ) - def get_masked_word(self): - pass + def get_status(self) -> str: + """ + Returns the status of the game. - def get_status(self): - pass + Returns: + str: The status of the game ('win', 'lose', or 'ongoing'). + """ + return self.status diff --git a/practice/knapsack/knapsack.py b/practice/knapsack/knapsack.py index 80ad24c..3d1ffca 100644 --- a/practice/knapsack/knapsack.py +++ b/practice/knapsack/knapsack.py @@ -1,2 +1,24 @@ -def maximum_value(maximum_weight, items): - pass +def maximum_value(max_weight, items): + """ + Computes the maximum value achievable with a given maximum weight limit using dynamic programming. + + Args: + max_weight (int): The maximum weight limit. + items (list): A list of dictionaries representing items, where each dictionary has keys "weight" and "value". + + Returns: + int: The maximum value achievable with the provided weight limit. + """ + n = len(items) + # Initialize a 2D array to store maximum values for different combinations of items and weight limits + dp = [[0] * (max_weight + 1) for _ in range(n + 1)] + + for i in range(1, n + 1): + for j in range(1, max_weight + 1): + # Decide whether to include or exclude the current item based on weight + if items[i - 1]["weight"] <= j: + dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - items[i - 1]["weight"]] + items[i - 1]["value"]) + else: + dp[i][j] = dp[i - 1][j] + + return dp[n][max_weight] diff --git a/practice/ledger/ledger.py b/practice/ledger/ledger.py index c0dcf94..360da37 100644 --- a/practice/ledger/ledger.py +++ b/practice/ledger/ledger.py @@ -1,22 +1,33 @@ -# -*- coding: utf-8 -*- -from datetime import datetime +""" +This script formats ledger entries into tables for different currencies and locales. +Approach: +- `create_entry`: Creates a `LedgerEntry` object with parsed date. +- `format_entries`: Formats entries into a table. + - Supports 'en_US' and 'nl_NL' locales. + - Generates a header row and sorts entries. + - Formats date and truncates descriptions. + - Formats change according to currency. +""" +from datetime import datetime + + class LedgerEntry: def __init__(self): self.date = None self.description = None self.change = None - - + + def create_entry(date, description, change): entry = LedgerEntry() entry.date = datetime.strptime(date, '%Y-%m-%d') entry.description = description entry.change = change return entry - - + + def format_entries(currency, locale, entries): if locale == 'en_US': # Generate Header Row @@ -29,10 +40,10 @@ def format_entries(currency, locale, entries): table += '| Change' for _ in range(7): table += ' ' - + while len(entries) > 0: table += '\n' - + # Find next entry in order min_entry_index = -1 for i in range(len(entries)): @@ -59,7 +70,7 @@ def format_entries(currency, locale, entries): continue entry = entries[min_entry_index] entries.pop(min_entry_index) - + # Write entry date to table month = entry.date.month month = str(month) @@ -80,7 +91,7 @@ def format_entries(currency, locale, entries): date_str += year table += date_str table += ' | ' - + # Write entry description to table # Truncate if necessary if len(entry.description) > 25: @@ -94,7 +105,7 @@ def format_entries(currency, locale, entries): else: table += ' ' table += ' | ' - + # Write entry change to table if currency == 'USD': change_str = '' @@ -172,10 +183,10 @@ def format_entries(currency, locale, entries): table += '| Verandering' for _ in range(2): table += ' ' - + while len(entries) > 0: table += '\n' - + # Find next entry in order min_entry_index = -1 for i in range(len(entries)): @@ -202,7 +213,7 @@ def format_entries(currency, locale, entries): continue entry = entries[min_entry_index] entries.pop(min_entry_index) - + # Write entry date to table day = entry.date.day day = str(day) @@ -223,7 +234,7 @@ def format_entries(currency, locale, entries): date_str += year table += date_str table += ' | ' - + # Write entry description to table # Truncate if necessary if len(entry.description) > 25: @@ -237,7 +248,7 @@ def format_entries(currency, locale, entries): else: table += ' ' table += ' | ' - + # Write entry change to table if currency == 'USD': change_str = '$ ' @@ -296,4 +307,4 @@ def format_entries(currency, locale, entries): change_str = ' ' + change_str table += change_str return table - + \ No newline at end of file diff --git a/practice/markdown/markdown.py b/practice/markdown/markdown.py index 3c4bd2f..184a3aa 100644 --- a/practice/markdown/markdown.py +++ b/practice/markdown/markdown.py @@ -1,7 +1,14 @@ import re - def parse(markdown): + """Parses markdown text into HTML. + + Args: + markdown (str): The markdown text to be parsed. + + Returns: + str: The HTML representation of the parsed markdown. + """ lines = markdown.split('\n') res = '' in_list = False diff --git a/practice/meetup/meetup.py b/practice/meetup/meetup.py index 10f6bca..238c601 100644 --- a/practice/meetup/meetup.py +++ b/practice/meetup/meetup.py @@ -1,13 +1,56 @@ -# subclassing the built-in ValueError to create MeetupDayException -class MeetupDayException(ValueError): - """Exception raised when the Meetup weekday and count do not result in a valid date. +from datetime import date +import calendar - message: explanation of the error. +class MeetupDayException(Exception): + """Custom exception for invalid meetup days.""" + + def __init__(self, err, message="That day does not exist."): + """Initialize the MeetupDayException.""" + self.err = err + self.message = message + super().__init__(self.message) + + +def meetup(year, month, number, weekday): """ - def __init__(self): - pass + Calculate meetup days based on the provided year, month, number, and weekday. + + Args: + year (int): The year. + month (int): The month (1-12). + number (str): The position of the meetup day ('first', 'second', 'third', 'fourth', 'fifth', 'teenth', 'last'). + weekday (str): The weekday ('Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'). + + Returns: + date: The calculated meetup day. + + Raises: + MeetupDayException: If the meetup day is not valid. + """ + days_to_numbers = {'Monday': 0, 'Tuesday': 1, 'Wednesday': 2, 'Thursday': 3, 'Friday': 4, 'Saturday': 5, 'Sunday': 6} + number_to_week = {'first': 0, 'second': 1, 'third': 2, 'fourth': 3, 'fifth': 4, 'last': -1} + + day_number = days_to_numbers[weekday] + number = number.lower() + + if number == 'teenth': + teenth_cal = calendar.Calendar() + teenths = sorted(item for item in teenth_cal.itermonthdays4(year, month) + if 13 <= item[2] <= 19 and item[3] == day_number) + + if teenths: + return date(*teenths[0][:3]) + else: + raise MeetupDayException(f'Teenth not found for {weekday}') + else: + week_number = number_to_week[number] + remaining_cal = calendar.Calendar() + candidates = sorted(item for item in remaining_cal.itermonthdays4(year, month) + if item[1] == month and item[3] == day_number) -def meetup(year, month, week, day_of_week): - pass + try: + return date(*candidates[week_number][:3]) + except (ValueError, IndexError) as err: + raise MeetupDayException(err) from None diff --git a/practice/paasio/paasio.py b/practice/paasio/paasio.py index 62987c6..3378a4b 100644 --- a/practice/paasio/paasio.py +++ b/practice/paasio/paasio.py @@ -2,76 +2,138 @@ class MeteredFile(io.BufferedRandom): - """Implement using a subclassing model.""" + """ + Measure I/O operations performed on files. + + Attributes: + _read_ops (int): Number of read operations. + _write_ops (int): Number of write operations. + _read_bytes (int): Number of bytes read. + _write_bytes (int): Number of bytes written. + """ def __init__(self, *args, **kwargs): - pass + """Initialize MeteredFile.""" + super().__init__(*args, **kwargs) + self._read_ops = 0 + self._write_ops = 0 + self._read_bytes = 0 + self._write_bytes = 0 def __enter__(self): - pass + """Enter context manager.""" + return self def __exit__(self, exc_type, exc_val, exc_tb): - pass + """Exit context manager.""" + return super().__exit__(exc_type, exc_val, exc_tb) def __iter__(self): - pass + """Iterator.""" + return self def __next__(self): - pass + """Next iterator.""" + line = super().readline() + self._read_ops += 1 + self._read_bytes += len(line) + if line: + return line + raise StopIteration() def read(self, size=-1): - pass + """Read from file.""" + buf = super().read(size) + self._read_ops += 1 + self._read_bytes += len(buf) + return buf @property def read_bytes(self): - pass + """Get number of bytes read.""" + return self._read_bytes @property def read_ops(self): - pass + """Get number of read operations.""" + return self._read_ops def write(self, b): - pass + """Write to file.""" + bytes_written = super().write(b) + self._write_ops += 1 + self._write_bytes += bytes_written + return bytes_written @property def write_bytes(self): - pass + """Get number of bytes written.""" + return self._write_bytes @property def write_ops(self): - pass + """Get number of write operations.""" + return self._write_ops class MeteredSocket: - """Implement using a delegation model.""" + """ + Measure I/O operations performed on sockets. + + Attributes: + _recv_ops (int): Number of receive operations. + _send_ops (int): Number of send operations. + _recv_bytes (int): Number of bytes received. + _send_bytes (int): Number of bytes sent. + _socket: The underlying socket object. + """ def __init__(self, socket): - pass + """Initialize MeteredSocket.""" + self._recv_ops = 0 + self._send_ops = 0 + self._recv_bytes = 0 + self._send_bytes = 0 + self._socket = socket def __enter__(self): - pass + """Enter context manager.""" + return self def __exit__(self, exc_type, exc_val, exc_tb): - pass + """Exit context manager.""" + return self._socket.__exit__(exc_type, exc_val, exc_tb) def recv(self, bufsize, flags=0): - pass + """Receive data from the socket.""" + self._recv_ops += 1 + received_data = self._socket.recv(bufsize, flags) + self._recv_bytes += len(received_data) + return received_data @property def recv_bytes(self): - pass + """Get number of bytes received.""" + return self._recv_bytes @property def recv_ops(self): - pass + """Get number of receive operations.""" + return self._recv_ops def send(self, data, flags=0): - pass + """Send data through the socket.""" + self._send_ops += 1 + bytes_sent = self._socket.send(data, flags) + self._send_bytes += bytes_sent + return bytes_sent @property def send_bytes(self): - pass + """Get number of bytes sent.""" + return self._send_bytes @property def send_ops(self): - pass + """Get number of send operations.""" + return self._send_ops diff --git a/practice/palindrome-products/palindrome_products.py b/practice/palindrome-products/palindrome_products.py index d4c2668..b380d39 100644 --- a/practice/palindrome-products/palindrome_products.py +++ b/practice/palindrome-products/palindrome_products.py @@ -1,24 +1,60 @@ -def largest(min_factor, max_factor): - """Given a range of numbers, find the largest palindromes which - are products of two numbers within that range. +def palindrome(s): + """ + Check if a number or string is a palindrome. + + Args: + s (int or str): The number or string to check. - :param min_factor: int with a default value of 0 - :param max_factor: int - :return: tuple of (palindrome, iterable). - Iterable should contain both factors of the palindrome in an arbitrary order. + Returns: + bool: True if the input is a palindrome, False otherwise. """ + s = str(s) + return s[::-1] == s - pass +def largest(min_factor, max_factor): + """ + Find the largest palindrome product of numbers within the specified range. -def smallest(min_factor, max_factor): - """Given a range of numbers, find the smallest palindromes which - are products of two numbers within that range. + Args: + min_factor (int): The minimum factor. + max_factor (int): The maximum factor. - :param min_factor: int with a default value of 0 - :param max_factor: int - :return: tuple of (palindrome, iterable). - Iterable should contain both factors of the palindrome in an arbitrary order. + Returns: + tuple: A tuple containing the largest palindrome product and its factors. """ + factors_list = [] + if min_factor > max_factor: + raise ValueError("min must be <= max") + for x in range(max_factor ** 2, min_factor ** 2 - 1, -1): + if palindrome(x): + for y in range(min_factor, max_factor + 1): + if x % y == 0 and min_factor <= x / y <= max_factor: + factors_list.append([y, int(x / y)]) + if factors_list: + return x, factors_list + return None, factors_list - pass + +def smallest(min_factor, max_factor): + """ + Find the smallest palindrome product of numbers within the specified range. + + Args: + min_factor (int): The minimum factor. + max_factor (int): The maximum factor. + + Returns: + tuple: A tuple containing the smallest palindrome product and its factors. + """ + factors_list = [] + if min_factor > max_factor: + raise ValueError("min must be <= max") + for x in range(min_factor ** 2, (max_factor + 1) ** 2): + if palindrome(x): + for y in range(min_factor, max_factor + 1): + if x % y == 0 and min_factor <= x / y <= max_factor: + factors_list.append([y, int(x / y)]) + if factors_list: + return x, factors_list + return None, factors_list diff --git a/practice/pov/pov.py b/practice/pov/pov.py index 21dbe4b..320a48c 100644 --- a/practice/pov/pov.py +++ b/practice/pov/pov.py @@ -2,24 +2,147 @@ class Tree: + """A class representing a tree and its operations.""" + def __init__(self, label, children=None): + """ + Initialize a Tree object with a label and optional children. + + Args: + label (any): The label of the tree node. + children (list, optional): List of child Tree objects. Defaults to None. + """ self.label = label self.children = children if children is not None else [] - def __dict__(self): - return {self.label: [c.__dict__() for c in sorted(self.children)]} + def to_dict(self): + """ + Convert the tree to a dictionary representation. + + Returns: + dict: Dictionary representation of the tree. + """ + return {self.label: [c.to_dict() for c in sorted(self.children)]} def __str__(self, indent=None): - return dumps(self.__dict__(), indent=indent) + """ + Convert the tree to a JSON-formatted string. + + Args: + indent (int, optional): Indentation level. Defaults to None. + + Returns: + str: JSON-formatted string representing the tree. + """ + return dumps(self.to_dict(), indent=indent) def __lt__(self, other): + """ + Compare two Tree objects based on their labels. + + Args: + other (Tree): Another Tree object. + + Returns: + bool: True if the current object's label is less than the other object's label, False otherwise. + """ return self.label < other.label def __eq__(self, other): - return self.__dict__() == other.__dict__() + """ + Compare two Tree objects for equality. + + Args: + other (Tree): Another Tree object. + + Returns: + bool: True if the two objects are equal, False otherwise. + """ + return self.to_dict() == other.to_dict() def from_pov(self, from_node): - pass + """ + Reorient the tree from the perspective of a specific node. + + Args: + from_node (any): The label of the node from which the tree should be reoriented. + + Returns: + Tree: The reoriented tree. + + Raises: + ValueError: If the specified node is not found in the tree. + """ + cur_path = [[self, 0]] + while cur_path: + subtree, next_index = cur_path[-1] + if subtree.label == from_node: + break + if next_index < len(subtree.children): + next_node = subtree.children[next_index] + cur_path[-1][1] += 1 + cur_path.append([next_node, 0]) + else: + cur_path.pop() + if not cur_path: + raise ValueError("Tree could not be reoriented") + it = iter(cur_path) + p = next(it) + for c in it: + p[0].children.pop(p[1] - 1) + c[0].children.append(p[0]) + p = c + return cur_path[-1][0] def path_to(self, from_node, to_node): - pass + """ + Find the path between two nodes in the tree. + + Args: + from_node (any): The label of the starting node. + to_node (any): The label of the target node. + + Returns: + list: The path from the starting node to the target node. + + Raises: + ValueError: If the starting node is not found in the tree or if no path is found between the two nodes. + """ + to_find = {from_node, to_node} + found_paths = dict() + cur_path = [] + + def helper(subtree): + nonlocal to_find, found_paths, cur_path + cur_path.append(subtree.label) + if subtree.label in to_find: + found_paths[subtree.label] = cur_path.copy() + to_find.remove(subtree.label) + for c in subtree.children: + if len(to_find) == 0: + break + helper(c) + cur_path.pop() + + helper(self) + if from_node in to_find: + raise ValueError("Tree could not be reoriented") + if to_node in to_find: + raise ValueError("No path found") + + from_path = found_paths[from_node] + to_path = found_paths[to_node] + from_part = [] + to_part = [] + + while len(from_path) > len(to_path): + from_part.append(from_path.pop()) + while len(to_path) > len(from_path): + to_part.append(to_path.pop()) + while from_path[-1] != to_path[-1]: + from_part.append(from_path.pop()) + to_part.append(to_path.pop()) + from_part.append(from_path[-1]) + to_part.reverse() + from_part.extend(to_part) + return from_part diff --git a/practice/react/react.py b/practice/react/react.py index ab6be31..d5f4bed 100644 --- a/practice/react/react.py +++ b/practice/react/react.py @@ -1,15 +1,131 @@ class InputCell: def __init__(self, initial_value): - self.value = None + """ + Initialize an InputCell. + + Args: + initial_value: The initial value of the InputCell. + """ + self._value = initial_value + self._observers = set() + + @property + def value(self): + """ + Getter for the value property. + + Returns: + The current value of the InputCell. + """ + return self._value + + @value.setter + def value(self, new_value): + """ + Setter for the value property. + + Args: + new_value: The new value to be set. + """ + self._value = new_value + # Notify observers upon value change + for observer in self._observers: + observer.compute() + + def register_observer(self, observer): + """ + Register an observer for this InputCell. + + Args: + observer: The observer to be registered. + """ + self._observers.add(observer) + + def compute(self): + """ + Placeholder method for computing the value of InputCell. + """ + pass class ComputeCell: def __init__(self, inputs, compute_function): - self.value = None + """ + Initialize a ComputeCell. + + Args: + inputs: The list of InputCells that this ComputeCell depends on. + compute_function: The function used to compute the value based on inputs. + """ + self.inputs = inputs + self.compute_function = compute_function + self.callbacks = set() + self._value = None + self._start_observing_ultimate_observables() + self.compute() + + @property + def value(self): + """ + Getter for the value property. + + Returns: + The current value of the ComputeCell. + """ + return self._value + + @value.setter + def value(self, computed_value): + """ + Setter for the value property. + + Args: + computed_value: The new computed value to be set. + """ + self._value = computed_value + # Notify callbacks upon value change + for callback in self.callbacks: + callback(self.value) def add_callback(self, callback): - pass + """ + Add a callback function to be called when the value changes. + + Args: + callback: The callback function to be added. + """ + self.callbacks.add(callback) def remove_callback(self, callback): - pass - \ No newline at end of file + """ + Remove a callback function. + + Args: + callback: The callback function to be removed. + """ + self.callbacks.discard(callback) + + def _start_observing_ultimate_observables(self): + """ + Start observing ultimate observables in the input hierarchy. + """ + def ultimate_observables(inputs): + for input_ in inputs: + if isinstance(input_, ComputeCell): + yield from ultimate_observables(input_.inputs) + else: # InputCell + yield input_ + + for ultimate_observable in ultimate_observables(self.inputs): + ultimate_observable.register_observer(self) + + def compute(self): + """ + Compute the value based on inputs and compute function. + """ + for input_ in self.inputs: + input_.compute() + computed_value = self.compute_function([input_.value for input_ in self.inputs]) + # Update value if computed value differs + if computed_value != self.value: + self.value = computed_value diff --git a/practice/rest-api/rest_api.py b/practice/rest-api/rest_api.py index 0e448a4..ab3229e 100644 --- a/practice/rest-api/rest_api.py +++ b/practice/rest-api/rest_api.py @@ -1,9 +1,228 @@ +import json + class RestAPI: + user_database = dict() + get_handlers = dict() + post_handlers = dict() + def __init__(self, database=None): - pass + """ + Initialize the RestAPI with an optional database. + + Args: + database (dict, optional): The initial user database. Defaults to None. + """ + self.user_database = database or {'users': []} + self.get_handlers['/users'] = self.get_users + self.post_handlers['/add'] = self.add_user + self.post_handlers['/iou'] = self.add_iou def get(self, url, payload=None): - pass + """ + Handle GET requests. + + Args: + url (str): The URL for the GET request. + payload (str, optional): The payload for the GET request. Defaults to None. + + Returns: + str: The response to the GET request. + """ + return self.execute_handler(self.get_handlers, url, payload) def post(self, url, payload=None): - pass + """ + Handle POST requests. + + Args: + url (str): The URL for the POST request. + payload (str, optional): The payload for the POST request. Defaults to None. + + Returns: + str: The response to the POST request. + """ + return self.execute_handler(self.post_handlers, url, payload) + + @staticmethod + def execute_handler(handlers, url, payload): + """ + Execute the handler for the given URL. + + Args: + handlers (dict): Dictionary of handlers. + url (str): The URL for the request. + payload (str): The payload for the request. + + Returns: + str: The response to the request. + """ + handler = handlers.get(url) + if not handler: + raise ValueError(f"No route defined for {url}.") + return handler(payload) + + def get_users(self, payload): + """ + Handle GET request for users. + + Args: + payload (str): The payload for the request. + + Returns: + str: The response to the request. + """ + if payload: + return self.get_named_users(payload) + else: + return json.dumps(self.user_database) + + def get_named_users(self, payload): + """ + Handle GET request for named users. + + Args: + payload (str): The payload for the request. + + Returns: + str: The response to the request. + """ + try: + names = json.loads(payload)['users'] + names.sort() + except KeyError: + raise ValueError("Payload does not contain 'users' key.") + except Exception as e: + raise ValueError(f"Error parsing 'users' key of JSON payload as array: {e}") + + users = [user for user in self.user_database['users'] if user['name'] in names] + return json.dumps({"users": users}) + + def add_user(self, payload): + """ + Handle POST request to add a user. + + Args: + payload (str): The payload for the request. + + Returns: + str: The response to the request. + """ + try: + name = json.loads(payload)['user'] + except KeyError: + raise ValueError("Payload does not contain 'user' key.") + except Exception as e: + raise ValueError(f"Error parsing 'user' key of JSON payload: {e}") + + if any(user['name'] == name for user in self.user_database['users']): + raise ValueError(f"User {name} already exists") + + user = self.create_user(name) + self.user_database['users'].append(user) + return json.dumps(user) + + @staticmethod + def create_user(name): + """ + Create a user with the given name. + + Args: + name (str): The name of the user. + + Returns: + dict: The user dictionary. + """ + return {"name": name, "owes": {}, "owed_by": {}, "balance": 0.0} + + def add_iou(self, payload): + """ + Handle POST request to add an IOU. + + Args: + payload (str): The payload for the request. + + Returns: + str: The response to the request. + """ + try: + iou = json.loads(payload) + lender = self.find_user(iou['lender']) + borrower = self.find_user(iou['borrower']) + amount = iou['amount'] + except KeyError: + raise ValueError("Payload is missing required keys.") + except Exception as e: + raise ValueError(f"Error parsing JSON payload: {e}") + + if not lender: + raise ValueError(f"User {iou['lender']} not found.") + if not borrower: + raise ValueError(f"User {iou['borrower']} not found.") + + self.adjust_balances(lender, True, borrower['name'], amount) + lender['balance'] += amount + + self.adjust_balances(borrower, False, lender['name'], amount) + borrower['balance'] -= amount + + users = [lender, borrower] if lender['name'] < borrower['name'] else [borrower, lender] + return json.dumps({"users": users}) + + def adjust_balances(self, user, user_is_lender, name, amount): + """ + Adjust balances for a user. + + Args: + user (dict): The user dictionary. + user_is_lender (bool): Indicates if the user is the lender. + name (str): The name of the other party involved. + amount (float): The amount of the IOU. + """ + reduce_balance = 'owes' if user_is_lender else 'owed_by' + increase_balance = 'owed_by' if user_is_lender else 'owes' + + if name in user[reduce_balance]: + previous_balance = user[reduce_balance][name] + new_balance = previous_balance - amount + if amount < previous_balance: + user[reduce_balance][name] = new_balance + else: + del user[reduce_balance][name] + remainder = -1 * new_balance + self.increase_named_balance(user[increase_balance], name, remainder) + else: + self.increase_named_balance(user[increase_balance], name, amount) + + @staticmethod + def increase_named_balance(balances, name, amount): + """ + Increase the named balance. + + Args: + balances (dict): The balance dictionary. + name (str): The name of the other party involved. + amount (float): The amount of the IOU. + """ + if name in balances: + new_balance = balances[name] + amount + if new_balance: + balances[name] = new_balance + else: + del balances[name] + elif amount: + balances[name] = amount + + def find_user(self, name): + """ + Find a user by name. + + Args: + name (str): The name of the user. + + Returns: + dict: The user dictionary if found, None otherwise. + """ + for user in self.user_database['users']: + if user['name'] == name: + return user + return None diff --git a/practice/robot-simulator/robot_simulator.py b/practice/robot-simulator/robot_simulator.py index e5da22d..a918537 100644 --- a/practice/robot-simulator/robot_simulator.py +++ b/practice/robot-simulator/robot_simulator.py @@ -1,11 +1,36 @@ # Globals for the directions -# Change the values as you see fit -EAST = None -NORTH = None -WEST = None -SOUTH = None - +# Change the values as needed +NORTH = [0, 1] +EAST = [1, 0] +SOUTH = [0, -1] +WEST = [-1, 0] class Robot: def __init__(self, direction=NORTH, x_pos=0, y_pos=0): - pass + """ + Initialize the Robot with direction and coordinates. + + Args: + direction (list, optional): Initial direction. Defaults to NORTH. + x_pos (int, optional): Initial x-coordinate. Defaults to 0. + y_pos (int, optional): Initial y-coordinate. Defaults to 0. + """ + self.direction = direction + self.coordinates = (x_pos, y_pos) + + def move(self, instructions): + """ + Move the robot based on the provided instructions. + + Args: + instructions (str): A string containing movement instructions ('R', 'L', 'A'). + """ + directions = [NORTH, EAST, SOUTH, WEST] + for instruction in instructions: + if instruction == "R": + self.direction = directions[(directions.index(self.direction) + 1) % 4] + elif instruction == "L": + self.direction = directions[(directions.index(self.direction) - 1) % 4] + elif instruction == "A": + dx, dy = self.direction + self.coordinates = (self.coordinates[0] + dx, self.coordinates[1] + dy) diff --git a/practice/satellite/satellite.py b/practice/satellite/satellite.py index 2810ec6..bcc11ad 100644 --- a/practice/satellite/satellite.py +++ b/practice/satellite/satellite.py @@ -1,2 +1,41 @@ +def build_tree(preorder_traversal, inorder_traversal): + """Recursively constructs a binary tree. + + Args: + preorder_traversal (list): Preorder traversal of the binary tree. + inorder_traversal (list): Inorder traversal of the binary tree. + + Returns: + dict: The constructed binary tree. + """ + if len(preorder_traversal) == 0: + return {} + root_val = preorder_traversal.pop(0) + root_index = inorder_traversal.index(root_val) + inorder_traversal.pop(root_index) + return { + 'v': root_val, + 'l': build_tree(preorder_traversal[:root_index], inorder_traversal[:root_index]), + 'r': build_tree(preorder_traversal[root_index:], inorder_traversal[root_index:])} + + def tree_from_traversals(preorder, inorder): - pass + """Constructs a binary tree from its preorder and inorder traversals. + + Args: + preorder (list): Preorder traversal of the binary tree. + inorder (list): Inorder traversal of the binary tree. + + Returns: + dict: The constructed binary tree. + + Raises: + ValueError: If traversals are invalid. + """ + if len(preorder) != len(inorder): + raise ValueError("traversals must have the same length") + if set(preorder) != set(inorder): + raise ValueError("traversals must have the same elements") + if len(preorder) != len(set(preorder)): + raise ValueError("traversals must contain unique items") + return build_tree(preorder, inorder) diff --git a/practice/scale-generator/scale_generator.py b/practice/scale-generator/scale_generator.py index eddc130..2764093 100644 --- a/practice/scale-generator/scale_generator.py +++ b/practice/scale-generator/scale_generator.py @@ -1,9 +1,72 @@ class Scale: + NOTES = ["A", "AB", "B", "C", "CD", "D", "DE", "E", "F", "FG", "G", "GA"] + SEL_SHARP = ["C", "a", "G", "D", "A", "E", "B", "e", "b"] + def __init__(self, tonic): - pass + """Initialize the Scale object with the tonic note. + Args: + tonic (str): The tonic note for the scale. + """ + self.tonic = tonic + def chromatic(self): - pass + """Generate a chromatic scale based on the tonic note. - def interval(self, intervals): - pass + Returns: + list: A list of notes in the chromatic scale. + """ + return self.interval() + + def interval(self, intervals="".join(["m" for note in NOTES[:-1]])): + """Generate a scale based on specified intervals. + + Args: + intervals (str, optional): String specifying intervals ('m' for minor, 'M' for major, 'A' for augmented). Defaults to "mmmmmmmmmmmm". + + Returns: + list: A list of notes in the scale based on the specified intervals. + """ + scale = self.get_scale(self.get_keysig()) + result = [] + result.append(self.tonic.capitalize()) + index = scale.index(self.tonic.capitalize()) + for interval in intervals: + if interval == "m": + index += 1 + if interval == "M": + index += 2 + if interval == "A": + index += 3 + index = index % len(scale) + result.append(scale[index]) + return result + + def get_scale(self, keysig): + """Determine the scale based on the key signature. + + Args: + keysig (str): The key signature ("Sharp" or "Flat"). + + Returns: + list: A list of notes in the scale. + """ + scale = [] + for note in self.NOTES: + if len(note) == 1: + scale.append(note) + elif keysig == "Flat": + scale.append("".join([note[1], "b"])) + else: + scale.append("".join([note[0], "#"])) + return scale + + def get_keysig(self): + """Determine the key signature based on the tonic note. + + Returns: + str: The key signature ("Sharp" or "Flat"). + """ + if self.tonic in self.SEL_SHARP or self.tonic[-1] == "#": + return "Sharp" + return "Flat" diff --git a/practice/sgf-parsing/sgf_parsing.py b/practice/sgf-parsing/sgf_parsing.py index fa30f3f..b1144d2 100644 --- a/practice/sgf-parsing/sgf_parsing.py +++ b/practice/sgf-parsing/sgf_parsing.py @@ -1,9 +1,30 @@ +''' Approach: +The provided code defines a class `SgfTree` to represent a tree structure with properties and children, and a function `parse` to parse SGF (Smart Game Format) strings into tree structures. +The `SgfTree` class has methods for equality comparison, representation, and inequality comparison. +The `parse` function takes an SGF string as input and constructs an `SgfTree` object representing the parsed tree structure. +The parsing process involves handling property-value pairs, nested tree structures, and handling escape characters. +The function iterates through the input string, extracting property-value pairs and recursively parsing nested tree structures. +The code utilizes regular expressions to extract property-value pairs and handle escape characters efficiently. +''' + +import re + class SgfTree: + """Represent a tree structure with properties and children.""" + def __init__(self, properties=None, children=None): + """ + Initialize an SgfTree object with properties and children. + + Args: + properties (dict, optional): Dictionary of properties. Defaults to None. + children (list, optional): List of child SgfTree objects. Defaults to None. + """ self.properties = properties or {} self.children = children or [] def __eq__(self, other): + """Check if two SgfTree objects are equal.""" if not isinstance(other, SgfTree): return False for key, value in self.properties.items(): @@ -21,9 +42,52 @@ def __eq__(self, other): return False return True + def __repr__(self): + """Return a string representation of the SgfTree object.""" + return f"SgfTree(properties={self.properties!r}, children={self.children!r})" + def __ne__(self, other): + """Check if two SgfTree objects are not equal.""" return not self == other - def parse(input_string): - pass + """Parse an SGF string into an SgfTree object.""" + if input_string == "(;)": + return SgfTree() + if not input_string.startswith("("): + raise ValueError("tree missing") + prop_val_regex = r"\[(?:\\.|[^\]])+\]" + prop_regex = r"[A-Za-z]+(?:" + prop_val_regex + ")*" + m = re.search(r"^\(;((?:" + prop_regex + ")+)(.*)\)$", input_string) + if not m: + raise ValueError("tree with no nodes") + props, children = m[1], m[2] + properties = {} + for prop in re.findall(prop_regex, props): + name = prop.split("[")[0] + vals = [ + re.sub(r"\\(.)", r"\1", x[1:-1].replace("\t", " ")).replace("\\\n", "") + for x in re.findall(prop_val_regex, prop) + ] + if not vals: + raise ValueError("properties without delimiter") + if not name.isupper(): + raise ValueError("property must be in uppercase") + properties[name] = vals + if children.startswith(";"): + children = f"({children})" + child_list = [] + curr_child = "" + depth = 0 + for c in children: + curr_child += c + if c == "(": + depth += 1 + elif c == ")": + depth -= 1 + if depth < 0: + raise ValueError("unbalanced parentheses") + if depth == 0: + child_list.append(curr_child) + curr_child = "" + return SgfTree(properties=properties, children=list(map(parse, child_list))) diff --git a/practice/spiral-matrix/spiral_matrix.py b/practice/spiral-matrix/spiral_matrix.py index 52192ab..eb11f00 100644 --- a/practice/spiral-matrix/spiral_matrix.py +++ b/practice/spiral-matrix/spiral_matrix.py @@ -1,2 +1,30 @@ +""" +Spiral Matrix - spiral_matrix generates a square matrix of a given size in a spiral pattern. +It initializes a matrix with None values and then fills it with numbers in a spiral pattern. +The function uses itertools.cycle to iterate through a cycle of movement directions: +(0,1) for moving right, (1,0) for moving down, (0,-1) for moving left, and (-1,0) for moving up. +The matrix is filled in a spiral pattern by updating the current cell's position and direction +based on the cycle and checking for boundaries and filled cells. +""" + +from itertools import cycle + def spiral_matrix(size): - pass + matrix = [[None] * size for _ in range(size)] + r, c = 0, 0 + # this cycle determines the movement of the "current cell" + # (0,1) represents moving along a row to the right + # (1,0) represents moving down a column + deltas = cycle(((0,1), (1,0), (0,-1), (-1,0))) + dr, dc = next(deltas) + for i in range(size**2): + matrix[r][c] = i+1 + if ( + not 0 <= r+dr < size or + not 0 <= c+dc < size or + matrix[r+dr][c+dc] is not None + ): + dr, dc = next(deltas) + r += dr + c += dc + return matrix \ No newline at end of file diff --git a/practice/tree-building/tree_building.py b/practice/tree-building/tree_building.py index 75082a6..0584ed7 100644 --- a/practice/tree-building/tree_building.py +++ b/practice/tree-building/tree_building.py @@ -1,50 +1,71 @@ +"""" +Build Tree - Build a tree structure from a list of records. + +Approach: +1. Define a Record class to represent each record with attributes record_id and parent_id. +2. Define a Node class to represent each node in the tree with an ID and a list of children nodes. +3. Implement the BuildTree function to build a tree structure from a list of records. +4. Sort the records based on record_id to ensure they are in order. +5. Check if the record_ids are valid and in order, and if the parent_id of each node is smaller than its record_id. +6. Create a list of nodes corresponding to the records. +7. Iterate through the records to build the tree structure: + - For each record, add its corresponding node to the children list of its parent node. +8. Return the root node of the tree. + +Args: + records (list): A list of Record objects representing records with record_id and parent_id. + +Returns: + Node or None: The root node of the built tree structure, or None if no records are provided. + +Raises: + ValueError: If record_id is invalid or out of order, if parent_id is not smaller than the record_id, + or if a node other than the root has equal record and parent id. + +Example: + >>> records = [Record(0, 0), Record(1, 0), Record(2, 0)] + >>> BuildTree(records) + Node(node_id=0, children=[Node(node_id=1, children=[]), Node(node_id=2, children=[])]) +""" + class Record: + """Represents a record with attributes record_id and parent_id.""" def __init__(self, record_id, parent_id): self.record_id = record_id self.parent_id = parent_id - class Node: + """Represents a node in the tree with an ID and a list of children nodes.""" def __init__(self, node_id): self.node_id = node_id self.children = [] - def BuildTree(records): + """ + Build a tree structure from a list of records. + + Args: + records (list): A list of Record objects representing records with record_id and parent_id. + + Returns: + Node or None: The root node of the built tree structure, or None if no records are provided. + + Raises: + ValueError: If record_id is invalid or out of order, if parent_id is not smaller than the record_id, + or if a node other than the root has equal record and parent id. + """ root = None records.sort(key=lambda x: x.record_id) - ordered_id = [i.record_id for i in records] - if records: - if ordered_id[-1] != len(ordered_id) - 1: - raise ValueError('broken tree') - if ordered_id[0] != 0: - raise ValueError('invalid') - trees = [] - parent = {} - for i in range(len(ordered_id)): - for j in records: - if ordered_id[i] == j.record_id: - if j.record_id == 0: - if j.parent_id != 0: - raise ValueError('error!') - if j.record_id < j.parent_id: - raise ValueError('something went wrong!') - if j.record_id == j.parent_id: - if j.record_id != 0: - raise ValueError('error!') - trees.append(Node(ordered_id[i])) - for i in range(len(ordered_id)): - for j in trees: - if i == j.node_id: - parent = j - for j in records: - if j.parent_id == i: - for k in trees: - if k.node_id == 0: - continue - if j.record_id == k.node_id: - child = k - parent.children.append(child) + if records and records[-1].record_id != len(records) - 1: + raise ValueError('Record id is invalid or out of order.') + trees = [Node(records[i].record_id) for i in range(len(records))] + for i in range(len(records)): + if records[i].record_id < records[i].parent_id: + raise ValueError('Node parent_id should be smaller than it\'s record_id.') + if (records[i].record_id == records[i].parent_id and records[i].record_id != 0) or (records[i].record_id == 0 and records[i].parent_id != 0): + raise ValueError('Only root should have equal record and parent id.') + if records[i].record_id != 0: + trees[records[i].parent_id].children.append(trees[i]) if len(trees) > 0: root = trees[0] return root diff --git a/practice/two-bucket/two_bucket.py b/practice/two-bucket/two_bucket.py index 7029bcb..13ce247 100644 --- a/practice/two-bucket/two_bucket.py +++ b/practice/two-bucket/two_bucket.py @@ -1,2 +1,138 @@ +"""" +Water Bucket Problem - Measure the minimum number of moves required to achieve a specific goal volume of water using two buckets of different sizes. + +Approach: +1. Initialize the buckets with their maximum capacities and current volumes. +2. Fill the specified start bucket to begin the process. +3. Check if the goal is already achieved. If yes, return the result. +4. Explore all possible moves by pouring, emptying, or filling the buckets, considering the constraints and avoiding redundant states. +5. Perform depth-first search (DFS) to find the minimum moves required to reach the goal. +6. Utilize a recursive approach to explore all possible move sequences, keeping track of visited states to avoid cycles. +7. Return the minimum move required to reach the goal. + +Args: + bucket_one (int): The maximum capacity of the first bucket. + bucket_two (int): The maximum capacity of the second bucket. + goal (int): The desired volume of water to measure. + start_bucket (str): The bucket to start with, either "one" or "two". + +Returns: + tuple: A tuple containing the minimum number of moves required to achieve the goal and the corresponding bucket status upon reaching the goal. + +Raises: + ValueError: If the start_bucket is neither "one" nor "two". + +Example: + >>> measure(5, 7, 3, "one") + (1, "one", 0) +""" + +import copy + def measure(bucket_one, bucket_two, goal, start_bucket): - pass + """ + Measure the minimum number of moves required to achieve a specific goal volume of water. + + Args: + bucket_one (int): The maximum capacity of the first bucket. + bucket_two (int): The maximum capacity of the second bucket. + goal (int): The desired volume of water to measure. + start_bucket (str): The bucket to start with, either "one" or "two". + + Returns: + tuple: A tuple containing the minimum number of moves required to achieve the goal and the corresponding bucket status upon reaching the goal. + + Raises: + ValueError: If the start_bucket is neither "one" nor "two". + """ + buckets = [{"max": bucket_one, "cur": 0}, {"max": bucket_two, "cur": 0}] + fill(buckets, 0 if start_bucket == "one" else 1) + + result = is_done(buckets, goal) + if result: + return (1, *result) + + moves = [] + for i in range(6): + _r = move(i, buckets, goal, start_bucket, 1, [buckets]) + if _r: + moves.append(_r) + + return min(moves) + +def is_done(buckets, goal): + """Check if the goal is achieved.""" + if buckets[0]["cur"] == goal: + return ("one", buckets[1]["cur"]) + if buckets[1]["cur"] == goal: + return ("two", buckets[0]["cur"]) + return False + +def move(index, buckets, goal, start_bucket, count, history): + """Perform a move and explore subsequent moves recursively.""" + _buckets = copy.deepcopy(buckets) + + if index == 0: + pour(_buckets, 0, 1) # Pour from bucket one to bucket two + elif index == 1: + pour(_buckets, 1, 0) # Pour from bucket two to bucket one + elif index == 2: + empty(_buckets, 0) # Empty bucket one + elif index == 3: + empty(_buckets, 1) # Empty bucket two + elif index == 4: + fill(_buckets, 0) # Fill bucket one + elif index == 5: + fill(_buckets, 1) # Fill bucket two + + if _buckets[0]["cur"] + _buckets[1]["cur"] == 0: + return False + + first_bucket = 0 if start_bucket == "one" else 1 + second_bucket = 1 if start_bucket == "one" else 0 + + if is_empty(_buckets, first_bucket) and is_full(_buckets, second_bucket): + return False + if _buckets in history: + return False + + result = is_done(_buckets, goal) + if result: + return (count+1, *result) + + moves = [] + for i in range(6): + if i == index: + continue + _r = move(i, _buckets, goal, start_bucket, count+1, [*history, _buckets]) + if _r: + moves.append(_r) + + if moves: + return min(moves) + +def is_empty(buckets, which_bucket): + """Check if a bucket is empty.""" + return buckets[which_bucket]["cur"] == 0 + +def is_full(buckets, which_bucket): + """Check if a bucket is full.""" + return buckets[which_bucket]["cur"] == buckets[which_bucket]["max"] + +def pour(buckets, from_bucket, to_bucket): + """Pour water from one bucket to another.""" + left = buckets[to_bucket]["max"] - buckets[to_bucket]["cur"] + if buckets[from_bucket]["cur"] > left: + fill(buckets, to_bucket) + buckets[from_bucket]["cur"] -= left + else: + buckets[to_bucket]["cur"] += buckets[from_bucket]["cur"] + empty(buckets, from_bucket) + +def empty(buckets, which_bucket): + """Empty a bucket.""" + buckets[which_bucket]["cur"] = 0 + +def fill(buckets, which_bucket): + """Fill a bucket.""" + buckets[which_bucket]["cur"] = buckets[which_bucket]["max"] diff --git a/practice/word-search/word_search.py b/practice/word-search/word_search.py index b12e6b0..4d5ccde 100644 --- a/practice/word-search/word_search.py +++ b/practice/word-search/word_search.py @@ -1,15 +1,104 @@ class Point: def __init__(self, x, y): - self.x = None - self.y = None + """Initialize Point object with x and y coordinates.""" + self.x = x + self.y = y def __eq__(self, other): + """Check if two Point objects are equal.""" return self.x == other.x and self.y == other.y + def __repr__(self): + """Return string representation of Point object.""" + return f"Point(x={self.x}, y={self.y})" + class WordSearch: def __init__(self, puzzle): - pass + """Initialize WordSearch object with puzzle.""" + self.puzzle = puzzle def search(self, word): - pass + """Search for the word in all possible directions.""" + return ( + self.left_to_right_search(word) + or self.right_to_left_search(word) + or self.up_to_down_search(word) + or self.down_to_up_search(word) + or self.top_right_to_bottom_left_search(word) + or self.bottom_left_to_top_right_search(word) + or None + ) + + def left_to_right_search(self, word): + """Search for the word from left to right in puzzle rows.""" + for y, row in enumerate(self.puzzle): + if word in row: + x1 = row.find(word) + x2 = x1 + len(word) - 1 + return (Point(x1, y), Point(x2, y)) + + def right_to_left_search(self, word): + """Search for the word from right to left in puzzle rows.""" + for y, row in enumerate(self.puzzle): + row = "".join(reversed(row)) + if word in row: + x1 = len(row) - row.find(word) - 1 + x2 = x1 - len(word) + 1 + return (Point(x1, y), Point(x2, y)) + + def up_to_down_search(self, word): + """Search for the word from top to bottom in puzzle columns.""" + columns = zip(*self.puzzle) + for x, column in enumerate(columns): + column = "".join(column) + if word in column: + y1 = column.find(word) + y2 = y1 + len(word) - 1 + return (Point(x, y1), Point(x, y2)) + + def down_to_up_search(self, word): + """Search for the word from bottom to top in puzzle columns.""" + columns = zip(*self.puzzle) + for x, column in enumerate(columns): + column = "".join(reversed(column)) + if word in column: + y1 = len(column) - column.find(word) - 1 + y2 = y1 - len(word) + 1 + return (Point(x, y1), Point(x, y2)) + + def top_right_to_bottom_left_search(self, word): + """Search for the word diagonally from top right to bottom left.""" + for y in range(0, len(self.puzzle) - len(word)): + for x in range(0, len(self.puzzle[0]) - len(word)): + chars = [] + for i in range(len(word)): + chars.append(self.puzzle[y + i][x + i]) + diag = "".join(chars) + if word in diag: + i1 = diag.find(word) + i2 = i1 + len(word) - 1 + return (Point(x + i1, y + i1), Point(x + i2, y + i2)) + diag = "".join(reversed(diag)) + if word in diag: + i1 = len(diag) - diag.find(word) - 1 + i2 = i1 - len(word) + 1 + return (Point(x + i1, y + i1), Point(x + i2, y + i2)) + + def bottom_left_to_top_right_search(self, word): + """Search for the word diagonally from bottom left to top right.""" + for y in range(len(word) - 1, len(self.puzzle)): + for x in range(0, len(self.puzzle[0]) - len(word)): + chars = [] + for i in range(len(word)): + chars.append(self.puzzle[y - i][x + i]) + diag = "".join(chars) + if word in diag: + i1 = diag.find(word) + i2 = i1 + len(word) - 1 + return (Point(x + i1, y - i1), Point(x + i2, y - i2)) + diag = "".join(reversed(diag)) + if word in diag: + i1 = len(diag) - diag.find(word) - 1 + i2 = i1 - len(word) + 1 + return (Point(x + i1, y - i1), Point(x + i2, y - i2)) diff --git a/practice/zebra-puzzle/zebra_puzzle.py b/practice/zebra-puzzle/zebra_puzzle.py index 2d78762..92bc3af 100644 --- a/practice/zebra-puzzle/zebra_puzzle.py +++ b/practice/zebra-puzzle/zebra_puzzle.py @@ -1,6 +1,149 @@ +from itertools import permutations + +def solve(): + """ + Iterate through all possible permutations of attributes for each house, + applying the given constraints and return the solution when found. + + Returns: + list: A list containing dictionaries with solutions for each attribute. + Each dictionary represents a house with its attributes. + Example: [{'person': 'Norwegian', 'color': 'yellow', 'drink': 'water', 'animal': 'fox', 'smoke': 'Chesterfields'}, ...] + + """ + people = ("Norwegian", "Englishman", "Spaniard", "Ukrainian", "Japanese") + colors = ("green", "blue", "red", "yellow", "ivory") + animals = ("dog", "snails", "horse", "fox", "zebra") + drinks = ("tea", "milk", "coffee", "orange juice", "water") + smokes = ("Old Gold", "Chesterfields", "Kools", "Lucky Strike", "Parliaments") + + peoplemix = [x for x in permutations(people) if x[0] == "Norwegian" and x[2] == "Englishman" and x[1] != "Spaniard"] + colormix = [x for x in permutations(colors) if x[1] == "blue" and x.index("ivory") - x.index("green") == 1 and x[0] != "red" and x[2] != "green"] + drinkmix = [x for x in permutations(drinks) if x[2] == "milk" and x[3] == "coffee" and "tea" not in (x[0], x[2]) and x[0] != "orange juice"] + animalmix = [x for x in permutations(animals) if x[0] != "dog" and x[1] == "horse"] + smokemix = [x for x in permutations(smokes) if x[0] == "Kools" and "Lucky Strike" not in (x[2], x[3]) and x[1] != "Old Gold"] + + for p in peoplemix: + for c in colormix: + for d in drinkmix: + for a in animalmix: + for s in smokemix: + if p.index("Ukrainian") == d.index("tea") and p.index("Spaniard") == a.index("dog") and abs(s.index("Chesterfields") - a.index("fox")) == 1 and s.index("Old Gold") == a.index("snails") and s.index("Lucky Strike") == d.index("orange juice") and p.index("Japanese") == s.index("Parliaments"): + return [{'person': p[i], 'color': c[i], 'drink': d[i], 'animal': a[i], 'smoke': s[i]} for i in range(5)] + +solution = solve() + def drinks_water(): - pass + """ + Returns the person who drinks water. + + Returns: + str: The name of the person who drinks water. + """ + for t in solution: + if t['drink'] == 'water': + return t['person'] def owns_zebra(): - pass + """ + Returns the person who owns the zebra. + + Returns: + str: The name of the person who owns the zebra. + + """ + for t in solution: + if t['animal'] == 'zebra': + return t['person'] + +class TreeNode: + """ + Represents a node in a binary search tree. + + Attributes: + data: The data stored in the node. + left: The left child of the node. + right: The right child of the node. + """ + + def __init__(self, data, left=None, right=None): + self.data = data + self.left = left + self.right = right + + def __str__(self): + return f'TreeNode(data={self.data}, left={self.left}, right={self.right})' + + +class BinarySearchTree: + """ + Represents a binary search tree. + + Attributes: + root: The root node of the binary search tree. + """ + + def __init__(self, tree_data): + """ + Initialize the binary search tree with data. + + Args: + tree_data (list): A list of data items to initialize the tree. + """ + self.root = None + for data in tree_data: + self.add(data) + + def add(self, data): + """ + Inserts new data item into the binary search tree while maintaining the sorted order. + + Args: + data: The data item to be inserted. + """ + if self.root is None: + self.root = TreeNode(data) + return + inserted = False + cur_node = self.root + + while not inserted: + if data <= cur_node.data: + if cur_node.left: + cur_node = cur_node.left + else: + cur_node.left = TreeNode(data) + inserted = True + elif data > cur_node.data: + if cur_node.right: + cur_node = cur_node.right + else: + cur_node.right = TreeNode(data) + inserted = True + + def _inorder_traverse(self, node, elements): + if node is not None: + self._inorder_traverse(node.left, elements) + elements.append(node.data) + self._inorder_traverse(node.right, elements) + + def data(self): + """ + Returns the root node of the binary search tree. + + Returns: + TreeNode: The root node of the binary search tree. + """ + return self.root + + def sorted_data(self): + """ + Performs in-order traversal of the tree to retrieve all data items in sorted order. + + Returns: + list: A list of data items in sorted order. + """ + elements = [] + self._inorder_traverse(self.root, elements) + return elements diff --git a/practice/zipper/zipper.py b/practice/zipper/zipper.py index c4bf1dd..9f9a743 100644 --- a/practice/zipper/zipper.py +++ b/practice/zipper/zipper.py @@ -1,28 +1,119 @@ class Zipper: @staticmethod def from_tree(tree): - pass + """ + Constructs a Zipper instance from a given tree. + + Args: + tree (dict): The tree to construct the Zipper instance from. + + Returns: + Zipper: The constructed Zipper instance. + """ + return Zipper(dict(tree), []) + + def __init__(self, tree, ancestors): + """ + Initializes a Zipper instance with a tree and ancestors. + + Args: + tree (dict): The tree associated with the Zipper instance. + ancestors (list): The list of ancestors of the current node in the tree. + """ + self.tree = tree + self.ancestors = ancestors def value(self): - pass + """ + Returns the value of the current node. - def set_value(self): - pass + Returns: + Any: The value of the current node. + """ + return self.tree['value'] + + def set_value(self, value): + """ + Sets the value of the current node. + + Args: + value (Any): The value to set for the current node. + + Returns: + Zipper: The updated Zipper instance. + """ + self.tree['value'] = value + return self def left(self): - pass + """ + Moves to the left child node if it exists. + + Returns: + Zipper or None: The Zipper instance representing the left child node, + or None if the left child node does not exist. + """ + if self.tree['left'] is None: + return None + return Zipper(self.tree['left'], self.ancestors + [self.tree]) - def set_left(self): - pass + def set_left(self, tree): + """ + Sets the left child of the current node. + + Args: + tree (dict): The tree to set as the left child of the current node. + + Returns: + Zipper: The updated Zipper instance. + """ + self.tree['left'] = tree + return self def right(self): - pass + """ + Moves to the right child node if it exists. + + Returns: + Zipper or None: The Zipper instance representing the right child node, + or None if the right child node does not exist. + """ + if self.tree['right'] is None: + return None + return Zipper(self.tree['right'], self.ancestors + [self.tree]) + + def set_right(self, tree): + """ + Sets the right child of the current node. - def set_right(self): - pass + Args: + tree (dict): The tree to set as the right child of the current node. + + Returns: + Zipper: The updated Zipper instance. + """ + self.tree['right'] = tree + return self def up(self): - pass + """ + Moves to the parent node if it exists. + + Returns: + Zipper or None: The Zipper instance representing the parent node, + or None if the parent node does not exist. + """ + if not self.ancestors: + return None + return Zipper(self.ancestors[-1], self.ancestors[:-1]) def to_tree(self): - pass + """ + Returns the root of the tree. + + Returns: + dict: The root of the tree associated with the Zipper instance. + """ + if any(self.ancestors): + return self.ancestors[0] + return self.tree