diff --git a/.gitignore b/.gitignore index 63040b6..3b44555 100644 --- a/.gitignore +++ b/.gitignore @@ -20,7 +20,6 @@ uv.lock *.swo # Project specific -problems/ .solved.json # OS diff --git a/problems/easy/palindrome-checker/solution.py b/problems/easy/palindrome-checker/solution.py new file mode 100644 index 0000000..f6ef597 --- /dev/null +++ b/problems/easy/palindrome-checker/solution.py @@ -0,0 +1,33 @@ +""" +Palindrome Checker + +You're building a word game that awards bonus points for palindromes. +Given a string, determine if it reads the same forwards and backwards, +ignoring case and non-alphanumeric characters. + +Example 1: + Input: text = "A man, a plan, a canal: Panama" + Output: True + Explanation: "amanaplanacanalpanama" is a palindrome + +Example 2: + Input: text = "race a car" + Output: False + Explanation: "raceacar" is not a palindrome + +Example 3: + Input: text = " " + Output: True + Explanation: Empty after removing non-alphanumeric + +Constraints: + - Input is always a string + - Ignore spaces, punctuation, and case + - Empty string is considered a palindrome +""" + + +def is_palindrome(text: str) -> bool: + """Return True if text is a palindrome, False otherwise.""" + pass # Your implementation here + diff --git a/problems/easy/palindrome-checker/tests.py b/problems/easy/palindrome-checker/tests.py new file mode 100644 index 0000000..63bda68 --- /dev/null +++ b/problems/easy/palindrome-checker/tests.py @@ -0,0 +1,44 @@ +"""Tests for palindrome-checker.""" +import pytest +from solution import is_palindrome + + +class TestBasicCases: + """Test basic functionality with typical inputs.""" + + def test_simple_palindrome(self): + """Test basic palindrome word.""" + assert is_palindrome("racecar") == True + + def test_sentence_palindrome(self): + """Test palindrome with spaces and punctuation.""" + assert is_palindrome("A man, a plan, a canal: Panama") == True + + def test_not_palindrome(self): + """Test non-palindrome string.""" + assert is_palindrome("hello") == False + + +class TestEdgeCases: + """Test edge cases and boundary conditions.""" + + def test_empty_string(self): + """Test with empty input.""" + assert is_palindrome("") == True + + def test_single_character(self): + """Test with single character.""" + assert is_palindrome("a") == True + + def test_only_spaces(self): + """Test with only whitespace.""" + assert is_palindrome(" ") == True + + def test_mixed_case(self): + """Test case insensitivity.""" + assert is_palindrome("RaceCar") == True + + def test_numbers_in_string(self): + """Test with numbers.""" + assert is_palindrome("12321") == True + diff --git a/problems/easy/two-sum/solution.py b/problems/easy/two-sum/solution.py new file mode 100644 index 0000000..2cb606b --- /dev/null +++ b/problems/easy/two-sum/solution.py @@ -0,0 +1,31 @@ +""" +Two Sum + +You are a cashier at a busy store. A customer wants to pay exactly +a target amount using exactly two items from their basket. + +Given a list of item prices and a target total, find the indices +of the two items that add up to the target. + +Example 1: + Input: prices = [2, 7, 11, 15], target = 9 + Output: [0, 1] + Explanation: prices[0] + prices[1] = 2 + 7 = 9 + +Example 2: + Input: prices = [3, 2, 4], target = 6 + Output: [1, 2] + +Constraints: + - 2 <= len(prices) <= 10^4 + - Each price is a positive integer + - Exactly one solution exists +""" + + +def two_sum(prices: list[int], target: int) -> list[int]: + """Return indices of two prices that add up to target.""" + for i in range(len(prices)): + for j in range(i+1, len(prices)): + if prices[i] + prices[j] == target: + return [i,j] diff --git a/problems/easy/two-sum/tests.py b/problems/easy/two-sum/tests.py new file mode 100644 index 0000000..83a5d46 --- /dev/null +++ b/problems/easy/two-sum/tests.py @@ -0,0 +1,35 @@ +"""Tests for two-sum.""" +import pytest +from solution import two_sum + + +class TestBasicCases: + """Test basic functionality with typical inputs.""" + + def test_example_one(self): + """Test first example from problem description.""" + assert sorted(two_sum([2, 7, 11, 15], 9)) == [0, 1] + + def test_example_two(self): + """Test second example from problem description.""" + assert sorted(two_sum([3, 2, 4], 6)) == [1, 2] + + def test_adjacent_elements(self): + """Test when answer elements are adjacent.""" + assert sorted(two_sum([1, 2, 3, 4], 7)) == [2, 3] + + +class TestEdgeCases: + """Test edge cases and boundary conditions.""" + + def test_same_value_twice(self): + """Test with duplicate values that sum to target.""" + assert sorted(two_sum([3, 3], 6)) == [0, 1] + + def test_first_and_last(self): + """Test when answer is first and last elements.""" + assert sorted(two_sum([2, 4, 6, 8, 10, 7], 9)) == [0, 5] + + def test_negative_numbers(self): + """Test with negative numbers in the list.""" + assert sorted(two_sum([-1, -2, -3, -4, -5], -8)) == [2, 4] diff --git a/problems/hard/lru-cache/solution.py b/problems/hard/lru-cache/solution.py new file mode 100644 index 0000000..4c3c20f --- /dev/null +++ b/problems/hard/lru-cache/solution.py @@ -0,0 +1,47 @@ +""" +LRU Cache + +You're building a caching layer for a database-heavy web application. +To reduce load, implement a Least Recently Used (LRU) cache that +automatically evicts the oldest unused entries when capacity is reached. + +Example 1: + cache = LRUCache(capacity=2) + cache.put("a", 1) + cache.put("b", 2) + cache.get("a") # Returns 1 (a is now most recently used) + cache.put("c", 3) # Evicts "b" (least recently used) + cache.get("b") # Returns -1 (not found) + cache.get("c") # Returns 3 + +Example 2: + cache = LRUCache(capacity=1) + cache.put("x", 10) + cache.put("y", 20) # Evicts "x" + cache.get("x") # Returns -1 + cache.get("y") # Returns 20 + +Constraints: + - capacity >= 1 + - Keys are strings, values are integers + - get() returns -1 if key not found + - put() updates value if key exists (and marks as recently used) + - Both get() and put() should be O(1) average time +""" + + +class LRUCache: + """Least Recently Used cache with O(1) operations.""" + + def __init__(self, capacity: int): + """Initialize cache with given capacity.""" + pass # Your implementation here + + def get(self, key: str) -> int: + """Return value for key, or -1 if not found.""" + pass # Your implementation here + + def put(self, key: str, value: int) -> None: + """Insert or update key-value pair, evicting LRU if needed.""" + pass # Your implementation here + diff --git a/problems/hard/lru-cache/tests.py b/problems/hard/lru-cache/tests.py new file mode 100644 index 0000000..c3655a3 --- /dev/null +++ b/problems/hard/lru-cache/tests.py @@ -0,0 +1,86 @@ +"""Tests for lru-cache.""" +import pytest +from solution import LRUCache + + +class TestBasicCases: + """Test basic functionality with typical inputs.""" + + def test_basic_put_get(self): + """Test basic put and get operations.""" + cache = LRUCache(2) + cache.put("a", 1) + cache.put("b", 2) + assert cache.get("a") == 1 + assert cache.get("b") == 2 + + def test_eviction_lru(self): + """Test that least recently used is evicted.""" + cache = LRUCache(2) + cache.put("a", 1) + cache.put("b", 2) + cache.get("a") # a is now most recent + cache.put("c", 3) # b should be evicted + assert cache.get("b") == -1 + assert cache.get("a") == 1 + assert cache.get("c") == 3 + + def test_update_existing_key(self): + """Test updating an existing key.""" + cache = LRUCache(2) + cache.put("a", 1) + cache.put("a", 10) + assert cache.get("a") == 10 + + +class TestEdgeCases: + """Test edge cases and boundary conditions.""" + + def test_capacity_one(self): + """Test with capacity of 1.""" + cache = LRUCache(1) + cache.put("a", 1) + cache.put("b", 2) + assert cache.get("a") == -1 + assert cache.get("b") == 2 + + def test_get_nonexistent(self): + """Test getting a key that doesn't exist.""" + cache = LRUCache(2) + assert cache.get("missing") == -1 + + def test_put_updates_recency(self): + """Test that put on existing key updates recency.""" + cache = LRUCache(2) + cache.put("a", 1) + cache.put("b", 2) + cache.put("a", 10) # a is now most recent + cache.put("c", 3) # b should be evicted + assert cache.get("b") == -1 + assert cache.get("a") == 10 + + def test_many_operations(self): + """Test a sequence of many operations.""" + cache = LRUCache(3) + cache.put("a", 1) + cache.put("b", 2) + cache.put("c", 3) + cache.get("a") + cache.put("d", 4) # evicts b + cache.put("e", 5) # evicts c + assert cache.get("a") == 1 + assert cache.get("b") == -1 + assert cache.get("c") == -1 + assert cache.get("d") == 4 + assert cache.get("e") == 5 + + def test_get_updates_recency(self): + """Test that get updates the access order.""" + cache = LRUCache(2) + cache.put("a", 1) + cache.put("b", 2) + cache.get("a") # touch a + cache.put("c", 3) # evicts b, not a + assert cache.get("a") == 1 + assert cache.get("b") == -1 + diff --git a/problems/hard/rate-limiter/solution.py b/problems/hard/rate-limiter/solution.py new file mode 100644 index 0000000..b2e6b3f --- /dev/null +++ b/problems/hard/rate-limiter/solution.py @@ -0,0 +1,49 @@ +""" +Rate Limiter + +You're building an API gateway for a SaaS platform. To prevent abuse +and ensure fair usage, you need to implement a rate limiter that tracks +requests per user using a sliding window algorithm. + +The limiter should allow at most `max_requests` per user within any +`window_seconds` time period. + +Example 1: + limiter = RateLimiter(max_requests=3, window_seconds=60) + limiter.allow_request("user1", timestamp=0) # True (1st request) + limiter.allow_request("user1", timestamp=30) # True (2nd request) + limiter.allow_request("user1", timestamp=45) # True (3rd request) + limiter.allow_request("user1", timestamp=50) # False (limit reached) + limiter.allow_request("user1", timestamp=61) # True (1st expired) + +Example 2: + limiter = RateLimiter(max_requests=2, window_seconds=10) + limiter.allow_request("user1", timestamp=0) # True + limiter.allow_request("user2", timestamp=0) # True (different user) + limiter.allow_request("user1", timestamp=5) # True + limiter.allow_request("user1", timestamp=8) # False + +Constraints: + - max_requests >= 1 + - window_seconds >= 1 + - Timestamps are non-negative integers (seconds) + - Timestamps are non-decreasing per user + - user_id is a non-empty string +""" + + +class RateLimiter: + """Sliding window rate limiter for API request throttling.""" + + def __init__(self, max_requests: int, window_seconds: int): + """Initialize with request limit and time window.""" + pass # Your implementation here + + def allow_request(self, user_id: str, timestamp: int) -> bool: + """Return True if request allowed, False if rate limited.""" + pass # Your implementation here + + def get_remaining(self, user_id: str, timestamp: int) -> int: + """Return remaining requests allowed for user at timestamp.""" + pass # Your implementation here + diff --git a/problems/hard/rate-limiter/tests.py b/problems/hard/rate-limiter/tests.py new file mode 100644 index 0000000..2331e9e --- /dev/null +++ b/problems/hard/rate-limiter/tests.py @@ -0,0 +1,70 @@ +"""Tests for rate-limiter.""" +import pytest +from solution import RateLimiter + + +class TestBasicCases: + """Test basic functionality with typical inputs.""" + + def test_allow_within_limit(self): + """Test requests within the limit are allowed.""" + limiter = RateLimiter(max_requests=3, window_seconds=60) + assert limiter.allow_request("user1", 0) == True + assert limiter.allow_request("user1", 30) == True + assert limiter.allow_request("user1", 45) == True + + def test_block_over_limit(self): + """Test requests over limit are blocked.""" + limiter = RateLimiter(max_requests=2, window_seconds=60) + assert limiter.allow_request("user1", 0) == True + assert limiter.allow_request("user1", 30) == True + assert limiter.allow_request("user1", 45) == False + + def test_multiple_users_independent(self): + """Test each user has independent limits.""" + limiter = RateLimiter(max_requests=1, window_seconds=60) + assert limiter.allow_request("user1", 0) == True + assert limiter.allow_request("user2", 0) == True + assert limiter.allow_request("user1", 30) == False + assert limiter.allow_request("user2", 30) == False + + def test_get_remaining_basic(self): + """Test remaining count decreases with requests.""" + limiter = RateLimiter(max_requests=3, window_seconds=60) + assert limiter.get_remaining("user1", 0) == 3 + limiter.allow_request("user1", 0) + assert limiter.get_remaining("user1", 0) == 2 + + +class TestEdgeCases: + """Test edge cases and boundary conditions.""" + + def test_window_expiration(self): + """Test old requests expire from window.""" + limiter = RateLimiter(max_requests=2, window_seconds=60) + assert limiter.allow_request("user1", 0) == True + assert limiter.allow_request("user1", 30) == True + assert limiter.allow_request("user1", 45) == False + assert limiter.allow_request("user1", 61) == True + + def test_single_request_limit(self): + """Test with limit of 1 request.""" + limiter = RateLimiter(max_requests=1, window_seconds=10) + assert limiter.allow_request("user1", 0) == True + assert limiter.allow_request("user1", 5) == False + assert limiter.allow_request("user1", 11) == True + + def test_new_user_full_allowance(self): + """Test new users start with full allowance.""" + limiter = RateLimiter(max_requests=5, window_seconds=60) + limiter.allow_request("user1", 0) + assert limiter.get_remaining("new_user", 20) == 5 + + def test_rapid_same_timestamp(self): + """Test multiple requests at same timestamp.""" + limiter = RateLimiter(max_requests=3, window_seconds=1) + assert limiter.allow_request("user1", 0) == True + assert limiter.allow_request("user1", 0) == True + assert limiter.allow_request("user1", 0) == True + assert limiter.allow_request("user1", 0) == False + diff --git a/problems/medium/group-transactions/solution.py b/problems/medium/group-transactions/solution.py new file mode 100644 index 0000000..7b4a1b3 --- /dev/null +++ b/problems/medium/group-transactions/solution.py @@ -0,0 +1,33 @@ +""" +Transaction Grouper + +You're building a financial dashboard for a budgeting app. Users want +to see their spending grouped by category with totals, so they can +understand where their money is going each month. + +Example 1: + Input: transactions = [ + {"amount": 50, "category": "food", "date": "2024-01-01"}, + {"amount": 30, "category": "food", "date": "2024-01-02"}, + {"amount": 100, "category": "transport", "date": "2024-01-01"} + ] + Output: {"food": 80, "transport": 100} + Explanation: food: 50+30=80, transport: 100 + +Example 2: + Input: transactions = [] + Output: {} + Explanation: No transactions means empty result + +Constraints: + - Each transaction has "amount" (positive int), "category" (str), "date" (str) + - Categories are case-sensitive + - Return categories in any order + - Amount is always positive +""" + + +def group_transactions(transactions: list[dict]) -> dict[str, int]: + """Return dictionary mapping each category to its total amount.""" + pass # Your implementation here + diff --git a/problems/medium/group-transactions/tests.py b/problems/medium/group-transactions/tests.py new file mode 100644 index 0000000..32d8cbd --- /dev/null +++ b/problems/medium/group-transactions/tests.py @@ -0,0 +1,58 @@ +"""Tests for group-transactions.""" +import pytest +from solution import group_transactions + + +class TestBasicCases: + """Test basic functionality with typical inputs.""" + + def test_multiple_categories(self): + """Test grouping across different categories.""" + txns = [ + {"amount": 50, "category": "food", "date": "2024-01-01"}, + {"amount": 30, "category": "food", "date": "2024-01-02"}, + {"amount": 100, "category": "transport", "date": "2024-01-01"} + ] + assert group_transactions(txns) == {"food": 80, "transport": 100} + + def test_single_category(self): + """Test all transactions in one category.""" + txns = [ + {"amount": 10, "category": "food", "date": "2024-01-01"}, + {"amount": 20, "category": "food", "date": "2024-01-02"}, + {"amount": 30, "category": "food", "date": "2024-01-03"} + ] + assert group_transactions(txns) == {"food": 60} + + def test_single_transaction(self): + """Test with just one transaction.""" + txns = [{"amount": 25, "category": "entertainment", "date": "2024-01-01"}] + assert group_transactions(txns) == {"entertainment": 25} + + +class TestEdgeCases: + """Test edge cases and boundary conditions.""" + + def test_empty_list(self): + """Test with no transactions.""" + assert group_transactions([]) == {} + + def test_case_sensitive_categories(self): + """Test that categories are case-sensitive.""" + txns = [ + {"amount": 10, "category": "Food", "date": "2024-01-01"}, + {"amount": 20, "category": "food", "date": "2024-01-02"} + ] + result = group_transactions(txns) + assert result == {"Food": 10, "food": 20} + + def test_many_categories(self): + """Test with many different categories.""" + txns = [ + {"amount": 1, "category": "a", "date": "2024-01-01"}, + {"amount": 2, "category": "b", "date": "2024-01-01"}, + {"amount": 3, "category": "c", "date": "2024-01-01"}, + {"amount": 4, "category": "d", "date": "2024-01-01"} + ] + assert group_transactions(txns) == {"a": 1, "b": 2, "c": 3, "d": 4} + diff --git a/problems/medium/word-frequency/solution.py b/problems/medium/word-frequency/solution.py new file mode 100644 index 0000000..6ae229e --- /dev/null +++ b/problems/medium/word-frequency/solution.py @@ -0,0 +1,32 @@ +""" +Word Frequency Counter + +You're building a text analytics tool for a content marketing team. +Given a block of text, return the top N most frequently used words, +sorted by frequency (highest first), then alphabetically for ties. + +Example 1: + Input: text = "the quick brown fox jumps over the lazy dog the fox" + n = 2 + Output: [("the", 3), ("fox", 2)] + Explanation: "the" appears 3 times, "fox" appears 2 times + +Example 2: + Input: text = "hello world hello" + n = 5 + Output: [("hello", 2), ("world", 1)] + Explanation: Only 2 unique words, return all of them + +Constraints: + - Words are separated by whitespace + - Case-insensitive (convert to lowercase) + - Ignore punctuation attached to words + - n >= 1 + - If fewer than n unique words exist, return all of them +""" + + +def top_words(text: str, n: int) -> list[tuple[str, int]]: + """Return top n words by frequency as list of (word, count) tuples.""" + pass # Your implementation here + diff --git a/problems/medium/word-frequency/tests.py b/problems/medium/word-frequency/tests.py new file mode 100644 index 0000000..eebf55c --- /dev/null +++ b/problems/medium/word-frequency/tests.py @@ -0,0 +1,52 @@ +"""Tests for word-frequency.""" +import pytest +from solution import top_words + + +class TestBasicCases: + """Test basic functionality with typical inputs.""" + + def test_basic_frequency(self): + """Test basic word counting.""" + result = top_words("the quick brown fox jumps over the lazy dog the fox", 2) + assert result == [("the", 3), ("fox", 2)] + + def test_all_unique(self): + """Test when all words are unique.""" + result = top_words("one two three", 2) + assert result == [("one", 1), ("three", 1)] or result == [("one", 1), ("two", 1)] + + def test_single_word_repeated(self): + """Test with one word repeated.""" + result = top_words("hello hello hello", 1) + assert result == [("hello", 3)] + + +class TestEdgeCases: + """Test edge cases and boundary conditions.""" + + def test_empty_string(self): + """Test with empty input.""" + result = top_words("", 5) + assert result == [] + + def test_n_greater_than_unique_words(self): + """Test when n exceeds unique word count.""" + result = top_words("hello world", 10) + assert len(result) == 2 + + def test_case_insensitive(self): + """Test that counting is case-insensitive.""" + result = top_words("Hello HELLO hello", 1) + assert result == [("hello", 3)] + + def test_punctuation_ignored(self): + """Test that punctuation is stripped.""" + result = top_words("hello, world! hello.", 1) + assert result == [("hello", 2)] + + def test_alphabetical_tiebreaker(self): + """Test alphabetical ordering for same frequency.""" + result = top_words("cat bat ant", 3) + assert result == [("ant", 1), ("bat", 1), ("cat", 1)] +