diff --git a/.github/workflows/ci-checks.yml b/.github/workflows/ci-checks.yml index b3d7b0052..3b95f5dfe 100644 --- a/.github/workflows/ci-checks.yml +++ b/.github/workflows/ci-checks.yml @@ -4,19 +4,19 @@ on: [push, pull_request, workflow_dispatch] jobs: ls_linting: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: ls-lint/action@v2.2.3 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ls-lint/action@v2.2.3 md_formatting: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: nosborn/github-action-markdown-cli@v3.3.0 - with: - files: . - config_file: .markdownlint.yml + - uses: actions/checkout@v4 + - uses: nosborn/github-action-markdown-cli@v3.3.0 + with: + files: . + config_file: .markdownlint.yml py_formatting: runs-on: ubuntu-latest @@ -44,11 +44,11 @@ jobs: if: always() - name: install pylint run: | - python - m pip install --upgrade pip + python -m pip install --upgrade pip pip install pylint shell: bash - name: Python - Check Linting - pylint - run: "pylint solutions tests || echo '::warning title=Pylint Error(s)::Discuss solutions and trade-offs in code review.'" + run: "pylint solutions tests || echo '::warning title=Pylint Error(s)::Discuss solutions and trade-offs in code review.'" shell: bash py_tests: @@ -59,22 +59,13 @@ jobs: - name: python version run: python --version shell: bash - + - name: Check for test files id: check_tests run: | - test_files=$(find ./solutions/tests -type f -name "test_*.py") - if [ -n "$test_files" ]; then - echo "Found test files:" - echo "$test_files" - echo "has_tests=true" >> $GITHUB_OUTPUT - else - echo "No test files found matching pattern ./solutions/tests/test_*.py" - echo "has_tests=false" >> $GITHUB_OUTPUT - fi - shell: bash - + - name: Python - Run Tests if: steps.check_tests.outputs.has_tests == 'true' - run: python -m unittest + run: echo "PYTHONPATH=$GITHUB_WORKSPACE" >> $GITHUB_ENV shell: bash + diff --git a/.markdownlint.yml b/.markdownlint.yml index ee6205f5c..8cc0eea94 100644 --- a/.markdownlint.yml +++ b/.markdownlint.yml @@ -1,3 +1,9 @@ ignore: - venv - .github + + +MD013: + line_length: 180 + ignore_urls: true + diff --git a/.vscode/settings.json b/.vscode/settings.json index 252022b48..aceff92c0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -119,8 +119,7 @@ "editor.defaultFormatter": "charliermarsh.ruff", "editor.formatOnSave": true, "editor.codeActionsOnSave": { - "source.fixAll.ruff": "explicit", - "source.organizeImports.ruff": "explicit" + } } } diff --git a/README.md b/README.md index e69de29bb..edc238599 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,48 @@ +# Collab Coders + +> Welcome to the **Collab Coders** repository! πŸš€ +We are a team of passionate Junior developers From MET. +> dedicated to building impactful solutions through collaboration and innovation. + +--- + +## 🌟 About Us + +> At **Collab Coders**, we believe in the power of teamwork: + +- to tackle complex challenges and create meaningful projects. +- This repository serves as a hub for our collaborative efforts. +- where ideas turn into reality. + +--- + +## πŸ“‚ Repository Structure + +Here’s how the repository is organized: + +- `main`: The primary branch containing stable and reviewed code. +- `feature-branches`: Each team member has their own branch +- for working on features or tasks. +- Branches are named after it is functionality for clarity +- (e.g., `challenges/factorial-function`, `challenges/sort-list`). + +--- + +## πŸ› οΈ Technologies Used + +> Our team use stack of tools and technologies +> based on the program specifications and requirements: + +- **Languages**: Python, Markdown +- **Frameworks**: N/A +- **Tools**: Github,Git, VS Code +- **Communication_channels**: Slack, Google Meet, Zoom, Whatsapp + +--- + +## 🌱 How to Contribute + +1. Clone the repository: + + ```bash + git clone diff --git a/collaboration/communication.md b/collaboration/communication.md index 484652e0f..6d82423b8 100644 --- a/collaboration/communication.md +++ b/collaboration/communication.md @@ -7,41 +7,46 @@ you can share the rest in confidence with you group by another channel --> -# Communication +# Communication πŸ“¬ ______________________________________________________________________ ## Communication Schedule -| Day | How | The topic of discussion | | --- | :-: | ----------------------- | +| Day & Time | How | The topic of discussion | +| --- | :-: | ----------------------- | +|Every Friday, 9 EST| Google Meet| Activity Updates| +| | | | | | | | ## Communication Channels how often will we get in touch on each channel, and what we will discuss there: -- **Issues**: -- **Pull Requests**: -- **Slack/Discord**: -- **Video Calls**: +- **Issues**: To track team progress, update as needed, and assign tasks where needed +- **Pull Requests**: Reviewed within 48 hours for feedback, approving the change and merging it to the main branch + +- **Slack**: For daily task updates and resolving small problems from team members +- **Video Calls**: Zoom/Google meet preferably for general group meetings and weekly updates ______________________________________________________________________ ## Availability -### Availability for calling/messaging - -| Day | Monday | Tuesday | Wednesday | Thursday | Friday | Saturday | Sunday | | ------- | :----: | :-----: | :-------: | :------: | :----: | :------: | :----: | -| _name_ | | | | | | | | - -### How many hours everyone has per day - -- name: _5h_; -- name: _6h_; -- name: _5h_; -- name: _4h_; -- name: _3h_; +### Availability for calling/messaging(+how many hours each day in EST) + +| Day | Monday | Tuesday | Wednesday | Thursday | Friday | Saturday | Sunday | +|:------ | :----: | :-----: | :-------: | :------: | :----: | :------: | ----: | +| Hiba | 9-12 | 9-12 | 9-12 | 9-12 | 9-12 | 9-12 | 9-12 | +| Jeffery | 9-12 | 9-12 | 9-12 | 9-12 | 9-12 | 9-12 | 9-12 | +| Kelvin | | | | | | | | +| Mahdia | 9-12 | 9-12 | 10-12 | 10-12 | 9-12 | 9-12 | 10-12 | +| Martha | 9-12 | 9-12 | 9-12 | 9-12 | 9-12 | 9-12 | 9-12 | +| Osei | 10-13 |11-15 | 9-12 | 9-12 | 9-15 | 13-15 | 9-15| +| Rina | | 7-15 | | 7-15 | 7-15 | 9-15 | 9-15 | +| Terry | 9-11 | 9-11 | 9-11 | 6-9 | 9-11 | 9-12 | 9-11 | +| Yool | 9-12 | 9-12 | 10-13 | 8-12 | 9-12 | 10-13 | 10-13 | +|Shagun | | | | | | | | ## Asking for Help diff --git a/collaboration/constraints.md b/collaboration/constraints.md index 24079505c..92d48196e 100644 --- a/collaboration/constraints.md +++ b/collaboration/constraints.md @@ -2,7 +2,8 @@ # Constraints -Some boundaries around our project. +In any collaborative project, understanding and addressing constraints is essential for success. +A survey was conducted within the group to know the constraint of each member of our group. Below are the internal and external constraint of the group. ## External @@ -15,6 +16,13 @@ Some boundaries around our project. - ... --> +- **Primary Constraint:** + - Technology limitations (e.g., Git, GitHub, and communication tools) were the most frequently cited + external constraint, affecting 75% of respondents. +- Time zone differences and internet connectivity are also notable external constraints, each affecting 50% of respondents. +- **Other Concerns:** + - Project deadlines, inconsistent power supply, and access to a personal computer were mentioned by 25% of respondents each. + ## Internal: Involuntary -## Internal: Voluntary +- **Primary Constraint:** + - The individual skill levels of group members were cited by 62.5% of respondents, making it the most significant internal constraint. +- **Other notable constraints (each at 37.5%) include:** + - The amount of time available to work on the project. + - Individual work schedules. + - Inability to respond quickly to messages. +- **Least Concern:** + - The number of hours members want to spend working (12.5%). +## Internal: Voluntary + +- **Primary Constraint**: + - Technology limitations (e.g., Git, GitHub, and communication tools) were the most frequently cited external constraint, affecting 75% of respondents. +- **Time Zone Differences and Connectivity**: + - Time zone differences and internet connectivity are notable external constraints, each affecting 50% of respondents. +- **Other Concerns**: + - Project deadlines, inconsistent power supply, and access to a personal computer were mentioned by 25% of respondents each. diff --git a/collaboration/groupnorms.md b/collaboration/groupnorms.md new file mode 100644 index 000000000..2f5dbd9ee --- /dev/null +++ b/collaboration/groupnorms.md @@ -0,0 +1,157 @@ +# Collaboration + + + +## Group norms for ET6-Foundation-group-09's project + +**These group norms will serve as the guidelines and directives, everyone is expected to comply with, +yet no repercussions will follow if they are disregarded.** + +## Communication + +### Primary communication channel + +Slack + +WhatsAPP + +GitHub + +Zoom Meeting + +Google meet + +#### Response Time + +Everyone is expected to response to the messages on all the communication channels within 24 hours. + +#### Language of communication + +The official language of communication is English, and we strongly recommend using clear, concise language, avoid using jargon. + +#### Official Meeting platforms + +Zoom Meeting + +Google Meet + +The group meeting will be conducted on the official platforms at least once in the week which can be adjusted based on group demands. + +The time will be discussed and agreed by all the participants on slack and WhatsAPP. + +All the meetings are expected to last for one hour. + +The recording will be made available after the meeting and uploaded to the group channels, it will be helpful to the participants who missed the meeting. + +All the participants who missed the meeting are advised to watch the recording before sharing their opinions. + +## Group Organization + +**The team organization will ensure effective communication among the group members, making informed decisions +and ensuring that everyone is working on the same goal.** + +### Organizational Structure + +### Project lead + +### Responsibilities + +Oversees the project, ensuring timely completion and making sure no member is left behind. + +The project lead has no absolute authority over the group members. + +Coordinates the resolution of key issues and pull requests + +### Head of the project Communication + +### Key Responsibilities + +Discussion management on Github and WhatsAPP + +Schedule and control the group meeting + +### Deputy Head of the project Communication + +Assist the Head of communication in implementing communication plan + +Provides supports to group members, helping in resolving issues and answering questions. + +### Technical lead + +Assist in code review and conflict resolution + +## Collaboration Norms + +### collaboration Tools + +Github + +Git + +Visual Code Studio (VS Code) + +### Task Assignment + +Each team member will assign him/her self to the task using Github issues + +Everyone is expected to work individually and push it to the branch upon completion + +### Code Review + +Team members will review and provide feedback on each others's code within 48 hours + +we anticipate everyone bear in mind that we are reviewing codes not individuals. + +### Branching and Merging + +we will use feature branches and pull request to manage different version of codes + +## Conflict Resolution + +### Open Communication + +All the members are expected to communicate their concerns and challenges + +### Constructive Feedback + +We encourage the culture of constructive feedback and respectful criticism + +Remember we are reviewing codes not people + +### Conflict resolution process + +All sorts of conflict and misunderstanding shall be resolved through open discussion and mutual understanding + +## Respect and Inclusion + +### Equality + +All the group members have equal rights and their opinion must be respected. + +We equal participation from all the group members. + +### Respectful Communication + +Group members are expected to communicate respectful and professionally by avoiding offensive language. + +### Decision Making + +All the team members will have their voices in decision making. + +### Diversity + +We endeavor to create an inclusive environment that fosters diversity. + + + +## Groups Norms List + +### Communication Norms + +### Organization norms + +### Collaboration_Norms + +### Conflict Resolution Norms + +### Respect and Inclusion Norms diff --git a/collaboration/learning_goals.md b/collaboration/learning_goals.md index 11c583d2b..bf41a3eba 100644 --- a/collaboration/learning_goals.md +++ b/collaboration/learning_goals.md @@ -1,5 +1,23 @@ -# Learning Goals +# Learning Goals πŸ§‘πŸ½β€πŸ’» -## Collective +## Collective Goals -## Individual +- Collaboration and productivity between team members in GitHub and Slack. +- Get used to Git and GitHub. +- Communicate effectively and have an inclusive and friendly environment. +- Learn more about documentation and testing in programming. +- Learn more about Python language features. + +## Individual Goals + +| Name | Goals | +| ------- | ------------------------------------------------------------------------------------------------------------------------------ | +| Mahdia | Enhance skills in Python for data analysis and automation scripts. | +| Jeffery | Master foundational data analysis skills, build a portfolio with real-world projects, earn certifications to kickstart my career in data analysis.| +| Hiba | Enhance my collaboration skills and develop good programming habits like documenting. | +| Rina | Learn how to use GitHub and the basics of coding. | +| Yool | Gain a solid understanding of Python, Git, GitHub, and teamwork to advance my computer science career, build projects, gain collaborative experience.| +| Osei | Learn the basics of Git and GitHub, how to send a pull request, and the basics of Python, which include documenting and testing. | +| Martha | Master skills in version control for tracking changes, collaborating on projects, and improve my knowledge in Python for data analysis. | +| Kelvin | | +| Terry | Deepen Python and Git/GitHub skills, collaborate effectively with teammates, and create a program that helps others in the foundation for growth.| diff --git a/solutions/celsius_to_fahrenheit.py b/solutions/celsius_to_fahrenheit.py new file mode 100644 index 000000000..298b8b365 --- /dev/null +++ b/solutions/celsius_to_fahrenheit.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +A module for converting temperatures between Celsius and Fahrenheit. + +Module contents: + - celsius_to_fahrenheit: converts a temperature from Celsius to Fahrenheit. + +Created on 30 12 2024 +@author: Terry Aziaba +""" + +import math + + +def celsius_to_fahrenheit(celsius: float) -> float: + """Converts a temperature from Celsius to Fahrenheit. + + Parameters: + celsius: float, the temperature in Celsius. + + Returns -> float: + The temperature in Fahrenheit. + + Raises: + AssertionError: if the argument is not a float or int. + + Examples: + >>> celsius_to_fahrenheit(0) + 32.0 + + >>> celsius_to_fahrenheit(100) + 212.0 + + >>> celsius_to_fahrenheit(-40) + -40.0 + """ + # This line ensures the input (temperature) is a number (int or float) + assert isinstance(celsius, (int, float)), "Input temperature must be a number." + assert not math.isnan(celsius), "Input temperature must not be NaN." + + # Conversion + fahrenheit = (celsius * 9 / 5) + 32 + + return fahrenheit diff --git a/solutions/find_longest_palindrome.py b/solutions/find_longest_palindrome.py new file mode 100644 index 000000000..8297df846 --- /dev/null +++ b/solutions/find_longest_palindrome.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +A module for finding the longest palindromic substring in a string. + +Module contents: + - find_longest_palindrome: finds the longest palindrome within a string. + - is_palindrome: helper function to check if a string is a palindrome. + +Created on 28 12 2024 +@author: Mahdia Ahmadi +""" + + +def find_longest_palindrome(input_string: str) -> str: + """ + Finds the longest palindromic substring within the input string. + + A palindrome reads the same forwards and backwards. This function returns + the first occurrence of the longest palindrome if multiple exist. + + Parameters: + text: str, the input string to search for palindromes + + Returns: + str: the longest palindromic substring found + + Raises: + AssertionError: if the input is not a string + + Examples: + >>> find_longest_palindrome("babad") + 'bab' + >>> find_longest_palindrome("racecar") + 'racecar' + >>> find_longest_palindrome("hello") + 'll' + >>> find_longest_palindrome("python") + 'p' + >>> find_longest_palindrome("") + '' + """ + # Input validation + assert isinstance(input_string, str), "Input must be a string" + + # Handle empty string and single characters + if len(input_string) <= 1: + return input_string + + # Initialize variables to track the longest palindrome + longest_start = 0 + longest_length = 1 + + # Check all possible substrings + for start in range(len(input_string)): + for end in range(start + longest_length, len(input_string) + 1): + substring = input_string[start:end] + # Only check if this could be a longer palindrome + if is_palindrome(substring) and len(substring) > longest_length: + longest_start = start + longest_length = len(substring) + + # Return the longest palindrome found + return input_string[longest_start : longest_start + longest_length] + + +def is_palindrome(input_string: str) -> bool: + """ + Checks if the input string is a palindrome. + + Parameters: + text: str, string to check + + Returns: + bool: True if the string is a palindrome, False otherwise + + Examples: + >>> is_palindrome("racecar") + True + >>> is_palindrome("hello") + False + >>> is_palindrome("a") + True + >>> is_palindrome("") + True + """ + # Compare string with its reverse + return input_string == input_string[::-1] diff --git a/solutions/find_two_sum_indices.py b/solutions/find_two_sum_indices.py new file mode 100644 index 000000000..f6232093b --- /dev/null +++ b/solutions/find_two_sum_indices.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +A module for providing the indices of two numbers in a list that add up to a target sum. + +Module contents: Takes a list of numbers (integers or floats) and a target sum as input and returns the indices of the two numbers that add up to the target sum. + +Created on 2025-01-04 + +@author: Jeffery Offei Darko +""" + + +def find_two_sum_indices(nums: list[float], target: float) -> list[int]: + """ + Find two indices in the list such that their values add up to the target. + + Parameters: + nums (list[float]): The list of numbers (integers or floats). + target (float): The target sum. + + Returns: + list[int]: The indices of the two numbers that add up to the target. + + Raises: + AssertionError: If the input is not valid or no solution is found. + + Examples: + >>> find_two_sum_indices([2, 7, 11, 15], 9) + [0, 1] + + >>> find_two_sum_indices([3, 2.5, 4], 6.5) + [1, 2] + + >>> find_two_sum_indices([3.0, 3], 6.0) + [0, 1] + """ + import math + + # Validate inputs + assert isinstance(nums, list), "Input must be a list." + assert all( + isinstance(num, (int, float)) for num in nums + ), "List must contain numbers." + assert isinstance(target, (int, float)), "Target must be a number." + assert len(nums) > 1, "List must contain at least two numbers." + assert not any( + math.isnan(x) for x in nums + ), "Input list must not contain NaN values." + assert not math.isnan(target), "Target must not be NaN." + + # Main logic with floating-point precision handling + epsilon = 1e-7 + num_map = {} + for i, num in enumerate(nums): + complement = target - num + for seen_num, seen_index in num_map.items(): + if abs(seen_num - complement) < epsilon: + return [seen_index, i] + num_map[num] = i + + assert False, "No two numbers add up to the target sum." diff --git a/solutions/longest_substring.py b/solutions/longest_substring.py new file mode 100644 index 000000000..86e7bfa84 --- /dev/null +++ b/solutions/longest_substring.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +A module for generating the longest substring + +Module contents: + longest_substring: generates the longest substring without + repetition for a given string. + +Created on 2025-01-05 +@author: Hiba Daffallah +""" + + +def longest_substring(input_string: str) -> str: + """ + Generates a string containing the longest substring without repitition + from the input_string. + + Parameters: + input_string: str + + Returns : + substring: the longest substring + + Raises: + AssertionError: if the argument is not a string + + >>> longest_substring('') + '' + >>> longest_substring('bbbbbb') + 'b' + >>> longest_substring('abcdef') + 'abcdef' + >>> longest_substring('pwwkew') + 'wke' + >>> longest_substring('celelocceli') + 'eloc' + """ + # Ensure the input is a string + assert isinstance(input_string, str) + + n = len(input_string) + # If the string is empty, return an empty string as the result + if n == 0: + return "" + # Initialize a set to keep track of unique characters in the current substring + sub = set() + # Initialize the longest substring variable + longest_sub = "" + # Initialize the left pointer of the sliding window + left = 0 + for right in range(n): + # If the character at the right pointer is already in the set, + # slide the left pointer to remove characters until the duplicate is removed + while input_string[right] in sub: + sub.remove(input_string[left]) + left += 1 + + # Add the current character at the right pointer to the set + sub.add(input_string[right]) + current_len = right - left + 1 + + # If the current substring is longer than the previously recorded longest substring, + # update the longest substring + if current_len > len(longest_sub): + longest_sub = input_string[left : right + 1] + + # Return the longest substring found + return longest_sub diff --git a/solutions/parse_email_alias.py b/solutions/parse_email_alias.py new file mode 100644 index 000000000..89b17b61a --- /dev/null +++ b/solutions/parse_email_alias.py @@ -0,0 +1,225 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +A module for parsing and validating email aliases. + +This module provides functionality to validate email addresses and parse their components, +including support for Gmail-style plus (+) aliases and subdomain addresses. The implementation +combines specific validation checks with regex pattern matching to provide detailed error +messages while maintaining RFC 5322 compliance. + +Module contents: + - parse_email_alias: validates and extracts components of an email alias + +Features: + - Full RFC 5322 compliant email validation + - Support for Gmail-style + aliases + - Support for subdomains + - Detailed error reporting for specific validation failures + - Comprehensive component extraction + +Created on 4 01 2025 +@author: Mahdia Ahamdi +""" + +import re + + +def parse_email_alias(email: str) -> dict: + """ + Parse and validate an email address with optional alias tags. + + This function performs comprehensive validation of email addresses, providing + specific error messages for different types of validation failures. It supports + Gmail-style plus (+) aliases and subdomains while validating against RFC 5322 + standards for email format. + + Parameters: + email: str + The email address to parse. Must be a string containing the email address + to be validated and parsed. + + Returns: + dict + A dictionary containing the following keys: + - valid (bool): Whether the email is valid + - local_part (str | None): Part before the @ (None if invalid) + - domain (str | None): Part after the @ (None if invalid) + - tags (list[str]): Any alias tags found (empty if none) + - errors (list[str]): Any validation errors found + + Raises: + AssertionError: If the input is not a string + + Examples: + >>> # Test basic valid email + >>> result = parse_email_alias("user@example.com") + >>> result['valid'] + True + >>> result['local_part'] + 'user' + >>> result['domain'] + 'example.com' + >>> result['tags'] + [] + >>> result['errors'] + [] + + >>> # Test email with single alias tag + >>> result = parse_email_alias("user+newsletter@example.com") + >>> result['valid'] + True + >>> result['local_part'] + 'user' + >>> result['domain'] + 'example.com' + >>> result['tags'] + ['newsletter'] + + >>> # Test email with multiple alias tags + >>> result = parse_email_alias("user+tag1+tag2@example.com") + >>> result['valid'] + True + >>> result['local_part'] + 'user' + >>> result['tags'] + ['tag1', 'tag2'] + + >>> # Test email with subdomain + >>> result = parse_email_alias("user@sub.example.com") + >>> result['valid'] + True + >>> result['domain'] + 'sub.example.com' + + >>> # Test missing @ symbol + >>> result = parse_email_alias("invalid.email") + >>> result['valid'] + False + >>> result['errors'] + ['Missing @ symbol'] + + >>> # Test empty local part + >>> result = parse_email_alias("@example.com") + >>> result['valid'] + False + >>> result['errors'] + ['Empty local part'] + + >>> # Test empty domain + >>> result = parse_email_alias("user@") + >>> result['valid'] + False + >>> result['errors'] + ['Empty domain'] + + >>> # Test invalid characters in local part + >>> result = parse_email_alias("user<>@example.com") + >>> result['valid'] + False + >>> result['errors'] + ['Invalid characters in local part'] + + >>> # Test missing TLD + >>> result = parse_email_alias("user@domain") + >>> result['valid'] + False + >>> result['errors'] + ['Invalid domain (missing TLD)'] + + >>> # Test invalid domain format + >>> result = parse_email_alias("user@[192.168.1.1]") + >>> result['valid'] + False + >>> result['errors'] + ['Invalid domain format'] + + >>> # Test empty email + >>> result = parse_email_alias("") + >>> result['valid'] + False + >>> result['errors'] + ['Email cannot be empty'] + + >>> # Test non-string input + >>> try: + ... parse_email_alias(123) + ... except AssertionError as e: + ... print(str(e)) + Input must be a string + """ + assert isinstance(email, str), "Input must be a string" + + result = { + "valid": False, + "local_part": None, + "domain": None, + "tags": [], + "errors": [], + } + + if not email: + result["errors"].append("Email cannot be empty") + return result + + # Check for @ symbol first + if "@" not in email: + result["errors"].append("Missing @ symbol") + return result + + # Split into local part and domain + local_part, domain = email.split("@", 1) + + # Check empty local part + if not local_part: + result["errors"].append("Empty local part") + return result + + # Check empty domain + if not domain: + result["errors"].append("Empty domain") + return result + + # Validate local part characters using regular expression(regx) + allowed_chars = set( + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.!#$%&'*+-/=?^_`{|}~" + ) + if not all(c in allowed_chars for c in local_part.split("+")[0]): + result["errors"].append("Invalid characters in local part") + return result + + # Validate domain + if not domain: + result["errors"].append("Empty domain") + return result + + if ".." in domain: + result["errors"].append("Invalid domain format") + return result + + # Check for valid domain format + domain_parts = domain.split(".") + + # Check for missing or invalid TLD + if len(domain_parts) == 1 or not domain_parts[-1]: + result["errors"].append("Invalid domain (missing TLD)") + return result + + # Validate domain format using regx + domain_pattern = r"^[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?)*\.[a-zA-Z]{2,}$" + if not re.match(domain_pattern, domain): + result["errors"].append("Invalid domain format") + return result + + # Extract tags if present + base_local = local_part + tags = [] + if "+" in local_part: + base_local, *tags = local_part.split("+") + + result["valid"] = True + result["local_part"] = base_local + result["domain"] = domain + result["tags"] = tags + + return result diff --git a/solutions/phone_letter_combinations.py b/solutions/phone_letter_combinations.py new file mode 100644 index 000000000..099de1c34 --- /dev/null +++ b/solutions/phone_letter_combinations.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +A module for all the possible letter combinations for a given number + +Module contents: + phone_letter_combinations: Creates a new list with possible + letter combinations in phone keyboard for the numbers given + for simplification I made it maximum of 2 digits + +Created on 2024-01-07 +Author: Hiba Daffallah +""" + + +def phone_letter_combinations(digits: str) -> list[str]: + """ + phone_letter_combinations function creates a new list with possible + letter combinations in phone keyboard for the numbers given + + + Parameters: + digits: string of numbers it should be from 2 to 9 + + Returns: + possible_combinations: list of characters + + Raises: + AssertionError if the input was not a number + + Example: + >>> phone_letter_combinations("23") + ['ad', 'ae', 'af', 'bd', 'be', 'bf', 'cd', 'ce', 'cf'] + >>> phone_letter_combinations("") + [] + >>> phone_letter_combinations("234") + ['adg', 'adh', 'adi', 'aeg', 'aeh', 'aei', 'afg', 'afh', + 'afi', 'bdg', 'bdh', 'bdi', 'beg', 'beh', 'bei', 'bfg', + 'bfh', 'bfi', 'cdg', 'cdh', 'cdi', 'ceg', 'ceh', 'cei', 'cfg', 'cfh', 'cfi'] + >>> phone_letter_combinations("1") + Input must only contain digits from 2 to 9. + """ + if not all(char in "23456789" for char in digits): + raise AssertionError("Input must only contain digits from 2 to 9.") + if not digits: + return [] + digit_to_letters = { + "2": "abc", + "3": "def", + "4": "ghi", + "5": "jkl", + "6": "mno", + "7": "pqrs", + "8": "tuv", + "9": "wxyz", + } + + def possibility(i, temp): + if i == len(digits): + combination.append(temp[:]) + return + for letter in digit_to_letters[digits[i]]: + possibility(i + 1, temp + letter) + + combination = [] + possibility(0, "") + + return combination diff --git a/solutions/tests/test_celsius_to_fahrenheit.py b/solutions/tests/test_celsius_to_fahrenheit.py new file mode 100644 index 000000000..804ece60d --- /dev/null +++ b/solutions/tests/test_celsius_to_fahrenheit.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Test module for conversion from celsius to fahrenheit. + +This module contains unittest test cases for the celsius_to_fahrenheit + +Created on 30 12 2024 +@author: Terry Aziaba +""" + +import unittest + +from ..celsius_to_fahrenheit import celsius_to_fahrenheit + + +class TestCelsiusToFahrenheit(unittest.TestCase): + """Unit tests for the celsius_to_fahrenheit function.""" + + def test_freezing_point(self): + """Test the freezing point of water.""" + self.assertAlmostEqual(celsius_to_fahrenheit(0), 32.0) + + def test_boiling_point(self): + """Test the boiling point of water.""" + self.assertAlmostEqual(celsius_to_fahrenheit(100), 212.0) + + def test_negative_temperature(self): + """Test a negative temperature.""" + self.assertAlmostEqual(celsius_to_fahrenheit(-40), -40.0) + + def test_fractional_input(self): + """Test a fractional Celsius value.""" + self.assertAlmostEqual(celsius_to_fahrenheit(37.5), 99.5) + + def test_large_positive_value(self): + """Test a very large positive Celsius value.""" + self.assertAlmostEqual(celsius_to_fahrenheit(1000), 1832.0) + + def test_large_negative_value(self): + """Test a very large negative Celsius value""" + self.assertAlmostEqual(celsius_to_fahrenheit(-273.15), -459.67) + + def test_infinity_positive(self): + """Test positive infinity.""" + result = celsius_to_fahrenheit(float("inf")) + self.assertEqual(result, float("inf")) + + def test_infinity_negative(self): + """Test negative infinity.""" + result = celsius_to_fahrenheit(float("-inf")) + self.assertEqual(result, float("-inf")) + + def test_nan_value(self): + """Test if NaN is handled correctly.""" + with self.assertRaises(AssertionError): + celsius_to_fahrenheit(float("nan")) + + def test_zero_input(self): + """Test zero as input.""" + self.assertAlmostEqual(celsius_to_fahrenheit(0), 32.0) + + def test_small_fractional_input(self): + """Test a small fractional input.""" + self.assertAlmostEqual(celsius_to_fahrenheit(0.5), 32.9) + + def test_assertion_error_non_numeric(self): + """Test if an assertion error is raised for non-numeric input.""" + with self.assertRaises(AssertionError): + celsius_to_fahrenheit("not a number") + + def test_assertion_error_none(self): + """Test if an assertion error is raised for None input.""" + with self.assertRaises(AssertionError): + celsius_to_fahrenheit(None) diff --git a/solutions/tests/test_find_longest_palindrome.py b/solutions/tests/test_find_longest_palindrome.py new file mode 100644 index 000000000..543f3901c --- /dev/null +++ b/solutions/tests/test_find_longest_palindrome.py @@ -0,0 +1,156 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Test module for the longest palindrome finder functionality. + +This module contains unittest test cases for the find_longest_palindrome and +is_palindrome functions, including edge cases and invalid inputs. + +Created on 28 12 2024 +@author: Mahdia Ahmadi +""" + +import unittest + +from solutions.find_longest_palindrome import find_longest_palindrome, is_palindrome + + +class TestPalindromeFinder(unittest.TestCase): + """ + Test suite for palindrome finding functions. + + This class contains unit tests for both the main palindrome finding function + and its helper function, covering normal cases, edge cases, and error conditions. + """ + + def test_empty_string(self): + """ + Test that an empty string returns an empty string. + """ + self.assertEqual(find_longest_palindrome(""), "") + + def test_single_character(self): + """ + Test that a single character string returns that character. + """ + self.assertEqual(find_longest_palindrome("a"), "a") + + def test_two_same_characters(self): + """ + Test string with two identical characters. + """ + self.assertEqual(find_longest_palindrome("aa"), "aa") + + def test_two_different_characters(self): + """ + Test string with two different characters returns first character. + """ + self.assertEqual(find_longest_palindrome("ab"), "a") + + def test_simple_odd_palindrome(self): + """ + Test finding a simple odd-length palindrome. + """ + self.assertEqual(find_longest_palindrome("racecar"), "racecar") + + def test_simple_even_palindrome(self): + """ + Test finding a simple even-length palindrome. + """ + self.assertEqual(find_longest_palindrome("abba"), "abba") + + def test_multiple_palindromes(self): + """ + Test string containing multiple palindromes returns the longest. + """ + self.assertEqual(find_longest_palindrome("abbaracecarxy"), "racecar") + + def test_overlapping_palindromes(self): + """ + Test string with overlapping palindromes. + """ + self.assertEqual(find_longest_palindrome("abababa"), "abababa") + + def test_no_long_palindrome(self): + """ + Test string with no palindromes longer than one character. + """ + self.assertEqual(find_longest_palindrome("python"), "p") + + def test_case_sensitive(self): + """ + Test that the function is case-sensitive. + """ + self.assertNotEqual(find_longest_palindrome("Racecar"), "Racecar") + + def test_with_spaces(self): + """ + Test string containing spaces. + """ + self.assertEqual(find_longest_palindrome("race car"), "r") + + def test_with_special_characters(self): + """ + Test string containing special characters. + """ + self.assertEqual(find_longest_palindrome("a!@#a"), "a") + + # defensive testcase + def test_invalid_input_type(self): + """ + Test that non-string input raises an AssertionError. + """ + with self.assertRaises(AssertionError): + find_longest_palindrome(123) + + +class TestIsPalindrome(unittest.TestCase): + """ + Test suite for the is_palindrome helper function. + """ + + def test_empty_string_palindrome(self): + """ + Test that an empty string is considered a palindrome. + """ + self.assertEqual(is_palindrome(""), True, "Empty string should be a palindrome") + + def test_single_char_palindrome(self): + """ + Test that a single character is a palindrome. + """ + self.assertEqual( + is_palindrome("x"), True, "Single character should be a palindrome" + ) + + def test_simple_palindrome(self): + """ + Test a simple palindrome string. + """ + self.assertEqual( + is_palindrome("level"), True, "The word 'level' should be a palindrome" + ) + + def test_non_palindrome(self): + """ + Test a non-palindrome string. + """ + self.assertEqual( + is_palindrome("python"), + False, + "The word 'python' should not be a palindrome", + ) + + def test_palindrome_with_spaces(self): + """ + Test that spaces affect palindrome detection. + """ + self.assertEqual( + is_palindrome("race car"), + False, + "String with spaces should not be considered a palindrome", + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/solutions/tests/test_find_two_sum_indices.py b/solutions/tests/test_find_two_sum_indices.py new file mode 100644 index 000000000..ff1fccf14 --- /dev/null +++ b/solutions/tests/test_find_two_sum_indices.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Test module for find_two_sum_indices function. + +Created on 2025-01-05 + +Author: Jeffery Offei Darko +""" + +import unittest +from solutions.find_two_sum_indices import find_two_sum_indices + + +class TestFindTwoSumIndices(unittest.TestCase): + # Basic tests + def test_with_simple_target_sum(self): + """Test basic case with a simple target sum.""" + self.assertEqual(find_two_sum_indices([2, 7, 11, 15], 9), [0, 1]) + + def test_with_different_array_and_target(self): + """Test basic case with a different array and target.""" + self.assertEqual(find_two_sum_indices([3, 2, 4], 6), [1, 2]) + + def test_with_duplicates(self): + """Test basic case with duplicate values.""" + self.assertEqual(find_two_sum_indices([3, 3], 6), [0, 1]) + + # Edge cases with integers + def test_with_negative_numbers(self): + """Test case with negative numbers.""" + self.assertEqual(find_two_sum_indices([-3, 4, 3, 90], 0), [0, 2]) + + def test_with_zeroes(self): + """Test case where all numbers are zero.""" + self.assertEqual(find_two_sum_indices([0, 0, 0], 0), [0, 1]) + + def test_with_large_numbers(self): + """Test case with large integers.""" + self.assertEqual( + find_two_sum_indices([1000000, 2500000, 1500000], 3500000), [0, 1] + ) + + # Cases where no solution exists + def test_no_solution(self): + """Test case where no solution exists.""" + with self.assertRaises(AssertionError): + find_two_sum_indices([1, 2, 3], 7) + + def test_empty_list(self): + """Test case with an empty list.""" + with self.assertRaises(AssertionError): + find_two_sum_indices([], 5) + + def test_single_element(self): + """Test case with a single element.""" + with self.assertRaises(AssertionError): + find_two_sum_indices([1], 1) + + def test_repeated_elements(self): + """Test case with repeated elements where no solution exists.""" + with self.assertRaises(AssertionError): + find_two_sum_indices([1, 1, 1, 1], 3) + + # Tests with floats + def test_with_floats_basic(self): + """Test with floats that add up to the target.""" + self.assertEqual(find_two_sum_indices([2.5, 7.1, 11.3], 9.6), [0, 1]) + + def test_with_floats_and_integers(self): + """Test with a mix of floats and integers.""" + self.assertEqual(find_two_sum_indices([1, 2.5, 4.5], 7), [1, 2]) + + def test_with_floats_no_solution(self): + """Test with floats where no solution exists.""" + with self.assertRaises(AssertionError): + find_two_sum_indices([1.2, 3.4, 5.6], 10) + + def test_with_float_target(self): + """Test with a float target.""" + self.assertEqual(find_two_sum_indices([2, 3.5, 5], 8.5), [1, 2]) + + def test_with_float_and_negative(self): + """Test with floats and negative numbers.""" + self.assertEqual(find_two_sum_indices([-1.5, 3.0, 2.5], 1.0), [0, 2]) + + # Invalid input tests + def test_invalid_input_not_list(self): + """Test with an invalid input that is not a list.""" + with self.assertRaises(AssertionError): + find_two_sum_indices("123", 5) + + def test_invalid_input_with_non_number_elements(self): + """Test with invalid input containing non-number elements.""" + with self.assertRaises(AssertionError): + find_two_sum_indices([1, "two", 3], 5) + + def test_invalid_target_not_number(self): + """Test with an invalid target that is not a number.""" + with self.assertRaises(AssertionError): + find_two_sum_indices([1, 2, 3], "five") + + def test_invalid_input_none(self): + """Test with an invalid input of None.""" + with self.assertRaises(AssertionError): + find_two_sum_indices(None, 5) + + def test_invalid_target_none(self): + """Test with an invalid target of None.""" + with self.assertRaises(AssertionError): + find_two_sum_indices([1, 2, 3], None) + + # Edge cases for precision and NaN + def test_with_precision(self): + """Test edge case with float precision.""" + self.assertEqual(find_two_sum_indices([1.0000001, 2.9999999], 4.0), [0, 1]) + + def test_with_nan(self): + """Test with NaN input or target.""" + with self.assertRaises(AssertionError): + find_two_sum_indices([float("nan"), 2.5], float("nan")) + + def test_with_zero_target(self): + """Test when the target is zero.""" + self.assertEqual(find_two_sum_indices([-1, 1, 2], 0), [0, 1]) + + +if __name__ == "__main__": + unittest.main() diff --git a/solutions/tests/test_longest_substring.py b/solutions/tests/test_longest_substring.py new file mode 100644 index 000000000..230e4cbb6 --- /dev/null +++ b/solutions/tests/test_longest_substring.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on 2024-01-01 + +@author: Hiba Daffallah +""" + +import unittest + +from ..longest_substring import longest_substring + + +class TestLongestSubstring(unittest.TestCase): + """Test the longest_substring function""" + + def test_empty_string(self): + """Checks what will happen if the input is an empty + string""" + self.assertEqual(longest_substring(""), "") + + def test_one_char_repeated(self): + """Checks what will happen if the input is a repeated + character""" + self.assertEqual(longest_substring("bbbbbb"), "b") + + def test_no_repitition(self): + """Checks what will happen if the input didn't have + repeated characters""" + self.assertEqual(longest_substring("abcdef"), "abcdef") + + def test_repeated_substring(self): + """Checks what will happen if the input conatins a + repeated substring""" + self.assertEqual(longest_substring("pwwkew"), "wke") + + def test_multiple_substring(self): + """Checks what will happen if the input contains multiple + substrings""" + self.assertEqual(longest_substring("celelocceli"), "eloc") + + def test_input_numbers(self): + """Checks what will happen if the input contains numbers""" + self.assertEqual(longest_substring("991215888"), "2158") + + def test_input_conatins_numbers(self): + """Checks what will happen if the input contains numbers""" + self.assertEqual(longest_substring("9cort9kl9m"), "cort9kl") + + def test_input_non_string(self): + """Checks what will happen if the input is not a string""" + with self.assertRaises(AssertionError): + longest_substring(["abc"]) diff --git a/solutions/tests/test_parse_email_alias.py b/solutions/tests/test_parse_email_alias.py new file mode 100644 index 000000000..1bb3081d3 --- /dev/null +++ b/solutions/tests/test_parse_email_alias.py @@ -0,0 +1,154 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Tests for the parse_email_alias module. + +This module contains unit tests for the parse_email_alias function, +including normal cases, edge cases, and error conditions. + +Created on 4 01 2025 +@author: Mahdia Ahmadi +""" + +import unittest + +from solutions.parse_email_alias import parse_email_alias + + +class TestParseEmailAlias(unittest.TestCase): + """ + Test suite for the parse_email_alias function. + + This class contains unit tests covering various email parsing scenarios, + including valid emails, invalid formats, and edge cases. + """ + + def test_valid_email(self): + """ + Test a basic valid email address. + + Verifies that the function correctly parses a standard email address. + """ + self.assertEqual(parse_email_alias("user@example.com")["valid"], True) + + def test_email_with_tags(self): + """ + Test an email with alias tags. + + Checks if the function properly handles Gmail-style + aliases. + """ + self.assertEqual(parse_email_alias("user+tag@example.com")["tags"], ["tag"]) + + def test_multiple_tags(self): + """ + Test an email with multiple alias tags. + + Ensures the function can handle multiple + separated tags. + """ + self.assertEqual( + parse_email_alias("user+tag1+tag2@example.com")["tags"], ["tag1", "tag2"] + ) + + def test_subdomain(self): + """ + Test an email with a subdomain. + + Verifies correct handling of domain names with subdomains. + """ + self.assertEqual( + parse_email_alias("user@sub.example.com")["domain"], "sub.example.com" + ) + + def test_invalid_email_format(self): + """ + Test an email without @ symbol. + + Checks if the function correctly identifies missing @ as an error. + """ + self.assertIn("Missing @ symbol", parse_email_alias("invalid.email")["errors"]) + + def test_empty_local_part(self): + """ + Test an email with empty local part. + + Ensures the function recognizes an empty local part as an error. + """ + self.assertIn("Empty local part", parse_email_alias("@example.com")["errors"]) + + def test_invalid_characters_in_local_part(self): + """ + Test an email with invalid characters in the local part. + + Verifies that the function detects non-standard characters in the local part. + """ + self.assertIn( + "Invalid characters in local part", + parse_email_alias("user<>@example.com")["errors"], + ) + + def test_empty_domain(self): + """ + Test an email with empty domain. + + Checks if the function correctly identifies an empty domain as an error. + """ + self.assertIn("Empty domain", parse_email_alias("user@")["errors"]) + + def test_missing_top_level_domain(self): + """ + Test an email with missing top-level domain. + + Verifies that the function detects domains without TLDs as invalid. + """ + self.assertIn( + "Invalid domain (missing TLD)", parse_email_alias("user@sub.")["errors"] + ) + + def test_invalid_domain_format(self): + """ + Test an email with an invalid domain format. + + Checks if the function correctly identifies non-standard domain formats. + """ + self.assertIn( + "Invalid domain format", parse_email_alias("user@[192.168.1.1]")["errors"] + ) + + def test_empty_email(self): + """ + Test an empty email string. + + Ensures the function handles empty inputs correctly. + """ + self.assertIn("Email cannot be empty", parse_email_alias("")["errors"]) + + def test_non_string_input(self): + """ + Test non-string input to check defensive assertion. + + Verifies that the function raises an AssertionError for non-string inputs. + """ + with self.assertRaises(AssertionError): + parse_email_alias(123) + + def test_long_local_part(self): + """ + Test an email with a very long local part. + + Checks if the function can handle unusually long local parts. + """ + long_local = "a" * 1000 + self.assertTrue(parse_email_alias(f"{long_local}@example.com")["valid"]) + + def test_long_domain(self): + """ + Test an email with a very long domain. + + Ensures the function can handle unusually long domain names. + """ + long_domain = "example.com" * 100 + self.assertTrue(parse_email_alias(f"user@{long_domain}")["valid"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/solutions/tests/test_phone_letter_combinations b/solutions/tests/test_phone_letter_combinations new file mode 100644 index 000000000..d135620cd --- /dev/null +++ b/solutions/tests/test_phone_letter_combinations @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Test module for phone_letter_combinations function. + +Created on 2024-12-30 +Author: Hiba Daffallah +""" + +import unittest + +from solutions.phone_letter_combinations import phone_letter_combinations + + +class TestPhoneLetterCombinations(unittest.TestCase): + """Test phone_letter_combinations function""" + + def test_numbers_within_range(self): + """It should return all the possible letter combinations for 2,3""" + self.assertEqual( + phone_letter_combinations("23"), + ["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"], + ) + + def test_empty(self): + """It should return empty list""" + self.assertEqual(phone_letter_combinations(""), []) + + def test_single_number(self): + """It should return all the letters for 2""" + self.assertEqual(phone_letter_combinations("2"), ["a", "b", "c"]) + + def test_numbers_out_of_range(self): + """It should return a message""" + with self.assertRaises(AssertionError) as context: + phone_letter_combinations("1") + self.assertEqual( + str(context.exception), "Input must only contain digits from 2 to 9." + ) + + def test_contains_numbers_out_of_range(self): + """It should return a message""" + with self.assertRaises(AssertionError) as context: + phone_letter_combinations("12") + self.assertEqual( + str(context.exception), "Input must only contain digits from 2 to 9." + ) + + # Defensive tests + def test_none_input(self): + """It should raise AssertionError for non-integer input""" + with self.assertRaises(AssertionError): + phone_letter_combinations("b") + + +if __name__ == "__main__": + unittest.main() diff --git a/solutions/tests/test_phone_letter_combinations.py b/solutions/tests/test_phone_letter_combinations.py new file mode 100644 index 000000000..ecd93f1df --- /dev/null +++ b/solutions/tests/test_phone_letter_combinations.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Test module for phone_letter_combinations function. + +Created on 2025-01-07 +Author: Hiba Daffallah +""" + +import unittest + +from solutions.phone_letter_combinations import phone_letter_combinations + + +class TestPhoneLetterCombinations(unittest.TestCase): + """Test phone_letter_combinations function""" + + def test_numbers_within_range(self): + """It should return all the possible letter combinations for 2,3""" + self.assertEqual( + phone_letter_combinations("23"), + ["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"], + ) + + def test_empty(self): + """It should return empty list""" + self.assertEqual(phone_letter_combinations(""), []) + + def test_single_number(self): + """It should return all the letters for 2""" + self.assertEqual(phone_letter_combinations("2"), ["a", "b", "c"]) + + def test_numbers_out_of_range(self): + """It should return a message""" + with self.assertRaises(AssertionError) as context: + phone_letter_combinations("1") + self.assertEqual( + str(context.exception), "Input must only contain digits from 2 to 9." + ) + + def test_contains_numbers_out_of_range(self): + """It should return a message""" + with self.assertRaises(AssertionError) as context: + phone_letter_combinations("12") + self.assertEqual( + str(context.exception), "Input must only contain digits from 2 to 9." + ) + + def test_contains_multiple_numbers(self): + "Tests if it can handle multiple numbers" + self.assertEqual( + phone_letter_combinations("234"), + [ + "adg", + "adh", + "adi", + "aeg", + "aeh", + "aei", + "afg", + "afh", + "afi", + "bdg", + "bdh", + "bdi", + "beg", + "beh", + "bei", + "bfg", + "bfh", + "bfi", + "cdg", + "cdh", + "cdi", + "ceg", + "ceh", + "cei", + "cfg", + "cfh", + "cfi", + ], + ) + + # Defensive tests + def test_none_input(self): + """It should raise AssertionError for non-integer input""" + with self.assertRaises(AssertionError): + phone_letter_combinations("b") + + +if __name__ == "__main__": + unittest.main()