diff --git a/src/array_flattening.py b/src/array_flattening.py new file mode 100644 index 00000000..fa8bbb91 --- /dev/null +++ b/src/array_flattening.py @@ -0,0 +1,38 @@ +from typing import List, Union, Any + +def flatten_array(arr: List[Union[Any, List]]) -> List[Any]: + """ + Recursively flatten a nested list into a single-level list. + + Args: + arr (List[Union[Any, List]]): A potentially nested list to be flattened. + + Returns: + List[Any]: A flattened list with all nested elements extracted. + + Raises: + TypeError: If the input is not a list. + + Examples: + >>> flatten_array([1, [2, 3], [4, [5, 6]]]) + [1, 2, 3, 4, 5, 6] + >>> flatten_array([1, 2, 3]) + [1, 2, 3] + >>> flatten_array([]) + [] + """ + # Validate input is a list + if not isinstance(arr, list): + raise TypeError("Input must be a list") + + # Recursive flattening function + flattened = [] + for item in arr: + # If the item is a list, recursively flatten + if isinstance(item, list): + flattened.extend(flatten_array(item)) + else: + # Otherwise, append the item directly + flattened.append(item) + + return flattened \ No newline at end of file diff --git a/src/binary_search.py b/src/binary_search.py new file mode 100644 index 00000000..f3beccc7 --- /dev/null +++ b/src/binary_search.py @@ -0,0 +1,48 @@ +def binary_search(arr, target): + """ + Perform binary search on a sorted array to find the target element. + + Args: + arr (list): A sorted list of comparable elements + target: The element to search for + + Returns: + int: Index of the target element if found, otherwise -1 + + Raises: + TypeError: If input is not a list + ValueError: If the list is not sorted + """ + # Check if input is a list + if not isinstance(arr, list): + raise TypeError("Input must be a list") + + # Check if list is sorted + if arr != sorted(arr): + raise ValueError("Input list must be sorted in ascending order") + + # Handle empty list + if not arr: + return -1 + + # Perform binary search + left, right = 0, len(arr) - 1 + + while left <= right: + # Calculate mid point to avoid potential integer overflow + mid = left + (right - left) // 2 + + # Check if target is found + if arr[mid] == target: + return mid + + # If target is greater, ignore left half + elif arr[mid] < target: + left = mid + 1 + + # If target is smaller, ignore right half + else: + right = mid - 1 + + # Target not found + return -1 \ No newline at end of file diff --git a/src/rgb_to_hex.py b/src/rgb_to_hex.py new file mode 100644 index 00000000..812d3a20 --- /dev/null +++ b/src/rgb_to_hex.py @@ -0,0 +1,26 @@ +def rgb_to_hex(r: int, g: int, b: int) -> str: + """ + Convert RGB color values to a hexadecimal color representation. + + Args: + r (int): Red color value (0-255) + g (int): Green color value (0-255) + b (int): Blue color value (0-255) + + Returns: + str: Hexadecimal color representation (uppercase) + + Raises: + ValueError: If any color value is not between 0 and 255 + """ + # Validate input values + for color, name in [(r, 'Red'), (g, 'Green'), (b, 'Blue')]: + if not isinstance(color, int): + raise TypeError(f"{name} value must be an integer") + if color < 0 or color > 255: + raise ValueError(f"{name} value must be between 0 and 255") + + # Convert to hex and ensure two digits with leading zero if needed + hex_color = ''.join(f'{color:02X}' for color in [r, g, b]) + + return hex_color \ No newline at end of file diff --git a/src/string_reversal.py b/src/string_reversal.py new file mode 100644 index 00000000..2c9d1632 --- /dev/null +++ b/src/string_reversal.py @@ -0,0 +1,24 @@ +def reverse_string(s: str) -> str: + """ + Reverse a given string manually, preserving all characters. + + Args: + s (str): The input string to be reversed. + + Returns: + str: The reversed string. + + Raises: + TypeError: If the input is not a string. + """ + # Type checking + if not isinstance(s, str): + raise TypeError("Input must be a string") + + # Manual string reversal using a list-based approach + # This method preserves all characters, including spaces and special characters + reversed_chars = [] + for i in range(len(s) - 1, -1, -1): + reversed_chars.append(s[i]) + + return ''.join(reversed_chars) \ No newline at end of file diff --git a/src/url_parser.py b/src/url_parser.py new file mode 100644 index 00000000..56fba683 --- /dev/null +++ b/src/url_parser.py @@ -0,0 +1,58 @@ +from urllib.parse import urlparse, unquote +from typing import Dict, Any, Optional, Union, List + +def parse_url(url: str) -> Dict[str, Any]: + """ + Parse a given URL into its component parts. + + Args: + url (str): The URL to parse + + Returns: + Dict[str, Any]: A dictionary containing URL components + + Raises: + ValueError: If the URL is invalid or empty + """ + # Check for empty or None input + if not url or not isinstance(url, str): + raise ValueError("Invalid URL: URL must be a non-empty string") + + try: + # Parse the URL + parsed_url = urlparse(url) + + # Extract query parameters with raw URL-encoded values + query_params: Dict[str, Union[str, List[str]]] = {} + if parsed_url.query: + # Use custom parsing to handle multiple parameters + for param in parsed_url.query.split('&'): + if '=' in param: + key, value = param.split('=', 1) + decoded_key = unquote(key) + + # Handle multiple values for the same key + if decoded_key in query_params: + if isinstance(query_params[decoded_key], list): + query_params[decoded_key].append(value) # type: ignore + else: + query_params[decoded_key] = [query_params[decoded_key], value] # type: ignore + else: + query_params[decoded_key] = value + + # Construct and return the parsed URL dictionary + return { + 'scheme': parsed_url.scheme or None, + 'netloc': parsed_url.netloc or None, + 'path': parsed_url.path or None, + 'params': parsed_url.params or None, + 'query': query_params, + 'fragment': parsed_url.fragment or None, + 'username': parsed_url.username, + 'password': parsed_url.password, + 'hostname': parsed_url.hostname, + 'port': parsed_url.port + } + except Exception as e: + # Catch any unexpected parsing errors + raise ValueError(f"Error parsing URL: {str(e)}") \ No newline at end of file diff --git a/tests/test_array_flattening.py b/tests/test_array_flattening.py new file mode 100644 index 00000000..227fb4ee --- /dev/null +++ b/tests/test_array_flattening.py @@ -0,0 +1,33 @@ +import pytest +from src.array_flattening import flatten_array + +def test_flatten_simple_list(): + """Test flattening a simple list""" + assert flatten_array([1, 2, 3]) == [1, 2, 3] + +def test_flatten_nested_list(): + """Test flattening a nested list""" + assert flatten_array([1, [2, 3], [4, [5, 6]]]) == [1, 2, 3, 4, 5, 6] + +def test_flatten_deeply_nested_list(): + """Test flattening a deeply nested list""" + assert flatten_array([1, [2, [3, [4]]], 5]) == [1, 2, 3, 4, 5] + +def test_flatten_empty_list(): + """Test flattening an empty list""" + assert flatten_array([]) == [] + +def test_flatten_list_with_empty_sublists(): + """Test flattening a list with empty sublists""" + assert flatten_array([1, [], [2, []], 3]) == [1, 2, 3] + +def test_invalid_input_type(): + """Test that a TypeError is raised for non-list inputs""" + with pytest.raises(TypeError, match="Input must be a list"): + flatten_array("not a list") + flatten_array(123) + flatten_array(None) + +def test_mixed_type_list(): + """Test flattening a list with mixed types""" + assert flatten_array([1, "string", [2, 3.14], [True, [False]]]) == [1, "string", 2, 3.14, True, False] \ No newline at end of file diff --git a/tests/test_binary_search.py b/tests/test_binary_search.py new file mode 100644 index 00000000..750f6592 --- /dev/null +++ b/tests/test_binary_search.py @@ -0,0 +1,45 @@ +import pytest +from src.binary_search import binary_search + +def test_binary_search_basic(): + """Test basic functionality of binary search""" + arr = [1, 3, 5, 7, 9, 11, 13] + assert binary_search(arr, 7) == 3 + assert binary_search(arr, 1) == 0 + assert binary_search(arr, 13) == 6 + +def test_binary_search_not_found(): + """Test when target is not in the list""" + arr = [1, 3, 5, 7, 9, 11, 13] + assert binary_search(arr, 4) == -1 + assert binary_search(arr, 0) == -1 + assert binary_search(arr, 14) == -1 + +def test_binary_search_empty_list(): + """Test binary search on an empty list""" + assert binary_search([], 5) == -1 + +def test_binary_search_single_element(): + """Test binary search on a single-element list""" + arr = [5] + assert binary_search(arr, 5) == 0 + assert binary_search(arr, 4) == -1 + +def test_binary_search_type_error(): + """Test that TypeError is raised for non-list inputs""" + with pytest.raises(TypeError): + binary_search("not a list", 5) + with pytest.raises(TypeError): + binary_search(123, 5) + +def test_binary_search_unsorted_list(): + """Test that ValueError is raised for unsorted lists""" + with pytest.raises(ValueError): + binary_search([5, 3, 1, 2, 4], 3) + +def test_binary_search_duplicate_elements(): + """Test binary search with duplicate elements""" + arr = [1, 2, 2, 3, 3, 3, 4, 5] + # Returns the index of one of the duplicates (doesn't matter which) + assert binary_search(arr, 2) in [1, 2] + assert binary_search(arr, 3) in [3, 4, 5] \ No newline at end of file diff --git a/tests/test_rgb_to_hex.py b/tests/test_rgb_to_hex.py new file mode 100644 index 00000000..9153e2e7 --- /dev/null +++ b/tests/test_rgb_to_hex.py @@ -0,0 +1,41 @@ +import pytest +from src.rgb_to_hex import rgb_to_hex + +def test_rgb_to_hex_basic(): + """Test basic color conversion""" + assert rgb_to_hex(255, 128, 0) == 'FF8000' + assert rgb_to_hex(0, 0, 0) == '000000' + assert rgb_to_hex(255, 255, 255) == 'FFFFFF' + +def test_rgb_to_hex_edge_cases(): + """Test edge case values""" + assert rgb_to_hex(0, 0, 0) == '000000' + assert rgb_to_hex(255, 255, 255) == 'FFFFFF' + +def test_rgb_to_hex_lowercase_conversion(): + """Ensure output is always uppercase""" + result = rgb_to_hex(10, 20, 30) + assert result == result.upper() + +def test_rgb_to_hex_invalid_inputs(): + """Test error handling for invalid inputs""" + # Test out of range values + with pytest.raises(ValueError, match="Red value must be between 0 and 255"): + rgb_to_hex(-1, 0, 0) + + with pytest.raises(ValueError, match="Green value must be between 0 and 255"): + rgb_to_hex(0, 256, 0) + + with pytest.raises(ValueError, match="Blue value must be between 0 and 255"): + rgb_to_hex(0, 0, 300) + +def test_rgb_to_hex_type_errors(): + """Test type checking for inputs""" + with pytest.raises(TypeError, match="Red value must be an integer"): + rgb_to_hex('255', 0, 0) + + with pytest.raises(TypeError, match="Green value must be an integer"): + rgb_to_hex(0, '128', 0) + + with pytest.raises(TypeError, match="Blue value must be an integer"): + rgb_to_hex(0, 0, '64') \ No newline at end of file diff --git a/tests/test_string_reversal.py b/tests/test_string_reversal.py new file mode 100644 index 00000000..34fe674a --- /dev/null +++ b/tests/test_string_reversal.py @@ -0,0 +1,42 @@ +import pytest +from src.string_reversal import reverse_string + +def test_reverse_string_basic(): + """Test basic string reversal.""" + assert reverse_string("hello") == "olleh" + assert reverse_string("python") == "nohtyp" + +def test_reverse_string_empty(): + """Test reversal of an empty string.""" + assert reverse_string("") == "" + +def test_reverse_string_single_char(): + """Test reversal of a single character.""" + assert reverse_string("a") == "a" + +def test_reverse_string_with_spaces(): + """Test reversal of string with spaces.""" + assert reverse_string("hello world") == "dlrow olleh" + +def test_reverse_string_with_punctuation(): + """Test reversal of string with punctuation.""" + assert reverse_string("hello, world!") == "!dlrow ,olleh" + +def test_reverse_string_with_mixed_characters(): + """Test reversal of string with mixed character types.""" + assert reverse_string("a1b2c3") == "3c2b1a" + +def test_reverse_string_unicode(): + """Test reversal of string with unicode characters.""" + assert reverse_string("café") == "éfac" + +def test_reverse_string_invalid_input(): + """Test that TypeError is raised for non-string inputs.""" + with pytest.raises(TypeError, match="Input must be a string"): + reverse_string(123) + + with pytest.raises(TypeError, match="Input must be a string"): + reverse_string(None) + + with pytest.raises(TypeError, match="Input must be a string"): + reverse_string(["hello"]) \ No newline at end of file diff --git a/tests/test_url_parser.py b/tests/test_url_parser.py new file mode 100644 index 00000000..2576bdf0 --- /dev/null +++ b/tests/test_url_parser.py @@ -0,0 +1,60 @@ +import pytest +from src.url_parser import parse_url + +def test_full_url_parsing(): + url = "https://username:password@example.com:8080/path/to/page?key1=value1&key2=value2#fragment" + parsed = parse_url(url) + + assert parsed['scheme'] == 'https' + assert parsed['netloc'] == 'username:password@example.com:8080' + assert parsed['path'] == '/path/to/page' + assert parsed['query'] == {'key1': 'value1', 'key2': 'value2'} + assert parsed['fragment'] == 'fragment' + assert parsed['username'] == 'username' + assert parsed['password'] == 'password' + assert parsed['hostname'] == 'example.com' + assert parsed['port'] == 8080 + +def test_minimal_url(): + url = "http://example.com" + parsed = parse_url(url) + + assert parsed['scheme'] == 'http' + assert parsed['netloc'] == 'example.com' + assert parsed['path'] is None + assert parsed['query'] == {} + assert parsed['fragment'] is None + +def test_url_with_query_params(): + url = "https://example.com/search?q=python&category=programming" + parsed = parse_url(url) + + assert parsed['query'] == {'q': 'python', 'category': 'programming'} + +def test_url_with_multiple_same_query_params(): + url = "https://example.com/page?tag=python&tag=programming" + parsed = parse_url(url) + + assert parsed['query'] == {'tag': ['python', 'programming']} + +def test_invalid_url_input(): + with pytest.raises(ValueError, match="Invalid URL"): + parse_url("") + + with pytest.raises(ValueError, match="Invalid URL"): + parse_url(None) + +def test_url_without_scheme(): + url = "example.com/path" + parsed = parse_url(url) + + assert parsed['scheme'] is None + assert parsed['netloc'] is None + assert parsed['path'] == 'example.com/path' + +def test_url_with_special_characters(): + url = "https://example.com/path%20with%20spaces?key=value%20with%20spaces" + parsed = parse_url(url) + + assert parsed['path'] == '/path%20with%20spaces' + assert parsed['query'] == {'key': 'value%20with%20spaces'} \ No newline at end of file